@pellux/goodvibes-tui 1.1.0 → 1.9.0

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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. package/src/renderer/qr-renderer.ts +0 -120
@@ -0,0 +1,280 @@
1
+ // ---------------------------------------------------------------------------
2
+ // checkpoint-runtime.ts — /checkpoints, /checkpoint, /rewind
3
+ //
4
+ // UX over WorkspaceCheckpointManager (@pellux/goodvibes-sdk/platform/workspace),
5
+ // the whole-workspace, git-backed rewind engine wired onto RuntimeServices as
6
+ // `workspaceCheckpointManager` (src/runtime/services.ts) and threaded through
7
+ // CommandContext.workspace exactly like FileUndoManager already is.
8
+ //
9
+ // Architecture note (why the confirm step lives in DiffPanel, not here):
10
+ // SlashCommand.handler(args, ctx) is a single-shot call — there is no "await
11
+ // next keypress" primitive in the command layer. So `/rewind <id>` can only
12
+ // resolve the target, load the preview, open+focus the diff panel, and arm
13
+ // its confirm overlay (DiffPanel.confirmOverlay, a PanelConfirmOverlay —
14
+ // see panel-confirm-overlay.ts); the actual y/n/Enter/Esc handling happens
15
+ // in DiffPanel.handleInput() via the project's canonical ConfirmState<T>
16
+ // contract (confirm-state.ts), which every other destructive-action confirm
17
+ // in this codebase already uses (see git-panel.ts).
18
+ // ---------------------------------------------------------------------------
19
+
20
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
21
+ import type { WorkspaceCheckpoint, WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
22
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
23
+ import { requirePanelManager } from './runtime-services.ts';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Formatting helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /** Drop the redundant 'wcp_' prefix and cap length for compact listing. Full ids (and any unambiguous prefix of one) are still accepted by /rewind. */
30
+ function shortId(id: string): string {
31
+ const stripped = id.startsWith('wcp_') ? id.slice(4) : id;
32
+ return stripped.length > 12 ? stripped.slice(0, 12) : stripped;
33
+ }
34
+
35
+ function formatAge(createdAtMs: number, nowMs: number = Date.now()): string {
36
+ const deltaMs = Math.max(0, nowMs - createdAtMs);
37
+ const seconds = Math.floor(deltaMs / 1000);
38
+ if (seconds < 60) return `${seconds}s ago`;
39
+ const minutes = Math.floor(seconds / 60);
40
+ if (minutes < 60) return `${minutes}m ago`;
41
+ const hours = Math.floor(minutes / 60);
42
+ if (hours < 24) return `${hours}h ago`;
43
+ const days = Math.floor(hours / 24);
44
+ return `${days}d ago`;
45
+ }
46
+
47
+ function formatBytes(bytes: number): string {
48
+ if (bytes < 1024) return `${bytes}B`;
49
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
50
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
51
+ }
52
+
53
+ function plural(n: number, word: string): string {
54
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Checkpoint resolution — shared by /rewind
59
+ // ---------------------------------------------------------------------------
60
+
61
+ type ResolveResult =
62
+ | { checkpoint: WorkspaceCheckpoint }
63
+ | { error: string };
64
+
65
+ /**
66
+ * Resolve a user-supplied checkpoint reference ('last' or an id / id prefix,
67
+ * as shown by /checkpoints) against the live checkpoint list. Resolution
68
+ * happens entirely against list() results — never against
69
+ * WorkspaceCheckpointManager's own requireCheckpoint() — so an unknown ref is
70
+ * caught here, before anything touches the diff panel or arms a confirm.
71
+ *
72
+ * mgr.list() is guarded here (not left to the caller) so every caller gets a
73
+ * plain ResolveResult and can never be handed a rejected promise: if the
74
+ * manager's list() call fails (including a cached init() failure — see
75
+ * services.ts — which makes every WorkspaceCheckpointManager method reject
76
+ * forever), that surfaces as an honest `{ error }` instead of an unhandled
77
+ * rejection.
78
+ */
79
+ async function resolveCheckpointTarget(
80
+ mgr: WorkspaceCheckpointManager,
81
+ ref: string,
82
+ ): Promise<ResolveResult> {
83
+ let all: WorkspaceCheckpoint[];
84
+ try {
85
+ all = await mgr.list();
86
+ } catch (err) {
87
+ return { error: `Failed to list checkpoints: ${summarizeError(err)}` };
88
+ }
89
+ if (ref === 'last') {
90
+ const checkpoint = all[0];
91
+ if (!checkpoint) {
92
+ return { error: 'No checkpoints to rewind to. Use /checkpoint <label> to create one.' };
93
+ }
94
+ return { checkpoint };
95
+ }
96
+ const exact = all.find((c) => c.id === ref);
97
+ if (exact) return { checkpoint: exact };
98
+
99
+ const prefixMatches = all.filter((c) => c.id.startsWith(ref) || shortId(c.id).startsWith(ref));
100
+ if (prefixMatches.length === 1) return { checkpoint: prefixMatches[0]! };
101
+ if (prefixMatches.length > 1) {
102
+ return {
103
+ error: `Checkpoint id "${ref}" is ambiguous — matches ${prefixMatches.length} checkpoints. Use a longer prefix or run /checkpoints for full ids.`,
104
+ };
105
+ }
106
+ return { error: `Unknown checkpoint id: "${ref}". Run /checkpoints to see available checkpoints.` };
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Commands
111
+ // ---------------------------------------------------------------------------
112
+
113
+ export function registerCheckpointRuntimeCommands(registry: CommandRegistry): void {
114
+ registry.register({
115
+ name: 'checkpoints',
116
+ aliases: ['ckpts'],
117
+ description: 'List workspace checkpoints, newest first',
118
+ handler: async (_args, ctx: CommandContext) => {
119
+ const mgr = ctx.workspace.workspaceCheckpointManager;
120
+ if (!mgr) {
121
+ ctx.print('Checkpoints are not available in this session.');
122
+ return;
123
+ }
124
+ let checkpoints: WorkspaceCheckpoint[];
125
+ try {
126
+ checkpoints = await mgr.list();
127
+ } catch (err) {
128
+ ctx.print(`Failed to list checkpoints: ${summarizeError(err)}`);
129
+ return;
130
+ }
131
+ if (checkpoints.length === 0) {
132
+ ctx.print('No checkpoints yet. /checkpoint <label> creates one manually; turns and agent runs snapshot automatically.');
133
+ return;
134
+ }
135
+ ctx.print(`Checkpoints (${checkpoints.length}, newest first):`);
136
+ for (const cp of checkpoints) {
137
+ ctx.print(` ${shortId(cp.id).padEnd(12)} ${cp.kind.padEnd(9)} ${cp.label} ${formatAge(cp.createdAt)} ~${formatBytes(cp.sizeBytes)} new`);
138
+ }
139
+ // Files-changed counts are deliberately NOT shown per-row here: getting
140
+ // them would cost one diff spawn per checkpoint (O(checkpoints)), which
141
+ // stops being cheap once there are more than a handful. /rewind <id>
142
+ // loads exactly one diff, so the exact file list is available there.
143
+ ctx.print('Use /rewind <id|last> to preview the exact file changes and restore a checkpoint.');
144
+ },
145
+ });
146
+
147
+ registry.register({
148
+ name: 'checkpoint',
149
+ description: 'Create a manual workspace checkpoint (forensic retention)',
150
+ usage: '[label]',
151
+ argsHint: '[label]',
152
+ handler: async (args, ctx: CommandContext) => {
153
+ const mgr = ctx.workspace.workspaceCheckpointManager;
154
+ if (!mgr) {
155
+ ctx.print('Checkpoints are not available in this session.');
156
+ return;
157
+ }
158
+ const label = args.join(' ').trim() || undefined;
159
+ try {
160
+ const checkpoint = await mgr.create({ kind: 'manual', label, retentionClass: 'forensic' });
161
+ if (!checkpoint) {
162
+ ctx.print('No changes since the last checkpoint — nothing to save.');
163
+ return;
164
+ }
165
+ ctx.print(`Checkpoint created: ${shortId(checkpoint.id)} "${checkpoint.label}" (forensic retention).`);
166
+ } catch (err) {
167
+ ctx.print(`Checkpoint failed: ${summarizeError(err)}`);
168
+ }
169
+ },
170
+ });
171
+
172
+ registry.register({
173
+ name: 'rewind',
174
+ description: 'Preview and restore a workspace checkpoint (files only — conversation history is unchanged)',
175
+ usage: '<id|last>',
176
+ argsHint: '<id|last>',
177
+ handler: async (args, ctx: CommandContext) => {
178
+ const mgr = ctx.workspace.workspaceCheckpointManager;
179
+ if (!mgr) {
180
+ ctx.print('Checkpoints are not available in this session.');
181
+ return;
182
+ }
183
+ const ref = args[0];
184
+ if (!ref) {
185
+ ctx.print('Usage: /rewind <id|last>. Run /checkpoints to see available checkpoints.');
186
+ return;
187
+ }
188
+
189
+ const resolved = await resolveCheckpointTarget(mgr, ref);
190
+ if ('error' in resolved) {
191
+ ctx.print(resolved.error);
192
+ return;
193
+ }
194
+ const { checkpoint } = resolved;
195
+
196
+ let diff: Awaited<ReturnType<WorkspaceCheckpointManager['diff']>>;
197
+ try {
198
+ diff = await mgr.diff(checkpoint.id);
199
+ } catch (err) {
200
+ ctx.print(`Could not load checkpoint preview: ${summarizeError(err)}`);
201
+ return;
202
+ }
203
+
204
+ const { DiffPanel } = await import('../../panels/diff-panel.ts');
205
+ const pm = requirePanelManager(ctx);
206
+ let panel = pm.getAllOpen().find((p) => p.id === 'diff');
207
+ if (!panel) {
208
+ try {
209
+ panel = pm.open('diff');
210
+ } catch {
211
+ ctx.print('Could not open diff panel.');
212
+ return;
213
+ }
214
+ }
215
+ pm.activateById('diff');
216
+ if (!pm.isVisible()) pm.show();
217
+ // Must focus the panel — otherwise the user is left typing at the
218
+ // prompt while the panel silently shows a pending confirm they never
219
+ // see (the single most likely UX bug in this feature).
220
+ ctx.focusPanels?.();
221
+
222
+ const diffPanel = panel as InstanceType<typeof DiffPanel>;
223
+ if (diff.unifiedDiff.trim()) {
224
+ diffPanel.loadRawDiff(diff.unifiedDiff);
225
+ } else {
226
+ diffPanel.showDiff('(no changes)', '@@ -0,0 +0,0 @@\n Working tree already matches this checkpoint.');
227
+ }
228
+
229
+ diffPanel.confirmOverlay.arm({
230
+ id: checkpoint.id,
231
+ // renderConfirmLines (panels/confirm-state.ts) builds the on-screen
232
+ // prompt as `${verb} "${label}"?` itself — it supplies both the verb
233
+ // and the surrounding quotes. A label that already contained "Restore
234
+ // "..."" duplicated both, rendering as `Restore "Restore
235
+ // "baseline-pin"…`. label here must be just the descriptive subject,
236
+ // with no verb and no quotes of its own (same convention GitPanel
237
+ // already follows for its own confirms — see git-panel.ts:352,370).
238
+ label: `${checkpoint.label} (${plural(diff.files.length, 'file')} differ) — files only, conversation history is unchanged`,
239
+ verb: 'Restore',
240
+ onConfirm: async () => {
241
+ try {
242
+ const result = await mgr.restore(checkpoint.id, { safetyCheckpoint: true });
243
+ ctx.print(
244
+ `Rewind complete: restored ${plural(result.restoredFiles.length, 'file')}, removed ${plural(result.removedFiles.length, 'file')}. ` +
245
+ `Safety checkpoint: ${result.safetyCheckpointId ? shortId(result.safetyCheckpointId) : '(none — working tree already matched)'}.`,
246
+ );
247
+ // Conversation is NOT rewound: addSystemMessage (not ctx.print)
248
+ // persists this note into real turn/session history so it is
249
+ // indexed by getTranscriptEventIndex() and session save/load,
250
+ // while staying a distinct message kind from user/assistant turns.
251
+ ctx.session.conversationManager.addSystemMessage(
252
+ `[Rewind] Restored checkpoint "${checkpoint.label}" (${plural(result.restoredFiles.length, 'file')} restored, ${plural(result.removedFiles.length, 'file')} removed). ` +
253
+ 'Files were rewound — conversation history is unchanged. Use /undo to remove turns separately.',
254
+ );
255
+ // Auto-close the preview: once the restore has actually happened,
256
+ // the diff panel showing the (now-stale) pre-restore preview has
257
+ // nothing left to confirm and no obvious way to dismiss it
258
+ // otherwise (same pm.close(id) + focusPrompt() pattern as
259
+ // /panel close — see operator-panel-runtime.ts).
260
+ pm.close('diff');
261
+ ctx.focusPrompt?.();
262
+ ctx.renderRequest();
263
+ } catch (err) {
264
+ ctx.print(`Rewind failed: ${summarizeError(err)}`);
265
+ }
266
+ },
267
+ onCancel: () => {
268
+ ctx.print('Rewind cancelled — no files changed.');
269
+ // Same auto-close as the confirm path above — a denied restore also
270
+ // leaves nothing actionable in the preview panel.
271
+ pm.close('diff');
272
+ ctx.focusPrompt?.();
273
+ ctx.renderRequest();
274
+ },
275
+ });
276
+ ctx.print(`Previewing checkpoint ${shortId(checkpoint.id)} "${checkpoint.label}". Confirm in the diff panel: Enter/y to restore, n/Esc to cancel.`);
277
+ ctx.renderRequest();
278
+ },
279
+ });
280
+ }
@@ -0,0 +1,232 @@
1
+ // ---------------------------------------------------------------------------
2
+ // codebase-runtime.ts — /codebase
3
+ //
4
+ // Explicit-query + transparency surface over the repo source-tree code index
5
+ // (CodeIndexStore, @pellux/goodvibes-sdk/platform/state, landed on SDK main
6
+ // as wo802/W5.3 Stage A) via the TUI's own wiring in
7
+ // src/runtime/code-index-services.ts, threaded through
8
+ // CommandContext.session.codeIndexStore exactly like wrfcController/
9
+ // workstreamEngine already are (bootstrap-command-context.ts).
10
+ //
11
+ // Mirrors /recall's transparency idiom (recall-query.ts's handleRecallVector:
12
+ // status/rebuild) and /workstream's registerXRuntimeCommands function-export
13
+ // shape: build (schedules the index build — visible as a fleet 'code-index'
14
+ // node while it runs), status (counts, skips, degradation state, last
15
+ // build), search <query> (explicit retrieval, results labeled
16
+ // 'lexical'|'semantic' honestly — never implied as more precise than they
17
+ // are). Stage B (now landed) adds passive auto-injection into coding turns and
18
+ // tool-site incremental reindex; `status` surfaces both honestly (the
19
+ // auto-injection flag+setting gate, and the last reindex activity).
20
+ // ---------------------------------------------------------------------------
21
+
22
+ import type { CodeIndexStats, CodeContextResult, CodeIndexReindexActivity } from '@pellux/goodvibes-sdk/platform/state';
23
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
24
+ import { isCodeIndexAutoStartEnabled, CODE_INDEX_MAX_FILES, CODE_INDEX_MAX_FILE_BYTES, CODE_INDEX_MAX_TOTAL_BYTES } from '../../runtime/code-index-services.ts';
25
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Formatting helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function formatBytes(bytes: number): string {
32
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
33
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
34
+ return `${bytes}B`;
35
+ }
36
+
37
+ /**
38
+ * Honest one-line auto-injection state (Stage B): both the (default-off)
39
+ * `agent-passive-code-injection` feature flag AND storage.codeIndexEnabled must
40
+ * be on for passive code injection to run — state which is off, and why.
41
+ */
42
+ function renderAutoInjectionState(flagEnabled: boolean, settingEnabled: boolean): string {
43
+ if (flagEnabled && settingEnabled) {
44
+ return ' auto-injection: on (code hits may be injected into coding turns within the shared knowledge budget)';
45
+ }
46
+ const reasons: string[] = [];
47
+ if (!flagEnabled) reasons.push('agent-passive-code-injection flag off (default off) — enable in the settings modal flags section');
48
+ if (!settingEnabled) reasons.push('storage.codeIndexEnabled off — /config to change');
49
+ return ` auto-injection: off (${reasons.join('; ')})`;
50
+ }
51
+
52
+ /** Honest last-reindex activity line (Stage B tool-site incremental reindex). */
53
+ function renderReindexActivity(activity: CodeIndexReindexActivity | null, now: number): string {
54
+ if (!activity) return ' last reindex: none this session (writes/edits reindex touched files once the index is built)';
55
+ const agoSec = Math.max(0, Math.round((now - activity.at) / 1000));
56
+ const detail = activity.status === 'error'
57
+ ? `error: ${activity.error ?? 'unknown'}`
58
+ : activity.status === 'skipped'
59
+ ? `skipped (${activity.mode ?? 'empty'})`
60
+ : `indexed (${activity.mode ?? 'symbols'})`;
61
+ return ` last reindex: ${activity.path} — ${detail}, ${agoSec}s ago`;
62
+ }
63
+
64
+ interface CodeIndexStatusExtras {
65
+ readonly flagEnabled: boolean;
66
+ readonly reindexActivity: CodeIndexReindexActivity | null;
67
+ readonly now?: number;
68
+ }
69
+
70
+ function renderCodeIndexStatus(stats: CodeIndexStats, configManager: Pick<ConfigManager, 'get'>, extras: CodeIndexStatusExtras): string {
71
+ const lines: string[] = [];
72
+ lines.push(`Code index — backend: ${stats.backend}, available: ${stats.available ? 'yes' : 'no'}`);
73
+ lines.push(` path: ${stats.path}`);
74
+ lines.push(` indexed: ${stats.indexedFiles} file(s), ${stats.indexedChunks} chunk(s), ${stats.dimensions} dims`);
75
+ lines.push(` embedding provider: ${stats.embeddingProviderLabel} (${stats.embeddingProviderId})`);
76
+ lines.push(
77
+ stats.semanticRetrievalAvailable
78
+ ? ' search results labeled: semantic'
79
+ : ' search results labeled: lexical (hashed fallback — low precision)',
80
+ );
81
+ const autoStart = isCodeIndexAutoStartEnabled(configManager);
82
+ lines.push(
83
+ ` auto-build on startup: ${autoStart ? 'on' : 'off'} (storage.codeIndexEnabled, default off — /config to change)`,
84
+ );
85
+ // Stage B surfacing: passive code auto-injection state (flag + setting) and the last tool-site reindex.
86
+ lines.push(renderAutoInjectionState(extras.flagEnabled, autoStart));
87
+ lines.push(renderReindexActivity(extras.reindexActivity, extras.now ?? Date.now()));
88
+ lines.push(
89
+ ` bounds: max ${CODE_INDEX_MAX_FILES} files (maxFiles), ${formatBytes(CODE_INDEX_MAX_FILE_BYTES)} per file (maxFileBytes),`
90
+ + ` ${formatBytes(CODE_INDEX_MAX_TOTAL_BYTES)} total per build (maxTotalBytes)`,
91
+ );
92
+ // Provider-space honesty (SDK finding): vectors embedded under a different
93
+ // provider than the current default disable the vector search path until a
94
+ // rebuild re-embeds — say so, in the store's own words.
95
+ if (stats.embeddingProviderMismatch) {
96
+ lines.push(` provider mismatch: ${stats.embeddingProviderMismatch} (vector search disabled — lexical fallback only)`);
97
+ }
98
+
99
+ if (stats.building) {
100
+ lines.push(' build: in progress');
101
+ } else if (stats.lastBuild) {
102
+ const b = stats.lastBuild;
103
+ lines.push(
104
+ ` last build: ${b.filesIndexed} indexed, ${b.filesUnchanged} unchanged, ${b.filesRemoved} removed,`
105
+ + ` ${b.chunksIndexed} chunk(s) embedded (${b.chunksUnchanged} unchanged), ${b.durationMs}ms`,
106
+ );
107
+ const skip = b.skip;
108
+ const skipParts: string[] = [];
109
+ if (skip.tooLarge) skipParts.push(`${skip.tooLarge} too large`);
110
+ if (skip.overFileCap) skipParts.push(`${skip.overFileCap} over file cap (maxFiles)`);
111
+ if (skip.overTotalBytes) skipParts.push(`${skip.overTotalBytes} over total byte budget (maxTotalBytes)`);
112
+ if (skip.binary) skipParts.push(`${skip.binary} binary`);
113
+ if (skip.ignoredByGitignore) skipParts.push(`${skip.ignoredByGitignore} gitignored`);
114
+ if (skip.readErrors) skipParts.push(`${skip.readErrors} read error(s)`);
115
+ if (skip.chunkedByWindow) skipParts.push(`${skip.chunkedByWindow} windowed (no tree-sitter symbols)`);
116
+ lines.push(skipParts.length > 0 ? ` skipped: ${skipParts.join(', ')}` : ' skipped: none');
117
+ } else {
118
+ lines.push(' last build: never — run /codebase build');
119
+ }
120
+
121
+ if (stats.error) lines.push(` error: ${stats.error}`);
122
+
123
+ const degradation = stats.semanticRetrievalAvailable ? null : describeDegradationFallback(stats);
124
+ if (degradation) lines.push(` ${degradation}`);
125
+
126
+ return lines.join('\n');
127
+ }
128
+
129
+ /** Reuses the exact wording CodeIndexStore.describeDegradation() would return for a hashed-only provider, without requiring a live call the status renderer doesn't otherwise need. */
130
+ function describeDegradationFallback(stats: CodeIndexStats): string | null {
131
+ if (stats.semanticRetrievalAvailable) return null;
132
+ return 'code auto-retrieval disabled: no semantic embedding provider configured';
133
+ }
134
+
135
+ function renderSearchResults(results: readonly CodeContextResult[]): string {
136
+ return results
137
+ .map((result) => {
138
+ const c = result.chunk;
139
+ const pct = Math.round(result.similarity * 100);
140
+ const symbol = c.symbol ? ` ${c.symbol}` : '';
141
+ return ` ${c.path}:${c.startLine}-${c.endLine} [${result.label}] sim ${pct}% ${c.kind}${symbol}`;
142
+ })
143
+ .join('\n');
144
+ }
145
+
146
+ function parseLimitArg(args: readonly string[]): { readonly limit: number | undefined; readonly queryTokens: string[] } {
147
+ const limitIdx = args.indexOf('--limit');
148
+ let limit: number | undefined;
149
+ if (limitIdx !== -1 && args[limitIdx + 1]) {
150
+ const parsed = parseInt(args[limitIdx + 1]!, 10);
151
+ if (!Number.isNaN(parsed)) limit = parsed;
152
+ }
153
+ const queryTokens = args.filter((token, index) => {
154
+ if (token === '--limit') return false;
155
+ if (limitIdx !== -1 && index === limitIdx + 1) return false;
156
+ return true;
157
+ });
158
+ return { limit, queryTokens };
159
+ }
160
+
161
+ const USAGE = 'Usage:\n'
162
+ + ' /codebase build\n'
163
+ + ' /codebase status\n'
164
+ + ' /codebase search <query...> [--limit n]';
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Command
168
+ // ---------------------------------------------------------------------------
169
+
170
+ export function registerCodebaseRuntimeCommands(registry: CommandRegistry): void {
171
+ registry.register({
172
+ name: 'codebase',
173
+ description: 'Repo source-tree code index — build, inspect, and search',
174
+ usage: 'build | status | search <query...> [--limit n]',
175
+ argsHint: 'build | status | search <query>',
176
+ handler(args: string[], ctx: CommandContext) {
177
+ const store = ctx.session.codeIndexStore;
178
+ if (!store) {
179
+ ctx.print('The code index is not available in this session.');
180
+ return;
181
+ }
182
+
183
+ const sub = args[0];
184
+
185
+ if (!sub || sub === 'status') {
186
+ ctx.print(renderCodeIndexStatus(store.stats(), ctx.platform.configManager, {
187
+ flagEnabled: ctx.session.isPassiveCodeInjectionFlagEnabled?.() ?? false,
188
+ reindexActivity: ctx.session.codeIndexReindexScheduler?.lastActivity() ?? null,
189
+ }));
190
+ return;
191
+ }
192
+
193
+ if (sub === 'build' || sub === 'reindex') {
194
+ if (store.isBuilding()) {
195
+ const progress = store.buildProgress();
196
+ ctx.print(
197
+ progress
198
+ ? `A build is already in progress (${progress.scanned}/${progress.total} files scanned).`
199
+ : 'A build is already in progress.',
200
+ );
201
+ return;
202
+ }
203
+ store.scheduleBuild();
204
+ ctx.print('Build scheduled — track progress with /codebase status or the fleet panel (code-index node).');
205
+ return;
206
+ }
207
+
208
+ if (sub === 'search') {
209
+ const { limit, queryTokens } = parseLimitArg(args.slice(1));
210
+ const query = queryTokens.join(' ').trim();
211
+ if (!query) {
212
+ ctx.print('Usage: /codebase search <query...> [--limit n]');
213
+ return;
214
+ }
215
+ const results = store.search(query, limit !== undefined ? { limit } : undefined);
216
+ if (results.length === 0) {
217
+ const stats = store.stats();
218
+ ctx.print(
219
+ stats.indexedChunks === 0
220
+ ? 'No results — the code index is empty. Run /codebase build first.'
221
+ : 'No matching chunks found.',
222
+ );
223
+ return;
224
+ }
225
+ ctx.print(`${results.length} result(s):\n${renderSearchResults(results)}`);
226
+ return;
227
+ }
228
+
229
+ ctx.print(USAGE);
230
+ },
231
+ });
232
+ }
@@ -1,13 +1,45 @@
1
1
  import type { CommandRegistry } from '../command-registry.ts';
2
+ import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
3
+ import { setActiveThemeMode, type ThemeMode } from '../../renderer/theme.ts';
2
4
 
3
5
  export function registerConfigCommand(registry: CommandRegistry): void {
4
6
  registry.register({
5
7
  name: 'config',
6
8
  aliases: ['cfg'],
7
- description: 'Open the fullscreen configuration workspace',
8
- usage: '[category|key]',
9
- argsHint: '[category|key]',
9
+ description: 'Open the fullscreen configuration workspace, or set a key directly',
10
+ usage: '[category|key] | set <key> <value>',
11
+ argsHint: '[category|key] | set <key> <value>',
10
12
  handler(args, ctx) {
13
+ // Batch replay D2: `/config set <key> <value>` used to fall through to
14
+ // the workspace with the assignment silently ignored — the dishonest-
15
+ // fallthrough class. `set` is now a real verb.
16
+ if (args[0] === 'set') {
17
+ const key = args[1];
18
+ const value = args.slice(2).join(' ');
19
+ if (!key || value === '') {
20
+ ctx.print('Usage: /config set <key> <value> — e.g. /config set display.themeMode light');
21
+ return;
22
+ }
23
+ try {
24
+ const before = ctx.platform.configManager.get(key as ConfigKey);
25
+ ctx.platform.configManager.setDynamic(key as ConfigKey, coerceConfigValue(value));
26
+ const after = ctx.platform.configManager.get(key as ConfigKey);
27
+ ctx.print(`Set ${key}: ${JSON.stringify(before)} → ${JSON.stringify(after)}`);
28
+ // Forced theme modes apply immediately (matches the settings-modal
29
+ // path); 'auto' re-probes on the next launch (stated honestly).
30
+ if (key === 'display.themeMode') {
31
+ if (after === 'dark' || after === 'light') {
32
+ setActiveThemeMode(after as ThemeMode);
33
+ ctx.print('Theme applied. Note: transcript, modal, and header/footer/thinking chrome all flip; only the background colour follows your terminal.');
34
+ } else {
35
+ ctx.print('Theme mode "auto" probes the terminal background on the next launch.');
36
+ }
37
+ }
38
+ } catch (e) {
39
+ ctx.print(`Could not set ${key}: ${e instanceof Error ? e.message : String(e)}`);
40
+ }
41
+ return;
42
+ }
11
43
  if (ctx.openSettingsModal) {
12
44
  ctx.openSettingsModal(args[0]);
13
45
  return;
@@ -16,3 +48,11 @@ export function registerConfigCommand(registry: CommandRegistry): void {
16
48
  },
17
49
  });
18
50
  }
51
+
52
+ /** Coerce CLI text to the JSON-ish scalar the config schema expects. */
53
+ function coerceConfigValue(raw: string): unknown {
54
+ if (raw === 'true') return true;
55
+ if (raw === 'false') return false;
56
+ if (raw !== '' && Number.isFinite(Number(raw))) return Number(raw);
57
+ return raw;
58
+ }
@@ -14,7 +14,7 @@ import { EvalRunner } from '@/runtime/index.ts';
14
14
  import { BUILTIN_SUITES } from '@/runtime/index.ts';
15
15
  import { formatScorecard } from '@/runtime/index.ts';
16
16
  import { loadBaseline, captureBaseline, formatBaselineComparison, writeBaseline } from '@/runtime/index.ts';
17
- import type { EvalRegistry } from '../../panels/eval-panel.ts';
17
+ import type { EvalRegistry } from '../../panels/eval-registry.ts';
18
18
  import { formatSuiteResult, formatGateResult } from '@/runtime/index.ts';
19
19
  import { requireShellPaths } from './runtime-services.ts';
20
20
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
@@ -39,13 +39,13 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
39
39
  name: 'health',
40
40
  aliases: ['doctor'],
41
41
  description: 'Health workspace for startup posture, service readiness, sandbox posture, and provider health',
42
- usage: '[open|review|setup|services|sandbox|provider|accounts|auth|settings|intelligence|remote|mcp|continuity|worktrees|maintenance|term|repair [domain]]',
42
+ usage: '[report|review|open|setup|services|sandbox|provider|accounts|auth|settings|intelligence|remote|mcp|continuity|worktrees|maintenance|term|repair [domain]] — bare and report stay a cross-domain transcript report (see also /health provider for the providers modal)',
43
43
  async handler(args, ctx) {
44
44
  const sub = (args[0] ?? 'review').toLowerCase();
45
45
  const readModels = requireReadModels(ctx);
46
46
 
47
47
  if (sub === 'open' || sub === 'panel' || sub === 'provider') {
48
- openCommandPanel(ctx, 'provider-health');
48
+ ctx.openModal?.('providers-modal'); // W6.1: provider-health panel -> config modal
49
49
  return;
50
50
  }
51
51
 
@@ -424,6 +424,14 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
424
424
 
425
425
  ctx.print([
426
426
  'Health Review',
427
+ // DEBT-5 item 3: /health stays a cross-domain transcript report (it
428
+ // genuinely spans settings/accounts/auth/sandbox/etc — no single
429
+ // modal owns this data), but the providers domain DOES have one, so
430
+ // point at it honestly. "/provider" (no domain) is a DIFFERENT
431
+ // command (switch/manage custom providers) — the real front door to
432
+ // the providers modal is /health provider (see sub === 'provider'
433
+ // above), so that is what this hint names.
434
+ ' see also: the providers modal — /health provider',
427
435
  ` session: ${snapshot.sessionId}`,
428
436
  ` setup issues: ${snapshot.issues.length}`,
429
437
  ` service issues: ${snapshot.serviceIssues.length}`,