@pellux/goodvibes-tui 1.1.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 +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/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 +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- 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-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- 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-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -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 +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -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 +24 -27
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +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 +36 -10
- package/src/panels/diff-panel.ts +12 -43
- 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/index.ts +7 -31
- 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/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -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/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- 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 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -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,7 +22,7 @@ import {
|
|
|
22
22
|
DEFAULT_PANEL_PALETTE,
|
|
23
23
|
type PanelWorkspaceSection,
|
|
24
24
|
} from './polish.ts';
|
|
25
|
-
import { calcSessionCost, isModelPriced } 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
|
|
|
@@ -108,9 +108,20 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
108
108
|
|
|
109
109
|
// Budget alert threshold in USD (0 = disabled). Mutable at runtime via the
|
|
110
110
|
// in-panel 'b' numeric entry or the /cost budget <usd> command — both call
|
|
111
|
-
// 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.
|
|
112
117
|
private budgetThreshold: number;
|
|
113
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
|
+
|
|
114
125
|
// Draft buffer for the in-panel budget-entry mode ('b' key). Non-null while
|
|
115
126
|
// entry is active; unlike LocalAuthPanel's masked entry, the value is not
|
|
116
127
|
// secret so it is echoed directly.
|
|
@@ -138,15 +149,27 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
138
149
|
turnEvents: UiEventFeed<TurnEvent>,
|
|
139
150
|
agentEvents: UiEventFeed<AgentEvent>,
|
|
140
151
|
getOrchestratorUsage: () => UsageSnapshot & { model?: string },
|
|
141
|
-
opts: {
|
|
152
|
+
opts: {
|
|
153
|
+
budgetThreshold?: number;
|
|
154
|
+
getAgentStatus?: (agentId: string) => AgentRecord | null;
|
|
155
|
+
configAccess?: BudgetAlertConfigAccess;
|
|
156
|
+
} = {},
|
|
142
157
|
) {
|
|
143
158
|
super('cost', 'Cost', '$', 'providers');
|
|
144
159
|
this.getOrchestratorUsage = getOrchestratorUsage;
|
|
145
160
|
this.getAgentStatus = opts.getAgentStatus;
|
|
146
|
-
this.
|
|
161
|
+
this.configAccess = opts.configAccess;
|
|
162
|
+
this.budgetThreshold = this.configAccess ? readBudgetAlertUsd(this.configAccess.get) : (opts.budgetThreshold ?? 0);
|
|
147
163
|
this.attachEvents(turnEvents, agentEvents);
|
|
148
164
|
}
|
|
149
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
|
+
|
|
150
173
|
// -------------------------------------------------------------------------
|
|
151
174
|
// Bus wiring
|
|
152
175
|
// -------------------------------------------------------------------------
|
|
@@ -309,6 +332,7 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
309
332
|
public setBudgetThreshold(usd: number): void {
|
|
310
333
|
if (!Number.isFinite(usd) || usd < 0) return;
|
|
311
334
|
this.budgetThreshold = usd;
|
|
335
|
+
this.configAccess?.set('behavior.budgetAlertUsd', usd);
|
|
312
336
|
this.markDirty();
|
|
313
337
|
}
|
|
314
338
|
|
|
@@ -330,7 +354,8 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
330
354
|
if (this.budgetEntry !== null) return this.handleBudgetEntryInput(key);
|
|
331
355
|
|
|
332
356
|
if (key === 'b') {
|
|
333
|
-
|
|
357
|
+
const threshold = this.currentBudgetThreshold();
|
|
358
|
+
this.budgetEntry = threshold > 0 ? String(threshold) : '';
|
|
334
359
|
this.markDirty();
|
|
335
360
|
return true;
|
|
336
361
|
}
|
|
@@ -405,12 +430,13 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
405
430
|
|
|
406
431
|
const totalInputTokens = this.sessionUsage.input + this.sessionUsage.cacheRead + this.sessionUsage.cacheWrite;
|
|
407
432
|
const sessionCost = calcSessionCost(this.sessionUsage.input, this.sessionUsage.output, this.sessionUsage.cacheRead, this.sessionUsage.cacheWrite, this.sessionModel);
|
|
408
|
-
const
|
|
433
|
+
const budgetThreshold = this.currentBudgetThreshold();
|
|
434
|
+
const overBudget = computeBudgetBreach(sessionCost, budgetThreshold);
|
|
409
435
|
const sparkline = buildSparkline(this.costHistory);
|
|
410
436
|
const costStr = formatCost(sessionCost, !isModelPriced(this.sessionModel));
|
|
411
437
|
const costFg = overBudget ? C.bad : C.cost;
|
|
412
|
-
const budgetStr =
|
|
413
|
-
? ` / ${formatCost(
|
|
438
|
+
const budgetStr = budgetThreshold > 0
|
|
439
|
+
? ` / ${formatCost(budgetThreshold)}`
|
|
414
440
|
: '';
|
|
415
441
|
const alertStr = overBudget ? ' ! OVER BUDGET' : '';
|
|
416
442
|
const sessionLines: Line[] = [
|
|
@@ -419,8 +445,8 @@ export class CostTrackerPanel extends BasePanel {
|
|
|
419
445
|
// Budget meter — the single most important glance for this panel: how much
|
|
420
446
|
// of the configured budget the session has consumed. Only shown when a
|
|
421
447
|
// budget is set (otherwise the bar would be meaningless).
|
|
422
|
-
if (
|
|
423
|
-
const ratio = sessionCost /
|
|
448
|
+
if (budgetThreshold > 0) {
|
|
449
|
+
const ratio = sessionCost / budgetThreshold;
|
|
424
450
|
const BAR_W = 24;
|
|
425
451
|
const filled = Math.max(0, Math.min(BAR_W, Math.round(ratio * BAR_W)));
|
|
426
452
|
const meterFg = overBudget ? C.bad : ratio >= 0.8 ? C.warn : C.good;
|