@pellux/goodvibes-tui 0.27.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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- 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 +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- 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 +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- 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 +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- 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/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- 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/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- 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 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- 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
|
+
}
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { BasePanel } from './base-panel.ts';
|
|
4
5
|
import type { UiCockpitSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
5
6
|
import type { CockpitRosterReadModel } from './cockpit-read-model.ts';
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
-
buildKeyValueLine,
|
|
8
|
+
buildAlignedRow,
|
|
9
9
|
buildPanelLine,
|
|
10
10
|
buildPanelWorkspace,
|
|
11
11
|
buildStatPill,
|
|
12
12
|
DEFAULT_PANEL_PALETTE,
|
|
13
|
+
type ColumnSpec,
|
|
13
14
|
type PanelWorkspaceSection,
|
|
14
15
|
} from './polish.ts';
|
|
16
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
15
17
|
import { agentStatusColor } from './agent-inspector-shared.ts';
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|
|
22
27
|
|
|
23
28
|
function pickColor(value: number, warnAt = 1, badAt = 3): string {
|
|
24
29
|
if (value >= badAt) return C.bad;
|
|
@@ -27,41 +32,143 @@ function pickColor(value: number, warnAt = 1, badAt = 3): string {
|
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export interface CockpitPanelActionCallbacks {
|
|
35
|
+
/** Open the quick agent-detail modal (bootstrap-shell openAgentDetail wiring). */
|
|
30
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;
|
|
31
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;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
const WORKSPACE_IDS = ['flow', 'governance', 'health', 'domains', 'agents'] as const;
|
|
35
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
|
+
}
|
|
36
114
|
|
|
37
115
|
function formatCost(cost: number | null): string {
|
|
38
116
|
if (cost === null) return 'n/a';
|
|
39
117
|
return cost < 0.01 ? '<$0.01' : `$${cost.toFixed(2)}`;
|
|
40
118
|
}
|
|
41
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
|
+
|
|
42
126
|
export class CockpitPanel extends BasePanel {
|
|
43
127
|
private readonly unsub: (() => void) | null;
|
|
44
128
|
private readonly rosterUnsub: (() => void) | null;
|
|
45
129
|
private readonly actionCallbacks: CockpitPanelActionCallbacks;
|
|
46
130
|
private selectedWorkspaceIndex = 0;
|
|
47
131
|
private agentCursorIndex = 0;
|
|
48
|
-
private
|
|
132
|
+
private domainCursorIndex = 0;
|
|
133
|
+
private confirm: ConfirmState<string> | null = null;
|
|
134
|
+
private stallTickTimer: ReturnType<typeof setInterval> | null = null;
|
|
49
135
|
|
|
50
136
|
public constructor(
|
|
51
137
|
private readonly readModel?: UiReadModel<UiCockpitSnapshot>,
|
|
52
138
|
private readonly rosterReadModel?: CockpitRosterReadModel,
|
|
53
139
|
actionCallbacks?: Partial<CockpitPanelActionCallbacks>,
|
|
54
140
|
) {
|
|
55
|
-
super('cockpit', 'Cockpit', 'O', '
|
|
141
|
+
super('cockpit', 'Cockpit', 'O', 'runtime-ops');
|
|
56
142
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
57
143
|
this.rosterUnsub = rosterReadModel ? rosterReadModel.subscribe(() => this.markDirty()) : null;
|
|
58
144
|
this.actionCallbacks = {
|
|
59
145
|
openAgentDetail: actionCallbacks?.openAgentDetail ?? ((_id: string) => { /* noop */ }),
|
|
146
|
+
inspectAgent: actionCallbacks?.inspectAgent ?? ((_id: string) => { /* noop */ }),
|
|
60
147
|
cancelAgent: actionCallbacks?.cancelAgent ?? ((_id: string) => false),
|
|
148
|
+
openPanel: actionCallbacks?.openPanel ?? ((_id: string) => { /* noop */ }),
|
|
61
149
|
};
|
|
62
150
|
}
|
|
63
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
|
+
|
|
64
170
|
public override onDestroy(): void {
|
|
171
|
+
super.onDestroy();
|
|
65
172
|
this.unsub?.();
|
|
66
173
|
this.rosterUnsub?.();
|
|
67
174
|
}
|
|
@@ -71,45 +178,51 @@ export class CockpitPanel extends BasePanel {
|
|
|
71
178
|
}
|
|
72
179
|
|
|
73
180
|
public handleInput(key: string): boolean {
|
|
74
|
-
// Confirm-cancel absorb: when a cancel is pending, only y/enter confirm,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
79
188
|
this.markDirty();
|
|
80
|
-
} else if (
|
|
81
|
-
this.
|
|
189
|
+
} else if (result === 'cancelled') {
|
|
190
|
+
this.confirm = null;
|
|
82
191
|
this.markDirty();
|
|
83
192
|
}
|
|
84
|
-
//
|
|
193
|
+
// 'absorbed' (and the confirmed/cancelled cases above) all consume the key.
|
|
85
194
|
return true;
|
|
86
195
|
}
|
|
87
196
|
if (key === 'left' || key === 'h') {
|
|
88
197
|
this.selectedWorkspaceIndex = Math.max(0, this.selectedWorkspaceIndex - 1);
|
|
89
198
|
this.agentCursorIndex = 0;
|
|
199
|
+
this.domainCursorIndex = 0;
|
|
90
200
|
this.markDirty();
|
|
91
201
|
return true;
|
|
92
202
|
}
|
|
93
203
|
if (key === 'right' || key === 'l') {
|
|
94
204
|
this.selectedWorkspaceIndex = Math.min(WORKSPACE_IDS.length - 1, this.selectedWorkspaceIndex + 1);
|
|
95
205
|
this.agentCursorIndex = 0;
|
|
206
|
+
this.domainCursorIndex = 0;
|
|
96
207
|
this.markDirty();
|
|
97
208
|
return true;
|
|
98
209
|
}
|
|
99
210
|
if (key === 'home') {
|
|
100
211
|
this.selectedWorkspaceIndex = 0;
|
|
101
212
|
this.agentCursorIndex = 0;
|
|
213
|
+
this.domainCursorIndex = 0;
|
|
102
214
|
this.markDirty();
|
|
103
215
|
return true;
|
|
104
216
|
}
|
|
105
217
|
if (key === 'end') {
|
|
106
218
|
this.selectedWorkspaceIndex = WORKSPACE_IDS.length - 1;
|
|
107
219
|
this.agentCursorIndex = 0;
|
|
220
|
+
this.domainCursorIndex = 0;
|
|
108
221
|
this.markDirty();
|
|
109
222
|
return true;
|
|
110
223
|
}
|
|
111
|
-
// Agents workspace: cursor movement, inspect, and cancel-initiation
|
|
112
224
|
if (this.selectedWorkspace === 'agents') {
|
|
225
|
+
// Agents workspace: roster cursor movement, inspect, and cancel-initiation.
|
|
113
226
|
const roster = this.rosterReadModel?.getSnapshot().roster ?? [];
|
|
114
227
|
if (key === 'up' || key === 'k') {
|
|
115
228
|
this.agentCursorIndex = Math.max(0, this.agentCursorIndex - 1);
|
|
@@ -121,18 +234,50 @@ export class CockpitPanel extends BasePanel {
|
|
|
121
234
|
this.markDirty();
|
|
122
235
|
return true;
|
|
123
236
|
}
|
|
124
|
-
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') {
|
|
125
247
|
const entry = roster[this.agentCursorIndex];
|
|
126
248
|
if (entry) {
|
|
127
|
-
this.actionCallbacks.
|
|
249
|
+
this.actionCallbacks.inspectAgent(entry.id);
|
|
128
250
|
}
|
|
129
251
|
return true;
|
|
130
252
|
}
|
|
131
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.
|
|
132
257
|
const entry = roster[this.agentCursorIndex];
|
|
133
|
-
if (entry
|
|
134
|
-
|
|
135
|
-
|
|
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);
|
|
136
281
|
}
|
|
137
282
|
return true;
|
|
138
283
|
}
|
|
@@ -150,29 +295,45 @@ export class CockpitPanel extends BasePanel {
|
|
|
150
295
|
lines.push(buildPanelLine(width, [[' Agent roster read model not wired.', C.empty]]));
|
|
151
296
|
return lines;
|
|
152
297
|
}
|
|
153
|
-
const { roster, stalledAgentCount, totalCost } = this.rosterReadModel.getSnapshot();
|
|
298
|
+
const { roster, stalledAgentCount, totalCost, totalInputTokens, totalOutputTokens } = this.rosterReadModel.getSnapshot();
|
|
154
299
|
lines.push(buildPanelLine(width, [
|
|
155
300
|
[` ${roster.length} agent${roster.length !== 1 ? 's' : ''}`, C.label],
|
|
156
301
|
stalledAgentCount > 0 ? [` ${stalledAgentCount} stalled`, C.bad] : ['', C.dim],
|
|
157
302
|
[` cost: ${formatCost(totalCost)}`, C.dim],
|
|
303
|
+
[` tokens: ${formatTokens(totalInputTokens, totalOutputTokens)}`, C.dim],
|
|
158
304
|
]));
|
|
159
305
|
const colors: Record<string, string> = {
|
|
160
306
|
pending: C.dim, running: C.value, completed: C.good, failed: C.bad, cancelled: C.dim, system: C.dim,
|
|
161
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
|
+
];
|
|
162
318
|
for (let i = 0; i < roster.length; i++) {
|
|
163
319
|
const entry = roster[i]!;
|
|
164
|
-
const
|
|
320
|
+
const selected = i === this.agentCursorIndex;
|
|
165
321
|
const shortId = entry.id.length > 8 ? entry.id.slice(-8) : entry.id;
|
|
166
322
|
const statusColor = agentStatusColor(entry.status, colors);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
[
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
));
|
|
176
337
|
}
|
|
177
338
|
if (roster.length === 0) {
|
|
178
339
|
lines.push(buildPanelLine(width, [[' No agents tracked yet.', C.empty]]));
|
|
@@ -180,9 +341,42 @@ export class CockpitPanel extends BasePanel {
|
|
|
180
341
|
return lines;
|
|
181
342
|
}
|
|
182
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
|
+
|
|
183
366
|
public render(width: number, height: number): Line[] {
|
|
184
367
|
this.needsRender = false;
|
|
185
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
|
+
|
|
186
380
|
if (!this.readModel) {
|
|
187
381
|
const lines: Line[] = [buildPanelLine(width, [[' Runtime read model not wired into this panel yet.', C.empty]])];
|
|
188
382
|
const workspace = buildPanelWorkspace(width, height, {
|
|
@@ -238,63 +432,47 @@ export class CockpitPanel extends BasePanel {
|
|
|
238
432
|
[' latest incident ', C.label],
|
|
239
433
|
[snapshot.latestIncident.classification, C.bad],
|
|
240
434
|
[' ', C.label],
|
|
241
|
-
[snapshot.latestIncident.summary
|
|
435
|
+
[truncateDisplay(snapshot.latestIncident.summary, Math.max(0, width - 19 - snapshot.latestIncident.classification.length)), C.dim],
|
|
242
436
|
]));
|
|
243
437
|
}
|
|
244
438
|
const domainLines: Line[] = [buildPanelLine(width, [[
|
|
245
439
|
`tasks:${snapshot.taskCount} agents:${snapshot.agentCount} graphs:${snapshot.totalGraphs} comms:${snapshot.communicationCount} mcp:${snapshot.mcpServerCount} plugins:${snapshot.pluginCount}`,
|
|
246
440
|
C.dim,
|
|
247
441
|
]])];
|
|
248
|
-
const workspaceLines: Line[] =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
} else if (selectedWorkspace === 'health') {
|
|
266
|
-
workspaceLines.push(buildKeyValueLine(width, [
|
|
267
|
-
{ label: 'incidents', value: String(snapshot.incidentCount), valueColor: pickColor(snapshot.incidentCount) },
|
|
268
|
-
{ label: 'plugins', value: String(snapshot.erroredPlugins), valueColor: pickColor(snapshot.erroredPlugins) },
|
|
269
|
-
{ label: 'integrations', value: String(snapshot.failingIntegrations), valueColor: pickColor(snapshot.failingIntegrations) },
|
|
270
|
-
], C));
|
|
271
|
-
workspaceLines.push(buildGuidanceLine(width, '/incident latest', 'inspect the latest incident bundle and replay fallout', C));
|
|
272
|
-
workspaceLines.push(buildGuidanceLine(width, '/plugins', 'review errored plugins and provenance posture', C));
|
|
273
|
-
} else if (selectedWorkspace === 'domains') {
|
|
274
|
-
workspaceLines.push(buildKeyValueLine(width, [
|
|
275
|
-
{ label: 'tasks', value: String(snapshot.taskCount), valueColor: C.value },
|
|
276
|
-
{ label: 'comms', value: String(snapshot.communicationCount), valueColor: C.value },
|
|
277
|
-
{ label: 'mcp', value: String(snapshot.mcpServerCount), valueColor: C.value },
|
|
278
|
-
], C));
|
|
279
|
-
workspaceLines.push(buildGuidanceLine(width, '/mcp', 'inspect trust, quarantine, and risky server posture', C));
|
|
280
|
-
workspaceLines.push(buildGuidanceLine(width, '/communication', 'review blocked lanes and agent message flow', C));
|
|
281
|
-
} else {
|
|
282
|
-
// 'agents' workspace — roster from rosterReadModel
|
|
283
|
-
workspaceLines.push(...this.renderAgentsWorkspace(width));
|
|
284
|
-
}
|
|
442
|
+
const workspaceLines: Line[] = selectedWorkspace === 'agents'
|
|
443
|
+
? this.renderAgentsWorkspace(width)
|
|
444
|
+
: this.renderDomainWorkspace(width, selectedWorkspace, snapshot);
|
|
445
|
+
|
|
446
|
+
// Visible workspace selector so focus and the available workspaces are
|
|
447
|
+
// obvious at a glance (not buried in a footer string). Rendered as a
|
|
448
|
+
// title-less row to keep it compact; the ▸ marker + accent show focus.
|
|
449
|
+
const selectorLine = buildPanelLine(width, [
|
|
450
|
+
[' switch ', C.label],
|
|
451
|
+
...WORKSPACE_IDS.flatMap((id, i): Array<[string, string, string?]> => {
|
|
452
|
+
const focused = i === this.selectedWorkspaceIndex;
|
|
453
|
+
return [
|
|
454
|
+
[focused ? '▸' : ' ', focused ? C.value : C.dim],
|
|
455
|
+
[`${id} `, focused ? C.header : C.dim, focused ? C.headerBg : undefined],
|
|
456
|
+
];
|
|
457
|
+
}),
|
|
458
|
+
]);
|
|
285
459
|
|
|
286
460
|
const sections: PanelWorkspaceSection[] = [
|
|
461
|
+
{ lines: [selectorLine] },
|
|
287
462
|
{ title: 'Flow', lines: flowLines },
|
|
288
463
|
{ title: 'Governance', lines: governanceLines },
|
|
289
464
|
{ title: 'Health', lines: healthLines },
|
|
290
465
|
{ title: 'Domains', lines: domainLines },
|
|
291
|
-
{ title:
|
|
466
|
+
{ title: `Workspace · ${selectedWorkspace}`, lines: workspaceLines },
|
|
292
467
|
];
|
|
468
|
+
// Context-aware footer: surface the keys that actually work in this view.
|
|
469
|
+
const footerHint = selectedWorkspace === 'agents'
|
|
470
|
+
? ' ←/→ workspace ↑/↓ select agent i quick view enter inspector c cancel Home/End jump'
|
|
471
|
+
: ' ←/→ workspace ↑/↓ select domain enter opens panel Home/End jump';
|
|
293
472
|
const lines = buildPanelWorkspace(width, height, {
|
|
294
473
|
title: 'Operator Cockpit',
|
|
295
|
-
intro: 'Live runtime pressure across orchestration, approvals, governance, integrations, and provider trust posture.',
|
|
296
474
|
sections,
|
|
297
|
-
footerLines: [buildPanelLine(width, [[
|
|
475
|
+
footerLines: [buildPanelLine(width, [[footerHint, C.dim]])],
|
|
298
476
|
palette: C,
|
|
299
477
|
});
|
|
300
478
|
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
|
|
21
21
|
import type { AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
22
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
22
23
|
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
23
24
|
import {
|
|
24
25
|
AGENT_TERMINAL_STATUSES,
|
|
@@ -137,7 +138,7 @@ export function buildCockpitRosterSnapshot(
|
|
|
137
138
|
totalCost += agentCost;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
const task = rec.task
|
|
141
|
+
const task = truncateDisplay(rec.task, 50);
|
|
141
142
|
|
|
142
143
|
return {
|
|
143
144
|
id: rec.id,
|