@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
package/src/main.ts
CHANGED
|
@@ -48,12 +48,15 @@ import { buildPersistedSessionContext, formatReturnContextForDisplay, getReturnC
|
|
|
48
48
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
49
49
|
import { prepareShellCliRuntime } from './cli/entrypoint.ts';
|
|
50
50
|
import { applyInitialTuiCliState } from './cli/tui-startup.ts';
|
|
51
|
+
import { applyRuntimeConfigDefault, applyRuntimeConfigValue } from './cli/config-overrides.ts';
|
|
52
|
+
import { renderToolCallBlock } from './renderer/tool-call.ts';
|
|
51
53
|
import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
52
54
|
import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
|
|
53
55
|
import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
|
|
54
|
-
import { ProjectPlanningCoordinator } from './planning/project-planning-coordinator.ts';
|
|
55
56
|
import { buildCommandArgsHint } from './input/command-args-hint.ts';
|
|
56
57
|
import { summarizeRunningAgents } from './renderer/process-summary.ts';
|
|
58
|
+
import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
59
|
+
import { wireStreamEventMetrics, type StreamMetrics } from './core/stream-event-wiring.ts';
|
|
57
60
|
|
|
58
61
|
const ALT_SCREEN_ENTER = '\x1b[?1049h';
|
|
59
62
|
const ALT_SCREEN_EXIT = '\x1b[?1049l';
|
|
@@ -127,6 +130,15 @@ async function main() {
|
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
// TUI default: show token speed ON. The SDK schema default is false;
|
|
134
|
+
// applyRuntimeConfigDefault reads both the global settings file and the project
|
|
135
|
+
// settings file from disk before deciding whether to apply the default. If the
|
|
136
|
+
// user has explicitly set this key to false in EITHER their global or project
|
|
137
|
+
// persisted config, their value is respected and the default is NOT applied.
|
|
138
|
+
// Only when the key is absent from both files (e.g. a new install) does the
|
|
139
|
+
// TUI default of true take effect in-memory — no disk write occurs either way.
|
|
140
|
+
applyRuntimeConfigDefault(configManager, 'display.showTokenSpeed', true);
|
|
141
|
+
|
|
130
142
|
const panelManager = ctx.services.panelManager;
|
|
131
143
|
const buildSessionContinuityHints = () => {
|
|
132
144
|
const sessionSnapshot = uiServices.readModels.session.getSnapshot();
|
|
@@ -154,12 +166,18 @@ async function main() {
|
|
|
154
166
|
render();
|
|
155
167
|
});
|
|
156
168
|
|
|
157
|
-
let streamStartTime = 0;
|
|
158
|
-
let streamDeltaCount = 0;
|
|
159
|
-
let streamTokenSpeed = 0;
|
|
160
|
-
|
|
161
169
|
let scrollTop = 0;
|
|
162
170
|
let scrollLocked = true;
|
|
171
|
+
// Stream and tool-timer state; mutated by wireStreamEventMetrics handlers, read during render.
|
|
172
|
+
const streamMetrics: StreamMetrics = {
|
|
173
|
+
startTime: 0,
|
|
174
|
+
deltaCount: 0,
|
|
175
|
+
tokenSpeed: 0,
|
|
176
|
+
ttftMs: undefined,
|
|
177
|
+
ttftRecorded: false,
|
|
178
|
+
activeToolStartedAtMs: undefined,
|
|
179
|
+
activeToolName: undefined,
|
|
180
|
+
};
|
|
163
181
|
|
|
164
182
|
const getPromptContentWidth = () => {
|
|
165
183
|
const w = stdout.columns || 80;
|
|
@@ -213,7 +231,8 @@ async function main() {
|
|
|
213
231
|
`[Critical] Multiple errors detected (${_unhandledRejectionCount} in 10s). If the issue persists, please restart. Latest: ${msg}`
|
|
214
232
|
);
|
|
215
233
|
} else {
|
|
216
|
-
|
|
234
|
+
const formatted = formatUserFacingErrorLine(reason);
|
|
235
|
+
systemMessageRouter.high(`[Error] ${formatted}`);
|
|
217
236
|
logger.error('unhandledRejection', { error: String(reason) });
|
|
218
237
|
}
|
|
219
238
|
render();
|
|
@@ -260,18 +279,6 @@ async function main() {
|
|
|
260
279
|
configManager,
|
|
261
280
|
notify: (message) => { systemMessageRouter.high(message); render(); },
|
|
262
281
|
}));
|
|
263
|
-
const projectPlanningCoordinator = new ProjectPlanningCoordinator({
|
|
264
|
-
service: ctx.services.projectPlanningService,
|
|
265
|
-
projectId: ctx.services.projectPlanningProjectId,
|
|
266
|
-
workingDirectory: workingDir,
|
|
267
|
-
notify: (message) => { systemMessageRouter.high(message); render(); },
|
|
268
|
-
openPanel: () => {
|
|
269
|
-
panelManager.open('project-planning');
|
|
270
|
-
panelManager.show();
|
|
271
|
-
render();
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
|
|
275
282
|
const submitInput = (text: string, content?: ContentPart[], options: { readonly spokenOutput?: boolean } = {}) => {
|
|
276
283
|
input.clearModalStack();
|
|
277
284
|
scrollLocked = true; // Re-lock on any user input
|
|
@@ -307,32 +314,6 @@ async function main() {
|
|
|
307
314
|
if (processedText || content) {
|
|
308
315
|
void (async () => {
|
|
309
316
|
let inputOptions = options.spokenOutput ? createSpokenTurnInputOptions() : undefined;
|
|
310
|
-
if (!options.spokenOutput && processedText) {
|
|
311
|
-
try {
|
|
312
|
-
const planning = await projectPlanningCoordinator.prepareTurn(processedText);
|
|
313
|
-
if (planning) {
|
|
314
|
-
if (planning.handledLocally) {
|
|
315
|
-
systemMessageRouter.high(planning.statusMessage);
|
|
316
|
-
render();
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
conversation.addSystemMessage(planning.systemMessage);
|
|
320
|
-
inputOptions = {
|
|
321
|
-
origin: {
|
|
322
|
-
source: 'project-planning',
|
|
323
|
-
surface: 'tui',
|
|
324
|
-
metadata: {
|
|
325
|
-
projectId: ctx.services.projectPlanningProjectId,
|
|
326
|
-
knowledgeSpaceId: planning.state.knowledgeSpaceId,
|
|
327
|
-
readiness: planning.evaluation.readiness,
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
} catch (err) {
|
|
333
|
-
systemMessageRouter.high(`[Planning] ${summarizeError(err)}`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
317
|
if (options.spokenOutput && processedText) {
|
|
337
318
|
spokenTurns.submitNextTurn(processedText);
|
|
338
319
|
}
|
|
@@ -515,7 +496,9 @@ async function main() {
|
|
|
515
496
|
workingDir,
|
|
516
497
|
provider: runtime.provider,
|
|
517
498
|
contextWindow: currentModel.contextWindow,
|
|
518
|
-
|
|
499
|
+
// behavior.autoCompactThreshold is stored as a percent integer (e.g. 80);
|
|
500
|
+
// the meter expects a fraction [0..1]. Clamp to [0,1] to guard nonsense values.
|
|
501
|
+
compactThreshold: Math.min(1, Math.max(0, (configManager.get('behavior.autoCompactThreshold') as number) / 100)),
|
|
519
502
|
dangerMode: (() => {
|
|
520
503
|
if (configManager.get('behavior.autoApprove')) return true;
|
|
521
504
|
const permMode = configManager.get('permissions.mode');
|
|
@@ -598,16 +581,25 @@ async function main() {
|
|
|
598
581
|
const showSpeed = configManager.get('display.showTokenSpeed') as boolean;
|
|
599
582
|
const showPreview = configManager.get('display.showToolPreview') as boolean;
|
|
600
583
|
const partialToolPreview = showPreview ? sessionSnapshot.streamToolPreview : undefined;
|
|
584
|
+
// Elapsed from turn start (stream or tool execution), used for the thinking indicator timer.
|
|
585
|
+
const turnElapsedMs = streamMetrics.startTime > 0 ? Date.now() - streamMetrics.startTime : undefined;
|
|
601
586
|
const thinking = UIFactory.createThinkingFragment(
|
|
602
587
|
conversationWidth,
|
|
603
588
|
orchestrator.getSpinner(),
|
|
604
589
|
orchestrator.thinkingFrame,
|
|
605
|
-
showSpeed ?
|
|
590
|
+
showSpeed ? streamMetrics.tokenSpeed : undefined,
|
|
606
591
|
showPreview ? partialToolPreview : undefined,
|
|
607
592
|
orchestrator.streamingInputTokens > 0 ? orchestrator.streamingInputTokens : undefined,
|
|
608
593
|
orchestrator.streamingOutputTokens > 0 ? orchestrator.streamingOutputTokens : undefined,
|
|
594
|
+
turnElapsedMs,
|
|
595
|
+
streamMetrics.ttftMs,
|
|
609
596
|
);
|
|
610
597
|
viewport.push(...thinking);
|
|
598
|
+
// Live tool timer: render the currently executing tool row with ticking elapsed.
|
|
599
|
+
if (streamMetrics.activeToolName !== undefined && streamMetrics.activeToolStartedAtMs !== undefined) {
|
|
600
|
+
const liveToolCall = { id: 'live', name: streamMetrics.activeToolName, arguments: {} };
|
|
601
|
+
viewport.push(...renderToolCallBlock(liveToolCall, 'executing', undefined, conversationWidth, undefined, undefined, undefined, streamMetrics.activeToolStartedAtMs));
|
|
602
|
+
}
|
|
611
603
|
}
|
|
612
604
|
|
|
613
605
|
if (pendingPermission) {
|
|
@@ -709,17 +701,16 @@ async function main() {
|
|
|
709
701
|
refreshGit();
|
|
710
702
|
}));
|
|
711
703
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}));
|
|
704
|
+
// --- Stream metrics + tool-timer event wiring ---
|
|
705
|
+
const streamUnsubs = wireStreamEventMetrics({
|
|
706
|
+
events: uiServices.events,
|
|
707
|
+
orchestrator,
|
|
708
|
+
providerRegistry,
|
|
709
|
+
systemMessageRouter,
|
|
710
|
+
render,
|
|
711
|
+
metrics: streamMetrics,
|
|
712
|
+
});
|
|
713
|
+
unsubs.push(...streamUnsubs);
|
|
723
714
|
|
|
724
715
|
// --- Terminal setup ---
|
|
725
716
|
stdin.setRawMode(true);
|
|
@@ -747,6 +738,15 @@ async function main() {
|
|
|
747
738
|
render,
|
|
748
739
|
loadRecoveryConversation: () => loadRecoveryConversation({ homeDirectory }),
|
|
749
740
|
deleteRecoveryFile: () => deleteRecoveryFile({ homeDirectory }),
|
|
741
|
+
reopenPanels: (snapshot) => {
|
|
742
|
+
const panels = snapshot.returnContext?.openPanels;
|
|
743
|
+
if (!panels || panels.length === 0) return;
|
|
744
|
+
for (const panelId of panels.slice(0, 4)) {
|
|
745
|
+
try { panelManager.open(panelId); } catch { /* unknown panel id — skip */ }
|
|
746
|
+
}
|
|
747
|
+
panelManager.show();
|
|
748
|
+
render();
|
|
749
|
+
},
|
|
750
750
|
});
|
|
751
751
|
pendingPermission = blocking.pendingPermission;
|
|
752
752
|
recoveryPending = blocking.recoveryPending;
|
|
@@ -76,7 +76,10 @@ export function registerAgentPanels(manager: PanelManager, deps: ResolvedBuiltin
|
|
|
76
76
|
preload: true,
|
|
77
77
|
factory: () => {
|
|
78
78
|
const ui = requireUiServices(deps);
|
|
79
|
-
return new WrfcPanel(ui.events.workflows, {
|
|
79
|
+
return new WrfcPanel(ui.events.workflows, {
|
|
80
|
+
controller: ui.agents.wrfcController,
|
|
81
|
+
cancelChain: (agentId: string) => ui.agents.agentManager.cancel(agentId),
|
|
82
|
+
});
|
|
80
83
|
},
|
|
81
84
|
});
|
|
82
85
|
|
|
@@ -65,7 +65,10 @@ export function registerDevelopmentPanels(manager: PanelManager, deps: ResolvedB
|
|
|
65
65
|
description: 'Estimated costs per session, agent, and plan with budget alerts',
|
|
66
66
|
factory: () => {
|
|
67
67
|
const ui = requireUiServices(deps);
|
|
68
|
-
return new CostTrackerPanel(ui.events.turns, ui.events.agents, getOrchestratorUsage, {
|
|
68
|
+
return new CostTrackerPanel(ui.events.turns, ui.events.agents, getOrchestratorUsage, {
|
|
69
|
+
budgetThreshold,
|
|
70
|
+
getAgentStatus: (id) => ui.agents.agentManager.getStatus(id),
|
|
71
|
+
});
|
|
69
72
|
},
|
|
70
73
|
});
|
|
71
74
|
}
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// useConfirmState<T> — reusable inline
|
|
2
|
+
// useConfirmState<T> — reusable inline confirm/cancel helper
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// ── Project-standard confirm contract (all panels must match) ──────────────
|
|
5
|
+
//
|
|
6
|
+
// CONFIRM: Enter, Return, or y
|
|
7
|
+
// CANCEL: Esc or n
|
|
8
|
+
// ABSORBED: any other key while confirm is active (keeps confirm pending)
|
|
9
|
+
//
|
|
10
|
+
// Implementation:
|
|
11
|
+
// - Composable: any panel holds a ConfirmState<T> field; no new base class
|
|
12
|
+
// - Call handleConfirmInput(confirm, key) BEFORE normal key dispatch.
|
|
13
|
+
// It handles all four outcomes and returns one of the four result tokens.
|
|
14
|
+
// - Call renderConfirmLines(width, state) to render the two-line overlay
|
|
15
|
+
// that replaces normal content while a confirm is pending.
|
|
16
|
+
//
|
|
17
|
+
// ── This file is the canonical contract for all confirm flows ─────────────
|
|
18
|
+
// Any new panel confirm flow must use ConfirmState<T> and handleConfirmInput;
|
|
19
|
+
// do not implement a bespoke two-press or Enter-only variant.
|
|
10
20
|
// ---------------------------------------------------------------------------
|
|
11
21
|
|
|
12
22
|
import type { Line } from '../types/grid.ts';
|
|
@@ -23,9 +33,14 @@ export interface ConfirmState<T = string> {
|
|
|
23
33
|
/**
|
|
24
34
|
* Call this from a panel's handleInput() BEFORE any other key handling.
|
|
25
35
|
*
|
|
36
|
+
* Project-standard confirm contract:
|
|
37
|
+
* - CONFIRM: Enter, Return, or y
|
|
38
|
+
* - CANCEL: Esc or n
|
|
39
|
+
* - ABSORBED: any other key while confirm is active (keeps confirm pending)
|
|
40
|
+
*
|
|
26
41
|
* Returns:
|
|
27
|
-
* - `'confirmed'` — user pressed y; caller must execute
|
|
28
|
-
* clear state (set confirm to null)
|
|
42
|
+
* - `'confirmed'` — user pressed Enter, Return, or y; caller must execute
|
|
43
|
+
* the action and clear state (set confirm to null)
|
|
29
44
|
* - `'cancelled'` — user pressed n or Esc; caller must clear state
|
|
30
45
|
* - `'absorbed'` — any other key while confirm is active; caller returns true
|
|
31
46
|
* - `'inactive'` — no confirm pending; caller continues normal dispatch
|
|
@@ -35,7 +50,7 @@ export function handleConfirmInput<T = string>(
|
|
|
35
50
|
key: string,
|
|
36
51
|
): 'confirmed' | 'cancelled' | 'absorbed' | 'inactive' {
|
|
37
52
|
if (!confirm) return 'inactive';
|
|
38
|
-
if (key === 'y') return 'confirmed';
|
|
53
|
+
if (key === 'y' || key === 'enter' || key === 'return') return 'confirmed';
|
|
39
54
|
if (key === 'n' || key === 'escape') return 'cancelled';
|
|
40
55
|
return 'absorbed';
|
|
41
56
|
}
|
|
@@ -52,8 +67,8 @@ export function renderConfirmLines<T = string>(width: number, state: ConfirmStat
|
|
|
52
67
|
palette.warn,
|
|
53
68
|
]]),
|
|
54
69
|
buildPanelLine(width, [
|
|
55
|
-
[' y', palette.info],
|
|
56
|
-
[' confirm
|
|
70
|
+
[' Enter / y', palette.info],
|
|
71
|
+
[' confirm', palette.dim],
|
|
57
72
|
[' n / Esc', palette.info],
|
|
58
73
|
[' cancel', palette.dim],
|
|
59
74
|
]),
|
|
@@ -7,6 +7,7 @@ import { createStyledCell, createEmptyLine } from '../types/grid.ts';
|
|
|
7
7
|
import { BasePanel } from './base-panel.ts';
|
|
8
8
|
import type { AgentEvent, TurnEvent } from '@/runtime/index.ts';
|
|
9
9
|
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
10
|
+
import type { AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
10
11
|
import {
|
|
11
12
|
buildEmptyState,
|
|
12
13
|
buildPanelLine,
|
|
@@ -16,68 +17,9 @@ import {
|
|
|
16
17
|
DEFAULT_PANEL_PALETTE,
|
|
17
18
|
type PanelWorkspaceSection,
|
|
18
19
|
} from './polish.ts';
|
|
20
|
+
import { getPricing } from '../export/cost-utils.ts';
|
|
19
21
|
|
|
20
|
-
//
|
|
21
|
-
// Pricing table (USD per 1M tokens)
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
interface ModelPricing {
|
|
25
|
-
input: number;
|
|
26
|
-
output: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const MODEL_PRICING: Record<string, ModelPricing> = {
|
|
30
|
-
// Free tier
|
|
31
|
-
'openrouter/free': { input: 0, output: 0 },
|
|
32
|
-
|
|
33
|
-
// InceptionLabs
|
|
34
|
-
'mercury-2': { input: 0.50, output: 1.50 },
|
|
35
|
-
'mercury-edit': { input: 0.50, output: 1.50 },
|
|
36
|
-
|
|
37
|
-
// OpenAI
|
|
38
|
-
'gpt-5.4': { input: 5, output: 15 },
|
|
39
|
-
'gpt-5.3-chat-latest': { input: 3, output: 10 },
|
|
40
|
-
'gpt-5-mini': { input: 0.15, output: 0.60 },
|
|
41
|
-
'gpt-5-nano': { input: 0.05, output: 0.20 },
|
|
42
|
-
'gpt-oss-120b': { input: 0, output: 0 },
|
|
43
|
-
|
|
44
|
-
// Anthropic (correct registry IDs)
|
|
45
|
-
'claude-opus-4-6': { input: 15, output: 75 },
|
|
46
|
-
'claude-sonnet-4-6': { input: 3, output: 15 },
|
|
47
|
-
'claude-haiku-4-5': { input: 0.80, output: 4 },
|
|
48
|
-
|
|
49
|
-
// Google
|
|
50
|
-
'gemini-3.1-pro': { input: 1.25, output: 5 },
|
|
51
|
-
'gemini-3-flash': { input: 0.075, output: 0.30 },
|
|
52
|
-
'gemini-3.1-flash-lite': { input: 0.02, output: 0.10 },
|
|
53
|
-
'gemini-2.5-pro': { input: 1.25, output: 5 },
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Look up pricing from the model catalog.
|
|
58
|
-
* Returns { input: 0, output: 0 } for free models and unknown models.
|
|
59
|
-
*/
|
|
60
|
-
function getCostFromCatalogForPanel(modelId: string): ModelPricing {
|
|
61
|
-
if (modelId.endsWith(':free')) return { input: 0, output: 0 };
|
|
62
|
-
return { input: 0, output: 0 };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getPricing(modelId: string): ModelPricing {
|
|
66
|
-
// 2. Hardcoded table — exact match
|
|
67
|
-
if (MODEL_PRICING[modelId]) return MODEL_PRICING[modelId]!;
|
|
68
|
-
// 1. OpenRouter :free suffix — treat as free
|
|
69
|
-
if (modelId.endsWith(':free')) return { input: 0, output: 0 };
|
|
70
|
-
// 2. Prefix match (e.g. "openrouter/free:..." or "claude-sonnet-4-6-20..")
|
|
71
|
-
for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
|
|
72
|
-
if (modelId.startsWith(key) || modelId.includes(key)) return pricing;
|
|
73
|
-
}
|
|
74
|
-
// 3. Unknown model — default to free-ish safe fallback
|
|
75
|
-
return getCostFromCatalogForPanel(modelId);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function calcCost(inputTokens: number, outputTokens: number, pricing: ModelPricing): number {
|
|
79
|
-
return (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
|
|
80
|
-
}
|
|
22
|
+
// Pricing lookups are provided by ../export/cost-utils.ts (single source of truth).
|
|
81
23
|
|
|
82
24
|
function formatCost(usd: number): string {
|
|
83
25
|
if (usd === 0) return '$0.00';
|
|
@@ -176,14 +118,19 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
176
118
|
// Getter for live orchestrator usage
|
|
177
119
|
private readonly getOrchestratorUsage: () => UsageSnapshot & { model?: string };
|
|
178
120
|
|
|
121
|
+
// Optional resolver for agent usage on completion — enables real cost attribution.
|
|
122
|
+
// When omitted, completed agents show $0 (honest: data unavailable).
|
|
123
|
+
private readonly getAgentStatus: ((agentId: string) => AgentRecord | null) | undefined;
|
|
124
|
+
|
|
179
125
|
constructor(
|
|
180
126
|
turnEvents: UiEventFeed<TurnEvent>,
|
|
181
127
|
agentEvents: UiEventFeed<AgentEvent>,
|
|
182
128
|
getOrchestratorUsage: () => UsageSnapshot & { model?: string },
|
|
183
|
-
opts: { budgetThreshold?: number } = {},
|
|
129
|
+
opts: { budgetThreshold?: number; getAgentStatus?: (agentId: string) => AgentRecord | null } = {},
|
|
184
130
|
) {
|
|
185
131
|
super('cost', 'Cost', '$', 'monitoring');
|
|
186
132
|
this.getOrchestratorUsage = getOrchestratorUsage;
|
|
133
|
+
this.getAgentStatus = opts.getAgentStatus;
|
|
187
134
|
this.budgetThreshold = opts.budgetThreshold ?? 0;
|
|
188
135
|
this.attachEvents(turnEvents, agentEvents);
|
|
189
136
|
}
|
|
@@ -214,12 +161,22 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
214
161
|
}),
|
|
215
162
|
);
|
|
216
163
|
|
|
217
|
-
// Agent completed — capture token
|
|
164
|
+
// Agent completed — capture real token usage via AgentRecord when available
|
|
218
165
|
this.unsubs.push(
|
|
219
166
|
agentEvents.on('AGENT_COMPLETED', (payload) => {
|
|
220
167
|
const entry = this.agents.get(payload.agentId);
|
|
221
168
|
if (entry) {
|
|
222
169
|
entry.status = 'done';
|
|
170
|
+
if (this.getAgentStatus) {
|
|
171
|
+
const rec = this.getAgentStatus(payload.agentId);
|
|
172
|
+
if (rec?.usage) {
|
|
173
|
+
entry.inputTokens = rec.usage.inputTokens + (rec.usage.cacheReadTokens ?? 0) + (rec.usage.cacheWriteTokens ?? 0);
|
|
174
|
+
entry.outputTokens = rec.usage.outputTokens;
|
|
175
|
+
const pricing = getPricing(rec.model ?? 'unknown');
|
|
176
|
+
entry.cost = (entry.inputTokens * pricing.input + entry.outputTokens * pricing.output) / 1_000_000;
|
|
177
|
+
if (rec.model && rec.model !== 'unknown') entry.model = rec.model;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
223
180
|
this.markDirty();
|
|
224
181
|
}
|
|
225
182
|
}),
|
|
@@ -248,9 +205,9 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
248
205
|
if (usage.model) this.sessionModel = usage.model;
|
|
249
206
|
|
|
250
207
|
// Record cost delta for sparkline
|
|
251
|
-
const sessionProvider = this.sessionModel.includes('/') ? this.sessionModel.split('/')[0]! : '';
|
|
252
208
|
const pricing = getPricing(this.sessionModel);
|
|
253
|
-
const
|
|
209
|
+
const billableInput = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
210
|
+
const totalCost = (billableInput * pricing.input + usage.output * pricing.output) / 1_000_000;
|
|
254
211
|
const delta = Math.max(0, totalCost - this.lastSessionCost);
|
|
255
212
|
this.lastSessionCost = totalCost;
|
|
256
213
|
this.costHistory.push(delta);
|
|
@@ -310,10 +267,9 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
310
267
|
render(width: number, height: number): Line[] {
|
|
311
268
|
if (height <= 0 || width <= 0) return [];
|
|
312
269
|
|
|
313
|
-
const sessionProvider = this.sessionModel.includes('/') ? this.sessionModel.split('/')[0]! : '';
|
|
314
270
|
const pricing = getPricing(this.sessionModel);
|
|
315
271
|
const totalInputTokens = this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite;
|
|
316
|
-
const sessionCost =
|
|
272
|
+
const sessionCost = (totalInputTokens * pricing.input + this.sessionUsage.output * pricing.output) / 1_000_000;
|
|
317
273
|
const overBudget = this.budgetThreshold > 0 && sessionCost > this.budgetThreshold;
|
|
318
274
|
const sparkline = buildSparkline(this.costHistory);
|
|
319
275
|
const costStr = formatCost(sessionCost);
|
package/src/panels/eval-panel.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { BasePanel } from './base-panel.ts';
|
|
9
9
|
import type { Line } from '../types/grid.ts';
|
|
10
|
+
import type { KeyName } from './types.ts';
|
|
10
11
|
import { createEmptyLine } from '../types/grid.ts';
|
|
11
12
|
import {
|
|
12
13
|
buildEmptyState,
|
|
@@ -136,21 +137,21 @@ export class EvalPanel extends BasePanel {
|
|
|
136
137
|
this._unsub = null;
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
public handleInput(key:
|
|
140
|
+
public handleInput(key: KeyName): boolean {
|
|
140
141
|
const suites = this._registry.getSuiteResults();
|
|
141
142
|
|
|
142
143
|
if (this._mode === 'list') {
|
|
143
|
-
if (key === '
|
|
144
|
+
if (key === 'up' || key === 'k') {
|
|
144
145
|
this._selectedSuiteIdx = Math.max(0, this._selectedSuiteIdx - 1);
|
|
145
146
|
this.markDirty();
|
|
146
147
|
return true;
|
|
147
148
|
}
|
|
148
|
-
if (key === '
|
|
149
|
+
if (key === 'down' || key === 'j') {
|
|
149
150
|
this._selectedSuiteIdx = Math.min(suites.length - 1, this._selectedSuiteIdx + 1);
|
|
150
151
|
this.markDirty();
|
|
151
152
|
return true;
|
|
152
153
|
}
|
|
153
|
-
if ((key === '
|
|
154
|
+
if ((key === 'enter' || key === 'return' || key === 'l') && suites.length > 0) {
|
|
154
155
|
this._mode = 'detail';
|
|
155
156
|
this._selectedScenarioIdx = 0;
|
|
156
157
|
this._scrollOffset = 0;
|
|
@@ -161,12 +162,12 @@ export class EvalPanel extends BasePanel {
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
// detail mode
|
|
164
|
-
if (key === '
|
|
165
|
+
if (key === 'escape' || key === 'q' || key === 'h') {
|
|
165
166
|
this._mode = 'list';
|
|
166
167
|
this.markDirty();
|
|
167
168
|
return true;
|
|
168
169
|
}
|
|
169
|
-
if (key === '
|
|
170
|
+
if (key === 'up' || key === 'k') {
|
|
170
171
|
const suite = suites[this._selectedSuiteIdx];
|
|
171
172
|
if (suite) {
|
|
172
173
|
this._selectedScenarioIdx = Math.max(0, this._selectedScenarioIdx - 1);
|
|
@@ -175,7 +176,7 @@ export class EvalPanel extends BasePanel {
|
|
|
175
176
|
}
|
|
176
177
|
return true;
|
|
177
178
|
}
|
|
178
|
-
if (key === '
|
|
179
|
+
if (key === 'down' || key === 'j') {
|
|
179
180
|
const suite = suites[this._selectedSuiteIdx];
|
|
180
181
|
if (suite) {
|
|
181
182
|
this._selectedScenarioIdx = Math.min(
|
|
@@ -187,12 +188,12 @@ export class EvalPanel extends BasePanel {
|
|
|
187
188
|
}
|
|
188
189
|
return true;
|
|
189
190
|
}
|
|
190
|
-
if (key === '
|
|
191
|
+
if (key === 'pageup') {
|
|
191
192
|
this._scrollOffset = Math.max(0, this._scrollOffset - 5);
|
|
192
193
|
this.markDirty();
|
|
193
194
|
return true;
|
|
194
195
|
}
|
|
195
|
-
if (key === '
|
|
196
|
+
if (key === 'pagedown') {
|
|
196
197
|
this._scrollOffset += 5;
|
|
197
198
|
this.markDirty();
|
|
198
199
|
return true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
3
|
+
import type { KeyName } from './types.ts';
|
|
3
4
|
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
4
5
|
import type { MemoryClass, MemoryRecord, MemoryRegistry, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
|
|
5
6
|
import {
|
|
@@ -102,7 +103,7 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
|
|
|
102
103
|
// Input
|
|
103
104
|
// ---------------------------------------------------------------------------
|
|
104
105
|
|
|
105
|
-
public handleInput(key:
|
|
106
|
+
public handleInput(key: KeyName): boolean {
|
|
106
107
|
// I1: y/n confirm for stale/contradict
|
|
107
108
|
if (this.confirm) {
|
|
108
109
|
const result = handleConfirmInput(this.confirm, key);
|
|
@@ -149,7 +150,7 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
|
|
|
149
150
|
|
|
150
151
|
const selected = this.records[this.selectedIndex];
|
|
151
152
|
|
|
152
|
-
if (key === '
|
|
153
|
+
if (key === 'enter' || key === 'return' || key === 'r') {
|
|
153
154
|
if (!selected) return false;
|
|
154
155
|
this.registry.review(selected.id, {
|
|
155
156
|
state: 'reviewed',
|
|
@@ -186,9 +187,6 @@ export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
|
|
|
186
187
|
return true;
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
// Normalize arrow keys to base class format
|
|
190
|
-
if (key === 'ArrowUp') return super.handleInput('up');
|
|
191
|
-
if (key === 'ArrowDown') return super.handleInput('down');
|
|
192
190
|
return super.handleInput(key);
|
|
193
191
|
}
|
|
194
192
|
|