@pellux/goodvibes-tui 1.0.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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. package/src/renderer/qr-renderer.ts +0 -120
@@ -0,0 +1,101 @@
1
+ import type { CommandRegistry } from '../command-registry.ts';
2
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
3
+
4
+ interface ParsedSearchArgs {
5
+ readonly query: string;
6
+ readonly limit?: number;
7
+ }
8
+
9
+ function parseSearchArgs(args: string[]): ParsedSearchArgs {
10
+ const words: string[] = [];
11
+ let limit: number | undefined;
12
+ for (let i = 0; i < args.length; i++) {
13
+ const token = args[i]!;
14
+ if ((token === '--limit' || token === '-n') && i + 1 < args.length) {
15
+ const parsed = Number.parseInt(args[++i]!, 10);
16
+ if (Number.isFinite(parsed) && parsed > 0) limit = parsed;
17
+ } else {
18
+ words.push(token);
19
+ }
20
+ }
21
+ return { query: words.join(' ').trim(), ...(limit !== undefined ? { limit } : {}) };
22
+ }
23
+
24
+ /**
25
+ * registerWebSearchRuntimeCommands - `/search <query> [--limit <n>]`.
26
+ *
27
+ * Calls webSearchService.search() directly (skipping the agent-tool wrapper's
28
+ * JSON-stringify contract — same shape as /schedule's ctx.ops.automationManager
29
+ * pattern: read the service off the context, guard on undefined, print) and
30
+ * renders ranked results + the instant answer into the transcript with
31
+ * source labels. webSearchService is already constructed in services.ts and
32
+ * already agent-reachable; this is a second, direct-command consumer of the
33
+ * same instance (ctx.platform.webSearchService).
34
+ */
35
+ export function registerWebSearchRuntimeCommands(registry: CommandRegistry): void {
36
+ registry.register({
37
+ name: 'search',
38
+ description: 'Search the web and render ranked results with source labels',
39
+ usage: '<query> [--limit <n>]',
40
+ argsHint: '<query> [--limit <n>]',
41
+ async handler(args, ctx) {
42
+ const service = ctx.platform.webSearchService;
43
+ if (!service) {
44
+ ctx.print('Web search is not available in this session.');
45
+ return;
46
+ }
47
+
48
+ const { query, limit } = parseSearchArgs(args);
49
+ if (!query) {
50
+ ctx.print('Usage: /search <query> [--limit <n>]');
51
+ return;
52
+ }
53
+
54
+ try {
55
+ const response = await service.search({
56
+ query,
57
+ ...(limit !== undefined ? { maxResults: limit } : {}),
58
+ });
59
+
60
+ const lines: string[] = [`Search: "${response.query}" (source: ${response.providerLabel})`];
61
+
62
+ const instantAnswer = response.instantAnswer;
63
+ if (instantAnswer) {
64
+ const heading = instantAnswer.heading ?? instantAnswer.source ?? 'Instant answer';
65
+ const body = instantAnswer.answer ?? instantAnswer.abstract;
66
+ if (body) {
67
+ lines.push('', `${heading}: ${body}`);
68
+ if (instantAnswer.url) lines.push(` ${instantAnswer.url}`);
69
+ }
70
+ }
71
+
72
+ if (response.results.length === 0) {
73
+ lines.push('', 'No results found.');
74
+ } else {
75
+ lines.push('');
76
+ for (const result of response.results) {
77
+ const title = result.title ?? result.displayUrl ?? result.url;
78
+ lines.push(`${result.rank}. ${title}`);
79
+ if (result.snippet) lines.push(` ${result.snippet}`);
80
+ lines.push(` ${result.url}`);
81
+ }
82
+ }
83
+
84
+ ctx.print(lines.join('\n'));
85
+ } catch (error) {
86
+ // Honest degradation: read the thrown error / the service's own status
87
+ // surface rather than inventing an env-var name. WebSearchService.search()
88
+ // throws (not a {success:false} result field) when no provider is
89
+ // registered or the provider itself fails.
90
+ let detail = summarizeError(error);
91
+ try {
92
+ const status = await service.getStatus();
93
+ if (!status.enabled) detail = `${detail} (${status.note})`;
94
+ } catch {
95
+ // getStatus() itself failing is not worth compounding into the error message.
96
+ }
97
+ ctx.print(`Search failed: ${detail}`);
98
+ }
99
+ },
100
+ });
101
+ }
@@ -1,7 +1,7 @@
1
1
  import type { CommandRegistry } from '../command-registry.ts';
