@pellux/goodvibes-tui 1.0.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -127
- package/README.md +32 -14
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
- package/src/cli/entrypoint.ts +2 -0
- 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/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +58 -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 +104 -2
- package/src/core/stream-stall-watchdog.ts +41 -17
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +126 -8
- package/src/input/command-registry.ts +54 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/diff-runtime.ts +61 -30
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- 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 +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- 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 +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- 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 +12 -0
- package/src/input/commands/session-workflow.ts +19 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/share-runtime.ts +9 -2
- package/src/input/commands/shell-core.ts +15 -7
- 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 +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-content-actions.ts +22 -8
- package/src/input/handler-feed-routes.ts +107 -7
- package/src/input/handler-feed.ts +49 -13
- package/src/input/handler-interactions.ts +3 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +3 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +10 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +41 -44
- package/src/panels/agent-inspector-shared.ts +35 -11
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +16 -62
- 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 +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +63 -14
- package/src/panels/diff-panel.ts +123 -60
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/git-panel.ts +9 -9
- package/src/panels/index.ts +7 -31
- package/src/panels/local-auth-panel.ts +10 -0
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -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 +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -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 +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/scrollable-list-panel.ts +9 -0
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/token-budget-panel.ts +5 -3
- package/src/panels/types.ts +26 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- 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/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/turn-injection.ts +114 -0
- package/src/renderer/ui-factory.ts +91 -7
- package/src/runtime/bootstrap-command-context.ts +24 -1
- package/src/runtime/bootstrap-command-parts.ts +36 -7
- package/src/runtime/bootstrap-core.ts +19 -4
- package/src/runtime/bootstrap-hook-bridge.ts +5 -0
- package/src/runtime/bootstrap-shell.ts +41 -17
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- 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 -233
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -609
- package/src/panels/docs-panel.ts +0 -375
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -664
- 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 -506
- 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 -557
- 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 -721
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -306
- package/src/panels/provider-health-views.ts +0 -560
- 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 -487
- 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 -530
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -465
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -656
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -1,120 +1,27 @@
|
|
|
1
|
-
import { networkInterfaces } from 'node:os';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
1
|
import type { PanelManager } from '../panel-manager.ts';
|
|
4
|
-
import { SessionBrowserPanel } from '../session-browser-panel.ts';
|
|
5
|
-
import { QrPanel } from '../qr-panel.ts';
|
|
6
|
-
import { DocsPanel } from '../docs-panel.ts';
|
|
7
|
-
import { PanelListPanel } from '../panel-list-panel.ts';
|
|
8
2
|
import { TokenBudgetPanel } from '../token-budget-panel.ts';
|
|
9
3
|
import type { ResolvedBuiltinPanelDeps } from './shared.ts';
|
|
10
4
|
import { requireUiServices } from './shared.ts';
|
|
11
|
-
import {
|
|
12
|
-
getOrCreateCompanionToken,
|
|
13
|
-
regenerateCompanionToken,
|
|
14
|
-
buildCompanionConnectionInfo,
|
|
15
|
-
} from '@pellux/goodvibes-sdk/platform/pairing';
|
|
16
|
-
import { copyToClipboard } from '../../utils/clipboard.ts';
|
|
17
|
-
|
|
18
|
-
function getLocalNetworkIp(): string {
|
|
19
|
-
const nets = networkInterfaces();
|
|
20
|
-
for (const name of Object.keys(nets)) {
|
|
21
|
-
for (const net of nets[name] ?? []) {
|
|
22
|
-
if (net.family === 'IPv4' && !net.internal) {
|
|
23
|
-
return net.address;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return 'localhost';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function readBootstrapPassword(credentialPath: string): string | undefined {
|
|
31
|
-
try {
|
|
32
|
-
const content = readFileSync(credentialPath, 'utf-8');
|
|
33
|
-
for (const line of content.split('\n')) {
|
|
34
|
-
if (line.startsWith('password=')) {
|
|
35
|
-
return line.slice('password='.length).trim();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
39
|
-
// credential file may not exist yet
|
|
40
|
-
}
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
5
|
|
|
6
|
+
// W6.1 (the purge): panel-list and system-messages were registered here
|
|
7
|
+
// before the purge — both DELETE-disposition. panel-list was a picker over a
|
|
8
|
+
// handful of panels (dead weight now that the registry is much smaller —
|
|
9
|
+
// the picker itself is replaced by a live-registry selection modal on
|
|
10
|
+
// Ctrl+P, see shell/ui-openers.ts). system-messages' buffered notices are
|
|
11
|
+
// rerouted to the transcript's system channel instead of vanishing: with no
|
|
12
|
+
// panel attached, SystemMessageRouter's delivery resolution
|
|
13
|
+
// (resolveSystemMessageDelivery, SDK) already falls back to
|
|
14
|
+
// conversation.addTypedSystemMessage for every kind/target combination — see
|
|
15
|
+
// bootstrap-shell.ts and core/system-message-router.ts.
|
|
16
|
+
//
|
|
17
|
+
// W6.1 (the purge) — group B (WO-P): 'qr-code', 'sessions', and 'docs' also
|
|
18
|
+
// migrated. 'qr-code' → the 'pairing-modal' surface; 'docs' → the
|
|
19
|
+
// 'keybindings-modal' surface (merged with the shortcuts-overlay content);
|
|
20
|
+
// 'sessions' folds into the existing session-picker modal ('sessions' redirects
|
|
21
|
+
// to 'sessionPicker'). Their surfaces AND panel→modal redirects are registered
|
|
22
|
+
// centrally in registerBuiltinModals (builtin-modals.ts). Only 'tokens' (KEEP)
|
|
23
|
+
// is still registered here.
|
|
44
24
|
export function registerSessionPanels(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
|
|
45
|
-
const ui = requireUiServices(deps);
|
|
46
|
-
manager.registerType({
|
|
47
|
-
id: 'qr-code',
|
|
48
|
-
name: 'QR Code',
|
|
49
|
-
icon: 'Q',
|
|
50
|
-
category: 'session',
|
|
51
|
-
description: 'QR code for companion app pairing — scan to connect a mobile or desktop companion',
|
|
52
|
-
factory: () => {
|
|
53
|
-
if (!deps.daemonHomeDir) throw new Error('daemonHomeDir must be provided to the session panel factory via BuiltinPanelDeps');
|
|
54
|
-
const daemonHomeDir = deps.daemonHomeDir;
|
|
55
|
-
const tokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
|
|
56
|
-
const daemonPort = deps.configManager.get('controlPlane.port');
|
|
57
|
-
const daemonHost = String(process.env['GOODVIBES_DAEMON_HOST'] ?? getLocalNetworkIp());
|
|
58
|
-
const daemonUrl = `http://${daemonHost}:${daemonPort}`;
|
|
59
|
-
const bootstrapPassword = readBootstrapPassword(deps.localUserAuthManager.getBootstrapCredentialPath());
|
|
60
|
-
const connectionInfo = buildCompanionConnectionInfo({
|
|
61
|
-
daemonUrl,
|
|
62
|
-
token: tokenRecord.token,
|
|
63
|
-
password: bootstrapPassword,
|
|
64
|
-
surface: 'tui',
|
|
65
|
-
});
|
|
66
|
-
const regenerate = (): typeof connectionInfo => {
|
|
67
|
-
const newRecord = regenerateCompanionToken({ daemonHomeDir });
|
|
68
|
-
return buildCompanionConnectionInfo({
|
|
69
|
-
daemonUrl,
|
|
70
|
-
token: newRecord.token,
|
|
71
|
-
password: bootstrapPassword,
|
|
72
|
-
surface: 'tui',
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
return new QrPanel(connectionInfo, regenerate, copyToClipboard, ui.readModels.controlPlane, deps.localUserAuthManager);
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
manager.registerType({
|
|
80
|
-
id: 'sessions',
|
|
81
|
-
name: 'Sessions',
|
|
82
|
-
icon: 'H',
|
|
83
|
-
category: 'session',
|
|
84
|
-
description: 'Browse, search, and resume past conversation sessions',
|
|
85
|
-
factory: () => new SessionBrowserPanel(deps.sessionManager, deps.resumeSession),
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
manager.registerType({
|
|
89
|
-
id: 'docs',
|
|
90
|
-
name: 'Docs',
|
|
91
|
-
icon: '?',
|
|
92
|
-
category: 'session',
|
|
93
|
-
description: 'Tool list, model capabilities, and keyboard shortcut reference',
|
|
94
|
-
factory: () => new DocsPanel(deps.toolRegistry, deps.providerRegistry, requireUiServices(deps).shell.keybindingsManager),
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
manager.registerType({
|
|
98
|
-
id: 'panel-list',
|
|
99
|
-
name: 'Panel List',
|
|
100
|
-
icon: 'L',
|
|
101
|
-
category: 'session',
|
|
102
|
-
description: 'Browse all registered panels grouped by category, with open/closed status and Enter-to-open',
|
|
103
|
-
factory: () => new PanelListPanel(manager, deps.componentHealthMonitor),
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
manager.registerType({
|
|
107
|
-
id: 'system-messages',
|
|
108
|
-
name: 'System Messages',
|
|
109
|
-
// WO-152: was 'J' (collided with intelligence + tasks).
|
|
110
|
-
icon: '▥',
|
|
111
|
-
category: 'runtime-ops',
|
|
112
|
-
description: 'Operational system messages routed away from the main conversation (scans, discovery, plugin events, tool status)',
|
|
113
|
-
preload: true,
|
|
114
|
-
retainOnClose: true,
|
|
115
|
-
factory: () => deps.systemMessagesPanel,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
25
|
manager.registerType({
|
|
119
26
|
id: 'tokens',
|
|
120
27
|
name: 'Tokens',
|
|
@@ -127,7 +34,9 @@ export function registerSessionPanels(manager: PanelManager, deps: ResolvedBuilt
|
|
|
127
34
|
description: 'Token + context console: gauge, true composition, per-turn history, inline cost, and one-key compact',
|
|
128
35
|
// Preloaded (absorbed from the retired ContextVisualizerPanel) so turn
|
|
129
36
|
// history and pressure accumulate in the background even before the user
|
|
130
|
-
// opens the tab.
|
|
37
|
+
// opens the tab. The only builtin panel that still preloads post-purge
|
|
38
|
+
// (W6.1) — see registerBuiltinPanels callers for the others' preload
|
|
39
|
+
// removal.
|
|
131
40
|
preload: true,
|
|
132
41
|
retainOnClose: true,
|
|
133
42
|
factory: () => {
|
|
@@ -70,8 +70,14 @@ export interface BuiltinPanelDeps {
|
|
|
70
70
|
dismissPlanning?: () => void;
|
|
71
71
|
/** ForensicsRegistry for the Forensics panel. */
|
|
72
72
|
forensicsRegistry?: import('@/runtime/index.ts').ForensicsRegistry;
|
|
73
|
-
/**
|
|
74
|
-
|
|
73
|
+
/**
|
|
74
|
+
* EvalRegistry for the `/eval` command surface. W6.1: 'eval' the panel was
|
|
75
|
+
* deleted (DELETE-disposition), but this field is left in place — no
|
|
76
|
+
* builtin panel factory reads it anymore, and it was never wired at
|
|
77
|
+
* bootstrap in production either way (the eval CLI command reads its own
|
|
78
|
+
* copy via CommandContext.extensions.evalRegistry).
|
|
79
|
+
*/
|
|
80
|
+
evalRegistry?: import('../eval-registry.ts').EvalRegistry;
|
|
75
81
|
/** MemoryRegistry for the Memory panel. */
|
|
76
82
|
memoryRegistry?: MemoryRegistry;
|
|
77
83
|
/** Shared policy runtime state for governance/policy diagnostics. */
|
|
@@ -109,8 +115,6 @@ export interface BuiltinPanelDeps {
|
|
|
109
115
|
projectPlanningProjectId?: string;
|
|
110
116
|
/** TUI-owned persistent work plan store. */
|
|
111
117
|
workPlanStore?: import('../../work-plans/work-plan-store.ts').WorkPlanStore;
|
|
112
|
-
/** Shared system-messages panel instance attached from boot so low-priority chatter stays out of conversation. */
|
|
113
|
-
systemMessagesPanel?: import('../system-messages-panel.ts').SystemMessagesPanel;
|
|
114
118
|
/** Explicit UI-facing runtime services for agent/process/WRFC/remote panels and modals. */
|
|
115
119
|
uiServices?: UiRuntimeServices;
|
|
116
120
|
/** Shared plugin manager for plugin and security panels (widened past the read-only observer surface — WO-134 — so PluginsPanel can drive enable/disable/verify/lift-quarantine). */
|
|
@@ -137,17 +141,9 @@ export interface BuiltinPanelDeps {
|
|
|
137
141
|
sessionChangeTracker?: Pick<SessionChangeTracker, 'getChangedFiles'>;
|
|
138
142
|
/**
|
|
139
143
|
* Open (or focus) a panel by id, wrapping `PanelManager.open`. Use for direct
|
|
140
|
-
* cross-panel navigation instead of printing a "/panel open …" signpost
|
|
141
|
-
* (mirrors the openAgentDetail callback below).
|
|
144
|
+
* cross-panel navigation instead of printing a "/panel open …" signpost.
|
|
142
145
|
*/
|
|
143
146
|
openPanel?: (panelId: string) => void;
|
|
144
|
-
/**
|
|
145
|
-
* Open the agent detail modal for the given agent id. Wired from
|
|
146
|
-
* InputHandler.agentDetailModal.open() at bootstrap — passed to the
|
|
147
|
-
* CockpitPanel factory so the agents workspace inspect key (i) works
|
|
148
|
-
* without the panel depending on the modal directly.
|
|
149
|
-
*/
|
|
150
|
-
openAgentDetail?: (agentId: string) => void;
|
|
151
147
|
}
|
|
152
148
|
|
|
153
149
|
export type ResolvedBuiltinPanelDeps = Omit<
|
|
@@ -164,7 +160,6 @@ export type ResolvedBuiltinPanelDeps = Omit<
|
|
|
164
160
|
| 'projectPlanningProjectId'
|
|
165
161
|
| 'workPlanStore'
|
|
166
162
|
| 'policyRuntimeState'
|
|
167
|
-
| 'systemMessagesPanel'
|
|
168
163
|
> & {
|
|
169
164
|
readonly configManager: ConfigManager;
|
|
170
165
|
readonly localUserAuthManager: UserAuthManager;
|
|
@@ -178,15 +173,8 @@ export type ResolvedBuiltinPanelDeps = Omit<
|
|
|
178
173
|
readonly projectPlanningProjectId: string;
|
|
179
174
|
readonly workPlanStore: import('../../work-plans/work-plan-store.ts').WorkPlanStore;
|
|
180
175
|
readonly policyRuntimeState: PolicyRuntimeState;
|
|
181
|
-
readonly systemMessagesPanel: import('../system-messages-panel.ts').SystemMessagesPanel;
|
|
182
176
|
};
|
|
183
177
|
|
|
184
|
-
export interface ControlPlanePanelFactoryDeps {
|
|
185
|
-
readonly approvalBroker: ApprovalBroker;
|
|
186
|
-
readonly sessionBroker: SharedSessionBroker;
|
|
187
|
-
readonly getControlPlaneRecentEvents: (limit: number) => readonly ControlPlaneRecentEvent[];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
178
|
function requireBuiltinPanelDep<TValue>(value: TValue | undefined, message: string): TValue {
|
|
191
179
|
if (value === undefined) {
|
|
192
180
|
throw new Error(message);
|
|
@@ -246,31 +234,9 @@ export function resolveBuiltinPanelDeps(deps: BuiltinPanelDeps): ResolvedBuiltin
|
|
|
246
234
|
uiServices.platform.policyRuntimeState,
|
|
247
235
|
'Policy runtime state must be wired at bootstrap for builtin panels.',
|
|
248
236
|
),
|
|
249
|
-
systemMessagesPanel: requireBuiltinPanelDep(
|
|
250
|
-
deps.systemMessagesPanel,
|
|
251
|
-
'System messages panel must be wired at bootstrap for builtin panels.',
|
|
252
|
-
),
|
|
253
237
|
};
|
|
254
238
|
}
|
|
255
239
|
|
|
256
|
-
export function requireControlPlanePanelDeps(deps: BuiltinPanelDeps): ControlPlanePanelFactoryDeps {
|
|
257
|
-
if (!deps.approvalBroker || !deps.sessionBroker || !deps.getControlPlaneRecentEvents) {
|
|
258
|
-
throw new Error('ControlPlanePanel requires approval/session brokers and recent-event access to be wired at bootstrap.');
|
|
259
|
-
}
|
|
260
|
-
return {
|
|
261
|
-
approvalBroker: deps.approvalBroker,
|
|
262
|
-
sessionBroker: deps.sessionBroker,
|
|
263
|
-
getControlPlaneRecentEvents: deps.getControlPlaneRecentEvents,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function requireAutomationManager(deps: BuiltinPanelDeps): AutomationManager {
|
|
268
|
-
if (!deps.automationManager) {
|
|
269
|
-
throw new Error('AutomationControlPanel requires an automation manager to be wired at bootstrap.');
|
|
270
|
-
}
|
|
271
|
-
return deps.automationManager;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
240
|
export function requireUiServices(deps: BuiltinPanelDeps): UiRuntimeServices {
|
|
275
241
|
if (!deps.uiServices) {
|
|
276
242
|
throw new Error('UI runtime services must be wired at bootstrap for agent, process, WRFC, and remote panels.');
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { networkInterfaces } from 'node:os';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import type { PanelManager } from './panel-manager.ts';
|
|
4
|
+
import { requireUiServices, type ResolvedBuiltinPanelDeps } from './builtin/shared.ts';
|
|
5
|
+
import type { ConfigModalSurface, ConfigModalView } from '../input/config-modal-types.ts';
|
|
6
|
+
import { createProviderRuntimeInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
7
|
+
import { createRuntimeProviderApi } from '@/runtime/index.ts';
|
|
8
|
+
import { copyToClipboard } from '../utils/clipboard.ts';
|
|
9
|
+
import { getOrCreateCompanionToken, buildCompanionConnectionInfo } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
10
|
+
// ── Group A: Providers & Connectivity + Security subset (WO-A) ───────────────
|
|
11
|
+
import { createServicesModalSurface } from './modals/services-modal.ts';
|
|
12
|
+
import { createSubscriptionModalSurface } from './modals/subscription-modal.ts';
|
|
13
|
+
import { createRemoteModalSurface } from './modals/remote-modal.ts';
|
|
14
|
+
import { createSettingsSyncModalSurface } from './modals/settings-sync-modal.ts';
|
|
15
|
+
import { createProviderHealthModalSurface } from './modals/provider-health-modal.ts';
|
|
16
|
+
import { createLocalAuthModalSurface } from './modals/local-auth-modal.ts';
|
|
17
|
+
import { createSandboxModalSurface } from './modals/sandbox-modal.ts';
|
|
18
|
+
// ── Group B: Ecosystem & Governance (WO-P, this work order) ──────────────────
|
|
19
|
+
import { createMarketplaceModalSurface } from './modals/marketplace-modal.ts';
|
|
20
|
+
import { createPluginsModalSurface } from './modals/plugins-modal.ts';
|
|
21
|
+
import { createSkillsModalSurface } from './modals/skills-modal.ts';
|
|
22
|
+
import { createHooksModalSurface } from './modals/hooks-modal.ts';
|
|
23
|
+
import { createSecurityModalSurface } from './modals/security-modal.ts';
|
|
24
|
+
import { createPolicyModalSurface } from './modals/policy-modal.ts';
|
|
25
|
+
import { createKnowledgeModalSurface } from './modals/knowledge-modal.ts';
|
|
26
|
+
import { createMemoryModalSurface, type MemoryModalDeps } from './modals/memory-modal.ts';
|
|
27
|
+
import { createWorkPlanModalSurface } from './modals/work-plan-modal.ts';
|
|
28
|
+
import { createKeybindingsModalSurface } from './modals/keybindings-modal.ts';
|
|
29
|
+
import { createPairingModalSurface, type PairingModalConnectionInfo } from './modals/pairing-modal.ts';
|
|
30
|
+
import { createPlanningModalSurface } from './modals/planning-modal.ts';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register the config-modal surfaces + their panel-id redirects (W6.1, the
|
|
34
|
+
* purge). Called once at startup from registerBuiltinPanels, AFTER the panels'
|
|
35
|
+
* deps are resolved (the surfaces close over the same read-models the retired
|
|
36
|
+
* panels used). For each MIGRATE-TO-MODAL surface this does two things:
|
|
37
|
+
* 1. registerModalSurface — the data + actions the config-modal host renders.
|
|
38
|
+
* 2. registerModalRedirect — so `/panel open <old-id>`, saved layouts, and any
|
|
39
|
+
* alias still resolve to the modal.
|
|
40
|
+
*
|
|
41
|
+
* Group A (WO-A) is the Providers & Connectivity + Security subset. Group B
|
|
42
|
+
* (WO-P) is the Ecosystem & Governance set — the 12 ported config-modal
|
|
43
|
+
* surfaces plus the `sessions` fold into the existing session-picker modal.
|
|
44
|
+
*/
|
|
45
|
+
export function registerBuiltinModals(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
|
|
46
|
+
const ui = requireUiServices(deps);
|
|
47
|
+
|
|
48
|
+
// ── Providers & Connectivity (WO-A) ─────────────────────────────────────────
|
|
49
|
+
manager.registerModalSurface(createServicesModalSurface(deps.serviceRegistry, deps.subscriptionManager));
|
|
50
|
+
manager.registerModalRedirect('services', 'services-modal');
|
|
51
|
+
|
|
52
|
+
manager.registerModalSurface(createSubscriptionModalSurface(deps.serviceRegistry, deps.subscriptionManager));
|
|
53
|
+
manager.registerModalRedirect('subscription', 'subscription-modal');
|
|
54
|
+
|
|
55
|
+
manager.registerModalSurface(createRemoteModalSurface(ui.readModels.remote));
|
|
56
|
+
manager.registerModalRedirect('remote', 'remote-modal');
|
|
57
|
+
|
|
58
|
+
const providerRuntime = createProviderRuntimeInspectionQuery(createRuntimeProviderApi({
|
|
59
|
+
benchmarkStore: ui.providers.benchmarkStore,
|
|
60
|
+
favoritesStore: ui.providers.favoritesStore,
|
|
61
|
+
providerRegistry: ui.providers.providerRegistry,
|
|
62
|
+
}));
|
|
63
|
+
manager.registerModalSurface(createProviderHealthModalSurface(providerRuntime, ui.readModels.providers));
|
|
64
|
+
manager.registerModalRedirect('provider-health', 'providers-modal');
|
|
65
|
+
manager.registerModalRedirect('providers', 'providers-modal');
|
|
66
|
+
manager.registerModalRedirect('accounts', 'providers-modal');
|
|
67
|
+
|
|
68
|
+
// ── Security & Governance (WO-A subset) ─────────────────────────────────────
|
|
69
|
+
manager.registerModalSurface(createSettingsSyncModalSurface(deps.configManager));
|
|
70
|
+
manager.registerModalRedirect('settings-sync', 'settings-sync-modal');
|
|
71
|
+
|
|
72
|
+
// local-auth-modal is reached via the /local-auth front-door only; no
|
|
73
|
+
// redirect (the 'local-auth' panel id must keep resolving to LocalAuthPanel
|
|
74
|
+
// for the masked password-entry path). See builtin/operations.ts.
|
|
75
|
+
manager.registerModalSurface(createLocalAuthModalSurface(deps.localUserAuthManager));
|
|
76
|
+
|
|
77
|
+
manager.registerModalSurface(createSandboxModalSurface(deps.configManager, deps.sandboxSessionRegistry, deps.requestRender));
|
|
78
|
+
manager.registerModalRedirect('sandbox', 'sandbox-modal');
|
|
79
|
+
|
|
80
|
+
// ── Ecosystem & Governance (WO-P — group B) ─────────────────────────────────
|
|
81
|
+
manager.registerModalSurface(createMarketplaceModalSurface({
|
|
82
|
+
readModel: ui.readModels.marketplace,
|
|
83
|
+
ecosystemPaths: {
|
|
84
|
+
cwd: ui.environment.shellPaths.workingDirectory,
|
|
85
|
+
homeDir: ui.environment.shellPaths.homeDirectory,
|
|
86
|
+
projectCatalogRoot: ui.environment.shellPaths.resolveProjectPath('tui', 'ecosystem'),
|
|
87
|
+
userCatalogRoot: ui.environment.shellPaths.resolveUserPath('tui', 'ecosystem'),
|
|
88
|
+
},
|
|
89
|
+
}));
|
|
90
|
+
manager.registerModalRedirect('marketplace', 'marketplace-modal');
|
|
91
|
+
|
|
92
|
+
// plugins/hooks/knowledge deps are wired at bootstrap in production
|
|
93
|
+
// (bootstrap-shell.ts) but may be absent in a partially-wired context (e.g. a
|
|
94
|
+
// release-gate harness) — register a degraded placeholder rather than throw,
|
|
95
|
+
// so the surface + redirect always resolve (the "always register, degrade
|
|
96
|
+
// honestly" charter pattern).
|
|
97
|
+
manager.registerModalSurface(deps.pluginManager
|
|
98
|
+
? createPluginsModalSurface({ pluginManager: deps.pluginManager })
|
|
99
|
+
: unwiredSurface('plugins-modal', 'Plugins', 'Plugin manager not wired into this session.'));
|
|
100
|
+
manager.registerModalRedirect('plugins', 'plugins-modal');
|
|
101
|
+
|
|
102
|
+
manager.registerModalSurface(createSkillsModalSurface({
|
|
103
|
+
shellPaths: {
|
|
104
|
+
workingDirectory: ui.environment.shellPaths.workingDirectory,
|
|
105
|
+
homeDirectory: ui.environment.shellPaths.homeDirectory,
|
|
106
|
+
},
|
|
107
|
+
ecosystemPaths: {
|
|
108
|
+
cwd: ui.environment.shellPaths.workingDirectory,
|
|
109
|
+
homeDir: ui.environment.shellPaths.homeDirectory,
|
|
110
|
+
projectCatalogRoot: ui.environment.shellPaths.resolveProjectPath('tui', 'ecosystem'),
|
|
111
|
+
userCatalogRoot: ui.environment.shellPaths.resolveUserPath('tui', 'ecosystem'),
|
|
112
|
+
},
|
|
113
|
+
}));
|
|
114
|
+
manager.registerModalRedirect('skills', 'skills-modal');
|
|
115
|
+
|
|
116
|
+
manager.registerModalSurface(deps.hookDispatcher && deps.hookWorkbench && deps.hookActivityTracker
|
|
117
|
+
? createHooksModalSurface({ hookDispatcher: deps.hookDispatcher, hookWorkbench: deps.hookWorkbench, hookActivityTracker: deps.hookActivityTracker })
|
|
118
|
+
: unwiredSurface('hooks-modal', 'Hooks', 'Hook dispatcher/workbench not wired into this session.'));
|
|
119
|
+
manager.registerModalRedirect('hooks', 'hooks-modal');
|
|
120
|
+
|
|
121
|
+
manager.registerModalSurface(createSecurityModalSurface({ readModel: ui.readModels.security }));
|
|
122
|
+
manager.registerModalRedirect('security', 'security-modal');
|
|
123
|
+
|
|
124
|
+
manager.registerModalSurface(createPolicyModalSurface({ policyRuntimeState: deps.policyRuntimeState }));
|
|
125
|
+
manager.registerModalRedirect('policy', 'policy-modal');
|
|
126
|
+
|
|
127
|
+
manager.registerModalSurface(deps.knowledgeApi
|
|
128
|
+
? createKnowledgeModalSurface({ knowledgeApi: deps.knowledgeApi })
|
|
129
|
+
: unwiredSurface('knowledge-modal', 'Knowledge', 'Knowledge API not wired into this session.'));
|
|
130
|
+
manager.registerModalRedirect('knowledge', 'knowledge-modal');
|
|
131
|
+
|
|
132
|
+
manager.registerModalSurface(createMemoryModalSurface({
|
|
133
|
+
memoryRegistry: deps.memoryRegistry as MemoryModalDeps['memoryRegistry'],
|
|
134
|
+
}));
|
|
135
|
+
manager.registerModalRedirect('memory', 'memory-modal');
|
|
136
|
+
|
|
137
|
+
manager.registerModalSurface(createWorkPlanModalSurface({ workPlanStore: deps.workPlanStore }));
|
|
138
|
+
manager.registerModalRedirect('work-plan', 'work-plan-modal');
|
|
139
|
+
|
|
140
|
+
manager.registerModalSurface(createKeybindingsModalSurface({
|
|
141
|
+
toolRegistry: deps.toolRegistry,
|
|
142
|
+
providerRegistry: deps.providerRegistry,
|
|
143
|
+
keybindingsManager: ui.shell.keybindingsManager,
|
|
144
|
+
}));
|
|
145
|
+
manager.registerModalRedirect('docs', 'keybindings-modal');
|
|
146
|
+
|
|
147
|
+
manager.registerModalSurface(createPairingModalSurface({
|
|
148
|
+
getConnectionInfo: () => buildPairingConnectionInfo(deps),
|
|
149
|
+
controlPlaneReadModel: ui.readModels.controlPlane,
|
|
150
|
+
copyToClipboard,
|
|
151
|
+
}));
|
|
152
|
+
manager.registerModalRedirect('qr-code', 'pairing-modal');
|
|
153
|
+
|
|
154
|
+
manager.registerModalSurface(createPlanningModalSurface({
|
|
155
|
+
service: deps.projectPlanningService,
|
|
156
|
+
projectId: deps.projectPlanningProjectId,
|
|
157
|
+
requestRender: deps.requestRender,
|
|
158
|
+
}));
|
|
159
|
+
manager.registerModalRedirect('project-planning', 'planning-modal');
|
|
160
|
+
|
|
161
|
+
// The retired 'sessions' panel folds into the EXISTING session-picker modal
|
|
162
|
+
// (no new config-modal surface); the redirect is a plain hand-off, moved here
|
|
163
|
+
// from the deleted ecosystem bridge.
|
|
164
|
+
manager.registerModalRedirect('sessions', 'sessionPicker');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build the companion-pairing connection info lazily (called from the pairing
|
|
169
|
+
* surface's first render, not at registration time) so a missing daemon home
|
|
170
|
+
* degrades to an honest "unavailable" modal instead of throwing at startup.
|
|
171
|
+
* Mirrors the retired QrPanel factory's construction.
|
|
172
|
+
*/
|
|
173
|
+
function buildPairingConnectionInfo(deps: ResolvedBuiltinPanelDeps): PairingModalConnectionInfo | null {
|
|
174
|
+
if (!deps.daemonHomeDir) return null;
|
|
175
|
+
try {
|
|
176
|
+
const daemonHomeDir = deps.daemonHomeDir;
|
|
177
|
+
const tokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
|
|
178
|
+
const daemonPort = deps.configManager.get('controlPlane.port');
|
|
179
|
+
const daemonHost = String(process.env['GOODVIBES_DAEMON_HOST'] ?? getLocalNetworkIp());
|
|
180
|
+
const daemonUrl = `http://${daemonHost}:${daemonPort}`;
|
|
181
|
+
const bootstrapPassword = readBootstrapPassword(deps.localUserAuthManager.getBootstrapCredentialPath());
|
|
182
|
+
return buildCompanionConnectionInfo({ daemonUrl, token: tokenRecord.token, password: bootstrapPassword, surface: 'tui' }) as PairingModalConnectionInfo;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A minimal config-modal surface that renders an honest "not wired" degraded
|
|
189
|
+
* state — used when a group-B surface's bootstrap dependency is absent. */
|
|
190
|
+
function unwiredSurface(name: string, title: string, reason: string): ConfigModalSurface {
|
|
191
|
+
return {
|
|
192
|
+
name,
|
|
193
|
+
title,
|
|
194
|
+
buildView: (): ConfigModalView => ({ title, degraded: reason, tabs: [{ id: 'main', label: title, rows: [] }] }),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getLocalNetworkIp(): string {
|
|
199
|
+
const nets = networkInterfaces();
|
|
200
|
+
for (const name of Object.keys(nets)) {
|
|
201
|
+
for (const net of nets[name] ?? []) {
|
|
202
|
+
if (net.family === 'IPv4' && !net.internal) return net.address;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return 'localhost';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function readBootstrapPassword(credentialPath: string): string | undefined {
|
|
209
|
+
try {
|
|
210
|
+
const content = readFileSync(credentialPath, 'utf-8');
|
|
211
|
+
for (const line of content.split('\n')) {
|
|
212
|
+
if (line.startsWith('password=')) return line.slice('password='.length).trim();
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// credential file may not exist yet
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
@@ -5,6 +5,7 @@ import { registerOperationsPanels } from './builtin/operations.ts';
|
|
|
5
5
|
import { registerAgentPanels } from './builtin/agent.ts';
|
|
6
6
|
import { registerSessionPanels } from './builtin/session.ts';
|
|
7
7
|
import { registerKnowledgePanels } from './builtin/knowledge.ts';
|
|
8
|
+
import { registerBuiltinModals } from './builtin-modals.ts';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Register all built-in panel types with the given PanelManager.
|
|
@@ -18,6 +19,10 @@ export function registerBuiltinPanels(manager: PanelManager, deps: BuiltinPanelD
|
|
|
18
19
|
registerAgentPanels(manager, resolved);
|
|
19
20
|
registerSessionPanels(manager, resolved);
|
|
20
21
|
registerKnowledgePanels(manager, resolved);
|
|
22
|
+
// W6.1 (the purge): register MIGRATE-TO-MODAL surfaces + their panel-id
|
|
23
|
+
// redirects. Must run after the panel registrations so a redirected id wins
|
|
24
|
+
// over any lingering panel registration in open().
|
|
25
|
+
registerBuiltinModals(manager, resolved);
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export type { BuiltinPanelDeps } from './builtin/shared.ts';
|
|
@@ -22,13 +22,15 @@ import {
|
|
|
22
22
|
DEFAULT_PANEL_PALETTE,
|
|
23
23
|
type PanelWorkspaceSection,
|
|
24
24
|
} from './polish.ts';
|
|
25
|
-
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
25
|
+
import { calcSessionCost, isModelPriced, computeBudgetBreach, readBudgetAlertUsd, type BudgetAlertConfigAccess } from '../export/cost-utils.ts';
|
|
26
26
|
import { abbreviateCount } from '../utils/format-number.ts';
|
|
27
27
|
import { isTextBackspace } from '../input/delete-key-policy.ts';
|
|
28
28
|
|
|
29
29
|
// Pricing lookups are provided by ../export/cost-utils.ts (single source of truth).
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/** unpriced=true renders an honest "unpriced" marker instead of a $0.00 that could be mistaken for a real zero cost. */
|
|
32
|
+
function formatCost(usd: number, unpriced = false): string {
|
|
33
|
+
if (unpriced) return 'unpriced';
|
|
32
34
|
if (usd === 0) return '$0.00';
|
|
33
35
|
if (usd < 0.0001) return '<$0.0001';
|
|
34
36
|
if (usd < 0.01) return `$${usd.toFixed(4)}`;
|
|
@@ -106,9 +108,20 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
106
108
|
|
|
107
109
|
// Budget alert threshold in USD (0 = disabled). Mutable at runtime via the
|
|
108
110
|
// in-panel 'b' numeric entry or the /cost budget <usd> command — both call
|
|
109
|
-
// setBudgetThreshold() below.
|
|
111
|
+
// setBudgetThreshold() below. When configAccess is wired (production path),
|
|
112
|
+
// this field is a cache only — currentBudgetThreshold() re-reads
|
|
113
|
+
// behavior.budgetAlertUsd from config on every use so the panel and the
|
|
114
|
+
// background budget-breach notifier (core/budget-breach-notifier.ts) never
|
|
115
|
+
// disagree about the threshold. Tests that omit configAccess fall back to
|
|
116
|
+
// this field as the sole source of truth, unchanged from prior behavior.
|
|
110
117
|
private budgetThreshold: number;
|
|
111
118
|
|
|
119
|
+
// Optional config-backed source of truth for the budget threshold. Absent
|
|
120
|
+
// in most existing unit tests (which construct the panel directly) and in
|
|
121
|
+
// any build that doesn't wire a config manager; present in production via
|
|
122
|
+
// builtin/development.ts.
|
|
123
|
+
private readonly configAccess: BudgetAlertConfigAccess | undefined;
|
|
124
|
+
|
|
112
125
|
// Draft buffer for the in-panel budget-entry mode ('b' key). Non-null while
|
|
113
126
|
// entry is active; unlike LocalAuthPanel's masked entry, the value is not
|
|
114
127
|
// secret so it is echoed directly.
|
|
@@ -136,15 +149,27 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
136
149
|
turnEvents: UiEventFeed<TurnEvent>,
|
|
137
150
|
agentEvents: UiEventFeed<AgentEvent>,
|
|
138
151
|
getOrchestratorUsage: () => UsageSnapshot & { model?: string },
|
|
139
|
-
opts: {
|
|
152
|
+
opts: {
|
|
153
|
+
budgetThreshold?: number;
|
|
154
|
+
getAgentStatus?: (agentId: string) => AgentRecord | null;
|
|
155
|
+
configAccess?: BudgetAlertConfigAccess;
|
|
156
|
+
} = {},
|
|
140
157
|
) {
|
|
141
158
|
super('cost', 'Cost', '$', 'providers');
|
|
142
159
|
this.getOrchestratorUsage = getOrchestratorUsage;
|
|
143
160
|
this.getAgentStatus = opts.getAgentStatus;
|
|
144
|
-
this.
|
|
161
|
+
this.configAccess = opts.configAccess;
|
|
162
|
+
this.budgetThreshold = this.configAccess ? readBudgetAlertUsd(this.configAccess.get) : (opts.budgetThreshold ?? 0);
|
|
145
163
|
this.attachEvents(turnEvents, agentEvents);
|
|
146
164
|
}
|
|
147
165
|
|
|
166
|
+
/** The live budget threshold: re-read from config when configAccess is wired
|
|
167
|
+
* (single source of truth shared with the background notifier), otherwise
|
|
168
|
+
* the locally-cached field (test/no-config-manager fallback). */
|
|
169
|
+
private currentBudgetThreshold(): number {
|
|
170
|
+
return this.configAccess ? readBudgetAlertUsd(this.configAccess.get) : this.budgetThreshold;
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
// -------------------------------------------------------------------------
|
|
149
174
|
// Bus wiring
|
|
150
175
|
// -------------------------------------------------------------------------
|
|
@@ -307,6 +332,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
307
332
|
public setBudgetThreshold(usd: number): void {
|
|
308
333
|
if (!Number.isFinite(usd) || usd < 0) return;
|
|
309
334
|
this.budgetThreshold = usd;
|
|
335
|
+
this.configAccess?.set('behavior.budgetAlertUsd', usd);
|
|
310
336
|
this.markDirty();
|
|
311
337
|
}
|
|
312
338
|
|
|
@@ -314,11 +340,22 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
314
340
|
// Input
|
|
315
341
|
// -------------------------------------------------------------------------
|
|
316
342
|
|
|
343
|
+
/**
|
|
344
|
+
* The budget-threshold entry field wants every character of a burst
|
|
345
|
+
* (paste, or fast typing landing in one input.feed() call) delivered one
|
|
346
|
+
* at a time, same as it always has — see the interface doc on
|
|
347
|
+
* `Panel.isCapturingTextBurst`.
|
|
348
|
+
*/
|
|
349
|
+
isCapturingTextBurst(): boolean {
|
|
350
|
+
return this.budgetEntry !== null;
|
|
351
|
+
}
|
|
352
|
+
|
|
317
353
|
handleInput(key: string): boolean {
|
|
318
354
|
if (this.budgetEntry !== null) return this.handleBudgetEntryInput(key);
|
|
319
355
|
|
|
320
356
|
if (key === 'b') {
|
|
321
|
-
|
|
357
|
+
const threshold = this.currentBudgetThreshold();
|
|
358
|
+
this.budgetEntry = threshold > 0 ? String(threshold) : '';
|
|
322
359
|
this.markDirty();
|
|
323
360
|
return true;
|
|
324
361
|
}
|
|
@@ -393,12 +430,13 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
393
430
|
|
|
394
431
|
const totalInputTokens = this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite;
|
|
395
432
|
const sessionCost = calcSessionCost(this.sessionUsage.input, this.sessionUsage.output, this.sessionUsage.cacheRead, this.sessionUsage.cacheWrite, this.sessionModel);
|
|
396
|
-
const
|
|
433
|
+
const budgetThreshold = this.currentBudgetThreshold();
|
|
434
|
+
const overBudget = computeBudgetBreach(sessionCost, budgetThreshold);
|
|
397
435
|
const sparkline = buildSparkline(this.costHistory);
|
|
398
|
-
const costStr = formatCost(sessionCost);
|
|
436
|
+
const costStr = formatCost(sessionCost, !isModelPriced(this.sessionModel));
|
|
399
437
|
const costFg = overBudget ? C.bad : C.cost;
|
|
400
|
-
const budgetStr =
|
|
401
|
-
? ` / ${formatCost(
|
|
438
|
+
const budgetStr = budgetThreshold > 0
|
|
439
|
+
? ` / ${formatCost(budgetThreshold)}`
|
|
402
440
|
: '';
|
|
403
441
|
const alertStr = overBudget ? ' ! OVER BUDGET' : '';
|
|
404
442
|
const sessionLines: Line[] = [
|
|
@@ -407,8 +445,8 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
407
445
|
// Budget meter — the single most important glance for this panel: how much
|
|
408
446
|
// of the configured budget the session has consumed. Only shown when a
|
|
409
447
|
// budget is set (otherwise the bar would be meaningless).
|
|
410
|
-
if (
|
|
411
|
-
const ratio = sessionCost /
|
|
448
|
+
if (budgetThreshold > 0) {
|
|
449
|
+
const ratio = sessionCost / budgetThreshold;
|
|
412
450
|
const BAR_W = 24;
|
|
413
451
|
const filled = Math.max(0, Math.min(BAR_W, Math.round(ratio * BAR_W)));
|
|
414
452
|
const meterFg = overBudget ? C.bad : ratio >= 0.8 ? C.warn : C.good;
|
|
@@ -451,10 +489,14 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
451
489
|
const planCost = agentList.reduce((sum, a) => sum + a.cost, 0);
|
|
452
490
|
const running = agentList.filter((a) => a.status === 'running').length;
|
|
453
491
|
const failed = agentList.filter((a) => a.status === 'failed').length;
|
|
492
|
+
// Plan total mixes the session model with every agent's model — flag it
|
|
493
|
+
// as unpriced (rather than showing a sum that silently omits unpriced
|
|
494
|
+
// agents' contribution) if any one of them has no real pricing.
|
|
495
|
+
const planHasUnpriced = !isModelPriced(this.sessionModel) || agentList.some((a) => !isModelPriced(a.model));
|
|
454
496
|
const agentRows: Line[] = [
|
|
455
497
|
buildStyledPanelLine(width, [
|
|
456
498
|
{ text: ' Plan total ', fg: C.label },
|
|
457
|
-
{ text: formatCost(planCost + sessionCost), fg: C.cost, bold: true },
|
|
499
|
+
{ text: formatCost(planCost + sessionCost, planHasUnpriced), fg: C.cost, bold: true },
|
|
458
500
|
{ text: ` ${agentList.length} agent${agentList.length === 1 ? '' : 's'}`, fg: C.dim },
|
|
459
501
|
...(running > 0 ? [{ text: ` ${running} running`, fg: C.running }] : []),
|
|
460
502
|
...(failed > 0 ? [{ text: ` ${failed} failed`, fg: C.bad }] : []),
|
|
@@ -483,7 +525,14 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
483
525
|
{ text: agent.model, fg: C.model },
|
|
484
526
|
{ text: agent.inputTokens > 0 ? formatTokens(agent.inputTokens) : '-', fg: C.dim },
|
|
485
527
|
{ text: agent.outputTokens > 0 ? formatTokens(agent.outputTokens) : '-', fg: C.dim },
|
|
486
|
-
{
|
|
528
|
+
{
|
|
529
|
+
// '-' means no usage reported yet; 'unpriced' means usage
|
|
530
|
+
// exists but the model has no real price; otherwise the cost.
|
|
531
|
+
text: agent.inputTokens > 0 && !isModelPriced(agent.model)
|
|
532
|
+
? 'unpriced'
|
|
533
|
+
: agent.cost > 0 ? formatCost(agent.cost) : '-',
|
|
534
|
+
fg: agent.cost > 0 ? C.cost : C.dim,
|
|
535
|
+
},
|
|
487
536
|
],
|
|
488
537
|
};
|
|
489
538
|
}),
|