Skip to content

Commit a0a2b76

Browse files
puritysbclaude
andcommitted
fix(preview): port the subagent and voice surfaces into the device mirrors
CI's mirror-sync gate caught three stale pins: the subagent work changed the e-ink card and the LED matrix page, and host push-to-talk changed the D200H deck, but the hand-maintained Apple previews still drew the older surfaces. The gate exists precisely so a pin bump alone is not the fix — the visual change has to be ported first, so it is. - InkDeck cards reserve the right strip and draw the static six-dot orbit with the child count, matching the firmware's fixed ring table. Static by design: e-ink pays for animation in ghosting. - The TC001 AGENTS page draws up to three cyan perimeter satellites per glyph, each with the dim wire pixel pulled halfway to the glyph centre — including the firmware's integer truncation, so the wire lands on the same LED. - The D200H detail view gains the VOICE tile with all four capture states; `transcribing` is inert there too, so a second press cannot restart capture mid-transcription. Live-follow draws no satellites: the counts live in TimelineStore, not in the DashboardState the preview snapshot is built from. Rather than invent a number, the synthetic palette carries children on one session so the element is actually exercised in the gallery — a preview that never renders an element cannot show that it drifted. Also regenerates the committed xcodeproj, which had gone stale against project.yml: SessionWeightRules.generated.swift landed with the --weight work but was never added, so the macOS target did not build from a fresh checkout. CI never noticed because it runs xcodegen itself. Verified with xcodebuild on the macOS scheme (BUILD SUCCEEDED), not only the sync gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f0f9842 commit a0a2b76

5 files changed

Lines changed: 168 additions & 11 deletions

File tree

