@pellux/goodvibes-tui 0.28.0 → 0.29.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 +101 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +5 -1
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +203 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +309 -128
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +21 -7
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +5 -4
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +41 -30
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -8,7 +8,7 @@ import type { ApprovalBroker, SharedSessionBroker } from '@pellux/goodvibes-sdk/
|
|
|
8
8
|
import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
|
|
9
9
|
import type { ControlPlaneRecentEvent } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
10
10
|
import type { UiRuntimeServices } from '../../runtime/ui-services.ts';
|
|
11
|
-
import type {
|
|
11
|
+
import type { PluginManagerControls } from '../plugins-panel.ts';
|
|
12
12
|
import type { HookWorkbench } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
13
13
|
import type { HookDispatcher } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
14
14
|
import type { HookActivityTracker } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
@@ -25,6 +25,15 @@ import type { ApiTokenAuditor } from '@pellux/goodvibes-sdk/platform/security';
|
|
|
25
25
|
import type { ComponentHealthMonitor } from '../../runtime/perf/panel-health-monitor.ts';
|
|
26
26
|
import type { WorktreeRegistry } from '@/runtime/index.ts';
|
|
27
27
|
import type { SandboxSessionRegistry } from '@/runtime/index.ts';
|
|
28
|
+
import type { OpsApi, PlanRuntimeService } from '@/runtime/index.ts';
|
|
29
|
+
import type { WatcherRegistry } from '@pellux/goodvibes-sdk/platform/watchers';
|
|
30
|
+
import type { RuntimeStore } from '../../runtime/store/index.ts';
|
|
31
|
+
import type { KnowledgeApi } from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
32
|
+
import type { SessionChangeTracker } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
33
|
+
import type { Line } from '../../types/grid.ts';
|
|
34
|
+
import type { Panel, PanelCategory } from '../types.ts';
|
|
35
|
+
import { BasePanel } from '../base-panel.ts';
|
|
36
|
+
import { buildEmptyState, buildPanelWorkspace, DEFAULT_PANEL_PALETTE } from '../polish.ts';
|
|
28
37
|
|
|
29
38
|
export interface BuiltinPanelDeps {
|
|
30
39
|
/** Config manager for settings-sync and other config-backed panels. */
|
|
@@ -45,7 +54,7 @@ export interface BuiltinPanelDeps {
|
|
|
45
54
|
subscriptionManager?: SubscriptionManager;
|
|
46
55
|
/** Shared service registry for services-backed panels. */
|
|
47
56
|
serviceRegistry?: ServiceRegistry;
|
|
48
|
-
/** Context window size in tokens (for
|
|
57
|
+
/** Context window size in tokens. Unused since WO-113 folded ContextVisualizerPanel into TokenBudgetPanel (which reads getCtxWindow instead); kept for source compatibility. */
|
|
49
58
|
contextWindow?: number;
|
|
50
59
|
/** Main Orchestrator instance for TokenBudgetPanel.wire(). */
|
|
51
60
|
orchestrator?: Orchestrator;
|
|
@@ -104,8 +113,8 @@ export interface BuiltinPanelDeps {
|
|
|
104
113
|
systemMessagesPanel?: import('../system-messages-panel.ts').SystemMessagesPanel;
|
|
105
114
|
/** Explicit UI-facing runtime services for agent/process/WRFC/remote panels and modals. */
|
|
106
115
|
uiServices?: UiRuntimeServices;
|
|
107
|
-
/** Shared plugin manager
|
|
108
|
-
pluginManager?:
|
|
116
|
+
/** 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). */
|
|
117
|
+
pluginManager?: PluginManagerControls;
|
|
109
118
|
/** Shared hook dispatcher for the hooks control-room panel. */
|
|
110
119
|
hookDispatcher?: Pick<HookDispatcher, 'listHooks' | 'getChains'>;
|
|
111
120
|
/** Shared hook workbench for the hooks control-room panel. */
|
|
@@ -114,6 +123,24 @@ export interface BuiltinPanelDeps {
|
|
|
114
123
|
hookActivityTracker?: Pick<HookActivityTracker, 'listRecent'>;
|
|
115
124
|
/** Shared MCP registry for security panels and MCP workspace commands. */
|
|
116
125
|
mcpRegistry?: McpRegistry;
|
|
126
|
+
/** Ops control-plane API (cancel/pause/resume/retry) for operator/ops panels to drive real actions. */
|
|
127
|
+
opsApi?: OpsApi;
|
|
128
|
+
/** Plan runtime service for plan/ops-strategy panels to drive adaptive-planner actions. */
|
|
129
|
+
planRuntime?: PlanRuntimeService;
|
|
130
|
+
/** Watcher registry for the watchers panel to drive watcher lifecycle actions. */
|
|
131
|
+
watcherRegistry?: WatcherRegistry;
|
|
132
|
+
/** Root runtime store for panels that need direct selector access to runtime state (see `src/runtime/store/selectors/index.ts`). */
|
|
133
|
+
runtimeStore?: RuntimeStore;
|
|
134
|
+
/** Knowledge API surface (graph nodes/sources/issues, search, schedules) for the Knowledge panel. */
|
|
135
|
+
knowledgeApi?: KnowledgeApi;
|
|
136
|
+
/** Optional session change tracker for the Git panel's session-changed file highlights. */
|
|
137
|
+
sessionChangeTracker?: Pick<SessionChangeTracker, 'getChangedFiles'>;
|
|
138
|
+
/**
|
|
139
|
+
* 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).
|
|
142
|
+
*/
|
|
143
|
+
openPanel?: (panelId: string) => void;
|
|
117
144
|
/**
|
|
118
145
|
* Open the agent detail modal for the given agent id. Wired from
|
|
119
146
|
* InputHandler.agentDetailModal.open() at bootstrap — passed to the
|
|
@@ -239,7 +266,7 @@ export function requireControlPlanePanelDeps(deps: BuiltinPanelDeps): ControlPla
|
|
|
239
266
|
|
|
240
267
|
export function requireAutomationManager(deps: BuiltinPanelDeps): AutomationManager {
|
|
241
268
|
if (!deps.automationManager) {
|
|
242
|
-
throw new Error('
|
|
269
|
+
throw new Error('AutomationControlPanel requires an automation manager to be wired at bootstrap.');
|
|
243
270
|
}
|
|
244
271
|
return deps.automationManager;
|
|
245
272
|
}
|
|
@@ -251,7 +278,7 @@ export function requireUiServices(deps: BuiltinPanelDeps): UiRuntimeServices {
|
|
|
251
278
|
return deps.uiServices;
|
|
252
279
|
}
|
|
253
280
|
|
|
254
|
-
export function requirePluginManager(deps: BuiltinPanelDeps):
|
|
281
|
+
export function requirePluginManager(deps: BuiltinPanelDeps): PluginManagerControls {
|
|
255
282
|
if (!deps.pluginManager) {
|
|
256
283
|
throw new Error('Plugin manager must be wired at bootstrap for plugin and security panels.');
|
|
257
284
|
}
|
|
@@ -279,3 +306,68 @@ export function requireMcpRegistry(deps: BuiltinPanelDeps): McpRegistry {
|
|
|
279
306
|
}
|
|
280
307
|
return deps.mcpRegistry;
|
|
281
308
|
}
|
|
309
|
+
|
|
310
|
+
export function requireKnowledgeApi(deps: BuiltinPanelDeps): KnowledgeApi {
|
|
311
|
+
if (!deps.knowledgeApi) {
|
|
312
|
+
throw new Error('Knowledge API must be wired at bootstrap for the Knowledge panel.');
|
|
313
|
+
}
|
|
314
|
+
return deps.knowledgeApi;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// WO-152: always-register conditional panels with a "dependency not
|
|
319
|
+
// configured" empty state instead of skipping registration entirely.
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Minimal placeholder Panel used when a builtin panel's runtime dependency
|
|
324
|
+
* (e.g. an orchestrator usage getter, a memory registry, an eval registry)
|
|
325
|
+
* was not wired at bootstrap for this build/session. Renders a single
|
|
326
|
+
* "dependency not configured" empty state via `buildEmptyState` so opening
|
|
327
|
+
* the panel id (`/panel open <id>`, a saved layout, a cross-panel jump)
|
|
328
|
+
* always resolves to a real panel instead of "Unknown panel" — the panel
|
|
329
|
+
* type is always registered; only its data source is sometimes absent.
|
|
330
|
+
*/
|
|
331
|
+
class UnconfiguredDependencyPanel extends BasePanel {
|
|
332
|
+
constructor(
|
|
333
|
+
id: string,
|
|
334
|
+
name: string,
|
|
335
|
+
icon: string,
|
|
336
|
+
category: PanelCategory,
|
|
337
|
+
private readonly emptyTitle: string,
|
|
338
|
+
private readonly emptyBody: string,
|
|
339
|
+
) {
|
|
340
|
+
super(id, name, icon, category);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
render(width: number, height: number): Line[] {
|
|
344
|
+
if (width <= 0 || height <= 0) return [];
|
|
345
|
+
return buildPanelWorkspace(width, height, {
|
|
346
|
+
title: `${this.name} Workspace`,
|
|
347
|
+
sections: [{
|
|
348
|
+
lines: buildEmptyState(width, this.emptyTitle, this.emptyBody, [], DEFAULT_PANEL_PALETTE),
|
|
349
|
+
}],
|
|
350
|
+
palette: DEFAULT_PANEL_PALETTE,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Build a factory that instantiates `configured()` when `dependencyPresent`
|
|
357
|
+
* is true, otherwise a placeholder Panel rendering `emptyTitle`/`emptyBody`
|
|
358
|
+
* via `buildEmptyState`. Use for builtin panels whose registration used to
|
|
359
|
+
* be gated behind an `if (deps.xyz)` check (cost/memory/incident/eval).
|
|
360
|
+
*/
|
|
361
|
+
export function withUnconfiguredFallback(
|
|
362
|
+
dependencyPresent: boolean,
|
|
363
|
+
id: string,
|
|
364
|
+
name: string,
|
|
365
|
+
icon: string,
|
|
366
|
+
category: PanelCategory,
|
|
367
|
+
emptyTitle: string,
|
|
368
|
+
emptyBody: string,
|
|
369
|
+
configured: () => Panel,
|
|
370
|
+
): () => Panel {
|
|
371
|
+
if (dependencyPresent) return configured;
|
|
372
|
+
return () => new UnconfiguredDependencyPanel(id, name, icon, category, emptyTitle, emptyBody);
|
|
373
|
+
}
|
|
@@ -5,21 +5,25 @@ import { BasePanel } from './base-panel.ts';
|
|
|
5
5
|
import type { UiCockpitSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
6
6
|
import type { CockpitRosterReadModel } from './cockpit-read-model.ts';
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
buildKeyValueLine,
|
|
8
|
+
buildAlignedRow,
|
|
10
9
|
buildPanelLine,
|
|
11
10
|
buildPanelWorkspace,
|
|
12
11
|
buildStatPill,
|
|
13
12
|
DEFAULT_PANEL_PALETTE,
|
|
13
|
+
type ColumnSpec,
|
|
14
14
|
type PanelWorkspaceSection,
|
|
15
15
|
} from './polish.ts';
|
|
16
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
16
17
|
import { agentStatusColor } from './agent-inspector-shared.ts';
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// Base chrome only — title band and text tokens come straight from
|
|
20
|
+
// DEFAULT_PANEL_PALETTE (WO-002).
|
|
21
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
22
|
+
|
|
23
|
+
// How often the panel force-refreshes while active so time-based state (the
|
|
24
|
+
// roster's stalled badges, which derive from wall-clock elapsed time rather
|
|
25
|
+
// than an event) stays current even when no agent lifecycle event has fired.
|
|
26
|
+
const STALL_TICK_MS = 5_000;
|
|
23
27
|
|
|
24
28
|
function pickColor(value: number, warnAt = 1, badAt = 3): string {
|
|
25
29
|
if (value >= badAt) return C.bad;
|
|
@@ -28,41 +32,143 @@ function pickColor(value: number, warnAt = 1, badAt = 3): string {
|
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export interface CockpitPanelActionCallbacks {
|
|
35
|
+
/** Open the quick agent-detail modal (bootstrap-shell openAgentDetail wiring). */
|
|
31
36
|
readonly openAgentDetail: (agentId: string) => void;
|
|
37
|
+
/** Deep-link into the Inspector panel focused on this agent (WO-110 inspectAgent route). */
|
|
38
|
+
readonly inspectAgent: (agentId: string) => void;
|
|
32
39
|
readonly cancelAgent: (agentId: string) => boolean;
|
|
40
|
+
/** Cross-panel jump for a domain mini-summary card (wraps PanelManager.open via deps.openPanel). */
|
|
41
|
+
readonly openPanel: (panelId: string) => void;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
const WORKSPACE_IDS = ['flow', 'governance', 'health', 'domains', 'agents'] as const;
|
|
36
45
|
type WorkspaceId = (typeof WORKSPACE_IDS)[number];
|
|
46
|
+
type DomainWorkspaceId = Exclude<WorkspaceId, 'agents'>;
|
|
47
|
+
|
|
48
|
+
/** A single live domain mini-summary card. Enter on the focused card jumps to `target` via deps.openPanel. */
|
|
49
|
+
interface DomainCard {
|
|
50
|
+
readonly label: string;
|
|
51
|
+
readonly target: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Replaces the old buildGuidanceLine ('/orchestration', '/policy', …) signposts:
|
|
55
|
+
// each non-agents workspace is now a short list of live, jumpable domain cards.
|
|
56
|
+
const DOMAIN_CARDS: Record<DomainWorkspaceId, readonly DomainCard[]> = {
|
|
57
|
+
flow: [
|
|
58
|
+
{ label: 'Orchestration', target: 'orchestration' },
|
|
59
|
+
{ label: 'Tasks', target: 'tasks' },
|
|
60
|
+
],
|
|
61
|
+
governance: [
|
|
62
|
+
{ label: 'Policy', target: 'policy' },
|
|
63
|
+
{ label: 'Security', target: 'security' },
|
|
64
|
+
],
|
|
65
|
+
health: [
|
|
66
|
+
{ label: 'Incidents', target: 'incident' },
|
|
67
|
+
],
|
|
68
|
+
domains: [
|
|
69
|
+
{ label: 'Communication', target: 'communication' },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Live stat triplet for a domain card, computed from the current cockpit snapshot. */
|
|
74
|
+
function domainCardStats(target: string, snapshot: UiCockpitSnapshot): ReadonlyArray<readonly [string, string, string]> {
|
|
75
|
+
switch (target) {
|
|
76
|
+
case 'orchestration':
|
|
77
|
+
return [
|
|
78
|
+
['graphs', String(snapshot.activeGraphs), pickColor(snapshot.activeGraphs, 1, 4)],
|
|
79
|
+
['running', String(snapshot.runningTasks), C.value],
|
|
80
|
+
['guards', String(snapshot.guardTrips), pickColor(snapshot.guardTrips)],
|
|
81
|
+
];
|
|
82
|
+
case 'tasks':
|
|
83
|
+
return [
|
|
84
|
+
['running', String(snapshot.runningTasks), C.value],
|
|
85
|
+
['blocked', String(snapshot.blockedTasks), pickColor(snapshot.blockedTasks)],
|
|
86
|
+
['failed', String(snapshot.failedTasks), pickColor(snapshot.failedTasks)],
|
|
87
|
+
];
|
|
88
|
+
case 'policy':
|
|
89
|
+
return [
|
|
90
|
+
['preflight', snapshot.preflightStatus.toUpperCase(), snapshot.preflightStatus === 'block' ? C.bad : snapshot.preflightStatus === 'warn' ? C.warn : snapshot.preflightStatus === 'pass' ? C.good : C.dim],
|
|
91
|
+
['issues', String(snapshot.preflightIssueCount), pickColor(snapshot.preflightIssueCount)],
|
|
92
|
+
['lint', String(snapshot.lintFindingCount), pickColor(snapshot.lintFindingCount)],
|
|
93
|
+
];
|
|
94
|
+
case 'security':
|
|
95
|
+
return [
|
|
96
|
+
['token blocked', String(snapshot.tokenBlockedCount), pickColor(snapshot.tokenBlockedCount)],
|
|
97
|
+
['overdue', String(snapshot.tokenRotationOverdueCount), pickColor(snapshot.tokenRotationOverdueCount)],
|
|
98
|
+
['scope violations', String(snapshot.tokenScopeViolationCount), pickColor(snapshot.tokenScopeViolationCount)],
|
|
99
|
+
];
|
|
100
|
+
case 'incident':
|
|
101
|
+
return [
|
|
102
|
+
['count', String(snapshot.incidentCount), pickColor(snapshot.incidentCount)],
|
|
103
|
+
['latest', snapshot.latestIncident ? snapshot.latestIncident.classification : 'none', snapshot.latestIncident ? C.bad : C.dim],
|
|
104
|
+
];
|
|
105
|
+
case 'communication':
|
|
106
|
+
return [
|
|
107
|
+
['messages', String(snapshot.communicationCount), C.value],
|
|
108
|
+
['blocked', String(snapshot.blockedMessages), pickColor(snapshot.blockedMessages)],
|
|
109
|
+
];
|
|
110
|
+
default:
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
37
114
|
|
|
38
115
|
function formatCost(cost: number | null): string {
|
|
39
116
|
if (cost === null) return 'n/a';
|
|
40
117
|
return cost < 0.01 ? '<$0.01' : `$${cost.toFixed(2)}`;
|
|
41
118
|
}
|
|
42
119
|
|
|
120
|
+
function formatTokens(inputTokens: number | null, outputTokens: number | null): string {
|
|
121
|
+
if (inputTokens === null && outputTokens === null) return 'n/a';
|
|
122
|
+
const fmt = (n: number | null) => (n === null ? '-' : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n));
|
|
123
|
+
return `${fmt(inputTokens)}/${fmt(outputTokens)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
43
126
|
export class CockpitPanel extends BasePanel {
|
|
44
127
|
private readonly unsub: (() => void) | null;
|
|
45
128
|
private readonly rosterUnsub: (() => void) | null;
|
|
46
129
|
private readonly actionCallbacks: CockpitPanelActionCallbacks;
|
|
47
130
|
private selectedWorkspaceIndex = 0;
|
|
48
131
|
private agentCursorIndex = 0;
|
|
49
|
-
private
|
|
132
|
+
private domainCursorIndex = 0;
|
|
133
|
+
private confirm: ConfirmState<string> | null = null;
|
|
134
|
+
private stallTickTimer: ReturnType<typeof setInterval> | null = null;
|
|
50
135
|
|
|
51
136
|
public constructor(
|
|
52
137
|
private readonly readModel?: UiReadModel<UiCockpitSnapshot>,
|
|
53
138
|
private readonly rosterReadModel?: CockpitRosterReadModel,
|
|
54
139
|
actionCallbacks?: Partial<CockpitPanelActionCallbacks>,
|
|
55
140
|
) {
|
|
56
|
-
super('cockpit', 'Cockpit', 'O', '
|
|
141
|
+
super('cockpit', 'Cockpit', 'O', 'runtime-ops');
|
|
57
142
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
58
143
|
this.rosterUnsub = rosterReadModel ? rosterReadModel.subscribe(() => this.markDirty()) : null;
|
|
59
144
|
this.actionCallbacks = {
|
|
60
145
|
openAgentDetail: actionCallbacks?.openAgentDetail ?? ((_id: string) => { /* noop */ }),
|
|
146
|
+
inspectAgent: actionCallbacks?.inspectAgent ?? ((_id: string) => { /* noop */ }),
|
|
61
147
|
cancelAgent: actionCallbacks?.cancelAgent ?? ((_id: string) => false),
|
|
148
|
+
openPanel: actionCallbacks?.openPanel ?? ((_id: string) => { /* noop */ }),
|
|
62
149
|
};
|
|
63
150
|
}
|
|
64
151
|
|
|
152
|
+
public override onActivate(): void {
|
|
153
|
+
super.onActivate();
|
|
154
|
+
// Periodic tick so the roster's time-derived stalled badges (see
|
|
155
|
+
// cockpit-read-model's AGENT_STALL_THRESHOLD_MS) refresh even when no
|
|
156
|
+
// agent lifecycle event fires while this panel is on screen.
|
|
157
|
+
if (this.stallTickTimer === null) {
|
|
158
|
+
this.stallTickTimer = this.registerTimer(setInterval(() => this.markDirty(), STALL_TICK_MS));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public override onDeactivate(): void {
|
|
163
|
+
super.onDeactivate();
|
|
164
|
+
if (this.stallTickTimer !== null) {
|
|
165
|
+
this.clearTimer(this.stallTickTimer);
|
|
166
|
+
this.stallTickTimer = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
65
170
|
public override onDestroy(): void {
|
|
171
|
+
super.onDestroy();
|
|
66
172
|
this.unsub?.();
|
|
67
173
|
this.rosterUnsub?.();
|
|
68
174
|
}
|
|
@@ -72,45 +178,51 @@ export class CockpitPanel extends BasePanel {
|
|
|
72
178
|
}
|
|
73
179
|
|
|
74
180
|
public handleInput(key: string): boolean {
|
|
75
|
-
// Confirm-cancel absorb: when a cancel is pending, only y/enter confirm,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
181
|
+
// Confirm-cancel absorb: when a cancel is pending, only y/enter/return confirm,
|
|
182
|
+
// escape/n dismiss, everything else is consumed (canonical ConfirmState contract).
|
|
183
|
+
if (this.confirm) {
|
|
184
|
+
const result = handleConfirmInput(this.confirm, key);
|
|
185
|
+
if (result === 'confirmed') {
|
|
186
|
+
this.actionCallbacks.cancelAgent(this.confirm.subject);
|
|
187
|
+
this.confirm = null;
|
|
80
188
|
this.markDirty();
|
|
81
|
-
} else if (
|
|
82
|
-
this.
|
|
189
|
+
} else if (result === 'cancelled') {
|
|
190
|
+
this.confirm = null;
|
|
83
191
|
this.markDirty();
|
|
84
192
|
}
|
|
85
|
-
//
|
|
193
|
+
// 'absorbed' (and the confirmed/cancelled cases above) all consume the key.
|
|
86
194
|
return true;
|
|
87
195
|
}
|
|
88
196
|
if (key === 'left' || key === 'h') {
|
|
89
197
|
this.selectedWorkspaceIndex = Math.max(0, this.selectedWorkspaceIndex - 1);
|
|
90
198
|
this.agentCursorIndex = 0;
|
|
199
|
+
this.domainCursorIndex = 0;
|
|
91
200
|
this.markDirty();
|
|
92
201
|
return true;
|
|
93
202
|
}
|
|
94
203
|
if (key === 'right' || key === 'l') {
|
|
95
204
|
this.selectedWorkspaceIndex = Math.min(WORKSPACE_IDS.length - 1, this.selectedWorkspaceIndex + 1);
|
|
96
205
|
this.agentCursorIndex = 0;
|
|
206
|
+
this.domainCursorIndex = 0;
|
|
97
207
|
this.markDirty();
|
|
98
208
|
return true;
|
|
99
209
|
}
|
|
100
210
|
if (key === 'home') {
|
|
101
211
|
this.selectedWorkspaceIndex = 0;
|
|
102
212
|
this.agentCursorIndex = 0;
|
|
213
|
+
this.domainCursorIndex = 0;
|
|
103
214
|
this.markDirty();
|
|
104
215
|
return true;
|
|
105
216
|
}
|
|
106
217
|
if (key === 'end') {
|
|
107
218
|
this.selectedWorkspaceIndex = WORKSPACE_IDS.length - 1;
|
|
108
219
|
this.agentCursorIndex = 0;
|
|
220
|
+
this.domainCursorIndex = 0;
|
|
109
221
|
this.markDirty();
|
|
110
222
|
return true;
|
|
111
223
|
}
|
|
112
|
-
// Agents workspace: cursor movement, inspect, and cancel-initiation
|
|
113
224
|
if (this.selectedWorkspace === 'agents') {
|
|
225
|
+
// Agents workspace: roster cursor movement, inspect, and cancel-initiation.
|
|
114
226
|
const roster = this.rosterReadModel?.getSnapshot().roster ?? [];
|
|
115
227
|
if (key === 'up' || key === 'k') {
|
|
116
228
|
this.agentCursorIndex = Math.max(0, this.agentCursorIndex - 1);
|
|
@@ -122,18 +234,50 @@ export class CockpitPanel extends BasePanel {
|
|
|
122
234
|
this.markDirty();
|
|
123
235
|
return true;
|
|
124
236
|
}
|
|
125
|
-
if (key === 'i'
|
|
237
|
+
if (key === 'i') {
|
|
238
|
+
// Quick view is only meaningful when the cursor is on a real roster
|
|
239
|
+
// entry — otherwise leave the key unconsumed instead of absorbing it
|
|
240
|
+
// as a silent no-op.
|
|
241
|
+
const entry = roster[this.agentCursorIndex];
|
|
242
|
+
if (!entry) return false;
|
|
243
|
+
this.actionCallbacks.openAgentDetail(entry.id);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (key === 'enter' || key === 'return') {
|
|
126
247
|
const entry = roster[this.agentCursorIndex];
|
|
127
248
|
if (entry) {
|
|
128
|
-
this.actionCallbacks.
|
|
249
|
+
this.actionCallbacks.inspectAgent(entry.id);
|
|
129
250
|
}
|
|
130
251
|
return true;
|
|
131
252
|
}
|
|
132
253
|
if (key === 'c') {
|
|
254
|
+
// Cancel is only meaningful on a real, non-terminal roster entry —
|
|
255
|
+
// otherwise leave the key unconsumed instead of absorbing it as a
|
|
256
|
+
// silent no-op.
|
|
133
257
|
const entry = roster[this.agentCursorIndex];
|
|
134
|
-
if (entry
|
|
135
|
-
|
|
136
|
-
|
|
258
|
+
if (!entry || this.isTerminal(entry.status)) return false;
|
|
259
|
+
const shortId = entry.id.length > 8 ? entry.id.slice(-8) : entry.id;
|
|
260
|
+
this.confirm = { subject: entry.id, label: `agent ${shortId}`, verb: 'Cancel' };
|
|
261
|
+
this.markDirty();
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
// Domain-summary workspaces: card cursor movement + Enter jump.
|
|
266
|
+
const cards = DOMAIN_CARDS[this.selectedWorkspace];
|
|
267
|
+
if (key === 'up' || key === 'k') {
|
|
268
|
+
this.domainCursorIndex = Math.max(0, this.domainCursorIndex - 1);
|
|
269
|
+
this.markDirty();
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
if (key === 'down' || key === 'j') {
|
|
273
|
+
this.domainCursorIndex = Math.min(cards.length - 1, this.domainCursorIndex + 1);
|
|
274
|
+
this.markDirty();
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (key === 'enter' || key === 'return') {
|
|
278
|
+
const card = cards[this.domainCursorIndex];
|
|
279
|
+
if (card) {
|
|
280
|
+
this.actionCallbacks.openPanel(card.target);
|
|
137
281
|
}
|
|
138
282
|
return true;
|
|
139
283
|
}
|
|
@@ -151,29 +295,45 @@ export class CockpitPanel extends BasePanel {
|
|
|
151
295
|
lines.push(buildPanelLine(width, [[' Agent roster read model not wired.', C.empty]]));
|
|
152
296
|
return lines;
|
|
153
297
|
}
|
|
154
|
-
const { roster, stalledAgentCount, totalCost } = this.rosterReadModel.getSnapshot();
|
|
298
|
+
const { roster, stalledAgentCount, totalCost, totalInputTokens, totalOutputTokens } = this.rosterReadModel.getSnapshot();
|
|
155
299
|
lines.push(buildPanelLine(width, [
|
|
156
300
|
[` ${roster.length} agent${roster.length !== 1 ? 's' : ''}`, C.label],
|
|
157
301
|
stalledAgentCount > 0 ? [` ${stalledAgentCount} stalled`, C.bad] : ['', C.dim],
|
|
158
302
|
[` cost: ${formatCost(totalCost)}`, C.dim],
|
|
303
|
+
[` tokens: ${formatTokens(totalInputTokens, totalOutputTokens)}`, C.dim],
|
|
159
304
|
]));
|
|
160
305
|
const colors: Record<string, string> = {
|
|
161
306
|
pending: C.dim, running: C.value, completed: C.good, failed: C.bad, cancelled: C.dim, system: C.dim,
|
|
162
307
|
};
|
|
308
|
+
// Fixed-width columns (id/status/stalled/model/tokens/cost) + task takes the remainder.
|
|
309
|
+
const columns: ColumnSpec[] = [
|
|
310
|
+
{ width: 9 },
|
|
311
|
+
{ width: 10 },
|
|
312
|
+
{ width: 8 },
|
|
313
|
+
{ width: 14 },
|
|
314
|
+
{ width: 13, align: 'right' },
|
|
315
|
+
{ width: 8, align: 'right' },
|
|
316
|
+
{ width: Math.max(8, width - 68) },
|
|
317
|
+
];
|
|
163
318
|
for (let i = 0; i < roster.length; i++) {
|
|
164
319
|
const entry = roster[i]!;
|
|
165
|
-
const
|
|
320
|
+
const selected = i === this.agentCursorIndex;
|
|
166
321
|
const shortId = entry.id.length > 8 ? entry.id.slice(-8) : entry.id;
|
|
167
322
|
const statusColor = agentStatusColor(entry.status, colors);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
[
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
323
|
+
lines.push(buildAlignedRow(
|
|
324
|
+
width,
|
|
325
|
+
[
|
|
326
|
+
{ text: shortId, fg: C.dim },
|
|
327
|
+
{ text: entry.status, fg: statusColor },
|
|
328
|
+
{ text: entry.stalled ? 'STALLED' : '', fg: C.bad },
|
|
329
|
+
{ text: entry.model, fg: C.info },
|
|
330
|
+
{ text: formatTokens(entry.inputTokens, entry.outputTokens), fg: C.dim },
|
|
331
|
+
{ text: formatCost(entry.cost), fg: C.value },
|
|
332
|
+
{ text: entry.task, fg: C.label },
|
|
333
|
+
],
|
|
334
|
+
columns,
|
|
335
|
+
{ selected, selectedBg: C.headerBg, marker: '▸' },
|
|
336
|
+
));
|
|
177
337
|
}
|
|
178
338
|
if (roster.length === 0) {
|
|
179
339
|
lines.push(buildPanelLine(width, [[' No agents tracked yet.', C.empty]]));
|
|
@@ -181,9 +341,42 @@ export class CockpitPanel extends BasePanel {
|
|
|
181
341
|
return lines;
|
|
182
342
|
}
|
|
183
343
|
|
|
344
|
+
/** Live domain mini-summary cards for a non-agents workspace (replaces the old signpost pair). */
|
|
345
|
+
private renderDomainWorkspace(width: number, workspace: DomainWorkspaceId, snapshot: UiCockpitSnapshot): Line[] {
|
|
346
|
+
const cards = DOMAIN_CARDS[workspace];
|
|
347
|
+
const lines: Line[] = [];
|
|
348
|
+
for (let i = 0; i < cards.length; i++) {
|
|
349
|
+
const card = cards[i]!;
|
|
350
|
+
const focused = i === this.domainCursorIndex;
|
|
351
|
+
const stats = domainCardStats(card.target, snapshot);
|
|
352
|
+
lines.push(buildPanelLine(width, [
|
|
353
|
+
[focused ? '▸ ' : ' ', focused ? C.value : C.dim],
|
|
354
|
+
[card.label, focused ? C.header : C.label, focused ? C.headerBg : undefined],
|
|
355
|
+
[' ', C.dim],
|
|
356
|
+
...stats.flatMap(([label, value, color]): Array<[string, string, string?]> => [
|
|
357
|
+
[`${label} `, C.dim],
|
|
358
|
+
[`${value} `, color],
|
|
359
|
+
]),
|
|
360
|
+
]));
|
|
361
|
+
}
|
|
362
|
+
lines.push(buildPanelLine(width, [[' ↑/↓ select domain Enter opens panel', C.dim]]));
|
|
363
|
+
return lines;
|
|
364
|
+
}
|
|
365
|
+
|
|
184
366
|
public render(width: number, height: number): Line[] {
|
|
185
367
|
this.needsRender = false;
|
|
186
368
|
|
|
369
|
+
if (this.confirm) {
|
|
370
|
+
const lines = buildPanelWorkspace(width, height, {
|
|
371
|
+
title: 'Operator Cockpit',
|
|
372
|
+
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
373
|
+
footerLines: [buildPanelLine(width, [[' y / Enter confirm n / Esc cancel', C.dim]])],
|
|
374
|
+
palette: C,
|
|
375
|
+
});
|
|
376
|
+
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
377
|
+
return lines.slice(0, height);
|
|
378
|
+
}
|
|
379
|
+
|
|
187
380
|
if (!this.readModel) {
|
|
188
381
|
const lines: Line[] = [buildPanelLine(width, [[' Runtime read model not wired into this panel yet.', C.empty]])];
|
|
189
382
|
const workspace = buildPanelWorkspace(width, height, {
|
|
@@ -246,43 +439,9 @@ export class CockpitPanel extends BasePanel {
|
|
|
246
439
|
`tasks:${snapshot.taskCount} agents:${snapshot.agentCount} graphs:${snapshot.totalGraphs} comms:${snapshot.communicationCount} mcp:${snapshot.mcpServerCount} plugins:${snapshot.pluginCount}`,
|
|
247
440
|
C.dim,
|
|
248
441
|
]])];
|
|
249
|
-
const workspaceLines: Line[] =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
{ label: 'running', value: String(snapshot.runningTasks), valueColor: C.value },
|
|
253
|
-
{ label: 'blocked', value: String(snapshot.blockedTasks), valueColor: pickColor(snapshot.blockedTasks) },
|
|
254
|
-
{ label: 'graphs', value: String(snapshot.activeGraphs), valueColor: pickColor(snapshot.activeGraphs, 1, 4) },
|
|
255
|
-
], C));
|
|
256
|
-
workspaceLines.push(buildGuidanceLine(width, '/orchestration', 'inspect graph state, retries, and subtree controls', C));
|
|
257
|
-
workspaceLines.push(buildGuidanceLine(width, '/tasks', 'review active task pressure and task-specific output', C));
|
|
258
|
-
} else if (selectedWorkspace === 'governance') {
|
|
259
|
-
workspaceLines.push(buildKeyValueLine(width, [
|
|
260
|
-
{ label: 'preflight', value: snapshot.preflightStatus.toUpperCase(), valueColor: snapshot.preflightStatus === 'block' ? C.bad : snapshot.preflightStatus === 'warn' ? C.warn : snapshot.preflightStatus === 'pass' ? C.good : C.dim },
|
|
261
|
-
{ label: 'lint', value: String(snapshot.lintFindingCount), valueColor: pickColor(snapshot.lintFindingCount) },
|
|
262
|
-
{ label: 'allow-all mcp', value: String(snapshot.elevatedMcp), valueColor: pickColor(snapshot.elevatedMcp) },
|
|
263
|
-
], C));
|
|
264
|
-
workspaceLines.push(buildGuidanceLine(width, '/policy', 'run simulation, preflight, and bundle review', C));
|
|
265
|
-
workspaceLines.push(buildGuidanceLine(width, '/security', 'inspect trust, tokens, quarantines, and incident pressure', C));
|
|
266
|
-
} else if (selectedWorkspace === 'health') {
|
|
267
|
-
workspaceLines.push(buildKeyValueLine(width, [
|
|
268
|
-
{ label: 'incidents', value: String(snapshot.incidentCount), valueColor: pickColor(snapshot.incidentCount) },
|
|
269
|
-
{ label: 'plugins', value: String(snapshot.erroredPlugins), valueColor: pickColor(snapshot.erroredPlugins) },
|
|
270
|
-
{ label: 'integrations', value: String(snapshot.failingIntegrations), valueColor: pickColor(snapshot.failingIntegrations) },
|
|
271
|
-
], C));
|
|
272
|
-
workspaceLines.push(buildGuidanceLine(width, '/incident latest', 'inspect the latest incident bundle and replay fallout', C));
|
|
273
|
-
workspaceLines.push(buildGuidanceLine(width, '/plugins', 'review errored plugins and provenance posture', C));
|
|
274
|
-
} else if (selectedWorkspace === 'domains') {
|
|
275
|
-
workspaceLines.push(buildKeyValueLine(width, [
|
|
276
|
-
{ label: 'tasks', value: String(snapshot.taskCount), valueColor: C.value },
|
|
277
|
-
{ label: 'comms', value: String(snapshot.communicationCount), valueColor: C.value },
|
|
278
|
-
{ label: 'mcp', value: String(snapshot.mcpServerCount), valueColor: C.value },
|
|
279
|
-
], C));
|
|
280
|
-
workspaceLines.push(buildGuidanceLine(width, '/mcp', 'inspect trust, quarantine, and risky server posture', C));
|
|
281
|
-
workspaceLines.push(buildGuidanceLine(width, '/communication', 'review blocked lanes and agent message flow', C));
|
|
282
|
-
} else {
|
|
283
|
-
// 'agents' workspace — roster from rosterReadModel
|
|
284
|
-
workspaceLines.push(...this.renderAgentsWorkspace(width));
|
|
285
|
-
}
|
|
442
|
+
const workspaceLines: Line[] = selectedWorkspace === 'agents'
|
|
443
|
+
? this.renderAgentsWorkspace(width)
|
|
444
|
+
: this.renderDomainWorkspace(width, selectedWorkspace, snapshot);
|
|
286
445
|
|
|
287
446
|
// Visible workspace selector so focus and the available workspaces are
|
|
288
447
|
// obvious at a glance (not buried in a footer string). Rendered as a
|
|
@@ -308,8 +467,8 @@ export class CockpitPanel extends BasePanel {
|
|
|
308
467
|
];
|
|
309
468
|
// Context-aware footer: surface the keys that actually work in this view.
|
|
310
469
|
const footerHint = selectedWorkspace === 'agents'
|
|
311
|
-
? ' ←/→ workspace ↑/↓ select agent i
|
|
312
|
-
: ' ←/→ workspace Home/End jump
|
|
470
|
+
? ' ←/→ workspace ↑/↓ select agent i quick view enter inspector c cancel Home/End jump'
|
|
471
|
+
: ' ←/→ workspace ↑/↓ select domain enter opens panel Home/End jump';
|
|
313
472
|
const lines = buildPanelWorkspace(width, height, {
|
|
314
473
|
title: 'Operator Cockpit',
|
|
315
474
|
sections,
|