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
+60 -33
View File
@@ -88,6 +88,7 @@ private final class CardModel {
let context: [MDBlock]
let firstChoice: [String: Int]
var busy = false
@ObservationIgnored var shown: Set<String> = []
init(_ item: InboxItem, _ store: Store, _ overlays: Overlays) {
self.item = item
@@ -144,6 +145,17 @@ private final class CardModel {
store.drafts?.setCheckedAnswer(item.id, options.filter { now.contains($0) })
}
func pick(_ text: String) {
if busy {
return
}
if multiple {
toggle(text, !checked.contains(text))
return
}
submit(InboxResponseCreate(itemId: item.id, _type: .option, options: [text]))
}
func submit(_ response: InboxResponseCreate, withImages: Bool = false) {
if busy {
return
@@ -225,7 +237,7 @@ private struct InboxCard: View {
}
private func move(_ model: CardModel, _ by: Int) -> KeyPress.Result {
var stops = model.options
var stops = model.firstChoice.filter { model.shown.contains($0.key) }.sorted { $0.value < $1.value }.map(\.key)
if !model.editorOpen {
stops.append(otherStop)
}
@@ -240,6 +252,13 @@ private struct InboxCard: View {
private struct CardBody: View {
let model: CardModel
var focus: FocusState<String?>.Binding
@ViewState private var wide = true
private var contextCtx: MDContext {
MDContext(taskId: model.item.taskId, choice: { md, first in
AnyView(ChoiceBox(model: model, item: md, first: first, ctx: self.contextCtx, focus: focus))
}, gridColumns: wide ? 4 : 2)
}
var body: some View {
let p = model.payload
@@ -247,10 +266,9 @@ private struct CardBody: View {
VStack(alignment: .leading, spacing: 0) {
switch item.kind {
case .question:
MarkdownView(blocks: model.context, ctx: MDContext(taskId: item.taskId, choice: { md in
AnyView(ChoiceBox(model: model, item: md, focus: focus))
}))
.padding(.vertical, 10)
MarkdownView(blocks: model.context, ctx: contextCtx)
.onGeometryChange(for: Bool.self) { $0.size.width > 480 } action: { wide = $0 }
.padding(.vertical, 10)
MarkdownView(blocks: parseMarkdown(jsString(p["question"] ?? "")), ctx: MDContext(taskId: item.taskId))
.padding(.top, 10)
.padding(.bottom, 4)
@@ -308,6 +326,8 @@ private struct WhyLine: View {
private struct ChoiceBox: View {
let model: CardModel
let item: MDItem
let first: Bool
let ctx: MDContext
var focus: FocusState<String?>.Binding
@Environment(Store.self) private var store
@ViewState private var hover = false
@@ -321,7 +341,7 @@ private struct ChoiceBox: View {
Text("•")
VStack(alignment: .leading, spacing: 6) {
InlineView(inlines: parseInline(item.raw), ctx: MDContext(taskId: model.item.taskId))
ItemBody(item: item, ctx: MDContext(taskId: model.item.taskId))
ItemBody(item: item, ctx: ctx)
}
}
}
@@ -333,6 +353,7 @@ private struct ChoiceBox: View {
let lit = (hover && !model.multiple) || focused
let hasDetail = !item.inline.isEmpty || !item.blocks.isEmpty
let on = model.checked.contains(text)
let keys: Set<KeyEquivalent> = model.multiple ? [.space] : [.return, .space]
return VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
if model.multiple {
@@ -342,29 +363,42 @@ private struct ChoiceBox: View {
.tint(Palette.accent)
.disabled(model.busy)
}
InlineView(inlines: parseInline(text), ctx: MDContext(taskId: model.item.taskId))
.font(Fonts.font(13, weight: 600))
InlineView(inlines: parseInline(text), ctx: MDContext(taskId: model.item.taskId, weight: 600))
.frame(maxWidth: .infinity, alignment: .leading)
.allowsHitTesting(false)
if rec {
Text("recommended").font(Fonts.font(11)).foregroundStyle(Palette.ok)
Text("recommended")
.font(Fonts.font(11))
.foregroundStyle(Palette.ok)
.allowsHitTesting(false)
}
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(lit ? Palette.accentTint : rec ? Palette.ok.opacity(0.12) : Palette.surface)
.background {
(lit ? Palette.accentTint : rec ? Palette.ok.opacity(0.12) : Palette.surface)
.onTapGesture {
model.pick(text)
}
}
if hasDetail {
ItemBody(item: MDItem(inline: item.inline, blocks: item.blocks), ctx: MDContext(taskId: model.item.taskId))
ItemBody(item: MDItem(inline: item.inline, blocks: item.blocks), ctx: ctx)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.allowsHitTesting(false)
.overlay(alignment: .top) {
Rectangle().fill(Palette.border).frame(height: 1)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(Palette.raised)
.background {
Palette.raised
.onTapGesture {
model.pick(text)
}
}
.environment(\.choiceTap, { [model] in
model.pick(text)
})
.clipShape(RoundedRectangle(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).stroke(lit ? Palette.accent : rec ? Palette.ok.opacity(0.55) : Palette.border))
.overlay {
@@ -377,26 +411,18 @@ private struct ChoiceBox: View {
.focusable(!model.busy)
.focusEffectDisabled()
.focused(focus, equals: text)
.onTapGesture {
pick(text)
}
.onKeyPress(keys: [.return, .space]) { _ in
pick(text)
.onKeyPress(keys: keys) { _ in
model.pick(text)
return .handled
}
.onHover { hover = $0 }
.padding(.top, 8)
}
private func pick(_ text: String) {
if model.busy {
return
.onAppear {
model.shown.insert(text)
}
if model.multiple {
model.toggle(text, !model.checked.contains(text))
return
.onDisappear {
model.shown.remove(text)
}
model.submit(InboxResponseCreate(itemId: model.item.id, _type: .option, options: [text]))
.padding(.top, first ? 0 : 6)
}
}
@@ -613,9 +639,7 @@ private struct Attachments: View {
}
private func code(_ s: String) -> Text {
var a = AttributedString(s)
a.backgroundColor = Palette.surface
return Text(a)
Text(verbatim: s)
}
@ViewBuilder
@@ -628,15 +652,18 @@ private struct Attachments: View {
Text("diff \(code(a.ref ?? ""))").textSelection(.enabled)
case .image:
let src = (a.image?.isEmpty == false ? a.image : a.path) ?? ""
let caption = a.caption ?? ""
if let source = worktreeImageSource(src, taskId) {
ByreImage(source: source, src: src)
} else if !src.isEmpty {
InlineView(inlines: [.link(href: src, [.text(caption.isEmpty ? src : caption)])], ctx: MDContext(taskId: taskId))
}
case .diagram:
DiagramView(source: a.mermaid ?? "")
case .task:
let id = a.taskId ?? ""
Button("task \(id)") {
store.focusTask(id)
store.route("#/task/\(id)")
}
.buttonStyle(.plain)
.foregroundStyle(store.taskColor(id) ?? Palette.accent)