apple/AgentDeck.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@
271271
AFF81F64527A02F26615AB61 /* TopologyRailHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D05CE505E1EC5729C69752E /* TopologyRailHelpersTests.swift */; };
272272
B13C4855773086AA7115A5D4 /* UsageAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBD731AC01994F5C7C1A78A /* UsageAPIClient.swift */; };
273273
B1B90826D46626CD1211ED7A /* TerrariumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69BCA057209786EF62993762 /* TerrariumView.swift */; };
274+
B1C62D9C3341BB901AA27D26 /* SessionWeightRules.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03941C6DD60609AB9D1E12C1 /* SessionWeightRules.generated.swift */; };
274275
B26650019DB2ED1B70E0B349 /* ApmeJudgeMlx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EB18086FC44B69D1CBE171 /* ApmeJudgeMlx.swift */; };
275276
B3A9C1B59F3239A9C482540F /* ApmeTaskBoundaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54B6A5B14E7FEFC4CD93EF2 /* ApmeTaskBoundaryTests.swift */; };
276277
B5479F2C2CBAE673EB1C3D93 /* AgentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15F55435772A15B8F0E92C1 /* AgentState.swift */; };
@@ -319,6 +320,7 @@
319320
CF27ABB66E4B268C31C44E6E /* IntegrationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B7BAB1080F7958F74A1B74 /* IntegrationsView.swift */; };
320321
CFCABF9422E74B8CF2A23C7E /* CodexOtelParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE1D983995443760EDE3A4A /* CodexOtelParserTests.swift */; };
321322
D036600F5679C1A191132F87 /* SingletonGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5C866AF54937CD8E433F49 /* SingletonGuard.swift */; };
323+
D0668EACC4D50BE30B10ACE5 /* SessionPushRegisterMergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73BB88D8C2921CB4B93EC8B9 /* SessionPushRegisterMergeTests.swift */; };
322324
D0A70501108B295ED44C169E /* PortFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6165A6BFF174139F6AF07CED /* PortFormatting.swift */; };
323325
D0FB0E0FFBBCE44786BA9B51 /* DaemonPortFallbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9A38D1BD38841DF83F3017 /* DaemonPortFallbackTests.swift */; };
324326
D14972CEAF0D69E6918C86BA /* AgentDeckPaths.swift in Sources */ = {isa = PBXBuildFile; fileRef = 056D70153C3F720A6D935C9F /* AgentDeckPaths.swift */; };
@@ -335,6 +337,7 @@
335337
D80DDBEF3EDB811EFF6B834A /* DaemonService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BD296835DE366BFDBB15B9 /* DaemonService.swift */; };
336338
D863C121DC06035A0007914B /* WaterSurface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F134F38758589B5D47652C8 /* WaterSurface.swift */; };
337339
D8EC7333D6CEB08ED84E4BC2 /* ProviderRailEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BBA59C83638DB2F6B90C44A /* ProviderRailEvaluatorTests.swift */; };
340+
D98FDB40FEA236A6370DE45E /* SessionPushRegisterMergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73BB88D8C2921CB4B93EC8B9 /* SessionPushRegisterMergeTests.swift */; };
338341
DAD883DE12AFEE958A79595A /* HookInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E0B3C7EBBFFD8C2EB08F11 /* HookInstaller.swift */; };
339342
DADAD3815A22603118F3EFBA /* ApmeExternalCloseSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F7DA99E490C72ED75FF711 /* ApmeExternalCloseSelectionTests.swift */; };
340343
DB2C3BF8D9DC234A8CBB1D39 /* FormatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2994478DEFAA4A1CC8EC54D /* FormatUtils.swift */; };
@@ -353,6 +356,7 @@
353356
E082EDBFACADF5992BE8A9E0 /* AgentDeckApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D587189DB0E504028BE4BEF /* AgentDeckApp.swift */; };
354357
E1027789C4EBBF134CA8D464 /* TerrariumCrayfishPresenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E75D955A31F999F568B7BED /* TerrariumCrayfishPresenceTests.swift */; };
355358
E22E6C703C183F1E8C36E7D9 /* SquatterCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B120CABC9029C801CF0A77A2 /* SquatterCleaner.swift */; };
359+
E29B9F7B467F17A2B4FB65EE /* SessionWeightRules.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03941C6DD60609AB9D1E12C1 /* SessionWeightRules.generated.swift */; };
356360
E352DD774E32BE1E365EBF90 /* MenuBarTopologyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509C0F3555343E9DF1AE1964 /* MenuBarTopologyList.swift */; };
357361
E35D8D73F3FFFCD795B1236A /* LocalSessionDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FD383032B3C47C9B547426 /* LocalSessionDiscovery.swift */; };
358362
E46B6EAACB14A6B0B5BC2C12 /* EinkEsp32Previews.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B2045E80851C81C327DFBC /* EinkEsp32Previews.swift */; };
@@ -422,6 +426,7 @@
422426
/* Begin PBXFileReference section */
423427
01B7BAB1080F7958F74A1B74 /* IntegrationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationsView.swift; sourceTree = "<group>"; };
424428
0392D39F45022F762AFE6BE0 /* CodexConfigInstallerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexConfigInstallerTests.swift; sourceTree = "<group>"; };
429+
03941C6DD60609AB9D1E12C1 /* SessionWeightRules.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionWeightRules.generated.swift; sourceTree = "<group>"; };
425430
03F618D3253FD55D4DD7D5AF /* SerialModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialModule.swift; sourceTree = "<group>"; };
426431
056D70153C3F720A6D935C9F /* AgentDeckPaths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentDeckPaths.swift; sourceTree = "<group>"; };
427432
05A9A073192C93C1277AE490 /* Creature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Creature.swift; sourceTree = "<group>"; };
@@ -519,6 +524,7 @@
519524
70F61897614FBE52B61AB8B4 /* MiniToml.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniToml.swift; sourceTree = "<group>"; };
520525
730925E1194B8183122443CA /* PixooRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixooRenderer.swift; sourceTree = "<group>"; };
521526
731118DC3B388D7BE3F1CDB7 /* DesignTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignTokens.swift; sourceTree = "<group>"; };
527+
73BB88D8C2921CB4B93EC8B9 /* SessionPushRegisterMergeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPushRegisterMergeTests.swift; sourceTree = "<group>"; };
522528
758AAD78310D811B993ADA21 /* ApmeScorers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApmeScorers.swift; sourceTree = "<group>"; };
523529
766909A21820E709DA63C7C2 /* apme-dashboard.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "apme-dashboard.html"; sourceTree = "<group>"; };
524530
767EE1D15C8564C2A5AC5713 /* ObservedAskUserQuestionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservedAskUserQuestionTests.swift; sourceTree = "<group>"; };
@@ -703,6 +709,7 @@
703709
5B7A8A00EB9D3B9345E36EB4 /* ProtocolTests.swift */,
704710
1BBA59C83638DB2F6B90C44A /* ProviderRailEvaluatorTests.swift */,
705711
41CF82AE8E20EDCAE99DCEDD /* SessionLauncherTests.swift */,
712+
73BB88D8C2921CB4B93EC8B9 /* SessionPushRegisterMergeTests.swift */,
706713
BB282455F078889B6DB35F93 /* TerrariumCloudFoldTests.swift */,
707714
2E75D955A31F999F568B7BED /* TerrariumCrayfishPresenceTests.swift */,
708715
27A351E073AB448BEE1DA392 /* TimeboxProtocolTests.swift */,
@@ -1066,6 +1073,7 @@
10661073
F15F55435772A15B8F0E92C1 /* AgentState.swift */,
10671074
E532E5457B6DFDE324D8F11C /* Protocol.swift */,
10681075
BBAB8DFED8E277FFB6136742 /* SessionGrouping.swift */,
1076+
03941C6DD60609AB9D1E12C1 /* SessionWeightRules.generated.swift */,
10691077
28DFE82D8F7AC7C7566940F3 /* Timeline.swift */,
10701078
);
10711079
path = Model;
@@ -1313,6 +1321,7 @@
13131321
27F0FAF7D849BC85134C3333 /* ProtocolTests.swift in Sources */,
13141322
D8EC7333D6CEB08ED84E4BC2 /* ProviderRailEvaluatorTests.swift in Sources */,
13151323
6D2EA7E15F98B8510CF048E9 /* SessionLauncherTests.swift in Sources */,
1324+
D98FDB40FEA236A6370DE45E /* SessionPushRegisterMergeTests.swift in Sources */,
13161325
56228049434E99DBC2BFD016 /* TerrariumCloudFoldTests.swift in Sources */,
13171326
805AC8BBF1600EA9BFB044F5 /* TerrariumCrayfishPresenceTests.swift in Sources */,
13181327
008D05FDF30426FE1B0DC37F /* TimeboxProtocolTests.swift in Sources */,
@@ -1350,6 +1359,7 @@
13501359
3173BC2B844664891B8F3934 /* ProtocolTests.swift in Sources */,
13511360
F9D579C6EF0D2683CD40F9FC /* ProviderRailEvaluatorTests.swift in Sources */,
13521361
9B1DF1D553619B6728566E86 /* SessionLauncherTests.swift in Sources */,
1362+
D0668EACC4D50BE30B10ACE5 /* SessionPushRegisterMergeTests.swift in Sources */,
13531363
7409E65BFDFE78944B389B63 /* TerrariumCloudFoldTests.swift in Sources */,
13541364
E1027789C4EBBF134CA8D464 /* TerrariumCrayfishPresenceTests.swift in Sources */,
13551365
D2335AB9E3A5DEE25405617F /* TimeboxProtocolTests.swift in Sources */,
@@ -1493,6 +1503,7 @@
14931503
EB83962F2BDBC13AB130DD6B /* SessionListPanel.swift in Sources */,
14941504
34444319598506828DE09006 /* SessionRegistry.swift in Sources */,
14951505
757405E03E9BF8DACBE91848 /* SessionSlotRenderer.swift in Sources */,
1506+
B1C62D9C3341BB901AA27D26 /* SessionWeightRules.generated.swift in Sources */,
14961507
D53BC123AAE467EB342A5041 /* SettingsScreen.swift in Sources */,
14971508
7FBCD51766F94FE9BD969378 /* SetupNeededCard.swift in Sources */,
14981509
62F20845F1953514EFB1C11B /* SingletonGuard.swift in Sources */,
@@ -1664,6 +1675,7 @@
16641675
B777A61FBB6AD6F51B165DA5 /* SessionListPanel.swift in Sources */,
16651676
151F41C1C4B7DF78558D546D /* SessionRegistry.swift in Sources */,
16661677
B6600F223195F4A4B3B17315 /* SessionSlotRenderer.swift in Sources */,
1678+
E29B9F7B467F17A2B4FB65EE /* SessionWeightRules.generated.swift in Sources */,
16671679
38E9C043A76BB267FCACB4B7 /* SettingsScreen.swift in Sources */,
16681680
EE91D7CCEFEA94B80A2A4FCD /* SetupNeededCard.swift in Sources */,
16691681
D036600F5679C1A191132F87 /* SingletonGuard.swift in Sources */,

