@pellux/goodvibes-tui 1.1.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. package/src/renderer/qr-renderer.ts +0 -120
@@ -0,0 +1,277 @@
1
+ import { loadPackageScripts, getSkippedGateReason } from '@pellux/goodvibes-sdk/platform/agents';
2
+ import type { ToolCall } from '@pellux/goodvibes-sdk/platform/types';
3
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
4
+ import { requireShellPaths } from './runtime-services.ts';
5
+
6
+ /**
7
+ * Timeout for a /test run, in milliseconds. Deliberately much longer than
8
+ * WRFC's WRFC_GATE_TIMEOUT_MS (120s, tuned for CI gates) — this repo's own
9
+ * suite is 600+ test files (scripts/run-tests.ts), and /test is an
10
+ * interactive command with no other caller waiting on it.
11
+ *
12
+ * Exported (rather than a private const) so tests can pass a shorter override
13
+ * straight through to runTestCommand's optional third parameter instead of
14
+ * waiting out the real timeout.
15
+ */
16
+ export const TEST_RUN_TIMEOUT_MS = 300_000;
17
+
18
+ /** How often buffered stdout/stderr lines are flushed to the transcript during
19
+ * a run. Batching avoids hammering the transcript.append_one perf budget
20
+ * (6ms, scripts/perf-baseline.json) with one ctx.print() call per output line. */
21
+ const STREAM_FLUSH_INTERVAL_MS = 250;
22
+
23
+ /** Cap on individually-listed failing test file names before truncating. */
24
+ const MAX_FAILING_NAMES_SHOWN = 20;
25
+
26
+ /** Lines of raw output shown when structured results could not be parsed. */
27
+ const RAW_TAIL_LINES = 30;
28
+
29
+ const FAIL_LINE_RE = /^==> (.+?)\s+\[FAIL\]$/gm;
30
+ const SUMMARY_LINE_RE = /^Test files: (\d+), passed: (\d+), failed: (\d+)$/m;
31
+ // Plain `bun test` output: " N pass" / " N fail" counters (tests, not files),
32
+ // "Ran N tests across M files.", and "(fail) suite > name" per failing test.
33
+ const BUN_PASS_RE = /^\s*(\d+) pass\s*$/m;
34
+ const BUN_FAIL_RE = /^\s*(\d+) fail\s*$/m;
35
+ const BUN_FILES_RE = /across (\d+) files?\./;
36
+ const BUN_FAIL_TEST_RE = /^\(fail\) (.+)$/gm;
37
+
38
+ interface ParsedTestResults {
39
+ totalFiles: number;
40
+ passed: number;
41
+ failed: number;
42
+ failingFiles: string[];
43
+ /** Whether passed/failed count files (goodvibes runner) or tests (bun test). */
44
+ unit: 'files' | 'tests';
45
+ }
46
+
47
+ /**
48
+ * Parse this repo's own scripts/run-tests.ts output shape:
49
+ * per-file: `==> path/to/file.test.ts [FAIL]` (only present on failure)
50
+ * summary: `Test files: N, passed: P, failed: F`
51
+ * Returns null when the summary line isn't present at all — the fallback
52
+ * path (raw tail, no fabricated counts) covers a different project's
53
+ * jest/vitest/pytest output, or any run that bypassed run-tests.ts.
54
+ */
55
+ export function parseTestOutput(output: string): ParsedTestResults | null {
56
+ const summaryMatch = SUMMARY_LINE_RE.exec(output);
57
+ if (summaryMatch) {
58
+ const failingFiles: string[] = [];
59
+ FAIL_LINE_RE.lastIndex = 0;
60
+ let match: RegExpExecArray | null;
61
+ while ((match = FAIL_LINE_RE.exec(output)) !== null) {
62
+ failingFiles.push(match[1]!);
63
+ }
64
+ return {
65
+ totalFiles: Number(summaryMatch[1]),
66
+ passed: Number(summaryMatch[2]),
67
+ failed: Number(summaryMatch[3]),
68
+ failingFiles,
69
+ unit: 'files',
70
+ };
71
+ }
72
+ // Plain `bun test`: counts are per-test, failing entries are test names.
73
+ const bunPass = BUN_PASS_RE.exec(output);
74
+ const bunFail = BUN_FAIL_RE.exec(output);
75
+ if (bunPass && bunFail) {
76
+ const failingTests: string[] = [];
77
+ BUN_FAIL_TEST_RE.lastIndex = 0;
78
+ let match: RegExpExecArray | null;
79
+ while ((match = BUN_FAIL_TEST_RE.exec(output)) !== null) {
80
+ failingTests.push(match[1]!);
81
+ }
82
+ return {
83
+ totalFiles: Number(BUN_FILES_RE.exec(output)?.[1] ?? 0),
84
+ passed: Number(bunPass[1]),
85
+ failed: Number(bunFail[1]),
86
+ failingFiles: failingTests,
87
+ unit: 'tests',
88
+ };
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * Single-quote shell-escape a value for safe interpolation into a
95
+ * `/bin/sh -c` string: wrap in single quotes, turning any embedded single
96
+ * quote into `'\''` (close quote, escaped literal quote, reopen quote).
97
+ * Mirrors the injection-risk class already present in the SDK's
98
+ * wrfc-gates.ts (executeGateCommand) and this repo's git-runtime.ts /
99
+ * diff-runtime.ts, which build shell command strings the same way — a
100
+ * pattern arg is never string-concatenated into the command raw.
101
+ */
102
+ export function shQuote(value: string): string {
103
+ return `'${value.replace(/'/g, `'\\''`)}'`;
104
+ }
105
+
106
+ /** Reads a ReadableStream<Uint8Array> incrementally, decoding and forwarding
107
+ * each chunk of text as it arrives (display-only accumulation happens in the
108
+ * caller; this just pumps the stream to completion). */
109
+ async function pumpStream(
110
+ stream: ReadableStream<Uint8Array> | undefined,
111
+ onChunk: (text: string) => void,
112
+ ): Promise<void> {
113
+ if (!stream) return;
114
+ const decoder = new TextDecoder();
115
+ for await (const chunk of stream) {
116
+ onChunk(decoder.decode(chunk, { stream: true }));
117
+ }
118
+ }
119
+
120
+ export function registerTestRuntimeCommands(registry: CommandRegistry): void {
121
+ registry.register({
122
+ name: 'test',
123
+ aliases: [],
124
+ description: 'Run the project test script and show pass/fail results',
125
+ usage: '[pattern]',
126
+ argsHint: '[pattern]',
127
+ async handler(args, ctx) {
128
+ await runTestCommand(args, ctx);
129
+ },
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Core /test implementation, factored out of the registered handler so tests
135
+ * can call it directly with a shortened `timeoutMs` override (the timeout
136
+ * path would otherwise take TEST_RUN_TIMEOUT_MS to exercise).
137
+ */
138
+ export async function runTestCommand(
139
+ args: string[],
140
+ ctx: CommandContext,
141
+ timeoutMs: number = TEST_RUN_TIMEOUT_MS,
142
+ ): Promise<void> {
143
+ const cwd = requireShellPaths(ctx).workingDirectory;
144
+ const pkgScripts = await loadPackageScripts(cwd);
145
+ const skip = getSkippedGateReason('test', cwd, pkgScripts);
146
+ if (skip) {
147
+ ctx.print(skip);
148
+ return;
149
+ }
150
+
151
+ const pattern = args.length > 0 ? args.join(' ') : undefined;
152
+ const command = args.length > 0
153
+ ? `${pkgScripts.test} ${args.map(shQuote).join(' ')}`
154
+ : pkgScripts.test!;
155
+ const toolCall: ToolCall = { id: 'test-run', name: 'test', arguments: pattern ? { pattern } : {} };
156
+
157
+ ctx.print(`Running: ${command}`);
158
+
159
+ const startedAt = Date.now();
160
+ const proc = Bun.spawn(['/bin/sh', '-c', command], { cwd, stdout: 'pipe', stderr: 'pipe' });
161
+
162
+ let timedOut = false;
163
+ const timer = setTimeout(() => {
164
+ timedOut = true;
165
+ proc.kill();
166
+ }, timeoutMs);
167
+ timer.unref?.();
168
+
169
+ let combinedOutput = '';
170
+ let pendingDisplay = '';
171
+ const appendChunk = (text: string): void => {
172
+ combinedOutput += text;
173
+ pendingDisplay += text;
174
+ };
175
+ const flushDisplay = (force = false): void => {
176
+ if (!pendingDisplay) return;
177
+ if (force) {
178
+ ctx.print(pendingDisplay);
179
+ pendingDisplay = '';
180
+ return;
181
+ }
182
+ const lastNewline = pendingDisplay.lastIndexOf('\n');
183
+ if (lastNewline === -1) return; // no complete line buffered yet
184
+ const toPrint = pendingDisplay.slice(0, lastNewline);
185
+ pendingDisplay = pendingDisplay.slice(lastNewline + 1);
186
+ if (toPrint) ctx.print(toPrint);
187
+ };
188
+ const flushTimer = setInterval(() => flushDisplay(false), STREAM_FLUSH_INTERVAL_MS);
189
+
190
+ let exitCode: number;
191
+ try {
192
+ [exitCode] = await Promise.all([
193
+ proc.exited,
194
+ pumpStream(proc.stdout, appendChunk),
195
+ pumpStream(proc.stderr, appendChunk),
196
+ ]);
197
+ } finally {
198
+ clearTimeout(timer);
199
+ clearInterval(flushTimer);
200
+ }
201
+
202
+ const durationMs = Date.now() - startedAt;
203
+
204
+ if (timedOut) {
205
+ flushDisplay(true);
206
+ const timeoutSeconds = Math.round(timeoutMs / 1000);
207
+ ctx.print(`Test run timed out after ${timeoutSeconds}s and was killed.`);
208
+ ctx.session.conversationManager.logToolResultBlock(
209
+ toolCall,
210
+ 'error',
211
+ 'timed out',
212
+ durationMs,
213
+ `timed out after ${timeoutSeconds}s`,
214
+ );
215
+ // Durable end state: logToolResultBlock (above) renders straight into the
216
+ // display-only history buffer and never touches the real message list, so
217
+ // it does not survive the next full rebuildHistory() (which rebuilds
218
+ // strictly from conversationManager.getMessageSnapshot()) — the very next
219
+ // dirty render silently wipes it back to how the transcript looked before
220
+ // /test ran. addSystemMessage persists the same content as a real message
221
+ // (same durable pattern as /rewind's confirm notice in
222
+ // checkpoint-runtime.ts) so the timeout outcome survives like any other
223
+ // command's output. The streamed output during the run is allowed to stay
224
+ // transient — only this final state needs to persist.
225
+ ctx.session.conversationManager.addSystemMessage(
226
+ `[Test] Timed out after ${timeoutSeconds}s and was killed.`,
227
+ );
228
+ ctx.renderRequest();
229
+ return;
230
+ }
231
+
232
+ flushDisplay(true);
233
+
234
+ const ok = exitCode === 0;
235
+ const parsed = parseTestOutput(combinedOutput);
236
+
237
+ if (parsed) {
238
+ const summary = parsed.unit === 'files'
239
+ ? `${parsed.passed}/${parsed.totalFiles} files passed`
240
+ : `${parsed.passed} passed, ${parsed.failed} failed${parsed.totalFiles ? ` across ${parsed.totalFiles} file${parsed.totalFiles === 1 ? '' : 's'}` : ''}`;
241
+ const failedNoun = parsed.unit === 'files' ? 'file' : 'test';
242
+ const errorMsg = ok ? undefined : `${parsed.failed} ${failedNoun}${parsed.failed === 1 ? '' : 's'} failed`;
243
+ ctx.session.conversationManager.logToolResultBlock(toolCall, ok ? 'done' : 'error', summary, durationMs, errorMsg);
244
+ const durableLines = [`[Test] ${summary}${errorMsg ? ` — ${errorMsg}` : ''}`];
245
+ if (parsed.failingFiles.length > 0) {
246
+ const shown = parsed.failingFiles.slice(0, MAX_FAILING_NAMES_SHOWN);
247
+ const failListHeader = parsed.unit === 'files' ? 'Failing test files:' : 'Failing tests:';
248
+ const lines = [failListHeader, ...shown.map((f) => ` - ${f}`)];
249
+ const remaining = parsed.failingFiles.length - shown.length;
250
+ if (remaining > 0) lines.push(` ...and ${remaining} more`);
251
+ ctx.print(lines.join('\n'));
252
+ durableLines.push(...lines);
253
+ }
254
+ // See the timeout branch's comment above: logToolResultBlock's tool-styled
255
+ // render is display-only and does not survive the next full history
256
+ // rebuild. addSystemMessage persists the pass/fail summary AND the
257
+ // failing-file detail as a real message so both remain in the transcript
258
+ // after the command completes, not just during the ~1s before the next
259
+ // render wipes the display-only copy.
260
+ ctx.session.conversationManager.addSystemMessage(durableLines.join('\n'));
261
+ } else {
262
+ ctx.session.conversationManager.logToolResultBlock(
263
+ toolCall,
264
+ ok ? 'done' : 'error',
265
+ `exit code ${exitCode}`,
266
+ durationMs,
267
+ ok ? undefined : `exit code ${exitCode}`,
268
+ );
269
+ const tail = combinedOutput.split('\n').slice(-RAW_TAIL_LINES).join('\n');
270
+ ctx.print(`(could not parse structured test results; showing raw tail)\n${tail}`);
271
+ ctx.session.conversationManager.addSystemMessage(
272
+ `[Test] exit code ${exitCode} (could not parse structured test results)`,
273
+ );
274
+ }
275
+
276
+ ctx.renderRequest();
277
+ }
@@ -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) {