@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.
- package/CHANGELOG.md +85 -21
- package/README.md +28 -18
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +90 -14
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +104 -51
- package/src/core/turn-cancellation.ts +25 -0
- package/src/core/turn-event-wiring.ts +72 -3
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/export/cost-utils.ts +36 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +84 -5
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +232 -0
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +10 -2
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +73 -35
- package/src/input/commands/operator-panel-runtime.ts +71 -31
- package/src/input/commands/planning-runtime.ts +116 -4
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +26 -6
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +16 -4
- package/src/input/commands/shell-core.ts +25 -8
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +488 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +175 -0
- package/src/input/config-modal.ts +592 -0
- package/src/input/feed-context-factory.ts +10 -8
- package/src/input/handler-command-route.ts +37 -20
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +74 -114
- package/src/input/handler-feed.ts +79 -27
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +136 -6
- package/src/input/handler-modal-stack.ts +14 -7
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +26 -102
- package/src/input/handler-shortcuts.ts +74 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +12 -29
- package/src/input/keybindings.ts +23 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +77 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +146 -2
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +49 -51
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +18 -46
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +758 -0
- package/src/panels/fleet-read-model.ts +520 -0
- package/src/panels/fleet-steer.ts +125 -0
- package/src/panels/fleet-stop.ts +153 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +166 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +42 -0
- package/src/panels/modals/pairing-modal.ts +163 -0
- package/src/panels/modals/planning-modal.ts +304 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +177 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +123 -9
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -11
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +37 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +216 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +95 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +15 -3
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +43 -1
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +62 -4
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +133 -0
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +27 -1
- package/src/runtime/bootstrap-command-parts.ts +41 -10
- package/src/runtime/bootstrap-core.ts +47 -15
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +48 -17
- package/src/runtime/bootstrap.ts +129 -22
- package/src/runtime/code-index-services.ts +135 -0
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +59 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +98 -46
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +27 -3
- package/src/runtime/workstream-services.ts +327 -0
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +68 -4
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +171 -26
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/utils/terminal-width.ts +52 -0
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- 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 {
|
|
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
|
-
|
|
29
|
-
|
|
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) {
|