@pellux/goodvibes-tui 0.20.2 → 0.21.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 +33 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +3 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +662 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +14 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +12 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +31 -1
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/platform-sandbox-qemu.ts +60 -16
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +3 -3
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +91 -12
- package/src/input/handler-modal-token-routes.ts +3 -0
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +55 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +5 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +4 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +18 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-data.ts +304 -0
- package/src/input/settings-modal-mutations.ts +154 -0
- package/src/input/settings-modal.ts +182 -220
- package/src/main.ts +57 -57
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +4 -1
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/knowledge-panel.ts +3 -5
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +11 -10
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +77 -8
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +14 -0
- package/src/runtime/bootstrap-core.ts +121 -13
- package/src/runtime/bootstrap.ts +2 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/sandbox-qemu-templates.ts +15 -0
- package/src/runtime/services.ts +21 -0
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -40,8 +40,30 @@ type AgentTaskArgs = {
|
|
|
40
40
|
readonly [key: string]: unknown;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Guard trace
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Emitted whenever the guard changes the effective routing decision:
|
|
49
|
+
* - 'spawn-forced-wrfc' — implementation-like spawn promoted to WRFC
|
|
50
|
+
* - 'spawn-suppressed-wrfc' — spawn judged read-only; WRFC suppressed
|
|
51
|
+
* - 'batch-collapsed-to-wrfc'— batch-spawn collapsed into a single WRFC owner chain
|
|
52
|
+
*/
|
|
53
|
+
export type WrfcGuardTraceKind =
|
|
54
|
+
| 'spawn-forced-wrfc'
|
|
55
|
+
| 'spawn-suppressed-wrfc'
|
|
56
|
+
| 'batch-collapsed-to-wrfc';
|
|
57
|
+
|
|
58
|
+
export type WrfcGuardTrace = {
|
|
59
|
+
readonly kind: WrfcGuardTraceKind;
|
|
60
|
+
readonly reason: string;
|
|
61
|
+
readonly task: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
43
64
|
type WrfcAgentToolGuardOptions = {
|
|
44
65
|
readonly getLastUserMessage?: () => string | null;
|
|
66
|
+
readonly onTrace?: (trace: WrfcGuardTrace) => void;
|
|
45
67
|
};
|
|
46
68
|
|
|
47
69
|
export function installWrfcAgentToolGuard(registry: ToolRegistry, options: WrfcAgentToolGuardOptions = {}): void {
|
|
@@ -68,8 +90,15 @@ export function validateWrfcAgentToolInvocation(args: AgentToolArgs): string | n
|
|
|
68
90
|
|
|
69
91
|
export function normalizeWrfcAgentToolInvocation(args: AgentToolArgs, options: WrfcAgentToolGuardOptions = {}): AgentToolArgs {
|
|
70
92
|
const lastUserMessage = cleanText(options.getLastUserMessage?.() ?? null);
|
|
93
|
+
const trace = options.onTrace;
|
|
71
94
|
if (args.mode === 'spawn') {
|
|
72
95
|
if (shouldRouteSpawnToWrfc(args)) {
|
|
96
|
+
const reason = resolveSpawnWrfcReason(args);
|
|
97
|
+
trace?.({
|
|
98
|
+
kind: 'spawn-forced-wrfc',
|
|
99
|
+
reason,
|
|
100
|
+
task: cleanText(args.task) || '(no task)',
|
|
101
|
+
});
|
|
73
102
|
const normalized: AgentToolArgs = {
|
|
74
103
|
...args,
|
|
75
104
|
task: selectAuthoritativeTask(args.task, lastUserMessage),
|
|
@@ -78,12 +107,22 @@ export function normalizeWrfcAgentToolInvocation(args: AgentToolArgs, options: W
|
|
|
78
107
|
};
|
|
79
108
|
return cleanText(args.template) ? normalized : { ...normalized, template: 'engineer' };
|
|
80
109
|
}
|
|
110
|
+
trace?.({
|
|
111
|
+
kind: 'spawn-suppressed-wrfc',
|
|
112
|
+
reason: isReadOnlyTask(cleanText(args.task)) ? 'task judged read-only' : 'no implementation signal detected',
|
|
113
|
+
task: cleanText(args.task) || '(no task)',
|
|
114
|
+
});
|
|
81
115
|
return { ...args, reviewMode: 'none', dangerously_disable_wrfc: true };
|
|
82
116
|
}
|
|
83
117
|
|
|
84
118
|
if (args.mode !== 'batch-spawn') return args;
|
|
85
119
|
const tasks = Array.isArray(args.tasks) ? args.tasks.filter(isRecord) : [];
|
|
86
120
|
if (shouldCollapseBatchToAuthoritativeWrfc(args, tasks) && lastUserMessage) {
|
|
121
|
+
trace?.({
|
|
122
|
+
kind: 'batch-collapsed-to-wrfc',
|
|
123
|
+
reason: `WRFC: collapsed ${tasks.length}-agent batch into one reviewed chain`,
|
|
124
|
+
task: lastUserMessage,
|
|
125
|
+
});
|
|
87
126
|
return buildAuthoritativeWrfcSpawn(args, tasks, lastUserMessage);
|
|
88
127
|
}
|
|
89
128
|
const wrfcTasks = tasks.filter((task) => isExplicitWrfcTask(task, args));
|
|
@@ -190,17 +229,39 @@ function isRootReviewRoleTask(task: AgentTaskArgs): boolean {
|
|
|
190
229
|
|| /\b(?:test|tests|testing|review|reviews|reviewing|verify|verifies|verifying|verification|validate|validates|validating|validation|qa)\s+(?:the|this|that|implementation|solution|feature|deliverable|code|changes|work|output|result|patch|diff)\b/i.test(text);
|
|
191
230
|
}
|
|
192
231
|
|
|
232
|
+
function resolveSpawnWrfcReason(args: AgentTaskArgs): string {
|
|
233
|
+
if (containsWrfcSignal(args.task)) return 'task contains explicit WRFC signal';
|
|
234
|
+
if (args.reviewMode === 'wrfc') return 'reviewMode explicitly set to wrfc';
|
|
235
|
+
if (isRootReviewRoleTask(args)) return 'task identified as root review-role (reviewer/tester/verifier)';
|
|
236
|
+
return 'task judged implementation-like';
|
|
237
|
+
}
|
|
238
|
+
|
|
193
239
|
function isImplementationLikeTask(task: AgentTaskArgs): boolean {
|
|
194
240
|
const text = cleanText(task.task);
|
|
195
241
|
if (!text) return false;
|
|
196
|
-
|
|
242
|
+
// Broad verb set: explicit action words + short imperative phrasings like
|
|
243
|
+
// "make the button blue", "wire up X", "connect X", "rename X", "move X"
|
|
244
|
+
return /\b(?:build|implement|create|add|write|fix|repair|update|refactor|change|modify|deliver|make|patch|wire|connect|rename|move|delete|remove|migrate|configure|set\s+up|set\s+the|turn\s+on|turn\s+off|enable|disable|initialize|init|register|replace|swap|convert|transform|extend|integrate|embed|inject|port|rewrite|restructure)\b/i.test(text)
|
|
197
245
|
&& !isReadOnlyTask(text);
|
|
198
246
|
}
|
|
199
247
|
|
|
200
248
|
function isReadOnlyTask(text: string): boolean {
|
|
249
|
+
// Branch A: explicit do-not-write guards
|
|
201
250
|
if (/\bdo\s+not\s+(?:write|edit|modify|change|create)\b|\bread[-\s]*only\b|\bwithout\s+(?:writing|editing|modifying|changing|creating)\b/i.test(text)) return true;
|
|
202
|
-
|
|
203
|
-
|
|
251
|
+
// Branch B: task leads with an analysis/reporting verb — treat it as read-only regardless
|
|
252
|
+
// of any action verbs that appear later in the sentence. A task that LEADS with
|
|
253
|
+
// "report", "investigate", "describe", "audit", etc. is describing or evaluating an
|
|
254
|
+
// action, not performing it. Examples that must NOT reach WRFC:
|
|
255
|
+
// "report on how to migrate the auth module"
|
|
256
|
+
// "investigate what to remove"
|
|
257
|
+
// "document how we would convert X to Y"
|
|
258
|
+
// "describe the steps to disable telemetry"
|
|
259
|
+
// "audit which modules to delete"
|
|
260
|
+
// "evaluate whether to migrate"
|
|
261
|
+
// The negative-lookahead is intentionally absent: the leading verb is authoritative.
|
|
262
|
+
// Note: 'review' is intentionally excluded — tasks leading with 'review' are caught
|
|
263
|
+
// by isRootReviewRoleTask() and routed to a WRFC chain as a reviewer role.
|
|
264
|
+
return /^\s*(?:inspect|research|read|find|list|summarize|analy[sz]e|explain|report|investigate|document|describe|audit|evaluate|assess|check|compare|tell|show)\b/i.test(text);
|
|
204
265
|
}
|
|
205
266
|
|
|
206
267
|
function selectAuthoritativeTask(candidate: unknown, lastUserMessage: string): string {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format elapsed time (in milliseconds) as a compact human-readable string.
|
|
3
|
+
*
|
|
4
|
+
* Ranges:
|
|
5
|
+
* < 1 second → "0.Xs" (one decimal, e.g. "0.4s")
|
|
6
|
+
* 1-59s → "Xs" (e.g. "3s", "59s")
|
|
7
|
+
* 1m-59m59s → "Xm YYs" (e.g. "1m04s", "59m59s")
|
|
8
|
+
* ≥ 1 hour → "Xh YYm" (e.g. "1h02m")
|
|
9
|
+
*
|
|
10
|
+
* Used by the thinking indicator and live tool timer.
|
|
11
|
+
*/
|
|
12
|
+
export function formatElapsed(ms: number): string {
|
|
13
|
+
if (ms < 0) ms = 0;
|
|
14
|
+
if (ms < 1000) {
|
|
15
|
+
// Truncate (not round) to one decimal so 999ms stays "0.9s" not "1.0s".
|
|
16
|
+
return `${(Math.floor(ms / 100) / 10).toFixed(1)}s`;
|
|
17
|
+
}
|
|
18
|
+
const totalSecs = Math.floor(ms / 1000);
|
|
19
|
+
if (totalSecs < 60) {
|
|
20
|
+
return `${totalSecs}s`;
|
|
21
|
+
}
|
|
22
|
+
const totalMins = Math.floor(totalSecs / 60);
|
|
23
|
+
if (totalMins < 60) {
|
|
24
|
+
const remSecs = totalSecs % 60;
|
|
25
|
+
return `${totalMins}m${String(remSecs).padStart(2, '0')}s`;
|
|
26
|
+
}
|
|
27
|
+
const hours = Math.floor(totalMins / 60);
|
|
28
|
+
const remMins = totalMins % 60;
|
|
29
|
+
return `${hours}h${String(remMins).padStart(2, '0')}m`;
|
|
30
|
+
}
|
|
@@ -1,9 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip ANSI SGR/CSI escape sequences and OSC-8 hyperlink sequences
|
|
3
|
+
* from a string so that only visible characters remain for width measurement.
|
|
4
|
+
*
|
|
5
|
+
* Covers:
|
|
6
|
+
* - CSI sequences: ESC [ ... <final byte 0x40-0x7E> (includes SGR \x1b[...m)
|
|
7
|
+
* - OSC sequences: ESC ] ... ST where ST is ESC\\ or BEL (0x07)
|
|
8
|
+
* - Simple ESC followed by a single non-bracket/non-] character (e.g. ESC c)
|
|
9
|
+
*/
|
|
10
|
+
function stripAnsi(text: string): string {
|
|
11
|
+
let result = '';
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (i < text.length) {
|
|
14
|
+
if (text[i] === '\x1b') {
|
|
15
|
+
const next = text[i + 1];
|
|
16
|
+
if (next === '[') {
|
|
17
|
+
// CSI: skip until final byte (0x40-0x7E)
|
|
18
|
+
i += 2;
|
|
19
|
+
while (i < text.length) {
|
|
20
|
+
const c = text.charCodeAt(i);
|
|
21
|
+
i++;
|
|
22
|
+
if (c >= 0x40 && c <= 0x7e) break;
|
|
23
|
+
}
|
|
24
|
+
} else if (next === ']') {
|
|
25
|
+
// OSC: skip until ST (ESC\\ or BEL)
|
|
26
|
+
i += 2;
|
|
27
|
+
while (i < text.length) {
|
|
28
|
+
if (text[i] === '\x07') { i++; break; }
|
|
29
|
+
if (text[i] === '\x1b' && text[i + 1] === '\\') { i += 2; break; }
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
// Simple two-byte escape (ESC + one char)
|
|
34
|
+
i += next !== undefined ? 2 : 1;
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
result += text[i];
|
|
38
|
+
i++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
1
44
|
/**
|
|
2
45
|
* Calculates the visual width of a string in the terminal.
|
|
3
46
|
* Handles CJK characters, emoji (including ZWJ sequences), and
|
|
4
47
|
* variation selectors correctly as double-width.
|
|
48
|
+
* ANSI escape sequences (SGR/CSI/OSC-8) are stripped before measurement.
|
|
5
49
|
*/
|
|
6
50
|
export function getDisplayWidth(text: string): number {
|
|
51
|
+
text = stripAnsi(text);
|
|
7
52
|
let width = 0;
|
|
8
53
|
let i = 0;
|
|
9
54
|
while (i < text.length) {
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.
|
|
9
|
+
let _version = '0.21.0';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { atomicWriteFileSync } from '@/config/atomic-write.ts';
|
|
4
|
+
import { join } from 'node:path';
|
|
4
5
|
|
|
5
6
|
export const WORK_PLAN_STATUSES = [
|
|
6
7
|
'pending',
|
|
@@ -331,10 +332,7 @@ export class WorkPlanStore {
|
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
private writePlan(plan: WorkPlan): void {
|
|
334
|
-
|
|
335
|
-
const tmp = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
336
|
-
writeFileSync(tmp, `${JSON.stringify(plan, null, 2)}\n`, { mode: 0o600 });
|
|
337
|
-
renameSync(tmp, this.filePath);
|
|
335
|
+
atomicWriteFileSync(this.filePath, `${JSON.stringify(plan, null, 2)}\n`, { mkdirp: true });
|
|
338
336
|
}
|
|
339
337
|
|
|
340
338
|
private resolveItem(plan: WorkPlan, idOrPrefix: string): WorkPlanItem {
|