Move questions and reviews into the transcript; agents answer findings

This commit is contained in:
Ian Gulliver
2026-10-19 09:14:21 -07:00
parent eb7e0a13c4
commit 27ff0f457b
21 changed files with 903 additions and 801 deletions
+25 -99
View File
@@ -2,33 +2,12 @@ import AppKit
import ByreAPI
import SwiftUI
enum TabBadge {
case none
case count(Int, String)
}
struct TabBadgeView: View {
let badge: TabBadge
var body: some View {
switch badge {
case .none:
EmptyView()
case .count(let n, let help):
Badge(count: n)
.help(help)
}
}
}
let tabLabels: [TaskTab: String] = [
.transcript: "Transcript",
.board: "Board",
.changes: "Changes",
.files: "Files",
.terminal: "Terminal",
.questions: "Questions",
.reviews: "Reviews",
.workflow: "Workflow",
.project: "Project",
]
@@ -94,30 +73,24 @@ struct MainView: View {
struct PaneTabButton: View {
let label: String
var active = false
var attention = false
var badge = TabBadge.none
let action: () -> Void
@Environment(\.paneAccent) private var accent
@ViewState private var hover = false
var body: some View {
Button(action: action) {
HStack(spacing: 6) {
Text(label)
TabBadgeView(badge: badge)
}
.font(Fonts.font(13, weight: attention ? 700 : 400))
.foregroundStyle(hover ? Palette.text : attention ? Palette.warn : active ? Palette.text : Palette.muted)
.padding(.horizontal, 10)
.padding(.top, 8)
.padding(.bottom, 6)
.background(attention ? Palette.warnTint : .clear)
.overlay(alignment: .bottom) {
Rectangle()
.fill(active ? accent : attention ? Palette.warn : .clear)
.frame(height: 2)
}
.contentShape(Rectangle())
Text(label)
.font(Fonts.font(13))
.foregroundStyle(hover || active ? Palette.text : Palette.muted)
.padding(.horizontal, 10)
.padding(.top, 8)
.padding(.bottom, 6)
.overlay(alignment: .bottom) {
Rectangle()
.fill(active ? accent : .clear)
.frame(height: 2)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.fixedSize()
@@ -196,7 +169,6 @@ private struct LeftPane: View {
private struct TaskPane: View {
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@ViewState private var seen: Set<String> = []
var body: some View {
let selected = store.selectedTaskId.flatMap { store.findTask($0) }
@@ -219,18 +191,6 @@ private struct TaskPane: View {
} action: { frame in
overlays.paneFrame = frame
}
.onAppear {
seen = Set(store.inbox.map(\.id))
}
.onChange(of: store.inbox.map(\.id)) {
let fresh = store.inbox.filter { !seen.contains($0.id) }
for item in fresh {
seen.insert(item.id)
}
if store.focus.tab == .transcript && !store.paneFocus.drafting && fresh.contains(where: { $0.taskId == store.selectedTaskId }) {
store.focusTab(.questions)
}
}
}
@ViewBuilder
@@ -247,10 +207,6 @@ private struct TaskPane: View {
FilesView()
case .terminal:
TerminalTab()
case .questions:
QuestionsView()
case .reviews:
ReviewsView()
case .workflow:
WorkflowView()
case .project:
@@ -381,27 +337,10 @@ private struct TabStrip: View {
private struct Info {
var order: [TaskTab]
var tab: TaskTab
var waiting: Int
var findings: Int
var reviewing: Bool
func count(_ t: TaskTab) -> Int {
t == .questions ? waiting : t == .reviews && !reviewing ? findings : 0
}
func badge(_ t: TaskTab) -> TabBadge {
count(t) > 0 ? .count(count(t), t == .questions ? "open questions" :"open findings in the latest review") : .none
}
}
private func info() -> Info {
let tab = store.focus.tab
let waiting = store.inbox.filter { $0.taskId == store.selectedTaskId }.count
let transcript = store.transcript
let latest = latestReview(transcript)
let reviewing = latest?.state == .running
let findings = latest.map(findingCount) ?? 0
let hasReviews = transcript.contains(where: isReviewEvent)
let workflow = store.selectedTaskId.flatMap { store.findTask($0) }?.workflowRun != nil
let team = store.selectedTaskId.map { !store.team($0).isEmpty } ?? false
let order = taskTabs.filter { t in
@@ -410,13 +349,11 @@ private struct TabStrip: View {
}
switch t {
case .board: return team
case .questions: return waiting > 0
case .reviews: return hasReviews
case .workflow: return workflow
default: return true
}
}
return Info(order: order, tab: tab, waiting: waiting, findings: findings, reviewing: reviewing)
return Info(order: order, tab: tab)
}
private func kept(_ info: Info) -> [TaskTab] {
@@ -444,17 +381,16 @@ private struct TabStrip: View {
let info = info()
let kept = kept(info)
let hidden = info.order.filter { !kept.contains($0) }
let flagged = hidden.filter { info.count($0) > 0 }
HStack(spacing: 4) {
ForEach(info.order.filter { kept.contains($0) }, id: \.self) { t in
PaneTabButton(label: tabLabels[t]!, active: t == info.tab, attention: info.count(t) > 0, badge: info.badge(t)) {
PaneTabButton(label: tabLabels[t]!, active: t == info.tab) {
store.focusTab(t)
}
}
if !hidden.isEmpty {
MoreButton(badges: flagged.map(info.badge)) {
MoreButton {
overlays.menu = OverlayMenu(anchor: moreFrame, accent: accent, entries: hidden.map { t in
MenuEntry(id: t.rawValue, label: tabLabels[t]!, badge: info.badge(t)) {
MenuEntry(id: t.rawValue, label: tabLabels[t]!) {
store.focusTab(t)
}
})
@@ -473,14 +409,14 @@ private struct TabStrip: View {
.background {
HStack(spacing: 4) {
ForEach(info.order, id: \.self) { t in
PaneTabButton(label: tabLabels[t]!, active: t == info.tab, attention: info.count(t) > 0, badge: info.badge(t)) {}
PaneTabButton(label: tabLabels[t]!, active: t == info.tab) {}
.onGeometryChange(for: CGFloat.self) { g in
g.size.width
} action: { w in
widths[t] = w
}
}
MoreButton(badges: []) {}
MoreButton {}
.onGeometryChange(for: CGFloat.self) { g in
g.size.width
} action: { w in
@@ -502,28 +438,18 @@ private struct TabStrip: View {
}
private struct MoreButton: View {
let badges: [TabBadge]
let action: () -> Void
@ViewState private var hover = false
var body: some View {
Button(action: action) {
HStack(spacing: 6) {
Text("More ▾")
ForEach(Array(badges.enumerated()), id: \.offset) { _, b in
TabBadgeView(badge: b)
}
}
.font(Fonts.font(13, weight: badges.isEmpty ? 400 : 700))
.foregroundStyle(hover ? Palette.text : badges.isEmpty ? Palette.muted : Palette.warn)
.padding(.horizontal, 10)
.padding(.top, 8)
.padding(.bottom, 6)
.background(badges.isEmpty ? .clear : Palette.warnTint)
.overlay(alignment: .bottom) {
Rectangle().fill(badges.isEmpty ? .clear : Palette.warn).frame(height: 2)
}
.contentShape(Rectangle())
Text("More ▾")
.font(Fonts.font(13))
.foregroundStyle(hover ? Palette.text : Palette.muted)
.padding(.horizontal, 10)
.padding(.top, 8)
.padding(.bottom, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.fixedSize()