@pellux/goodvibes-tui 1.1.0 → 1.7.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 (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. 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,192 @@
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). This is Stage A only: no auto-injection into coding turns (Stage B,
18
+ // out of scope — see the wo802/W5.3 design doc).
19
+ // ---------------------------------------------------------------------------
20
+
21
+ import type { CodeIndexStats, CodeContextResult } from '@pellux/goodvibes-sdk/platform/state';
22
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
23
+ import { isCodeIndexAutoStartEnabled, CODE_INDEX_MAX_FILES, CODE_INDEX_MAX_FILE_BYTES, CODE_INDEX_MAX_TOTAL_BYTES } from '../../runtime/code-index-services.ts';
24
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Formatting helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function formatBytes(bytes: number): string {
31
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
32
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
33
+ return `${bytes}B`;
34
+ }
35
+
36
+ function renderCodeIndexStatus(stats: CodeIndexStats, configManager: Pick<ConfigManager, 'get'>): string {
37
+ const lines: string[] = [];
38
+ lines.push(`Code index — backend: ${stats.backend}, available: ${stats.available ? 'yes' : 'no'}`);
39
+ lines.push(` path: ${stats.path}`);
40
+ lines.push(` indexed: ${stats.indexedFiles} file(s), ${stats.indexedChunks} chunk(s), ${stats.dimensions} dims`);
41
+ lines.push(` embedding provider: ${stats.embeddingProviderLabel} (${stats.embeddingProviderId})`);
42
+ lines.push(
43
+ stats.semanticRetrievalAvailable
44
+ ? ' search results labeled: semantic'
45
+ : ' search results labeled: lexical (hashed fallback — low precision)',
46
+ );
47
+ const autoStart = isCodeIndexAutoStartEnabled(configManager);
48
+ lines.push(
49
+ ` auto-build on startup: ${autoStart ? 'on' : 'off'} (storage.codeIndexEnabled, default off — /config to change)`,
50
+ );
51
+ lines.push(
52
+ ` bounds: max ${CODE_INDEX_MAX_FILES} files (maxFiles), ${formatBytes(CODE_INDEX_MAX_FILE_BYTES)} per file (maxFileBytes),`
53
+ + ` ${formatBytes(CODE_INDEX_MAX_TOTAL_BYTES)} total per build (maxTotalBytes)`,
54
+ );
55
+ // Provider-space honesty (SDK finding): vectors embedded under a different
56
+ // provider than the current default disable the vector search path until a
57
+ // rebuild re-embeds — say so, in the store's own words.
58
+ if (stats.embeddingProviderMismatch) {
59
+ lines.push(` provider mismatch: ${stats.embeddingProviderMismatch} (vector search disabled — lexical fallback only)`);
60
+ }
61
+
62
+ if (stats.building) {
63
+ lines.push(' build: in progress');
64
+ } else if (stats.lastBuild) {
65
+ const b = stats.lastBuild;
66
+ lines.push(
67
+ ` last build: ${b.filesIndexed} indexed, ${b.filesUnchanged} unchanged, ${b.filesRemoved} removed,`
68
+ + ` ${b.chunksIndexed} chunk(s) embedded (${b.chunksUnchanged} unchanged), ${b.durationMs}ms`,
69
+ );
70
+ const skip = b.skip;
71
+ const skipParts: string[] = [];
72
+ if (skip.tooLarge) skipParts.push(`${skip.tooLarge} too large`);
73
+ if (skip.overFileCap) skipParts.push(`${skip.overFileCap} over file cap (maxFiles)`);
74
+ if (skip.overTotalBytes) skipParts.push(`${skip.overTotalBytes} over total byte budget (maxTotalBytes)`);
75
+ if (skip.binary) skipParts.push(`${skip.binary} binary`);
76
+ if (skip.ignoredByGitignore) skipParts.push(`${skip.ignoredByGitignore} gitignored`);
77
+ if (skip.readErrors) skipParts.push(`${skip.readErrors} read error(s)`);
78
+ if (skip.chunkedByWindow) skipParts.push(`${skip.chunkedByWindow} windowed (no tree-sitter symbols)`);
79
+ lines.push(skipParts.length > 0 ? ` skipped: ${skipParts.join(', ')}` : ' skipped: none');
80
+ } else {
81
+ lines.push(' last build: never — run /codebase build');
82
+ }
83
+
84
+ if (stats.error) lines.push(` error: ${stats.error}`);
85
+
86
+ const degradation = stats.semanticRetrievalAvailable ? null : describeDegradationFallback(stats);
87
+ if (degradation) lines.push(` ${degradation}`);
88
+
89
+ return lines.join('\n');
90
+ }
91
+
92
+ /** 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. */
93
+ function describeDegradationFallback(stats: CodeIndexStats): string | null {
94
+ if (stats.semanticRetrievalAvailable) return null;
95
+ return 'code auto-retrieval disabled: no semantic embedding provider configured';
96
+ }
97
+
98
+ function renderSearchResults(results: readonly CodeContextResult[]): string {
99
+ return results
100
+ .map((result) => {
101
+ const c = result.chunk;
102
+ const pct = Math.round(result.similarity * 100);
103
+ const symbol = c.symbol ? ` ${c.symbol}` : '';
104
+ return ` ${c.path}:${c.startLine}-${c.endLine} [${result.label}] sim ${pct}% ${c.kind}${symbol}`;
105
+ })
106
+ .join('\n');
107
+ }
108
+
109
+ function parseLimitArg(args: readonly string[]): { readonly limit: number | undefined; readonly queryTokens: string[] } {
110
+ const limitIdx = args.indexOf('--limit');
111
+ let limit: number | undefined;
112
+ if (limitIdx !== -1 && args[limitIdx + 1]) {
113
+ const parsed = parseInt(args[limitIdx + 1]!, 10);
114
+ if (!Number.isNaN(parsed)) limit = parsed;
115
+ }
116
+ const queryTokens = args.filter((token, index) => {
117
+ if (token === '--limit') return false;
118
+ if (limitIdx !== -1 && index === limitIdx + 1) return false;
119
+ return true;
120
+ });
121
+ return { limit, queryTokens };
122
+ }
123
+
124
+ const USAGE = 'Usage:\n'
125
+ + ' /codebase build\n'
126
+ + ' /codebase status\n'
127
+ + ' /codebase search <query...> [--limit n]';
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Command
131
+ // ---------------------------------------------------------------------------
132
+
133
+ export function registerCodebaseRuntimeCommands(registry: CommandRegistry): void {
134
+ registry.register({
135
+ name: 'codebase',
136
+ description: 'Repo source-tree code index — build, inspect, and search (Wave-5 W5.3 Stage A)',
137
+ usage: 'build | status | search <query...> [--limit n]',
138
+ argsHint: 'build | status | search <query>',
139
+ handler(args: string[], ctx: CommandContext) {
140
+ const store = ctx.session.codeIndexStore;
141
+ if (!store) {
142
+ ctx.print('The code index is not available in this session.');
143
+ return;
144
+ }
145
+
146
+ const sub = args[0];
147
+
148
+ if (!sub || sub === 'status') {
149
+ ctx.print(renderCodeIndexStatus(store.stats(), ctx.platform.configManager));
150
+ return;
151
+ }
152
+
153
+ if (sub === 'build' || sub === 'reindex') {
154
+ if (store.isBuilding()) {
155
+ const progress = store.buildProgress();
156
+ ctx.print(
157
+ progress
158
+ ? `A build is already in progress (${progress.scanned}/${progress.total} files scanned).`
159
+ : 'A build is already in progress.',
160
+ );
161
+ return;
162
+ }
163
+ store.scheduleBuild();
164
+ ctx.print('Build scheduled — track progress with /codebase status or the fleet panel (code-index node).');
165
+ return;
166
+ }
167
+
168
+ if (sub === 'search') {
169
+ const { limit, queryTokens } = parseLimitArg(args.slice(1));
170
+ const query = queryTokens.join(' ').trim();
171
+ if (!query) {
172
+ ctx.print('Usage: /codebase search <query...> [--limit n]');
173
+ return;
174
+ }
175
+ const results = store.search(query, limit !== undefined ? { limit } : undefined);
176
+ if (results.length === 0) {
177
+ const stats = store.stats();
178
+ ctx.print(
179
+ stats.indexedChunks === 0
180
+ ? 'No results — the code index is empty. Run /codebase build first.'
181
+ : 'No matching chunks found.',
182
+ );
183
+ return;
184
+ }
185
+ ctx.print(`${results.length} result(s):\n${renderSearchResults(results)}`);
186
+ return;
187
+ }
188
+
189
+ ctx.print(USAGE);
190
+ },
191
+ });
192
+ }
@@ -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';
@@ -45,7 +45,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
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
 
@@ -0,0 +1,112 @@
1
+ import type { CommandRegistry } from '../command-registry.ts';
2
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
3
+
4
+ /**
5
+ * registerImageRuntimeCommands - `/imagine <prompt>`.
6
+ *
7
+ * Named 'imagine', not 'image' — `/image <path> [prompt]` already exists
8
+ * (local-runtime.ts) and does something entirely different (attach a local
9
+ * image file to the next message). Re-verified against the current registry
10
+ * before wiring this in: the brief that scoped this command was written
11
+ * before checking for the collision.
12
+ *
13
+ * First production caller of MediaProviderRegistry.generate() — the SDK
14
+ * plumbing existed with zero non-test call sites before this command.
15
+ * findProvider('generate') returns whichever generation-capable provider is
16
+ * configured; when none are, the command prints the registry's own
17
+ * per-provider status (id/state/detail) verbatim — those detail strings
18
+ * already name the exact env var per builtin provider
19
+ * (builtin-generation-providers.ts's buildStatus()), so this command never
20
+ * has to (and never should) guess or hardcode an env-var name itself.
21
+ */
22
+ export function registerImageRuntimeCommands(registry: CommandRegistry): void {
23
+ registry.register({
24
+ name: 'imagine',
25
+ description: 'Generate an image from a prompt via a configured media provider',
26
+ usage: '<prompt>',
27
+ argsHint: '<prompt>',
28
+ async handler(args, ctx) {
29
+ const mediaProviders = ctx.platform.mediaProviders;
30
+ const artifactStore = ctx.platform.artifactStore;
31
+ if (!mediaProviders || !artifactStore) {
32
+ ctx.print('Image generation is not available in this session.');
33
+ return;
34
+ }
35
+
36
+ const prompt = args.join(' ').trim();
37
+ if (!prompt) {
38
+ ctx.print('Usage: /imagine <prompt>');
39
+ return;
40
+ }
41
+
42
+ const provider = mediaProviders.findProvider('generate');
43
+ if (!provider?.generate) {
44
+ const statuses = await mediaProviders.status();
45
+ const generationStatuses = statuses.filter((status) => status.capabilities.includes('generate'));
46
+ const lines = ['No image-generation provider is configured.', ''];
47
+ if (generationStatuses.length === 0) {
48
+ lines.push('No media-generation providers are registered in this build.');
49
+ } else {
50
+ for (const status of generationStatuses) {
51
+ lines.push(` ${status.id} (${status.label}): ${status.state}${status.detail ? ` — ${status.detail}` : ''}`);
52
+ }
53
+ }
54
+ ctx.print(lines.join('\n'));
55
+ return;
56
+ }
57
+
58
+ try {
59
+ const result = await provider.generate({
60
+ prompt,
61
+ outputMimeType: 'image/png',
62
+ metadata: { source: 'tui-image-command' },
63
+ });
64
+
65
+ if (result.artifacts.length === 0) {
66
+ ctx.print(`${provider.label} returned no artifacts for this prompt.`);
67
+ return;
68
+ }
69
+
70
+ const lines = [`Generated ${result.artifacts.length} artifact(s) via ${provider.label} (${result.providerId}).`];
71
+ for (const artifact of result.artifacts) {
72
+ if (!artifact.mimeType.startsWith('image/')) {
73
+ lines.push(` Note: ${provider.label} produced ${artifact.mimeType}, not an image — no image-capable provider is configured in this build.`);
74
+ }
75
+ if (artifact.dataBase64) {
76
+ const descriptor = await artifactStore.create({
77
+ dataBase64: artifact.dataBase64,
78
+ mimeType: artifact.mimeType,
79
+ filename: artifact.filename ?? 'generated-image',
80
+ metadata: { ...artifact.metadata, generatedBy: result.providerId },
81
+ });
82
+ lines.push(` artifact: ${descriptor.id} (${descriptor.mimeType}, ${descriptor.sizeBytes} bytes)`);
83
+ } else if (artifact.uri) {
84
+ // Design note: this is the first production caller of generate(), and
85
+ // returned artifacts may be provider-hosted remote URLs rather than
86
+ // inline bytes. There is no existing precedent for whether to eagerly
87
+ // download remote-only output into ArtifactStore, and doing so
88
+ // unconditionally would turn a text command into an unbounded
89
+ // background fetch of arbitrary provider-hosted content. Conservative
90
+ // default: store a small JSON pointer record (not the media bytes) so
91
+ // the generation is still discoverable via the artifact store, and
92
+ // render the URL directly so the result stays immediately usable.
93
+ const descriptor = await artifactStore.create({
94
+ text: JSON.stringify({ remoteUrl: artifact.uri, mimeType: artifact.mimeType, providerId: result.providerId }, null, 2),
95
+ mimeType: 'application/json',
96
+ filename: `${artifact.filename ?? 'generated-image'}.reference.json`,
97
+ sourceUri: artifact.uri,
98
+ metadata: { ...artifact.metadata, generatedBy: result.providerId, remote: true },
99
+ });
100
+ lines.push(` artifact: ${descriptor.id} (remote reference — not downloaded)`);
101
+ lines.push(` url: ${artifact.uri}`);
102
+ } else {
103
+ lines.push(' artifact: (no retrievable content returned)');
104
+ }
105
+ }
106
+ ctx.print(lines.join('\n'));
107
+ } catch (error) {
108
+ ctx.print(`Image generation failed: ${summarizeError(error)}`);
109
+ }
110
+ },
111
+ });
112
+ }
@@ -55,7 +55,17 @@ export function registerIntelligenceRuntimeCommands(registry: CommandRegistry):
55
55
  async handler(args, ctx) {
56
56
  const sub = (args[0] ?? 'review').toLowerCase();
57
57
  if (sub === 'panel' || sub === 'open') {
58
- openCommandPanel(ctx, 'intelligence');
58
+ // W6.1 (the purge): 'intelligence'/IntelligencePanel was
59
+ // DELETE-disposition (no surviving human surface — its read model
60
+ // stays and still backs every subcommand below). There is no alias
61
+ // to resolve through, so this prints an honest notice and opens
62
+ // Fleet instead of throwing "Unknown panel".
63
+ ctx.print('Intelligence panel retired — use /intelligence review|diagnostics|symbols|outline|definition|references|hover|repair for the live data. Opening Fleet.');
64
+ try {
65
+ openCommandPanel(ctx, 'fleet');
66
+ } catch {
67
+ // Panel registry may be unavailable in lightweight command-only contexts.
68
+ }
59
69
  return;
60
70
  }
61
71
 
@@ -10,7 +10,10 @@ export function handleLocalAuthCommand(args: string[], ctx: CommandContext): voi
10
10
  const sub = (args[0] ?? 'review').toLowerCase();
11
11
  const auth = requireLocalUserAuthManager(ctx);
12
12
  if (sub === 'panel' || sub === 'open') {
13
- openCommandPanel(ctx, 'local-auth');
13
+ // W6.1: browse view moved to local-auth-modal. The LocalAuthPanel itself is
14
+ // kept (masked password-entry host) but is no longer the /local-auth panel
15
+ // destination — masked entry is reached via add-user/rotate-password below.
16
+ ctx.openModal?.('local-auth-modal');
14
17
  return;
15
18
  }
16
19
 
@@ -70,8 +70,14 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
70
70
  handler(args, ctx) {
71
71
  const sub = (args[0] ?? '').toLowerCase();
72
72
  if (sub === 'panel' || sub === 'review') {
73
+ // W6.1 (the purge): 'tools'/ToolInspectorPanel was DELETE-disposition
74
+ // (no surviving human surface — tool results render inline in the
75
+ // transcript, plus a per-node tool list in Fleet). There is no alias
76
+ // to resolve through, so this prints an honest notice and opens
77
+ // Fleet instead of the retired inspector.
78
+ ctx.print('Tools panel retired — tool activity now renders inline in the transcript and per-agent in the Fleet panel. Opening Fleet.');
73
79
  try {
74
- openCommandPanel(ctx, 'tools');
80
+ openCommandPanel(ctx, 'fleet');
75
81
  } catch {
76
82
  // Panel registry may be unavailable in lightweight command-only contexts.
77
83
  }
@@ -79,9 +85,9 @@ export function registerLocalRuntimeCommands(registry: CommandRegistry): void {
79
85
  ctx.print([
80
86
  'Tool Surface Review',
81
87
  ' Native file tools stay compact by default.',
82
- ' Read/write/edit/notebook capabilities are available through the native tool stack, with detail routed to the tools panel and approval surfaces instead of transcript bloat.',
88
+ ' Read/write/edit/notebook capabilities are available through the native tool stack, with detail routed inline and to approval surfaces instead of transcript bloat.',
83
89
  ' Shell and native tool approvals classify work into read, mutation, destructive, dependency, config, notebook, network, remote, and lifecycle risk families.',
84
- ' Use /tools panel to inspect risk class, output-policy actions, spill posture, compact summaries, and approval posture for recent calls.',
90
+ ' Use /tools panel to jump to the Fleet panel for live per-agent tool activity.',
85
91
  ' Use /approval review shell or /approval review file when you need the action-specific why-prompted posture.',
86
92
  ].join('\n'));
87
93
  }