apple/AgentDeck/UI/Preview/Devices/D200HLayoutModel.swift

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
// against; `scripts/check-preview-mirror-sync.mjs` verifies they match the
2525
// current `git hash-object` of each file and fails CI when the origin drifts
2626
// ahead of this mirror. Update them whenever you re-port.
27-
// SYNC-HASH shared/src/d200h-layout.ts 8de92b3940bd36365dddbe7b8b56f15ad93b7ae5
27+
// SYNC-HASH shared/src/d200h-layout.ts a2c68d8877938f9a6238a353f9aac3059fc75af2
2828
// SYNC-HASH shared/src/session-utils.ts b08adbcca7a9fe3386a44801248b2ec06b572a0e
2929
//
3030
// INTENTIONALLY OMITTED (not needed by a read-only preview):
@@ -203,12 +203,22 @@ public struct D200HDeckView: Sendable {
203203
public var page: Int
204204
/// Pin trailing/preferred keys to the global 5H/7D usage gauges.
205205
public var showUsage: Bool
206+
/// Host push-to-talk capture state (daemon `voice_state`). Drives the
207+
/// detail-view VOICE tile. A D200H press is single-fire, so the tile
208+
/// toggles start/stop rather than expressing hold-to-talk.
209+
public var voiceState: VoiceState
206210

207-
public init(mode: Mode = .list, openSessionId: String? = nil, page: Int = 0, showUsage: Bool = true) {
211+
public enum VoiceState: String, Sendable { case idle, recording, transcribing, error }
212+
213+
public init(
214+
mode: Mode = .list, openSessionId: String? = nil, page: Int = 0,
215+
showUsage: Bool = true, voiceState: VoiceState = .idle
216+
) {
208217
self.mode = mode
209218
self.openSessionId = openSessionId
210219
self.page = page
211220
self.showUsage = showUsage
221+
self.voiceState = voiceState
212222
}
213223
}
214224

@@ -462,7 +472,6 @@ public enum D200HLayoutModel {
462472
let content = slots.count >= 3 ? Array(slots[2..<(slots.count - 1)]) : []
463473

464474
// Build the content cells (kind + label + subtitle + action).
465-
struct Cell { let kind: D200HSlotKind; let label: String; let subtitle: String?; let action: D200HDeckAction }
466475
var cells: [Cell] = []
467476

468477
if isAwaiting(sState) {
@@ -492,6 +501,7 @@ public enum D200HLayoutModel {
492501
for (label, text) in presets {
493502
cells.append(Cell(kind: .actionPreset, label: label, subtitle: nil, action: .command(type: "send_prompt", payload: ["text": text])))
494503
}
504+
cells.append(voiceCell(view: view, sid: sid))
495505
}
496506

497507
// Paginate cells into content slots; reserve last content slot for MORE.
@@ -521,6 +531,35 @@ public enum D200HLayoutModel {
521531

522532
// MARK: Usage tiles (port of buildUsageTiles)
523533

534+
/// One detail-view content cell before it is paginated into a key slot.
535+
private struct Cell {
536+
let kind: D200HSlotKind
537+
let label: String
538+
let subtitle: String?
539+
let action: D200HDeckAction
540+
}
541+
542+
/// Host push-to-talk tile — mirrors `voiceTile` in d200h-layout.ts. The
543+
/// deck contributes only the button: the daemon owns the microphone,
544+
/// on-device transcription, and delivery through the same ladder the
545+
/// device-voice path uses. `transcribing` is deliberately inert, so a
546+
/// second press cannot restart capture mid-transcription.
547+
private static func voiceCell(view: D200HDeckView, sid: String) -> Cell {
548+
switch view.voiceState {
549+
case .recording:
550+
return Cell(kind: .actionPreset, label: "VOICE", subtitle: "● tap to send",
551+
action: .command(type: "voice", payload: ["action": "stop", "sessionId": sid]))
552+
case .transcribing:
553+
return Cell(kind: .actionPreset, label: "VOICE", subtitle: "transcribing…", action: .none)
554+
case .error:
555+
return Cell(kind: .actionPreset, label: "VOICE", subtitle: "no speech",
556+
action: .command(type: "voice", payload: ["action": "start", "sessionId": sid]))
557+
case .idle:
558+
return Cell(kind: .actionPreset, label: "VOICE", subtitle: "tap to talk",
559+
action: .command(type: "voice", payload: ["action": "start", "sessionId": sid]))
560+
}
561+
}
562+
524563
/// Every tile is hide-if-absent (TS 208b1afc): Claude 5H/7D appear only
525564
/// when that window's quota is actually known, so fewer (or zero) tiles are
526565
/// reserved and the freed slots flow to session tiles.

apple/AgentDeck/UI/Preview/Devices/DevicePreviewShared.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ struct PreviewDisplaySession: Identifiable {
148148
var projectName: String
149149
var modelName: String?
150150
var state: PixooPreviewState
151+
/// Active subagents under this session. The firmware draws them as
152+
/// decorative activity owned by the parent — a static miniature orbit on
153+
/// e-ink, perimeter satellite pixels on the LED matrix — never as another
154+
/// session. Zero means "draw nothing", not "unknown".
155+
var subagentCount: Int = 0
151156
}
152157

153158
/// One usage provider row (0…1 fractions) for the usage band.
@@ -198,7 +203,13 @@ extension DevicePreviewSelection {
198203
agent: PixooPreviewAgent.from(agentType: s.agentType),
199204
projectName: (s.projectName?.isEmpty == false) ? s.projectName! : "session",
200205
modelName: s.modelName,
201-
state: PixooPreviewState.from(sessionState: s.state)
206+
state: PixooPreviewState.from(sessionState: s.state),
207+
// Live subagent counts live in TimelineStore, not in the
208+
// DashboardState this snapshot is built from, so live-follow
209+
// draws no satellites yet. Plumbing them through
210+
// LivePreviewData is a separate change; leaving it at 0 is
211+
// honest (nothing drawn) rather than invented.
212+
subagentCount: 0
202213
)
203214
}
204215
}
@@ -208,7 +219,11 @@ extension DevicePreviewSelection {
208219
agent: agent,
209220
projectName: "\(agent.displayName.lowercased())-project",
210221
modelName: nil,
211-
state: previewState(for: index)
222+
state: previewState(for: index),
223+
// One busy session carries children so the mirrored orbit and
224+
// satellites actually render in the gallery — a preview that
225+
// never exercises the element cannot show it drifted.
226+
subagentCount: index == 1 ? 2 : 0
212227
)
213228
}
214229
}

0 commit comments

Comments
 (0)