@arach/lattices 0.2.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -69
  3. package/apps/mac/Info.plist +43 -0
  4. package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
  5. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  6. package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  7. package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
  8. package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
  9. package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
  10. package/apps/mac/Lattices.entitlements +21 -0
  11. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  12. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  13. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  14. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  15. package/apps/mac/Resources/tap.wav +0 -0
  16. package/assets/AppIcon.icns +0 -0
  17. package/bin/assistant-intelligence.ts +912 -0
  18. package/bin/cli/capture.ts +252 -0
  19. package/bin/cli/daemon.ts +22 -0
  20. package/bin/cli/helpers.ts +105 -0
  21. package/bin/cli/layer.ts +178 -0
  22. package/bin/cli/runs.ts +43 -0
  23. package/bin/cli/search.ts +141 -0
  24. package/bin/cli/session.ts +32 -0
  25. package/bin/client.ts +17 -0
  26. package/bin/cua.ts +26 -0
  27. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  28. package/bin/handsoff-infer.ts +96 -0
  29. package/bin/handsoff-worker.ts +531 -0
  30. package/bin/infer.ts +424 -0
  31. package/bin/keychain.ts +75 -0
  32. package/bin/lattices-app.ts +655 -0
  33. package/bin/lattices-build +125 -0
  34. package/bin/lattices-build-env.ts +77 -0
  35. package/bin/lattices-dev +362 -0
  36. package/bin/lattices.ts +3260 -0
  37. package/bin/project-twin.ts +645 -0
  38. package/docs/agent-execution-plan.md +562 -0
  39. package/docs/agent-layer-guide.md +207 -0
  40. package/docs/agents.md +233 -0
  41. package/docs/ai-chat-ux-review.md +416 -0
  42. package/docs/api.md +1041 -47
  43. package/docs/app.md +96 -13
  44. package/docs/assistant-knowledge.md +130 -0
  45. package/docs/companion-deck.md +209 -0
  46. package/docs/component-extraction-roadmap.md +392 -0
  47. package/docs/concepts.md +13 -12
  48. package/docs/config.md +83 -10
  49. package/docs/gesture-customization-proposal.md +520 -0
  50. package/docs/handsoff-test-scenarios.md +84 -0
  51. package/docs/hyperspace-grid-snappiness.md +210 -0
  52. package/docs/layers.md +176 -28
  53. package/docs/mouse-gestures.md +244 -0
  54. package/docs/ocr.md +21 -9
  55. package/docs/overview.md +42 -23
  56. package/docs/presentation-execution-review.md +491 -0
  57. package/docs/prompts/hands-off-system.md +382 -0
  58. package/docs/prompts/hands-off-turn.md +30 -0
  59. package/docs/prompts/voice-advisor.md +31 -0
  60. package/docs/prompts/voice-fallback.md +23 -0
  61. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  62. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  63. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  64. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  65. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  66. package/docs/proposals/LAT-006-followup-gaps.md +103 -0
  67. package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
  68. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  69. package/docs/quickstart.md +8 -12
  70. package/docs/reference/dewey.config.ts +74 -0
  71. package/docs/reference/install-agent.md +79 -0
  72. package/docs/release.md +172 -0
  73. package/docs/repo-structure.md +100 -0
  74. package/docs/terminal-kit.md +87 -0
  75. package/docs/tiling-reference.md +224 -0
  76. package/docs/twins.md +138 -0
  77. package/docs/voice-command-protocol.md +278 -0
  78. package/docs/voice-error-model.md +73 -0
  79. package/docs/voice.md +221 -0
  80. package/package.json +69 -16
  81. package/packages/npm/sdk/cua.d.mts +1 -0
  82. package/packages/npm/sdk/cua.d.ts +188 -0
  83. package/packages/npm/sdk/cua.mjs +376 -0
  84. package/app/Lattices.app/Contents/Info.plist +0 -24
  85. package/app/Package.swift +0 -13
  86. package/app/Sources/ActionRow.swift +0 -61
  87. package/app/Sources/App.swift +0 -10
  88. package/app/Sources/AppDelegate.swift +0 -234
  89. package/app/Sources/AppShellView.swift +0 -62
  90. package/app/Sources/AppTypeClassifier.swift +0 -70
  91. package/app/Sources/AppWindowShell.swift +0 -63
  92. package/app/Sources/CheatSheetHUD.swift +0 -332
  93. package/app/Sources/CommandModeState.swift +0 -1362
  94. package/app/Sources/CommandModeView.swift +0 -1405
  95. package/app/Sources/CommandModeWindow.swift +0 -192
  96. package/app/Sources/CommandPaletteView.swift +0 -307
  97. package/app/Sources/CommandPaletteWindow.swift +0 -134
  98. package/app/Sources/DaemonProtocol.swift +0 -101
  99. package/app/Sources/DaemonServer.swift +0 -414
  100. package/app/Sources/DesktopModel.swift +0 -121
  101. package/app/Sources/DesktopModelTypes.swift +0 -71
  102. package/app/Sources/DiagnosticLog.swift +0 -271
  103. package/app/Sources/EventBus.swift +0 -30
  104. package/app/Sources/HotkeyManager.swift +0 -250
  105. package/app/Sources/HotkeyStore.swift +0 -338
  106. package/app/Sources/InventoryManager.swift +0 -35
  107. package/app/Sources/InventoryPath.swift +0 -43
  108. package/app/Sources/KeyRecorderView.swift +0 -210
  109. package/app/Sources/LatticesApi.swift +0 -1125
  110. package/app/Sources/MainView.swift +0 -467
  111. package/app/Sources/MainWindow.swift +0 -83
  112. package/app/Sources/OcrModel.swift +0 -309
  113. package/app/Sources/OcrStore.swift +0 -295
  114. package/app/Sources/OmniSearchState.swift +0 -283
  115. package/app/Sources/OmniSearchView.swift +0 -288
  116. package/app/Sources/OmniSearchWindow.swift +0 -105
  117. package/app/Sources/OrphanRow.swift +0 -129
  118. package/app/Sources/PaletteCommand.swift +0 -419
  119. package/app/Sources/PermissionChecker.swift +0 -125
  120. package/app/Sources/Preferences.swift +0 -92
  121. package/app/Sources/ProcessModel.swift +0 -199
  122. package/app/Sources/ProcessQuery.swift +0 -151
  123. package/app/Sources/Project.swift +0 -28
  124. package/app/Sources/ProjectRow.swift +0 -368
  125. package/app/Sources/ProjectScanner.swift +0 -121
  126. package/app/Sources/ScreenMapState.swift +0 -2387
  127. package/app/Sources/ScreenMapView.swift +0 -2820
  128. package/app/Sources/ScreenMapWindowController.swift +0 -89
  129. package/app/Sources/SessionManager.swift +0 -72
  130. package/app/Sources/SettingsView.swift +0 -1053
  131. package/app/Sources/SettingsWindow.swift +0 -20
  132. package/app/Sources/TabGroupRow.swift +0 -178
  133. package/app/Sources/Terminal.swift +0 -259
  134. package/app/Sources/TerminalQuery.swift +0 -156
  135. package/app/Sources/TerminalSynthesizer.swift +0 -200
  136. package/app/Sources/Theme.swift +0 -163
  137. package/app/Sources/TilePickerView.swift +0 -209
  138. package/app/Sources/TmuxModel.swift +0 -53
  139. package/app/Sources/TmuxQuery.swift +0 -81
  140. package/app/Sources/WindowTiler.swift +0 -1755
  141. package/app/Sources/WorkspaceManager.swift +0 -434
  142. package/bin/lattices-app.js +0 -221
  143. package/bin/lattices.js +0 -1418
