@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,338 @@
1
+ // ---------------------------------------------------------------------------
2
+ // workstream-runtime.ts — /workstream
3
+ //
4
+ // UX over the OrchestrationEngine (@pellux/goodvibes-sdk/platform/orchestration,
5
+ // wo701/W4.1) via its command-facing facade wired onto RuntimeServices as
6
+ // `workstreamCommands` (src/runtime/workstream-services.ts) and threaded
7
+ // through CommandContext.session.workstreamEngine exactly like wrfcController
8
+ // already is (bootstrap-command-context.ts).
9
+ //
10
+ // Render precedent: TRANSCRIPT + subcommand approve (like /plan approve,
11
+ // planning-runtime.ts), NOT a panel — a multi-phase proposal is too rich for
12
+ // a one-line confirm overlay, and Pillar-3 doctrine keeps work visible in the
13
+ // transcript. create -> approve -> launch is a real three-step flow (edit and
14
+ // cancel apply to the pending proposal too): the engine's own createWorkstream
15
+ // immediately materializes a real, ticking-eligible Workstream with no
16
+ // pre-creation "draft" concept, so approval happens on a TUI-held draft
17
+ // (workstream-services.ts's WorkstreamDraft) before anything is created in
18
+ // the engine at all — see that module's header doc for the full reality-wins
19
+ // divergence from the wo703 design brief.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ import { AdaptivePlanner } from '@pellux/goodvibes-sdk/platform/core';
23
+ import type { PhaseKind, PhaseRole, WorkItemState, Workstream } from '@pellux/goodvibes-sdk/platform/orchestration';
24
+ import type { WorkstreamCommandService, WorkstreamDraft } from '../../runtime/workstream-services.ts';
25
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Formatting helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function shortId(id: string): string {
32
+ return id.length > 12 ? id.slice(0, 12) : id;
33
+ }
34
+
35
+ /**
36
+ * The engine's ONLY two terminal work-item states (mirrors the SDK's own
37
+ * internal TERMINAL_ITEM_STATES in platform/runtime/fleet/adapters/
38
+ * orchestration.ts and engine.ts's kill() guard — neither is exported, so
39
+ * this is kept in lockstep by hand). Deriving "active" as NOT-terminal
40
+ * instead of enumerating the non-terminal states means a state this module
41
+ * doesn't know about yet (as 'blocked-budget' once was here) is still
42
+ * correctly treated as in-flight rather than silently falling through the
43
+ * cancel path.
44
+ */
45
+ const TERMINAL_ITEM_STATES = new Set<WorkItemState>(['passed', 'failed']);
46
+
47
+ function isActiveItemState(state: WorkItemState): boolean {
48
+ return !TERMINAL_ITEM_STATES.has(state);
49
+ }
50
+
51
+ /** Mirrors the engine's templateForPhase (phase-runner.ts): only 'review'/'gate' phases run the general template — everything else, INCLUDING 'custom', runs the engineer template regardless of phase.role's text. */
52
+ function templateForPhaseKind(kind: PhaseKind): string {
53
+ return kind === 'review' || kind === 'gate' ? 'general' : 'engineer';
54
+ }
55
+
56
+ /**
57
+ * phase.role for a 'custom' phase is the free-text description passed to
58
+ * `/workstream insert-phase` — it is purely COSMETIC. Neither
59
+ * templateForPhase nor buildPhaseTask (phase-runner.ts, engine-side) ever
60
+ * reads it: a custom phase always runs the engineer template against the
61
+ * work item's own task text. Rendering it as `<kind> — <role>` like a real
62
+ * role would falsely imply the description drives what the phase does, so
63
+ * custom phases get an explicit "this is a description, not a role" label
64
+ * instead.
65
+ */
66
+ function formatPhaseLabel(phase: { readonly kind: PhaseKind; readonly role: PhaseRole }): string {
67
+ if (phase.kind === 'custom') {
68
+ return `custom: "${phase.role}" (runs the ${templateForPhaseKind(phase.kind)} template)`;
69
+ }
70
+ return `${phase.kind} — ${phase.role}`;
71
+ }
72
+
73
+ function summarizeItemStates(ws: Workstream): string {
74
+ const counts = new Map<string, number>();
75
+ for (const item of ws.items) counts.set(item.state, (counts.get(item.state) ?? 0) + 1);
76
+ return Array.from(counts.entries()).map(([state, n]) => `${n} ${state}`).join(', ') || 'no items';
77
+ }
78
+
79
+ /** The engine's own PlanProposal is deliberately not rendered — see workstream-services.ts's buildSpec doc. This renders the REAL launchable spec instead, so the proposal and the launch are always the same plan. */
80
+ function renderDraftProposal(draft: WorkstreamDraft): string {
81
+ const lines: string[] = [];
82
+ lines.push(`Workstream proposal ${draft.id} — "${draft.task}"`);
83
+ lines.push(
84
+ `Planner: strategy=${draft.gate.strategy} (${draft.gate.reasonCode}) — ${AdaptivePlanner.explainReasonCode(draft.gate.reasonCode)}`,
85
+ );
86
+ lines.push('Phases:');
87
+ draft.spec.phases.forEach((phase, i) => {
88
+ lines.push(` ${i + 1}. ${formatPhaseLabel(phase)} (capacity ${phase.capacity})`);
89
+ });
90
+ lines.push('Work items:');
91
+ for (const item of draft.spec.items) {
92
+ lines.push(` - ${item.title}`);
93
+ }
94
+ lines.push(
95
+ draft.approved
96
+ ? `Approved. Launch with: /workstream launch ${draft.id}`
97
+ : `Approve with: /workstream approve ${draft.id}`,
98
+ );
99
+ lines.push(`Edit the task: /workstream edit ${draft.id} <new task...> Discard: /workstream cancel ${draft.id}`);
100
+ // Honest limitation (see workstream-services.ts's REALITY-WINS doc): the
101
+ // engine has no pre-creation draft concept, so this proposal is
102
+ // process-lifetime, in-memory state only — never journaled like a launched
103
+ // Workstream is. State that plainly, the same way an unsent chat draft
104
+ // would be, rather than letting a restart silently make it vanish.
105
+ lines.push('Note: not saved — lost if the TUI restarts before launch.');
106
+ return lines.join('\n');
107
+ }
108
+
109
+ function renderWorkstreamStatus(ws: Workstream): string {
110
+ const lines: string[] = [];
111
+ lines.push(`Workstream ${ws.id} — "${ws.title}"`);
112
+ lines.push('Phases:');
113
+ for (const phase of ws.phases) {
114
+ lines.push(` [${phase.ordinal}] ${formatPhaseLabel(phase)} (capacity ${phase.capacity})`);
115
+ }
116
+ lines.push('Items:');
117
+ for (const item of ws.items) {
118
+ lines.push(` ${shortId(item.id)} [${item.state}] ${item.title} — phase: ${item.currentPhaseId ?? '—'}`);
119
+ }
120
+ return lines.join('\n');
121
+ }
122
+
123
+ function renderWorkstreamList(service: WorkstreamCommandService): string {
124
+ const drafts = service.listDrafts();
125
+ const live = service.engine.listWorkstreams();
126
+ if (drafts.length === 0 && live.length === 0) {
127
+ return 'No workstreams yet. Use /workstream create <task...> to propose one.';
128
+ }
129
+ const sections: string[] = [];
130
+ if (drafts.length > 0) {
131
+ const rows = drafts.map((d) => ` ${d.id} ${d.approved ? '[approved]' : '[draft] '} "${d.task}"`);
132
+ sections.push([`Pending proposals (${drafts.length}, not yet launched):`, ...rows].join('\n'));
133
+ }
134
+ if (live.length > 0) {
135
+ const rows = live.map((ws) => ` ${shortId(ws.id)} "${ws.title}" (${ws.phases.length} phases, ${summarizeItemStates(ws)})`);
136
+ sections.push([`Workstreams (${live.length}):`, ...rows].join('\n'));
137
+ }
138
+ return sections.join('\n\n');
139
+ }
140
+
141
+ function resolveWorkstream(service: WorkstreamCommandService, ref: string): Workstream | undefined {
142
+ const exact = service.engine.getWorkstream(ref);
143
+ if (exact) return exact;
144
+ return service.engine.listWorkstreams().find((ws) => ws.id.startsWith(ref));
145
+ }
146
+
147
+ /**
148
+ * approve/edit/launch all fail this way when `id` doesn't resolve to a held
149
+ * draft. A bare "No pending proposal found" reads like a typo'd id even when
150
+ * the real cause is that drafts are process-lifetime, in-memory-only state
151
+ * (see renderDraftProposal's note and workstream-services.ts's REALITY-WINS
152
+ * doc) and a TUI restart wiped every one of them. Only hint at that when the
153
+ * service is holding zero drafts at all — with other drafts present, a typo
154
+ * or stale id is the more honest guess and the plain message stays.
155
+ */
156
+ function draftNotFoundMessage(service: WorkstreamCommandService, id: string): string {
157
+ const base = `No pending proposal found: ${id}`;
158
+ if (service.listDrafts().length > 0) return base;
159
+ return `${base} (proposals aren't saved — a TUI restart since you created it would explain this; recreate it with /workstream create)`;
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Command
164
+ // ---------------------------------------------------------------------------
165
+
166
+ export function registerWorkstreamRuntimeCommands(registry: CommandRegistry): void {
167
+ registry.register({
168
+ name: 'workstream',
169
+ description: 'Author and oversee multi-phase agent workstreams (orchestration engine)',
170
+ usage: 'create <task...> | list | status [id] | insert-phase <id> <description...> | approve <id> | edit <id> <task...> | launch <id> | cancel <id>',
171
+ argsHint: 'create <task> | list | status [id] | approve | edit | launch | cancel',
172
+ handler: async (args, ctx: CommandContext) => {
173
+ const service = ctx.session.workstreamEngine;
174
+ if (!service) {
175
+ ctx.print('Workstreams are not available in this session.');
176
+ return;
177
+ }
178
+
179
+ const sub = args[0];
180
+
181
+ if (!sub || sub === 'list') {
182
+ ctx.print(renderWorkstreamList(service));
183
+ return;
184
+ }
185
+
186
+ if (sub === 'create') {
187
+ const task = args.slice(1).join(' ').trim();
188
+ if (!task) {
189
+ ctx.print('Usage: /workstream create <task...>');
190
+ return;
191
+ }
192
+ const draft = service.proposeDraft(task);
193
+ ctx.print(renderDraftProposal(draft));
194
+ return;
195
+ }
196
+
197
+ if (sub === 'status') {
198
+ const ref = args[1];
199
+ const live = service.engine.listWorkstreams();
200
+ const target = ref ? resolveWorkstream(service, ref) : (live.length === 1 ? live[0] : undefined);
201
+ if (!target) {
202
+ if (!ref && live.length > 1) {
203
+ ctx.print(`Multiple workstreams running — specify one: ${live.map((ws) => shortId(ws.id)).join(', ')}`);
204
+ return;
205
+ }
206
+ ctx.print(ref ? `No workstream found: ${ref}` : 'No workstreams running. Use /workstream list to see pending proposals.');
207
+ return;
208
+ }
209
+ ctx.print(renderWorkstreamStatus(target));
210
+ return;
211
+ }
212
+
213
+ if (sub === 'insert-phase') {
214
+ const id = args[1];
215
+ const description = args.slice(2).join(' ').trim();
216
+ if (!id || !description) {
217
+ ctx.print('Usage: /workstream insert-phase <id> <description...>');
218
+ return;
219
+ }
220
+ const ws = resolveWorkstream(service, id);
221
+ if (!ws) {
222
+ ctx.print(`No workstream found: ${id}`);
223
+ return;
224
+ }
225
+ const lastOrdinal = ws.phases.reduce((max, phase) => Math.max(max, phase.ordinal), 0);
226
+ const templateGate = ws.phases.at(-1)?.gate ?? { scope: 'scoped' as const, gates: [] };
227
+ const inserted = service.engine.insertPhase(ws.id, lastOrdinal, {
228
+ role: description,
229
+ capacity: 1,
230
+ kind: 'custom',
231
+ gate: templateGate,
232
+ });
233
+ if (!inserted) {
234
+ ctx.print(`Could not insert phase — is "${id}" still an active workstream?`);
235
+ return;
236
+ }
237
+ ctx.print(`Inserted phase "${description}" into ${shortId(ws.id)} after ordinal ${lastOrdinal} (new ordinal ${inserted.ordinal}).`);
238
+ return;
239
+ }
240
+
241
+ if (sub === 'approve') {
242
+ const id = args[1];
243
+ if (!id) {
244
+ ctx.print('Usage: /workstream approve <id>');
245
+ return;
246
+ }
247
+ const draft = service.approveDraft(id);
248
+ if (!draft) {
249
+ ctx.print(draftNotFoundMessage(service, id));
250
+ return;
251
+ }
252
+ ctx.print(`Approved ${id}. Launch with: /workstream launch ${id}`);
253
+ return;
254
+ }
255
+
256
+ if (sub === 'edit') {
257
+ const id = args[1];
258
+ const task = args.slice(2).join(' ').trim();
259
+ if (!id || !task) {
260
+ ctx.print('Usage: /workstream edit <id> <new task...>');
261
+ return;
262
+ }
263
+ const draft = service.editDraft(id, task);
264
+ if (!draft) {
265
+ ctx.print(draftNotFoundMessage(service, id));
266
+ return;
267
+ }
268
+ ctx.print(renderDraftProposal(draft));
269
+ return;
270
+ }
271
+
272
+ if (sub === 'launch') {
273
+ const id = args[1];
274
+ if (!id) {
275
+ ctx.print('Usage: /workstream launch <id>');
276
+ return;
277
+ }
278
+ const draft = service.getDraft(id);
279
+ if (!draft) {
280
+ ctx.print(draftNotFoundMessage(service, id));
281
+ return;
282
+ }
283
+ if (!draft.approved) {
284
+ ctx.print(`Proposal ${id} is not approved yet. Run /workstream approve ${id} first.`);
285
+ return;
286
+ }
287
+ const result = service.launchDraft(id);
288
+ if (!result) {
289
+ ctx.print(`Could not launch ${id}.`);
290
+ return;
291
+ }
292
+ ctx.print(`Launched workstream ${result.workstreamId} — track it with /workstream status ${result.workstreamId} or the Fleet panel.`);
293
+ return;
294
+ }
295
+
296
+ if (sub === 'cancel') {
297
+ const id = args[1];
298
+ if (!id) {
299
+ ctx.print('Usage: /workstream cancel <id>');
300
+ return;
301
+ }
302
+ if (service.getDraft(id)) {
303
+ service.removeDraft(id);
304
+ ctx.print(`Discarded pending proposal ${id}.`);
305
+ return;
306
+ }
307
+ const ws = resolveWorkstream(service, id);
308
+ if (!ws) {
309
+ ctx.print(`No workstream or pending proposal found: ${id}`);
310
+ return;
311
+ }
312
+ const active = ws.items.filter((item) => isActiveItemState(item.state));
313
+ let killedCount = 0;
314
+ for (const item of active) {
315
+ if (service.engine.kill(item.id)) killedCount++;
316
+ }
317
+ ctx.print(
318
+ killedCount > 0
319
+ ? `Cancelled ${killedCount} of ${active.length} in-flight work item(s) in ${shortId(ws.id)}.`
320
+ : `${shortId(ws.id)} has no in-flight work items to cancel.`,
321
+ );
322
+ return;
323
+ }
324
+
325
+ ctx.print(
326
+ 'Usage:\n'
327
+ + ' /workstream create <task...>\n'
328
+ + ' /workstream list\n'
329
+ + ' /workstream status [id]\n'
330
+ + ' /workstream insert-phase <id> <description...>\n'
331
+ + ' /workstream approve <id>\n'
332
+ + ' /workstream edit <id> <new task...>\n'
333
+ + ' /workstream launch <id>\n'
334
+ + ' /workstream cancel <id>',
335
+ );
336
+ },
337
+ });
338
+ }
@@ -15,6 +15,7 @@ import { registerOperatorRuntimeCommands } from './commands/operator-runtime.ts'
15
15
  import { registerPluginRuntimeCommands } from './commands/plugin-runtime.ts';
16
16
  import { registerDiffRuntimeCommands } from './commands/diff-runtime.ts';
17
17
  import { registerGitRuntimeCommands } from './commands/git-runtime.ts';
18
+ import { registerTestRuntimeCommands } from './commands/test-runtime.ts';
18
19
  import { registerNotifyRuntimeCommands } from './commands/notify-runtime.ts';
19
20
  import { registerReplayRuntimeCommands } from './commands/replay-runtime.ts';
20
21
  import { registerShareRuntimeCommands } from './commands/share-runtime.ts';
@@ -36,6 +37,9 @@ import { registerHooksRuntimeCommands } from './commands/hooks-runtime.ts';
36
37
  import { registerControlRoomRuntimeCommands } from './commands/control-room-runtime.ts';
37
38
  import { registerMcpRuntimeCommands } from './commands/mcp-runtime.ts';
38
39
  import { registerSessionContentCommands } from './commands/session-content.ts';
40
+ import { registerCheckpointRuntimeCommands } from './commands/checkpoint-runtime.ts';
41
+ import { registerWorkstreamRuntimeCommands } from './commands/workstream-runtime.ts';
42
+ import { registerCodebaseRuntimeCommands } from './commands/codebase-runtime.ts';
39
43
  import { registerLocalRuntimeCommands } from './commands/local-runtime.ts';
40
44
  import { registerExperienceRuntimeCommands } from './commands/experience-runtime.ts';
41
45
  import { registerIncidentRuntimeCommands } from './commands/incident-runtime.ts';
@@ -57,6 +61,8 @@ import { registerTtsRuntimeCommands } from './commands/tts-runtime.ts';
57
61
  import { registerCloudflareRuntimeCommands } from './commands/cloudflare-runtime.ts';
58
62
  import { registerWorkPlanRuntimeCommands } from './commands/work-plan-runtime.ts';
59
63
  import { registerCostRuntimeCommands } from './commands/cost-runtime.ts';
64
+ import { registerWebSearchRuntimeCommands } from './commands/websearch-runtime.ts';
65
+ import { registerImageRuntimeCommands } from './commands/image-runtime.ts';
60
66
 
61
67
  /**
62
68
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -69,6 +75,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
69
75
  registerPluginRuntimeCommands(registry);
70
76
  registerDiffRuntimeCommands(registry);
71
77
  registerGitRuntimeCommands(registry);
78
+ registerTestRuntimeCommands(registry);
72
79
  registerNotifyRuntimeCommands(registry);
73
80
  registerReplayRuntimeCommands(registry);
74
81
  registerShareRuntimeCommands(registry);
@@ -115,6 +122,11 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
115
122
  registerScheduleRuntimeCommands(registry);
116
123
  registerBranchRuntimeCommands(registry);
117
124
  registerSessionContentCommands(registry);
125
+ registerCheckpointRuntimeCommands(registry);
126
+ registerWorkstreamRuntimeCommands(registry);
127
+ registerCodebaseRuntimeCommands(registry);
128
+ registerWebSearchRuntimeCommands(registry);
129
+ registerImageRuntimeCommands(registry);
118
130
 
119
131
  // ── /policy ───────────────────────────────────────────────────────────────
120
132
  registry.register(policyCommand);
@@ -0,0 +1,161 @@
1
+ import type { ModalSectionStyle } from '../renderer/modal-factory.ts';
2
+
3
+ /**
4
+ * Config-modal host — the shared seam every W6.1 MIGRATE-TO-MODAL surface
5
+ * (provider-health, services, subscription, remote, local-auth, settings-sync,
6
+ * sandbox, and the WO-B ecosystem/governance set) is expressed through.
7
+ *
8
+ * A surface is PURE DATA + ACTIONS: it hands the host a `buildView()` that maps
9
+ * live read-models to a tabbed, list-structured view, plus a declarative action
10
+ * table. The host owns all key routing (tab switch, list nav, Esc, confirm) and
11
+ * the stable-layout liveness contract — the surface never touches the input
12
+ * system directly (the charter's "do not invent a parallel input system").
13
+ *
14
+ * Liveness doctrine (charter: "live data, stable layout"): `buildView()` is
15
+ * called every render tick so values (latency, status, counts) update in place,
16
+ * but the host renders against the row/tab STRUCTURE captured at the last
17
+ * interaction boundary (open, or a key press). Rows never reflow under the
18
+ * cursor mid-interaction; a structural change (a row appears/disappears) is
19
+ * deferred until the next key press. See ConfigModal for the mechanism.
20
+ */
21
+
22
+ /** A single row within a tab. Identity is `id` (structural); `label` is live. */
23
+ export interface ConfigModalRow {
24
+ /**
25
+ * Stable identity across live-refresh ticks. The liveness contract keys
26
+ * structural stability off this: two builds with the same ordered id set are
27
+ * "structurally identical" and repaint values in place; a differing id set is
28
+ * a structural change, deferred to the next interaction boundary.
29
+ */
30
+ readonly id: string;
31
+ /** Display label. MAY embed live value fields (latency, status dot, counts). */
32
+ readonly label: string;
33
+ /** Optional per-row style (e.g. bad/warn tone for an errored row). */
34
+ readonly style?: ModalSectionStyle;
35
+ /**
36
+ * Non-selectable informational row (sub-header, spacer text). Skipped by
37
+ * up/down navigation and never receives an action. Defaults to selectable.
38
+ */
39
+ readonly selectable?: boolean;
40
+ }
41
+
42
+ /** One tab (section) of a surface. Tabs are switched with left/right or Tab. */
43
+ export interface ConfigModalTab {
44
+ /** Stable id — preserves the active tab across refresh + re-open. */
45
+ readonly id: string;
46
+ /** Tab-strip label. */
47
+ readonly label: string;
48
+ /** Live status/posture lines rendered above the list (non-selectable). */
49
+ readonly header?: readonly string[];
50
+ /** Selectable/informational rows. */
51
+ readonly rows: readonly ConfigModalRow[];
52
+ /** Honest empty-state text shown when `rows` is empty. */
53
+ readonly emptyText?: string;
54
+ /** Footer hints specific to this tab, appended after the surface hints. */
55
+ readonly hints?: readonly string[];
56
+ }
57
+
58
+ /** The whole surface view for one render tick. */
59
+ export interface ConfigModalView {
60
+ /** Live title (may change tick-to-tick; not structural). */
61
+ readonly title: string;
62
+ readonly tabs: readonly ConfigModalTab[];
63
+ /**
64
+ * Non-null when the read-model is unavailable/degraded — the host renders a
65
+ * banner and still shows whatever tabs/rows are present (honest degraded
66
+ * state rather than a blank or a throw).
67
+ */
68
+ readonly degraded?: string;
69
+ /** Base footer hints (Esc close is always appended by the host). */
70
+ readonly hints?: readonly string[];
71
+ }
72
+
73
+ /** Context handed to a surface's action handler when an action key fires. */
74
+ export interface ConfigModalActionContext {
75
+ /** The selected row the action targets (null when the active tab has none). */
76
+ readonly row: ConfigModalRow | null;
77
+ /** The active tab's id. */
78
+ readonly tabId: string;
79
+ /** Print a line to the conversation transcript. */
80
+ readonly print: (message: string) => void;
81
+ /**
82
+ * Execute a slash command — the same seam panels used via
83
+ * `PanelIntegrationContext.executeCommand`. This is how a migrated action
84
+ * preserves parity: the mutation still runs through its existing, tested
85
+ * command (e.g. `/settings-sync resolve`, `/local-auth delete-user`).
86
+ */
87
+ readonly executeCommand?: (name: string, args: string[]) => Promise<unknown>;
88
+ /** Open another named modal surface (cross-surface navigation, e.g. the
89
+ * services surface jumping to the subscription surface). Swaps the active
90
+ * surface in place — the same seam ctx.openModal uses. */
91
+ readonly openModal?: (name: string) => void;
92
+ /** Request a re-render. */
93
+ readonly requestRender: () => void;
94
+ /** Set the modal's transient status line (e.g. a result or error message). */
95
+ readonly setStatus: (message: string) => void;
96
+ /** Close the modal (focus returns to whatever opened it). */
97
+ readonly close: () => void;
98
+ }
99
+
100
+ /** A declarative action bound to a key. */
101
+ export interface ConfigModalAction {
102
+ /**
103
+ * Trigger key: a single printable char ('r', 'd') or a named key ('enter').
104
+ * Must not collide with the host-reserved nav keys (up/down/left/right/tab/
105
+ * j/k/escape) — those are consumed by the host before actions are consulted.
106
+ */
107
+ readonly key: string;
108
+ /** Action id passed to `onAction`. */
109
+ readonly id: string;
110
+ /** Short verb for the footer hint (e.g. "refresh", "delete user"). */
111
+ readonly label: string;
112
+ /**
113
+ * Destructive: require an explicit second press of the same key to fire
114
+ * (the host shows "press <key> again to <label>" between presses). Matches
115
+ * the session-picker delete-confirm precedent.
116
+ */
117
+ readonly confirm?: boolean;
118
+ /**
119
+ * When set, the action is only offered while the selected row's id satisfies
120
+ * this predicate (e.g. sandbox "stop" only on a session row). Rows failing
121
+ * the predicate neither show the hint nor fire the action.
122
+ */
123
+ readonly enabledFor?: (row: ConfigModalRow | null, tabId: string) => boolean;
124
+ }
125
+
126
+ /**
127
+ * A named config-modal surface. Pure seam — the host knows nothing about the
128
+ * surface's data. Built once (closing over its read-models) and registered on
129
+ * PanelManager via `registerModalSurface`; opened by name from a panel-id
130
+ * redirect hit or `ctx.openModal(name)`.
131
+ */
132
+ export interface ConfigModalSurface {
133
+ /** Modal name — the key `registerModalRedirect` / `ctx.openModal` use. */
134
+ readonly name: string;
135
+ /** Fallback title (buildView().title wins per tick). */
136
+ readonly title: string;
137
+ /**
138
+ * Build the current view from live read-models. Called on open, on every
139
+ * interaction boundary, AND on every render tick. MUST be pure and cheap —
140
+ * snapshot reads only, no async, no mutation.
141
+ */
142
+ buildView(): ConfigModalView;
143
+ /** Declarative action table (optional — a read-only surface has none). */
144
+ readonly actions?: readonly ConfigModalAction[];
145
+ /** Handle an action fired by its key. */
146
+ onAction?(actionId: string, ctx: ConfigModalActionContext): void;
147
+ /**
148
+ * Optional lifecycle: called when the modal opens this surface and when it
149
+ * closes. A live surface uses `onOpen` to subscribe to its read-model/events
150
+ * (calling the provided `requestRender` on change so live values refresh
151
+ * between key presses) and `onClose` to unsubscribe. The host guarantees
152
+ * `onClose` runs exactly once per `onOpen`.
153
+ */
154
+ onOpen?(requestRender: () => void): void;
155
+ onClose?(): void;
156
+ }
157
+
158
+ /** Read-only registry the host consults to resolve a modal name to a surface. */
159
+ export interface ConfigModalSurfaceRegistry {
160
+ getModalSurface(name: string): ConfigModalSurface | undefined;
161
+ }