@pellux/goodvibes-tui 1.1.0 → 1.9.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 +28 -18
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +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/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +90 -14
- 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/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +104 -51
- package/src/core/turn-cancellation.ts +25 -0
- package/src/core/turn-event-wiring.ts +72 -3
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/export/cost-utils.ts +36 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +84 -5
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +232 -0
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +10 -2
- 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 +73 -35
- package/src/input/commands/operator-panel-runtime.ts +71 -31
- package/src/input/commands/planning-runtime.ts +116 -4
- 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 +26 -6
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +16 -4
- package/src/input/commands/shell-core.ts +25 -8
- 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 +488 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +175 -0
- package/src/input/config-modal.ts +592 -0
- package/src/input/feed-context-factory.ts +10 -8
- package/src/input/handler-command-route.ts +37 -20
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +74 -114
- package/src/input/handler-feed.ts +79 -27
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +136 -6
- package/src/input/handler-modal-stack.ts +14 -7
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +26 -102
- package/src/input/handler-shortcuts.ts +74 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +12 -29
- package/src/input/keybindings.ts +23 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +77 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +146 -2
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +49 -51
- 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 +18 -46
- 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-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +758 -0
- package/src/panels/fleet-read-model.ts +520 -0
- package/src/panels/fleet-steer.ts +125 -0
- package/src/panels/fleet-stop.ts +153 -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 +166 -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 +42 -0
- package/src/panels/modals/pairing-modal.ts +163 -0
- package/src/panels/modals/planning-modal.ts +304 -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 +177 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +123 -9
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -11
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +37 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +216 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- 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/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +95 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +15 -3
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +43 -1
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +62 -4
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +133 -0
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +27 -1
- package/src/runtime/bootstrap-command-parts.ts +41 -10
- package/src/runtime/bootstrap-core.ts +47 -15
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +48 -17
- package/src/runtime/bootstrap.ts +129 -22
- package/src/runtime/code-index-services.ts +135 -0
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +59 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +98 -46
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +27 -3
- package/src/runtime/workstream-services.ts +327 -0
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +68 -4
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +171 -26
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/utils/terminal-width.ts +52 -0
- 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,481 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
4
|
-
import { BasePanel } from './base-panel.ts';
|
|
5
|
-
import type { UiCockpitSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
6
|
-
import type { CockpitRosterReadModel } from './cockpit-read-model.ts';
|
|
7
|
-
import {
|
|
8
|
-
buildAlignedRow,
|
|
9
|
-
buildPanelLine,
|
|
10
|
-
buildPanelWorkspace,
|
|
11
|
-
buildStatPill,
|
|
12
|
-
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
type ColumnSpec,
|
|
14
|
-
type PanelWorkspaceSection,
|
|
15
|
-
} from './polish.ts';
|
|
16
|
-
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
17
|
-
import { agentStatusColor } from './agent-inspector-shared.ts';
|
|
18
|
-
|
|
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;
|
|
27
|
-
|
|
28
|
-
function pickColor(value: number, warnAt = 1, badAt = 3): string {
|
|
29
|
-
if (value >= badAt) return C.bad;
|
|
30
|
-
if (value >= warnAt) return C.warn;
|
|
31
|
-
return C.good;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface CockpitPanelActionCallbacks {
|
|
35
|
-
/** Open the quick agent-detail modal (bootstrap-shell openAgentDetail wiring). */
|
|
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;
|
|
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;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const WORKSPACE_IDS = ['flow', 'governance', 'health', 'domains', 'agents'] as const;
|
|
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
|
-
}
|
|
114
|
-
|
|
115
|
-
function formatCost(cost: number | null): string {
|
|
116
|
-
if (cost === null) return 'n/a';
|
|
117
|
-
return cost < 0.01 ? '<$0.01' : `$${cost.toFixed(2)}`;
|
|
118
|
-
}
|
|
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
|
-
|
|
126
|
-
export class CockpitPanel extends BasePanel {
|
|
127
|
-
private readonly unsub: (() => void) | null;
|
|
128
|
-
private readonly rosterUnsub: (() => void) | null;
|
|
129
|
-
private readonly actionCallbacks: CockpitPanelActionCallbacks;
|
|
130
|
-
private selectedWorkspaceIndex = 0;
|
|
131
|
-
private agentCursorIndex = 0;
|
|
132
|
-
private domainCursorIndex = 0;
|
|
133
|
-
private confirm: ConfirmState<string> | null = null;
|
|
134
|
-
private stallTickTimer: ReturnType<typeof setInterval> | null = null;
|
|
135
|
-
|
|
136
|
-
public constructor(
|
|
137
|
-
private readonly readModel?: UiReadModel<UiCockpitSnapshot>,
|
|
138
|
-
private readonly rosterReadModel?: CockpitRosterReadModel,
|
|
139
|
-
actionCallbacks?: Partial<CockpitPanelActionCallbacks>,
|
|
140
|
-
) {
|
|
141
|
-
super('cockpit', 'Cockpit', 'O', 'runtime-ops');
|
|
142
|
-
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
143
|
-
this.rosterUnsub = rosterReadModel ? rosterReadModel.subscribe(() => this.markDirty()) : null;
|
|
144
|
-
this.actionCallbacks = {
|
|
145
|
-
openAgentDetail: actionCallbacks?.openAgentDetail ?? ((_id: string) => { /* noop */ }),
|
|
146
|
-
inspectAgent: actionCallbacks?.inspectAgent ?? ((_id: string) => { /* noop */ }),
|
|
147
|
-
cancelAgent: actionCallbacks?.cancelAgent ?? ((_id: string) => false),
|
|
148
|
-
openPanel: actionCallbacks?.openPanel ?? ((_id: string) => { /* noop */ }),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
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
|
-
|
|
170
|
-
public override onDestroy(): void {
|
|
171
|
-
super.onDestroy();
|
|
172
|
-
this.unsub?.();
|
|
173
|
-
this.rosterUnsub?.();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private get selectedWorkspace(): WorkspaceId {
|
|
177
|
-
return WORKSPACE_IDS[this.selectedWorkspaceIndex] ?? 'flow';
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
public handleInput(key: string): boolean {
|
|
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;
|
|
188
|
-
this.markDirty();
|
|
189
|
-
} else if (result === 'cancelled') {
|
|
190
|
-
this.confirm = null;
|
|
191
|
-
this.markDirty();
|
|
192
|
-
}
|
|
193
|
-
// 'absorbed' (and the confirmed/cancelled cases above) all consume the key.
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
if (key === 'left' || key === 'h') {
|
|
197
|
-
this.selectedWorkspaceIndex = Math.max(0, this.selectedWorkspaceIndex - 1);
|
|
198
|
-
this.agentCursorIndex = 0;
|
|
199
|
-
this.domainCursorIndex = 0;
|
|
200
|
-
this.markDirty();
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
if (key === 'right' || key === 'l') {
|
|
204
|
-
this.selectedWorkspaceIndex = Math.min(WORKSPACE_IDS.length - 1, this.selectedWorkspaceIndex + 1);
|
|
205
|
-
this.agentCursorIndex = 0;
|
|
206
|
-
this.domainCursorIndex = 0;
|
|
207
|
-
this.markDirty();
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
if (key === 'home') {
|
|
211
|
-
this.selectedWorkspaceIndex = 0;
|
|
212
|
-
this.agentCursorIndex = 0;
|
|
213
|
-
this.domainCursorIndex = 0;
|
|
214
|
-
this.markDirty();
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
if (key === 'end') {
|
|
218
|
-
this.selectedWorkspaceIndex = WORKSPACE_IDS.length - 1;
|
|
219
|
-
this.agentCursorIndex = 0;
|
|
220
|
-
this.domainCursorIndex = 0;
|
|
221
|
-
this.markDirty();
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
|
-
if (this.selectedWorkspace === 'agents') {
|
|
225
|
-
// Agents workspace: roster cursor movement, inspect, and cancel-initiation.
|
|
226
|
-
const roster = this.rosterReadModel?.getSnapshot().roster ?? [];
|
|
227
|
-
if (key === 'up' || key === 'k') {
|
|
228
|
-
this.agentCursorIndex = Math.max(0, this.agentCursorIndex - 1);
|
|
229
|
-
this.markDirty();
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
if (key === 'down' || key === 'j') {
|
|
233
|
-
this.agentCursorIndex = Math.min(Math.max(0, roster.length - 1), this.agentCursorIndex + 1);
|
|
234
|
-
this.markDirty();
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
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') {
|
|
247
|
-
const entry = roster[this.agentCursorIndex];
|
|
248
|
-
if (entry) {
|
|
249
|
-
this.actionCallbacks.inspectAgent(entry.id);
|
|
250
|
-
}
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
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.
|
|
257
|
-
const entry = roster[this.agentCursorIndex];
|
|
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);
|
|
281
|
-
}
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
private isTerminal(status: string): boolean {
|
|
289
|
-
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private renderAgentsWorkspace(width: number): Line[] {
|
|
293
|
-
const lines: Line[] = [];
|
|
294
|
-
if (!this.rosterReadModel) {
|
|
295
|
-
lines.push(buildPanelLine(width, [[' Agent roster read model not wired.', C.empty]]));
|
|
296
|
-
return lines;
|
|
297
|
-
}
|
|
298
|
-
const { roster, stalledAgentCount, totalCost, totalInputTokens, totalOutputTokens } = this.rosterReadModel.getSnapshot();
|
|
299
|
-
lines.push(buildPanelLine(width, [
|
|
300
|
-
[` ${roster.length} agent${roster.length !== 1 ? 's' : ''}`, C.label],
|
|
301
|
-
stalledAgentCount > 0 ? [` ${stalledAgentCount} stalled`, C.bad] : ['', C.dim],
|
|
302
|
-
[` cost: ${formatCost(totalCost)}`, C.dim],
|
|
303
|
-
[` tokens: ${formatTokens(totalInputTokens, totalOutputTokens)}`, C.dim],
|
|
304
|
-
]));
|
|
305
|
-
const colors: Record<string, string> = {
|
|
306
|
-
pending: C.dim, running: C.value, completed: C.good, failed: C.bad, cancelled: C.dim, system: C.dim,
|
|
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
|
-
];
|
|
318
|
-
for (let i = 0; i < roster.length; i++) {
|
|
319
|
-
const entry = roster[i]!;
|
|
320
|
-
const selected = i === this.agentCursorIndex;
|
|
321
|
-
const shortId = entry.id.length > 8 ? entry.id.slice(-8) : entry.id;
|
|
322
|
-
const statusColor = agentStatusColor(entry.status, colors);
|
|
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
|
-
));
|
|
337
|
-
}
|
|
338
|
-
if (roster.length === 0) {
|
|
339
|
-
lines.push(buildPanelLine(width, [[' No agents tracked yet.', C.empty]]));
|
|
340
|
-
}
|
|
341
|
-
return lines;
|
|
342
|
-
}
|
|
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
|
-
|
|
366
|
-
public render(width: number, height: number): Line[] {
|
|
367
|
-
this.needsRender = false;
|
|
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
|
-
|
|
380
|
-
if (!this.readModel) {
|
|
381
|
-
const lines: Line[] = [buildPanelLine(width, [[' Runtime read model not wired into this panel yet.', C.empty]])];
|
|
382
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
383
|
-
title: 'Operator Cockpit',
|
|
384
|
-
sections: [{ lines }],
|
|
385
|
-
palette: C,
|
|
386
|
-
});
|
|
387
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
388
|
-
return workspace;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const snapshot = this.readModel.getSnapshot();
|
|
392
|
-
const selectedWorkspace = WORKSPACE_IDS[this.selectedWorkspaceIndex] ?? 'flow';
|
|
393
|
-
|
|
394
|
-
const flowLines: Line[] = [
|
|
395
|
-
buildPanelLine(width, [
|
|
396
|
-
...buildStatPill('graphs', String(snapshot.activeGraphs), C.label, pickColor(snapshot.activeGraphs, 1, 4)),
|
|
397
|
-
...buildStatPill('running', String(snapshot.runningTasks), C.label, C.value),
|
|
398
|
-
...buildStatPill('blocked', String(snapshot.blockedTasks), C.label, pickColor(snapshot.blockedTasks)),
|
|
399
|
-
...buildStatPill('failed', String(snapshot.failedTasks), C.label, pickColor(snapshot.failedTasks)),
|
|
400
|
-
...buildStatPill('guards', String(snapshot.guardTrips), C.label, pickColor(snapshot.guardTrips)),
|
|
401
|
-
]),
|
|
402
|
-
buildPanelLine(width, [
|
|
403
|
-
...buildStatPill('blocked comms', String(snapshot.blockedMessages), C.label, pickColor(snapshot.blockedMessages)),
|
|
404
|
-
...buildStatPill('pending approvals', String(snapshot.pendingPermissions), C.label, pickColor(snapshot.pendingPermissions)),
|
|
405
|
-
...buildStatPill('denied', String(snapshot.deniedPermissions), C.label, pickColor(snapshot.deniedPermissions)),
|
|
406
|
-
]),
|
|
407
|
-
];
|
|
408
|
-
const governanceLines: Line[] = [
|
|
409
|
-
buildPanelLine(width, [
|
|
410
|
-
...buildStatPill('preflight', snapshot.preflightStatus.toUpperCase(), C.label, snapshot.preflightStatus === 'block' ? C.bad : snapshot.preflightStatus === 'warn' ? C.warn : snapshot.preflightStatus === 'pass' ? C.good : C.dim),
|
|
411
|
-
...buildStatPill('issues', String(snapshot.preflightIssueCount), C.label, pickColor(snapshot.preflightIssueCount)),
|
|
412
|
-
...buildStatPill('lint', String(snapshot.lintFindingCount), C.label, pickColor(snapshot.lintFindingCount)),
|
|
413
|
-
...buildStatPill('allow-all MCP', String(snapshot.elevatedMcp), C.label, pickColor(snapshot.elevatedMcp)),
|
|
414
|
-
...buildStatPill('unhealthy MCP', String(snapshot.unhealthyMcp), C.label, pickColor(snapshot.unhealthyMcp)),
|
|
415
|
-
]),
|
|
416
|
-
buildPanelLine(width, [
|
|
417
|
-
...buildStatPill('token blocked', String(snapshot.tokenBlockedCount), C.label, pickColor(snapshot.tokenBlockedCount)),
|
|
418
|
-
...buildStatPill('overdue', String(snapshot.tokenRotationOverdueCount), C.label, pickColor(snapshot.tokenRotationOverdueCount)),
|
|
419
|
-
...buildStatPill('scope violations', String(snapshot.tokenScopeViolationCount), C.label, pickColor(snapshot.tokenScopeViolationCount)),
|
|
420
|
-
...buildStatPill('warnings', String(snapshot.tokenRotationWarningCount), C.label, pickColor(snapshot.tokenRotationWarningCount)),
|
|
421
|
-
]),
|
|
422
|
-
];
|
|
423
|
-
const healthLines: Line[] = [
|
|
424
|
-
buildPanelLine(width, [
|
|
425
|
-
...buildStatPill('incidents', String(snapshot.incidentCount), C.label, pickColor(snapshot.incidentCount)),
|
|
426
|
-
...buildStatPill('plugins', String(snapshot.erroredPlugins), C.label, pickColor(snapshot.erroredPlugins)),
|
|
427
|
-
...buildStatPill('integrations', String(snapshot.failingIntegrations), C.label, pickColor(snapshot.failingIntegrations)),
|
|
428
|
-
]),
|
|
429
|
-
];
|
|
430
|
-
if (snapshot.latestIncident) {
|
|
431
|
-
healthLines.push(buildPanelLine(width, [
|
|
432
|
-
[' latest incident ', C.label],
|
|
433
|
-
[snapshot.latestIncident.classification, C.bad],
|
|
434
|
-
[' ', C.label],
|
|
435
|
-
[truncateDisplay(snapshot.latestIncident.summary, Math.max(0, width - 19 - snapshot.latestIncident.classification.length)), C.dim],
|
|
436
|
-
]));
|
|
437
|
-
}
|
|
438
|
-
const domainLines: Line[] = [buildPanelLine(width, [[
|
|
439
|
-
`tasks:${snapshot.taskCount} agents:${snapshot.agentCount} graphs:${snapshot.totalGraphs} comms:${snapshot.communicationCount} mcp:${snapshot.mcpServerCount} plugins:${snapshot.pluginCount}`,
|
|
440
|
-
C.dim,
|
|
441
|
-
]])];
|
|
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
|
-
]);
|
|
459
|
-
|
|
460
|
-
const sections: PanelWorkspaceSection[] = [
|
|
461
|
-
{ lines: [selectorLine] },
|
|
462
|
-
{ title: 'Flow', lines: flowLines },
|
|
463
|
-
{ title: 'Governance', lines: governanceLines },
|
|
464
|
-
{ title: 'Health', lines: healthLines },
|
|
465
|
-
{ title: 'Domains', lines: domainLines },
|
|
466
|
-
{ title: `Workspace · ${selectedWorkspace}`, lines: workspaceLines },
|
|
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';
|
|
472
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
473
|
-
title: 'Operator Cockpit',
|
|
474
|
-
sections,
|
|
475
|
-
footerLines: [buildPanelLine(width, [[footerHint, C.dim]])],
|
|
476
|
-
palette: C,
|
|
477
|
-
});
|
|
478
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
479
|
-
return lines.slice(0, height);
|
|
480
|
-
}
|
|
481
|
-
}
|