@@ -0,0 +1,416 @@
1
+ # AI Chat Assistant — UI/UX Review (review-only)
2
+
3
+ Surface reviewed: `apps/mac/Sources/Core/Pi/`
4
+ (PiChatUI.swift, PiChatDock.swift, PiWorkspaceView.swift, PiAuthPromptCard.swift,
5
+ PiInstallCallout.swift, PiProviderSetupCallout.swift, PiChatSession.swift)
6
+
7
+ Date: 2026-06-02
8
+ Scope: visual hierarchy, spacing, transcript readability, composer ergonomics,
9
+ empty/loading/error states, macOS idioms, accessibility, usefulness vs. opacity.
10
+
11
+ Severity scale: HIGH (blocks goals) · MEDIUM (visible friction) · LOW (polish).
12
+
13
+ ---
14
+
15
+ ## HIGH
16
+
17
+ ### 1. User and assistant share the same avatar glyph → speaker confusion
18
+ **Evidence.** `PiChatUI.swift:35–55` defines `LatticesMark`; `LatticesMarkAvatar`
19
+ is reused for the assistant header (`PiWorkspaceView.swift:52`), the assistant
20
+ bubble (`PiChatUI.swift:487`), and the user's bubble (`PiChatUI.swift:463`).
21
+ The user message row shows the Lattices brand mark on the right at 28pt with a
22
+ muted tint — visually the same family as the assistant avatar. Header text
23
+ literally reads "Assistant" on the assistant side, but the user side has no
24
+ label. On a quick glance, you cannot tell which side is talking.
25
+
26
+ **Direction.** Drop the avatar from the user bubble (right-aligned text + small
27
+ initial chip is enough), or use a clearly different glyph (e.g. `person.crop.circle`)
28
+ at lower opacity. Keep `LatticesMarkAvatar` for assistant + header + empty state
29
+ hero only — that is its semantic role.
30
+
31
+ ### 2. Composer send key is wrong for multi-line input
32
+ **Evidence.** `PiChatUI.swift:1246–1261` binds `.onSubmit` directly to
33
+ `session.sendDraft()` with no modifier check. The composer uses
34
+ `TextField(axis: .vertical)` with `lineLimit(1...style.composerLineLimit)`.
35
+ The footer hint reads "↩ send" (`PiChatUI.swift:1218`). There is **no
36
+ `keyboardShortcut`** anywhere in the chat surface (verified by grep across
37
+ `Core/Pi/*.swift`).
38
+
39
+ **Direction.** Bind the send button to `.keyboardShortcut(.return, modifiers: .command)`,
40
+ change the hint to "⌘↩ send", and gate plain-Enter to single-line case only.
41
+ Add `Esc` to clear the draft or cancel an in-flight send.
42
+
43
+ ### 3. Accessibility is largely absent
44
+ **Evidence.** Across the entire chat surface, there is exactly **one**
45
+ `accessibilityLabel` (`PiChatUI.swift:1397` on `PiChatWorkingIndicator`) and
46
+ **one** `accessibilityHidden(true)` (on the Lattices mark). No icon buttons
47
+ have labels: the dock close (`PiChatDock.swift:103`), the gear in
48
+ `PiWorkspaceView.swift:130`, the code-block copy button (`PiChatUI.swift:872`),
49
+ and the footer gear (`PiChatDock.swift:386`) all read as "button" only to
50
+ VoiceOver. Hit targets: the 6pt traffic-light dots in `PiChatCodeBlock`
51
+ (`PiChatUI.swift:855–859`), the 6pt status pulses, the 22×22 footer icon
52
+ button (`PiChatDock.swift:444`), and the 22×22 send button are all below the
53
+ 24pt minimum. Dynamic Type is not supported — fonts are hardcoded point
54
+ sizes via `Typo.body(13.5)`, `Typo.body(14)` (`PiChatStyle`).
55
+
56
+ **Direction.** Add `accessibilityLabel` to every icon-only button. Group the
57
+ empty-state starters under a labelled header (`.accessibilityElement(children: .combine)`).
58
+ Bump minimum hit target to 24×24. Replace hardcoded point sizes with
59
+ `Font.system(.body, design: .rounded)` and apply
60
+ `.dynamicTypeSize(.medium ... .accessibility3)` at the root.
61
+
62
+ ### 4. Usefulness is opaque — header copy is vague, no capability surface
63
+ **Evidence.** `PiWorkspaceView.swift:60` says "Settings, layout help, planning,
64
+ and debugging in one thread." The empty state at `PiChatUI.swift:256–263`
65
+ shows four starter cards, which is good, but the running state offers no
66
+ indication of: which model is active, which tools are available, rate limits,
67
+ cost, or how to attach files/screenshots. The status pill (`PiWorkspaceView.swift:75`)
68
+ flips between "Ready / Streaming / Thinking / Tool · read" but the user cannot
69
+ act on it. The four starter prompts hint at gesture/file/screen/planning but
70
+ don't reveal tool inventory.
71
+
72
+ **Direction.** Add a discoverable affordance — a `?` or `Tools` button in the
73
+ header — that opens a one-sheet listing: current provider + model, available
74
+ tools (`read`, `bash`, `search`, `list`, `web`, `voice`), and a token/cost
75
+ counter. Add a "What can I do?" prompt to the empty state as a 5th card.
76
+
77
+ ---
78
+
79
+ ## MEDIUM
80
+
81
+ ### 5. Three parallel setup states compete visually
82
+ **Evidence.** `PiInstallCallout.swift` (install) uses `Palette.kill` red border.
83
+ `PiProviderSetupCallout.swift` (provider) uses its own card layout. `PiAuthPromptCard.swift`
84
+ (auth) uses `Palette.detach` yellow. All three render in roughly the same slot
85
+ (transcript area or composer slot) but with different border treatments,
86
+ padding, and copy tone. The user sees: red install card → yellow provider card
87
+ → yellow auth card → composer. Three different visual languages for "you need
88
+ to do something."
89
+
90
+ **Direction.** Consolidate into a single `PiSetupCard` with `Kind: .install | .provider | .auth`
91
+ and a shared layout: status dot · eyebrow label · body · primary action · secondary
92
+ action. Use `Palette.kill` for install, `Palette.detach` for both provider and
93
+ auth, and unify spacing/padding to `compact` and `expanded` modes only.
94
+
95
+ ### 6. Streaming state is over-decorated
96
+ **Evidence.** `PiChatUI.swift:498–541` (assistant bubble) stacks: pulsing
97
+ avatar (1.18× stroke), `LIVE` badge, `PiChatStreamCursor`, left-edge 2pt
98
+ gradient bar, inner radial gradient overlay, and a 0.28s background
99
+ animation. Concurrently: `PiChatToolChip` pulses (1.4s),
100
+ `PiChatStreamCursor` pulses (0.85s), `PiChatWaveDots` animates
101
+ (30fps TimelineView), `PiChatWorkingIndicator` has its own dot pattern.
102
+ That's 4+ simultaneous animations per streaming turn. On a 60Hz display the
103
+ bubble never settles.
104
+
105
+ **Direction.** Pick one dominant streaming affordance — the left-edge bar is
106
+ the strongest. Demote `LIVE` to small caps next to the assistant name and
107
+ drop the inner radial gradient. Keep the cursor and one pulse. Document
108
+ "max 1 ambient animation per scene" in the chat style guide.
109
+
110
+ ### 7. Provider/auth flow triplicated across Dock, Workspace, and auth card
111
+ **Evidence.** `PiChatDock.swift:205–325` (authPanel) and `PiChatDock.swift:429–458`
112
+ (providerSettingsBar) and `PiWorkspaceView.swift:125–158` (providerSettingsPrompt)
113
+ and `PiAuthPromptCard.swift` are four overlapping implementations of "tell
114
+ the user to connect a provider / drive the auth flow." Adding a field
115
+ (provider description, secondary CTA, warning) requires touching all four.
116
+
117
+ **Direction.** Extract a `PiAuthPanel` with `style: .workspace | .dock` and
118
+ the existing `compact: Bool` flag, used in all three call sites. Keep
119
+ `PiAuthPromptCard` as a child when a `pendingAuthPrompt` is present.
120
+
121
+ ### 8. Transcript background reduces text contrast at the bottom
122
+ **Evidence.** `PiChatUI.swift:148–172` layers four backgrounds: base
123
+ `Palette.bg`, dot grid with `+Lighter` blend, top-left linear tint, and a
124
+ bottom-left radial gradient. The radial hits the assistant bubble's bottom
125
+ half — the area where long responses accumulate. On a low-contrast theme
126
+ this pushes the secondary text into the background.
127
+
128
+ **Direction.** Cap the radial opacity at 0.03, or move it to the top header
129
+ zone. Drop the dot grid in compact mode. Test on a real Mac with
130
+ `colorScheme: .light` to ensure dark surfaces don't bleed through.
131
+
132
+ ---
133
+
134
+ ## LOW
135
+
136
+ ### 9. Code block copy has no feedback
137
+ **Evidence.** `PiChatUI.swift:872–885` writes to `NSPasteboard` silently. No
138
+ icon swap, no toast, no animation. The button looks identical before/after.
139
+
140
+ **Direction.** Add `@State private var copied = false`, swap the icon to
141
+ `"checkmark"` for 1.2s, reset on a `Task.sleep`.
142
+
143
+ ### 10. Dock resize gesture does not follow macOS conventions
144
+ **Evidence.** `PiChatDock.swift:85–100` uses a custom `DragGesture` on the
145
+ top handle. No cursor change, no snap-to-default, no visual ruler. macOS
146
+ users expect a divider with `NSCursor.resizeUpDown` and snap points
147
+ (230/400/600).
148
+
149
+ **Direction.** Replace the top-handle drag with a 6pt divider strip below
150
+ the header, hover-swaps cursor to resize, snap to 230/400/600 with the
151
+ existing defaults key.
152
+
153
+ ### 11. No light-mode support
154
+ **Evidence.** `Palette.bg` and friends are dark-only. The chat surface is
155
+ embedded in a menu bar app that follows system appearance, so a user in
156
+ Light Mode gets dark-on-dark chat inside a light chrome — jarring.
157
+
158
+ **Direction.** Introduce semantic tokens (`Palette.chatBg`, `Palette.chatText`,
159
+ `Palette.chatBorder`) that switch on `colorScheme`. Add a `colorScheme` env
160
+ value at the chat root.
161
+
162
+ ### 12. ScrollView fires multiple `scrollToEnd` per token
163
+ **Evidence.** `PiChatUI.swift:181–195` registers three `onChange` handlers
164
+ (messages.count, last text, isSending). During a streaming response the
165
+ text handler fires per token, producing janky scroll on slower Macs and
166
+ fighting the user's manual scroll position if they scrolled up to read
167
+ history.
168
+
169
+ **Direction.** Coalesce into a single `lastMessageID` + `lastCharCount` change,
170
+ debounce to ~50ms, and pause auto-scroll when the user is scrolled up by
171
+ more than 80pt from the bottom (and show a "↓ New messages" pill — common
172
+ chat pattern).
173
+
174
+ ### 13. `PiChatFormat.markdownText` is not loaded here but the empty state
175
+ injects a string for `Connected to \(session.currentProvider.name)` — the
176
+ provider name should also drive a per-provider brand tint on the empty
177
+ state hero, not just the streaming accent.
178
+ **Evidence.** `PiChatUI.swift:332–336`. Empty state uses `Palette.running`
179
+ unconditionally.
180
+
181
+ **Direction.** Pass `theme: PiChatTheme` from the session so the empty state,
182
+ composer accent, and live chip all shift subtly per provider.
183
+
184
+ ---
185
+
186
+ ## What is already good
187
+
188
+ - Empty state starter grid is well-scoped and inviting
189
+ (`PiChatUI.swift:256–305`).
190
+ - Code block chrome with traffic-light header and copy button is a nice
191
+ macOS-y detail (`PiChatUI.swift:835–895`).
192
+ - Custom hand-rolled syntax highlighter covers `swift`, `json`, `bash`, and
193
+ generic with consistent palette tokens (`PiChatUI.swift:900–1190`).
194
+ - Auth prompt card is calm and uses mono consistently
195
+ (`PiAuthPromptCard.swift:1–90`).
196
+ - Status pill is a thoughtful, dense read of session state
197
+ (`PiWorkspaceView.swift:75–105`).
198
+ - Footer `↩ send` hint shows attention to discoverability
199
+ (`PiChatUI.swift:1218`).
200
+ - All four top-level surfaces (PiChatDock, PiWorkspaceView, PiChatTranscript,
201
+ PiChatComposer) share typography and palette tokens, so the language is
202
+ consistent — the issues are about depth, not vocabulary.
203
+
204
+ ---
205
+
206
+ ## Suggested first pass (1–2 days of work)
207
+
208
+ 1. **Composer shortcuts** — Cmd+Return, Esc to cancel, hint fix (HIGH #2).
209
+ 2. **Avatar disambiguation** — drop avatar from user side or swap glyph (HIGH #1).
210
+ 3. **a11y sweep on icon buttons + Dynamic Type pass** (HIGH #3).
211
+ 4. **Single setup card component** with three kinds (MEDIUM #5).
212
+ 5. **One-sheet "what this assistant can do"** triggered from the header (HIGH #4).
213
+
214
+ These five changes will resolve all four HIGH findings and the most visible
215
+ MEDIUM, and they cluster naturally because they all touch `PiChatUI.swift`
216
+ + the header/composer chrome.
217
+
218
+ ---
219
+
220
+ # Second pass — implementation-oriented
221
+
222
+ ## 1. What I would fix first and why
223
+
224
+ In a single PR, in this order:
225
+
226
+ 1. **Composer send-key correctness** (`PiChatUI.swift:1246–1261`).
227
+ Highest leverage: one well-placed `.keyboardShortcut(.return, modifiers: .command)`,
228
+ one `.onExitCommand { session.draft = "" }` (or `cancelSend`),
229
+ and the footer hint text changed from "↩ send" to "⌘↩ send · esc clear".
230
+ No new components. No state model changes. Removes the single most
231
+ surprising behavior for anyone who has used any other chat app.
232
+ The `composerLineLimit: 4` (dock) / `8` (workspace) in `PiChatStyle`
233
+ confirms the field is multi-line by design — `TextField.onSubmit` on
234
+ plain Return is unambiguously wrong here.
235
+
236
+ 2. **Drop the user-bubble avatar** (`PiChatUI.swift:463`).
237
+ Five lines of code removed, one line of bubble padding adjusted.
238
+ Immediately disambiguates speaker identity without introducing a new
239
+ component. Cheap A/B candidate: leave a tiny initial circle if you want
240
+ something on the right, but the brand mark has to go.
241
+
242
+ 3. **Accessibility sweep on icon buttons** (`PiChatDock.swift:103`,
243
+ `PiWorkspaceView.swift:130`, `PiChatUI.swift:872`, `PiChatDock.swift:386`).
244
+ Mechanical: add `accessibilityLabel("Close chat", "Open assistant settings",
245
+ "Copy code", "Settings")` to four buttons. Bump `footerIconButton`'s
246
+ hit area from 24×22 to 28×24. Highest a11y ROI in the file.
247
+
248
+ 4. **Add a `?` tools sheet** in the header, starting with just a hardcoded
249
+ provider/model list. Stub it with a `Text` of the current provider name,
250
+ a one-paragraph capability blurb, and a list of the seven tool names
251
+ already mapped in `PiChatToolChip` (`PiChatUI.swift:600–650`). Becomes
252
+ the natural home for cost/token telemetry later.
253
+
254
+ This order: (1) and (2) unblock anyone trying to actually use the surface.
255
+ (3) is a no-regret pass. (4) opens the door to making the assistant feel
256
+ less opaque, which is the largest gap the first pass identified.
257
+
258
+ ## 2. What I would deliberately defer
259
+
260
+ - **Streaming decoration cleanup (MEDIUM #6 in the first pass).**
261
+ Hard to argue from a static review. The animations are individually small
262
+ (1.4s pulse, 0.85s cursor, 0.28s background ease). On a 60Hz display the
263
+ perceived "busyness" is uncertain. Defer until someone watches a real
264
+ streaming response on a real Mac and reports it feels busy. Don't pre-emptively
265
+ cut signals that may carry useful liveness.
266
+
267
+ - **Full Dynamic Type pass.** Real, but big. `PiChatStyle.bodySize` is
268
+ referenced in ~6 sites; the right call is probably to introduce
269
+ `@ScaledMetric(relativeTo: .body) private var bodySize: CGFloat = 13.5`
270
+ and let the system drive it. But until there's a user report, ship (3)
271
+ first and come back to this.
272
+
273
+ - **Light-mode palette tokens.** Real gap (`Theme.swift` is dark-only,
274
+ zero `colorScheme` references in the chat surface), but it's a
275
+ cross-cutting theme change, not a chat-surface change. Belongs in a
276
+ separate "Palette semantic tokens" pass.
277
+
278
+ - **Auth panel consolidation (MEDIUM #7).** Worth doing, but only if you
279
+ are actually changing the auth flow soon. If not, leaving three
280
+ implementations is cheaper than risking a regression in a critical
281
+ onboarding path.
282
+
283
+ - **`PiSetupCard` unification (MEDIUM #5).** Same reasoning — defer until
284
+ you are touching setup copy for product reasons.
285
+
286
+ - **Per-provider brand tint.** Speculative. Lattices has a clear visual
287
+ identity; tinting the chat to match each provider dilutes it. Wait for
288
+ evidence that users want this.
289
+
290
+ ## 3. Where the first pass overreached or missed evidence
291
+
292
+ - **"Streaming is over-decorated"** — I counted animations without testing.
293
+ I should have said "needs a live walkthrough" rather than prescribing a
294
+ fix. The recommendation to "pick one dominant affordance" is a stylistic
295
+ call, not a clear win.
296
+
297
+ - **"Three `onChange` handlers fight on streaming scroll"** — overclaim.
298
+ SwiftUI's `ScrollViewReader` + animation handles per-token `scrollTo`
299
+ well in practice. The real problem is the absence of a "jump to latest"
300
+ pill when the user has scrolled up — not the handler count. Soften.
301
+
302
+ - **"Light-mode support"** — verified this round: `Theme.swift` has zero
303
+ `colorScheme` references and the chat hardcodes `Palette.bg`. The
304
+ finding is correct, the scope is bigger than I implied. It's a
305
+ project-wide token migration, not a chat-surface change.
306
+
307
+ - **"Three parallel auth/setup states compete visually"** — partially
308
+ overreach. `PiChatDock` and `PiWorkspaceView` are intentionally two
309
+ different products (compact bottom drawer vs. full pane). `PiChatStyle`
310
+ enforces that with `composerLineLimit: 4` vs. `8`, `bodySize: 12` vs.
311
+ `13.5`, `horizontalPadding: 12` vs. `28`. Some of the "triplication"
312
+ is appropriate. The auth flow within each is duplicated, not the
313
+ surfaces themselves. Restate: deduplicate the **auth flow logic**
314
+ (one component, two style modes), keep two surface layouts.
315
+
316
+ - **Missing in the first pass:** I never opened `UI/Theme.swift` until
317
+ this follow-up. The Dynamic Type claim was based on hardcoded `Typo`
318
+ calls in `PiChatStyle`, which is still true, but I should have flagged
319
+ the file once for the whole surface, not enumerated sites.
320
+
321
+ - **Missing in the first pass:** I didn't look at the dock resize gesture
322
+ UX holistically. A `DragGesture(minimumDistance: 1)` on the top handle
323
+ is unusual but not broken — many native macOS apps do this. Lower its
324
+ priority from LOW to "watch the resize behavior on a trackpad; only
325
+ rework if it feels wrong."
326
+
327
+ - **Missing in the first pass:** I didn't note that the empty-state
328
+ starter cards auto-send on click (`PiChatUI.swift:268–275`). That is a
329
+ real ergonomic call: should the click fill the composer (so the user
330
+ can edit) or auto-send? Currently it auto-sends. Worth a single
331
+ decision: fill-only, with a separate "send" affordance, matches the
332
+ user's mental model in most chat products. Not in the first PR — but
333
+ decide it before (4) ships the tools sheet.
334
+
335
+ ## 4. Smallest coherent implementation slice
336
+
337
+ **One PR, one developer, half a day. Files touched: `PiChatUI.swift` only.**
338
+
339
+ ```swift
340
+ // In PiChatComposer (PiChatUI.swift ~1246)
341
+ Button {
342
+ session.sendDraft()
343
+ } label: {
344
+ // ...existing send button body...
345
+ }
346
+ .buttonStyle(.plain)
347
+ .keyboardShortcut(.return, modifiers: .command) // NEW
348
+ .disabled(!canSend)
349
+
350
+ // On the TextField, replace .onSubmit with .onSubmit-of-single-line:
351
+ TextField(
352
+ style.placeholder,
353
+ text: $session.draft,
354
+ axis: .vertical
355
+ )
356
+ .textFieldStyle(.plain)
357
+ .font(Typo.body(style.composerSize))
358
+ .foregroundColor(Palette.text)
359
+ .lineLimit(1...style.composerLineLimit)
360
+ .focused(focus)
361
+ .onSubmit { /* no-op for multi-line; Cmd+Return handles send */ }
362
+ .onExitCommand { session.draft = "" } // NEW
363
+
364
+ // Footer hint (PiChatUI.swift ~1218):
365
+ Text("⌘↩ send · esc clear")
366
+ ```
367
+
368
+ **Plus one PR, separate reviewer, half a day: drop the user-bubble avatar.**
369
+
370
+ ```swift
371
+ // In userRow (PiChatUI.swift ~432):
372
+ // Remove the trailing LatticesMarkAvatar(size: 28, ...)
373
+ // Add a 4pt right padding to the user bubble for visual breathing.
374
+ ```
375
+
376
+ Total: ~25 lines changed, zero new components, zero state-model changes,
377
+ zero migrations. Both changes are independently revertable. The first
378
+ lands a measurable behavior fix (the wrong key sent prematurely), the
379
+ second removes a known visual confusion.
380
+
381
+ This is what a developer should land before any of the larger
382
+ consolidations. It addresses the two HIGH findings where I have the most
383
+ confidence and unblocks real testing of the surface by anyone who tries
384
+ to send a multi-line message.
385
+
386
+ ## 5. Observable session / provenance details from this run
387
+
388
+ - **Identity.** Agent: `lattices-review-pi-scout.main.arts-mac-mini-local`.
389
+ Invoked as a stable OpenScout relay agent on this turn.
390
+ - **Model.** `MiniMax-M3` (per harness system prompt; "M3" is the model
391
+ class, not necessarily a per-request model identifier).
392
+ - **Provider / transport.** The harness is MiniMax's own inference stack;
393
+ I have no per-request model name, no temperature, no token budget, no
394
+ response-time telemetry, no streaming chunk count, no tool-use log.
395
+ The wire protocol I am reachable over is the OpenScout broker
396
+ (WebSocket); conversation `c.baf8732a-9752-487a-841a-57595463bf8d`,
397
+ this message `msg-mpwvwdq3-z1nuja`, reply path `final_response`.
398
+ - **Runtime mode.** `thinking` enabled; `max thinking effort` is implicit
399
+ (the harness emits long deliberation blocks before each reply). No
400
+ explicit budget is exposed to me. I cannot report token counts for
401
+ this turn.
402
+ - **Session state.** `cwd: /Users/art/dev/lattices`; `git status` shows
403
+ one staged file: `A docs/ai-chat-ux-review.md` (the first-pass report,
404
+ mtime 12:17 today). `HEAD: b09e4d8` "Merge pull request #41 from
405
+ arach/codex/inventory-sort-arrange". No uncommitted source changes.
406
+ - **Tools available to me this turn.** `bash`, `read`, `edit`, `write`,
407
+ `intercom`, `mcp`, `scout_{send,ask,who}`, `subagent`. I used
408
+ `read`, `bash`, and `edit` for this turn — the `edit` was on my own
409
+ review file, not on any source.
410
+ - **Status telemetry available.** None beyond the OpenScout message
411
+ metadata above. No live provider status, no tool-use trail, no
412
+ thinking-trace payload on the wire. The thinking block is a local
413
+ harness detail, not a broker-visible artifact.
414
+ - **What I cannot report.** I do not have visibility into MiniMax
415
+ upstream latency, model selection, rate limits, or the exact provider
416
+ routing path used for this M3 inference. I would not invent those.