2
2
  import type { WorkPlanItemStatus, WorkPlanStore } from '../../work-plans/work-plan-store.ts';
3
3
  import { WORK_PLAN_STATUSES } from '../../work-plans/work-plan-store.ts';
4
- import { requirePanelManager } from './runtime-services.ts';
4
+ import { openModalCommand } from './runtime-services.ts';
5
5
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
6
6
 
7
7
  const STATUS_COMMANDS: Record<string, WorkPlanItemStatus> = {
@@ -25,14 +25,8 @@ function getStore(ctx: import('../command-registry.ts').CommandContext): WorkPla
25
25
  }
26
26
 
27
27
  function openPanel(ctx: import('../command-registry.ts').CommandContext): void {
28
- if (ctx.showPanel) {
29
- ctx.showPanel('work-plan');
30
- return;
31
- }
32
- const panelManager = requirePanelManager(ctx);
33
- panelManager.open('work-plan');
34
- panelManager.show();
35
- ctx.renderRequest();
28
+ // W6.1: work-plan migrated to the 'work-plan' modal — open it via the seam.
29
+ openModalCommand(ctx, 'work-plan-modal');
36
30
  }
37
31
 
38
32
  function formatList(store: WorkPlanStore): string {
@@ -81,7 +75,7 @@ export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void
81
75
  name: 'work-plan',
82
76
  aliases: ['wp', 'todo', 'workplan'],
83
77
  description: 'Track a persistent workspace-scoped work plan',
84
- usage: '[panel|list|show|add <title> [--owner name] [--source label] [--notes text]|done <id>|start <id>|block <id>|fail <id>|cancel <id>|pending <id>|remove <id>|clear-done]',
78
+ usage: '[panel|list|show|export|add <title> [--owner name] [--source label] [--notes text]|edit <id> [<new title>] [--owner name] [--notes text]|done <id>|start <id>|block <id>|fail <id>|cancel <id>|pending <id>|remove <id>|clear-done]',
85
79
  argsHint: '[panel|add|list|done]',
86
80
  handler(args, ctx) {
87
81
  const store = getStore(ctx);
@@ -104,6 +98,14 @@ export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void
104
98
  ctx.print(store.toMarkdown());
105
99
  return;
106
100
  }
101
+ if (subcommand === 'export') {
102
+ // Thin wrapper over WorkPlanStore.exportMarkdown() — writes the plan's
103
+ // markdown alongside the JSON and reports the path. (W6 command-path
104
+ // parity: the work-plan modal's export action routes here.)
105
+ const { path } = store.exportMarkdown();
106
+ ctx.print(`Exported work plan markdown to ${path}`);
107
+ return;
108
+ }
107
109
  if (subcommand === 'add') {
108
110
  const parsed = parseAddArgs(args.slice(1));
109
111
  if (!parsed.title) {
@@ -120,6 +122,30 @@ export function registerWorkPlanRuntimeCommands(registry: CommandRegistry): void
120
122
  ctx.print(`Added work plan item ${item.id}.`);
121
123
  return;
122
124
  }
125
+ if (subcommand === 'edit' || subcommand === 'update') {
126
+ // Thin wrapper over WorkPlanStore.updateItem(idOrPrefix, patch).
127
+ // (W6 command-path parity: the work-plan modal's edit action routes
128
+ // here.) Only the flags the caller supplies are patched.
129
+ const id = args[1];
130
+ if (!id) {
131
+ ctx.print(`Usage: /work-plan ${subcommand} <id> [<new title>] [--owner name] [--source label] [--notes text]`);
132
+ return;
133
+ }
134
+ const parsed = parseAddArgs(args.slice(2));
135
+ const patch = {
136
+ ...(parsed.title ? { title: parsed.title } : {}),
137
+ ...(parsed.owner !== undefined ? { owner: parsed.owner } : {}),
138
+ ...(parsed.source !== undefined ? { source: parsed.source } : {}),
139
+ ...(parsed.notes !== undefined ? { notes: parsed.notes } : {}),
140
+ };
141
+ if (Object.keys(patch).length === 0) {
142
+ ctx.print(`Usage: /work-plan ${subcommand} <id> [<new title>] [--owner name] [--source label] [--notes text]`);
143
+ return;
144
+ }
145
+ const item = store.updateItem(id, patch);
146
+ ctx.print(`Updated work plan item ${item.id}: ${item.title}`);
147
+ return;
148
+ }
123
149
  if (subcommand === 'remove' || subcommand === 'delete' || subcommand === 'rm') {
124
150
  const id = args[1];
125
151
  if (!id) {
@@ -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);