macOS client parity review

This commit is contained in:
Ian Gulliver
2026-10-17 10:54:06 -07:00
parent b85de9a2d0
commit 31037e1c9f
24 changed files with 651 additions and 225 deletions
+56 -31
View File
@@ -210,9 +210,10 @@ final class ChatModel {
}
store.jump = nil
stuck = false
if i > 0, case .agent = rows[i - 1].kind {
switch i > 0 ? rows[i - 1].kind : nil {
case .agent, .subagentMessage:
requestScroll(rows[i - 1].id, .top)
} else {
default:
requestScroll(rows[i].id, .bottom)
}
}
@@ -528,12 +529,15 @@ private struct TranscriptScroll: View {
ForEach(model.builder.pending) { p in
PendingView(pending: p)
}
if running {
Scanner(color: accent)
}
}
}
.scrollTargetLayout()
.overlay(alignment: .bottomLeading) {
if running && model.key != nil {
Scanner(color: accent)
.offset(y: 8)
}
}
.padding(16)
}
.scrollPosition($position)
@@ -555,7 +559,10 @@ private struct TranscriptScroll: View {
scrollLog.notice("phase \(String(describing: old), privacy: .public) -> \(String(describing: phase), privacy: .public)")
scrolling = phase.isScrolling
}
.onChange(of: model.builder.pending.count) {
.onChange(of: model.builder.pending.last?.id) { old, new in
guard let new, new > old ?? 0 else {
return
}
scrollLog.notice("pending scrollTo bottom")
model.stuck = true
position.scrollTo(edge: .bottom)
@@ -631,6 +638,7 @@ struct DetailsBox<Summary: View, Content: View>: View {
var boxed = true
var nested = false
var working = false
var status: (text: String, color: Color)? = nil
@ViewBuilder let summary: Summary
@ViewBuilder let content: Content
@Environment(\.paneAccent) private var accent
@@ -651,6 +659,12 @@ struct DetailsBox<Summary: View, Content: View>: View {
.padding(.leading, 4)
}
Spacer(minLength: 0)
if let status {
Text(status.text)
.font(Fonts.font(12))
.foregroundStyle(status.color)
.padding(.leading, 12)
}
}
.contentShape(Rectangle())
}
@@ -738,8 +752,8 @@ struct RowView: View {
case .byrePrompt(let brief, let when, let blocks):
DetailsBox(nested: nested) {
Text("byre").font(Fonts.font(12, weight: 600))
Text(brief).font(Fonts.font(12)).foregroundStyle(Palette.muted).lineLimit(1)
Text(styled([("byre", 600, nil), (" ", 400, nil), (brief, 400, Palette.muted)]))
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 12)
WhenText(when: when).font(Fonts.font(12)).foregroundStyle(Palette.muted)
} content: {
@@ -790,13 +804,9 @@ struct RowView: View {
}
case .subagent(let title, let description, let prompt):
DetailsBox(nested: nested, working: model.working(row)) {
DetailsBox(nested: nested, working: model.working(row), status: (row.status, row.statusState == .done ? Palette.muted : row.statusState == .error ? Palette.danger : Palette.accent)) {
SubagentTag(show: row.tag)
Text(styled([(title, 600, nil), (" ", 400, nil), (description, 400, Palette.muted)]))
Spacer(minLength: 12)
Text(row.status)
.font(Fonts.font(12))
.foregroundStyle(row.statusState == .done ? Palette.muted : row.statusState == .error ? Palette.danger : Palette.accent)
} content: {
VStack(alignment: .leading, spacing: 6) {
MarkdownView(blocks: prompt, ctx: md)
@@ -1014,10 +1024,11 @@ private struct SnapshotRow: View {
let taskId: String?
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@Environment(\.paneAccent) private var accent
@ViewState private var forking = false
var body: some View {
let time = snap.createdAt.formatted(Date.FormatStyle().hour(.twoDigits(amPM: .omitted)).minute(.twoDigits).second(.twoDigits))
let time = clockText(snap.createdAt)
HStack(spacing: 8) {
Rectangle().fill(Palette.border).frame(height: 1)
Text("snapshot #\(snap.n)")
@@ -1058,26 +1069,40 @@ private struct SnapshotRow: View {
return Button {
store.showReview(snap.n)
} label: {
HStack(spacing: 2) {
Text("review")
.font(Fonts.font(11, weight: n > 0 ? 700 : 400))
if let review {
switch review.state {
case .running:
Spinner(color: Palette.accent).font(Fonts.font(10)).help("review in progress")
case .failed:
EmptyView()
case .skipped:
Text("●").font(Fonts.font(11, weight: 700)).foregroundStyle(Palette.muted).help("skipped: no changes")
case .done:
if n == 0 {
Text("✓").font(Fonts.font(11, weight: 700)).foregroundStyle(Palette.ok).help(review.findingStates.additionalProperties.isEmpty ? "no findings" : "every finding addressed")
} else {
Badge(count: n).help("\(n) open findings")
Text("review")
.font(Fonts.font(11, weight: n > 0 ? 700 : 400))
.overlay(alignment: .topTrailing) {
if let review {
switch review.state {
case .running:
Spinner(color: accent).font(Fonts.font(10)).help("review in progress")
.fixedSize()
.offset(x: 13, y: -4)
case .failed:
EmptyView()
case .skipped:
Text("●").font(Fonts.font(11, weight: 700)).foregroundStyle(Palette.muted).help("skipped: no changes")
.fixedSize()
.offset(x: 13, y: -7)
case .done:
if n == 0 {
Text("✓").font(Fonts.font(11, weight: 700)).foregroundStyle(Palette.ok).help(review.findingStates.additionalProperties.isEmpty ? "no findings" : "every finding addressed")
.fixedSize()
.offset(x: 13, y: -7)
} else {
Text("\(n)")
.font(Fonts.font(10, weight: 700))
.foregroundStyle(Palette.danger)
.frame(height: 14)
.padding(.horizontal, 5)
.background(Palette.dangerTint, in: Capsule())
.help("\(n) open findings")
.fixedSize()
.offset(x: 15, y: -9)
}
}
}
}
}
}
.buttonStyle(SmallLinkButtonStyle(color: n > 0 ? Palette.warn : Palette.accent))
.help("Show the review of snapshot \(snap.n) in the Reviews tab")