@pellux/goodvibes-tui 0.21.0 → 0.23.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 +45 -0
- package/README.md +1 -1
- package/package.json +2 -1
- package/src/cli/completions/generate.ts +4 -8
- package/src/cli/entrypoint.ts +6 -0
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management-utils.ts +352 -0
- package/src/cli/management.ts +36 -334
- package/src/cli/parser.ts +17 -0
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/types.ts +2 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/core/context-auto-compact.ts +110 -0
- package/src/core/conversation-rendering.ts +5 -2
- package/src/core/conversation-types.ts +24 -0
- package/src/core/conversation.ts +7 -12
- package/src/core/stream-event-wiring.ts +125 -7
- package/src/core/turn-event-wiring.ts +124 -0
- package/src/daemon/cli.ts +5 -0
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/channel-runtime.ts +139 -0
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/runtime-services.ts +30 -1
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +54 -4
- package/src/input/commands.ts +2 -2
- package/src/input/handler-modal-routes.ts +37 -0
- package/src/input/handler-modal-token-routes.ts +19 -5
- package/src/input/handler-onboarding.ts +18 -0
- package/src/input/handler.ts +1 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +10 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +14 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +6 -0
- package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
- package/src/input/settings-modal-behavior.ts +5 -0
- package/src/input/settings-modal-data.ts +77 -3
- package/src/input/settings-modal-mutations.ts +3 -0
- package/src/input/settings-modal-reset.ts +154 -0
- package/src/input/settings-modal.ts +55 -13
- package/src/main.ts +58 -50
- package/src/panels/agent-inspector-panel.ts +120 -18
- package/src/panels/agent-inspector-shared.ts +29 -0
- package/src/panels/builtin/development.ts +1 -0
- package/src/panels/builtin/knowledge.ts +14 -13
- package/src/panels/builtin/operations.ts +22 -1
- package/src/panels/builtin/shared.ts +7 -0
- package/src/panels/cockpit-panel.ts +123 -3
- package/src/panels/cockpit-read-model.ts +232 -0
- package/src/panels/index.ts +1 -1
- package/src/panels/knowledge-graph-panel.ts +84 -0
- package/src/panels/memory-panel.ts +370 -40
- package/src/panels/session-maintenance.ts +66 -15
- package/src/renderer/agent-detail-modal.ts +107 -3
- package/src/renderer/compaction-history-modal.ts +55 -0
- package/src/renderer/compaction-preview.ts +146 -0
- package/src/renderer/context-status-hint.ts +54 -0
- package/src/renderer/settings-modal-helpers.ts +2 -2
- package/src/renderer/settings-modal.ts +14 -3
- package/src/renderer/shell-surface.ts +10 -0
- package/src/runtime/bootstrap-command-parts.ts +4 -0
- package/src/runtime/bootstrap-core.ts +116 -0
- package/src/runtime/bootstrap-shell.ts +11 -0
- package/src/runtime/bootstrap.ts +7 -0
- package/src/runtime/services.ts +6 -1
- package/src/utils/browser.ts +29 -0
- package/src/version.ts +1 -1
- package/src/panels/knowledge-panel.ts +0 -343
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
resolveScrollablePanelSection,
|
|
22
22
|
DEFAULT_PANEL_PALETTE,
|
|
23
23
|
} from './polish.ts';
|
|
24
|
+
import {
|
|
25
|
+
type ConfirmState,
|
|
26
|
+
handleConfirmInput,
|
|
27
|
+
} from './confirm-state.ts';
|
|
24
28
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
25
29
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
26
30
|
import {
|
|
@@ -32,6 +36,8 @@ import {
|
|
|
32
36
|
formatAgentDuration as formatMs,
|
|
33
37
|
formatAgentTime as shortTime,
|
|
34
38
|
jsonlToTimeline,
|
|
39
|
+
AGENT_TERMINAL_STATUSES,
|
|
40
|
+
AGENT_STALL_THRESHOLD_MS,
|
|
35
41
|
} from './agent-inspector-shared.ts';
|
|
36
42
|
|
|
37
43
|
// ---------------------------------------------------------------------------
|
|
@@ -77,10 +83,14 @@ const COLOR = {
|
|
|
77
83
|
// AgentInspectorPanel
|
|
78
84
|
// ---------------------------------------------------------------------------
|
|
79
85
|
|
|
86
|
+
// AGENT_TERMINAL_STATUSES and AGENT_STALL_THRESHOLD_MS imported from agent-inspector-shared.ts
|
|
87
|
+
|
|
80
88
|
export interface AgentInspectorPanelDeps {
|
|
81
|
-
readonly agentManager: Pick<AgentManager, 'list' | 'getStatus'>;
|
|
89
|
+
readonly agentManager: Pick<AgentManager, 'list' | 'getStatus' | 'cancel'>;
|
|
82
90
|
readonly agentMessageBus: Pick<AgentMessageBus, 'getMessages'>;
|
|
83
91
|
readonly workingDirectory: string;
|
|
92
|
+
/** Cancel the agent by id. Uses the same orphan-free path as WRFC. Returns true if cancelled. */
|
|
93
|
+
readonly cancelAgent: (agentId: string) => boolean;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
export class AgentInspectorPanel extends BasePanel {
|
|
@@ -102,6 +112,9 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
102
112
|
// Row cache — cleared on markDirty(), computed once per render cycle
|
|
103
113
|
private _cachedRows: DisplayRow[] | null = null;
|
|
104
114
|
|
|
115
|
+
/** Pending cancel confirmation — subject is the agent id to cancel. */
|
|
116
|
+
private confirmCancel: ConfirmState<string> | null = null;
|
|
117
|
+
|
|
105
118
|
constructor(private readonly deps: AgentInspectorPanelDeps) {
|
|
106
119
|
super('inspector', 'Inspector', 'I', 'agent');
|
|
107
120
|
}
|
|
@@ -157,13 +170,36 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
157
170
|
// -------------------------------------------------------------------------
|
|
158
171
|
|
|
159
172
|
handleInput(key: string): boolean {
|
|
173
|
+
// Confirm-cancel flow takes priority — same contract as WRFC panel.
|
|
174
|
+
if (this.confirmCancel) {
|
|
175
|
+
const result = handleConfirmInput(this.confirmCancel, key);
|
|
176
|
+
if (result === 'confirmed') {
|
|
177
|
+
const rec = this.selectedAgentId
|
|
178
|
+
? this.deps.agentManager.getStatus(this.selectedAgentId)
|
|
179
|
+
: null;
|
|
180
|
+
if (rec && !AGENT_TERMINAL_STATUSES.has(rec.status)) {
|
|
181
|
+
this.deps.cancelAgent(rec.id);
|
|
182
|
+
}
|
|
183
|
+
this.confirmCancel = null;
|
|
184
|
+
this.markDirty();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (result === 'cancelled') {
|
|
188
|
+
this.confirmCancel = null;
|
|
189
|
+
this.markDirty();
|
|
190
|
+
}
|
|
191
|
+
// absorbed: confirm stays pending
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
160
195
|
switch (key) {
|
|
161
|
-
case 'up': this._moveCursor(-1);
|
|
162
|
-
case 'down': this._moveCursor(1);
|
|
163
|
-
case 'pageup': this._scroll(-10);
|
|
164
|
-
case 'pagedown': this._scroll(10);
|
|
165
|
-
case 'return': this._toggleExpand();
|
|
166
|
-
case 'tab': this._nextAgent();
|
|
196
|
+
case 'up': this._moveCursor(-1); return true;
|
|
197
|
+
case 'down': this._moveCursor(1); return true;
|
|
198
|
+
case 'pageup': this._scroll(-10); return true;
|
|
199
|
+
case 'pagedown': this._scroll(10); return true;
|
|
200
|
+
case 'return': this._toggleExpand(); return true;
|
|
201
|
+
case 'tab': this._nextAgent(); return true;
|
|
202
|
+
case 'c': this._beginCancelConfirm(); return true;
|
|
167
203
|
default: return false;
|
|
168
204
|
}
|
|
169
205
|
}
|
|
@@ -217,6 +253,11 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
217
253
|
}
|
|
218
254
|
|
|
219
255
|
summaryLines.push(this._renderAgentInfoSummary(width, rec));
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const isStalled = this._isAgentStalled(rec, now);
|
|
258
|
+
if (isStalled) {
|
|
259
|
+
summaryLines.push(buildPanelLine(width, [[' STALLED', '#f59e0b'], [' — no activity for 5+ minutes', DEFAULT_PANEL_PALETTE.dim]]));
|
|
260
|
+
}
|
|
220
261
|
const allRows = this._getCachedRows();
|
|
221
262
|
if (allRows.length === 0) {
|
|
222
263
|
return buildPanelWorkspace(width, height, {
|
|
@@ -243,13 +284,42 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
243
284
|
}
|
|
244
285
|
|
|
245
286
|
this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, allRows.length - 1));
|
|
287
|
+
const selectedRec = this.selectedAgentId
|
|
288
|
+
? this.deps.agentManager.getStatus(this.selectedAgentId)
|
|
289
|
+
: null;
|
|
290
|
+
const cancellable = selectedRec && !AGENT_TERMINAL_STATUSES.has(selectedRec.status);
|
|
246
291
|
const summarySection = { title: 'Summary', lines: summaryLines } as const;
|
|
247
292
|
const agentsSection = { title: 'Agents', lines: [selectorLine] } as const;
|
|
293
|
+
|
|
294
|
+
// Confirm-cancel overlay section.
|
|
295
|
+
const confirmSection = this.confirmCancel ? {
|
|
296
|
+
title: 'Confirm Cancel',
|
|
297
|
+
lines: [
|
|
298
|
+
buildPanelLine(width, [
|
|
299
|
+
[' Cancel agent "', DEFAULT_PANEL_PALETTE.warn],
|
|
300
|
+
[this.confirmCancel.label, DEFAULT_PANEL_PALETTE.value],
|
|
301
|
+
['"?', DEFAULT_PANEL_PALETTE.warn],
|
|
302
|
+
]),
|
|
303
|
+
buildPanelLine(width, [
|
|
304
|
+
[' y', DEFAULT_PANEL_PALETTE.info], [' confirm', DEFAULT_PANEL_PALETTE.dim],
|
|
305
|
+
[' Enter', DEFAULT_PANEL_PALETTE.info], [' confirm', DEFAULT_PANEL_PALETTE.dim],
|
|
306
|
+
[' n / Esc', DEFAULT_PANEL_PALETTE.info], [' cancel', DEFAULT_PANEL_PALETTE.dim],
|
|
307
|
+
]),
|
|
308
|
+
],
|
|
309
|
+
} : null;
|
|
310
|
+
|
|
311
|
+
const cancelHintFg = cancellable ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim;
|
|
312
|
+
const footerLine = buildPanelLine(width, [
|
|
313
|
+
[` L${this.cursorIndex + 1}/${allRows.length}`, DEFAULT_PANEL_PALETTE.dim],
|
|
314
|
+
[' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim],
|
|
315
|
+
[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim],
|
|
316
|
+
[' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim],
|
|
317
|
+
[' c', cancelHintFg], [cancellable ? ' cancel' : ' cancel (n/a)', DEFAULT_PANEL_PALETTE.dim],
|
|
318
|
+
]);
|
|
319
|
+
|
|
248
320
|
const timelineSection = resolveScrollablePanelSection(width, height, {
|
|
249
321
|
intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
|
|
250
|
-
footerLines: [
|
|
251
|
-
buildPanelLine(width, [[` L${this.cursorIndex + 1}/${allRows.length}`, DEFAULT_PANEL_PALETTE.dim], [' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim], [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
|
|
252
|
-
],
|
|
322
|
+
footerLines: [footerLine],
|
|
253
323
|
palette: DEFAULT_PANEL_PALETTE,
|
|
254
324
|
beforeSections: [summarySection, agentsSection],
|
|
255
325
|
section: {
|
|
@@ -259,20 +329,22 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
259
329
|
scrollOffset: this.scrollOffset,
|
|
260
330
|
minRows: 8,
|
|
261
331
|
},
|
|
332
|
+
afterSections: confirmSection ? [confirmSection] : undefined,
|
|
262
333
|
});
|
|
263
334
|
this.scrollOffset = timelineSection.scrollOffset;
|
|
264
335
|
|
|
336
|
+
const sections = [
|
|
337
|
+
summarySection,
|
|
338
|
+
agentsSection,
|
|
339
|
+
timelineSection.section,
|
|
340
|
+
...(confirmSection ? [confirmSection] : []),
|
|
341
|
+
];
|
|
342
|
+
|
|
265
343
|
return buildPanelWorkspace(width, height, {
|
|
266
344
|
title: ` Inspector [${agents.length} agent${agents.length !== 1 ? 's' : ''}]`,
|
|
267
345
|
intro: 'Inspect a selected agent timeline, tool activity, expanded details, and live/historical message flow.',
|
|
268
|
-
sections
|
|
269
|
-
|
|
270
|
-
agentsSection,
|
|
271
|
-
timelineSection.section,
|
|
272
|
-
],
|
|
273
|
-
footerLines: [
|
|
274
|
-
buildPanelLine(width, [[` L${this.cursorIndex + 1}/${allRows.length}`, DEFAULT_PANEL_PALETTE.dim], [' Tab', DEFAULT_PANEL_PALETTE.info], [' cycle agents', DEFAULT_PANEL_PALETTE.dim], [' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' expand', DEFAULT_PANEL_PALETTE.dim]]),
|
|
275
|
-
],
|
|
346
|
+
sections,
|
|
347
|
+
footerLines: [footerLine],
|
|
276
348
|
palette: DEFAULT_PANEL_PALETTE,
|
|
277
349
|
});
|
|
278
350
|
}
|
|
@@ -517,4 +589,34 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
517
589
|
this.inspectAgent(next.id);
|
|
518
590
|
}
|
|
519
591
|
}
|
|
592
|
+
|
|
593
|
+
// -------------------------------------------------------------------------
|
|
594
|
+
// Private — cancel + stall
|
|
595
|
+
// -------------------------------------------------------------------------
|
|
596
|
+
|
|
597
|
+
/** Initiate cancel-confirm flow for the selected agent (noop if terminal or none selected). */
|
|
598
|
+
private _beginCancelConfirm(): void {
|
|
599
|
+
if (!this.selectedAgentId) return;
|
|
600
|
+
const rec = this.deps.agentManager.getStatus(this.selectedAgentId);
|
|
601
|
+
if (!rec || AGENT_TERMINAL_STATUSES.has(rec.status)) return;
|
|
602
|
+
const label = rec.task.split('\n')[0]?.slice(0, 40) ?? rec.id.slice(-8);
|
|
603
|
+
this.confirmCancel = { subject: rec.id, label };
|
|
604
|
+
this.markDirty();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Returns whether an agent is considered stalled (non-terminal, running past threshold). */
|
|
608
|
+
private _isAgentStalled(rec: AgentRecord, now: number): boolean {
|
|
609
|
+
if (AGENT_TERMINAL_STATUSES.has(rec.status)) return false;
|
|
610
|
+
return (now - rec.startedAt) >= AGENT_STALL_THRESHOLD_MS;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Count of all tracked agents that are stalled (non-terminal, no activity
|
|
615
|
+
* for AGENT_STALL_THRESHOLD_MS). Exposed so callers can aggregate a
|
|
616
|
+
* stalledAgentCount for cockpit / roster read-models.
|
|
617
|
+
*/
|
|
618
|
+
getStalledAgentCount(): number {
|
|
619
|
+
const now = Date.now();
|
|
620
|
+
return this.deps.agentManager.list().filter(rec => this._isAgentStalled(rec, now)).length;
|
|
621
|
+
}
|
|
520
622
|
}
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
export type AgentInspectorEntryKind = 'user' | 'assistant' | 'tool_call' | 'tool_result' | 'session' | 'error';
|
|
2
2
|
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Shared agent status / stall constants
|
|
5
|
+
// Used by AgentInspectorPanel, AgentDetailModal, and cockpit read-model consumers.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/** Terminal statuses — cancel not offered; stall check skipped. */
|
|
9
|
+
export const AGENT_TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
10
|
+
|
|
11
|
+
/** Agents in a non-terminal state for longer than this are considered STALLED. */
|
|
12
|
+
export const AGENT_STALL_THRESHOLD_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Count stalled agents from a raw record list.
|
|
16
|
+
* An agent is stalled when it is non-terminal and has been running for at
|
|
17
|
+
* least AGENT_STALL_THRESHOLD_MS without completing.
|
|
18
|
+
*
|
|
19
|
+
* Extracted as a standalone export so read-models and panels can share the
|
|
20
|
+
* canonical stall-count logic (TASK-046).
|
|
21
|
+
*/
|
|
22
|
+
export function countStalledAgents(
|
|
23
|
+
records: ReadonlyArray<{ status: string; startedAt: number }>,
|
|
24
|
+
now: number = Date.now(),
|
|
25
|
+
): number {
|
|
26
|
+
return records.filter(
|
|
27
|
+
(r) => !AGENT_TERMINAL_STATUSES.has(r.status) && (now - r.startedAt) >= AGENT_STALL_THRESHOLD_MS,
|
|
28
|
+
).length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
3
32
|
export interface AgentTimelineEntry {
|
|
4
33
|
kind: AgentInspectorEntryKind;
|
|
5
34
|
timestamp: number;
|
|
@@ -51,6 +51,7 @@ export function registerDevelopmentPanels(manager: PanelManager, deps: ResolvedB
|
|
|
51
51
|
agentManager: ui.agents.agentManager,
|
|
52
52
|
agentMessageBus: ui.agents.agentMessageBus,
|
|
53
53
|
workingDirectory: ui.environment.workingDirectory,
|
|
54
|
+
cancelAgent: (agentId: string) => ui.agents.agentManager.cancel(agentId),
|
|
54
55
|
});
|
|
55
56
|
},
|
|
56
57
|
});
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import type { PanelManager } from '../panel-manager.ts';
|
|
2
2
|
import { MemoryPanel } from '../memory-panel.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { KnowledgeGraphPanel } from '../knowledge-graph-panel.ts';
|
|
4
4
|
import type { ResolvedBuiltinPanelDeps } from './shared.ts';
|
|
5
5
|
|
|
6
6
|
export function registerKnowledgePanels(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const { memoryRegistry } = deps;
|
|
7
|
+
// KnowledgeGraphPanel is a no-arg panel — always register it regardless of memoryRegistry.
|
|
10
8
|
manager.registerType({
|
|
11
9
|
id: 'knowledge',
|
|
12
10
|
name: 'Knowledge',
|
|
13
11
|
icon: 'K',
|
|
14
12
|
category: 'agent',
|
|
15
13
|
description: 'Structured project knowledge: risks, runbooks, architecture notes, incidents, and durable facts',
|
|
16
|
-
factory: () => new
|
|
17
|
-
});
|
|
18
|
-
manager.registerType({
|
|
19
|
-
id: 'memory',
|
|
20
|
-
name: 'Memory',
|
|
21
|
-
icon: 'M',
|
|
22
|
-
category: 'agent',
|
|
23
|
-
description: 'Project memory: decisions, constraints, incidents, and patterns with provenance links',
|
|
24
|
-
factory: () => new MemoryPanel(memoryRegistry),
|
|
14
|
+
factory: () => new KnowledgeGraphPanel(),
|
|
25
15
|
});
|
|
16
|
+
if (deps.memoryRegistry) {
|
|
17
|
+
const { memoryRegistry } = deps;
|
|
18
|
+
manager.registerType({
|
|
19
|
+
id: 'memory',
|
|
20
|
+
name: 'Memory',
|
|
21
|
+
icon: 'M',
|
|
22
|
+
category: 'agent',
|
|
23
|
+
description: 'Project memory: decisions, constraints, incidents, and patterns with provenance links',
|
|
24
|
+
factory: () => new MemoryPanel(memoryRegistry),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
26
27
|
}
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import { createRuntimeProviderApi } from '@/runtime/index.ts';
|
|
38
38
|
import type { ResolvedBuiltinPanelDeps } from './shared.ts';
|
|
39
39
|
import { requireAutomationManager, requireControlPlanePanelDeps, requireHookPanelDeps, requirePluginManager, requireUiServices } from './shared.ts';
|
|
40
|
+
import { createCockpitRosterReadModel } from '../cockpit-read-model.ts';
|
|
40
41
|
|
|
41
42
|
export function registerOperationsPanels(manager: PanelManager, deps: ResolvedBuiltinPanelDeps): void {
|
|
42
43
|
const ui = requireUiServices(deps);
|
|
@@ -52,13 +53,33 @@ export function registerOperationsPanels(manager: PanelManager, deps: ResolvedBu
|
|
|
52
53
|
environment: createEnvironmentVariableQuery(process.env),
|
|
53
54
|
});
|
|
54
55
|
|
|
56
|
+
const rosterReadModel = createCockpitRosterReadModel(ui.agents.agentManager);
|
|
57
|
+
// Subscribe to agent lifecycle events so the roster re-renders on state changes.
|
|
58
|
+
// AGENT_RUNNING covers status transitions; AGENT_CANCELLED covers the cancellation
|
|
59
|
+
// terminal state not emitted by AGENT_FAILED. Noisy mid-run events (STREAM_DELTA,
|
|
60
|
+
// AWAITING_TOOL, etc.) are intentionally excluded — they don't affect roster fields.
|
|
61
|
+
// Note: stall detection is time-based, so stalled/stalledAgentCount will only refresh
|
|
62
|
+
// on the next lifecycle event; a periodic tick would be needed for real-time stall display.
|
|
63
|
+
ui.events.agents.on('AGENT_SPAWNING', () => rosterReadModel.markDirty());
|
|
64
|
+
ui.events.agents.on('AGENT_RUNNING', () => rosterReadModel.markDirty());
|
|
65
|
+
ui.events.agents.on('AGENT_COMPLETED', () => rosterReadModel.markDirty());
|
|
66
|
+
ui.events.agents.on('AGENT_FAILED', () => rosterReadModel.markDirty());
|
|
67
|
+
ui.events.agents.on('AGENT_CANCELLED', () => rosterReadModel.markDirty());
|
|
68
|
+
|
|
55
69
|
manager.registerType({
|
|
56
70
|
id: 'cockpit',
|
|
57
71
|
name: 'Cockpit',
|
|
58
72
|
icon: 'O',
|
|
59
73
|
category: 'monitoring',
|
|
60
74
|
description: 'Unified operator summary for orchestration, permissions, communication, MCP, plugins, and integrations',
|
|
61
|
-
factory: () => new CockpitPanel(
|
|
75
|
+
factory: () => new CockpitPanel(
|
|
76
|
+
ui.readModels.cockpit,
|
|
77
|
+
rosterReadModel,
|
|
78
|
+
{
|
|
79
|
+
openAgentDetail: (agentId: string) => deps.openAgentDetail?.(agentId),
|
|
80
|
+
cancelAgent: (agentId: string) => ui.agents.agentManager.cancel(agentId),
|
|
81
|
+
},
|
|
82
|
+
),
|
|
62
83
|
});
|
|
63
84
|
|
|
64
85
|
manager.registerType({
|
|
@@ -114,6 +114,13 @@ export interface BuiltinPanelDeps {
|
|
|
114
114
|
hookActivityTracker?: Pick<HookActivityTracker, 'listRecent'>;
|
|
115
115
|
/** Shared MCP registry for security panels and MCP workspace commands. */
|
|
116
116
|
mcpRegistry?: McpRegistry;
|
|
117
|
+
/**
|
|
118
|
+
* Open the agent detail modal for the given agent id. Wired from
|
|
119
|
+
* InputHandler.agentDetailModal.open() at bootstrap — passed to the
|
|
120
|
+
* CockpitPanel factory so the agents workspace inspect key (i) works
|
|
121
|
+
* without the panel depending on the modal directly.
|
|
122
|
+
*/
|
|
123
|
+
openAgentDetail?: (agentId: string) => void;
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
export type ResolvedBuiltinPanelDeps = Omit<
|
|
@@ -2,6 +2,7 @@ import type { Line } from '../types/grid.ts';
|
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
3
|
import { BasePanel } from './base-panel.ts';
|
|
4
4
|
import type { UiCockpitSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
5
|
+
import type { CockpitRosterReadModel } from './cockpit-read-model.ts';
|
|
5
6
|
import {
|
|
6
7
|
buildGuidanceLine,
|
|
7
8
|
buildKeyValueLine,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
DEFAULT_PANEL_PALETTE,
|
|
12
13
|
type PanelWorkspaceSection,
|
|
13
14
|
} from './polish.ts';
|
|
15
|
+
import { agentStatusColor } from './agent-inspector-shared.ts';
|
|
14
16
|
|
|
15
17
|
const C = {
|
|
16
18
|
...DEFAULT_PANEL_PALETTE,
|
|
@@ -24,45 +26,160 @@ function pickColor(value: number, warnAt = 1, badAt = 3): string {
|
|
|
24
26
|
return C.good;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
export interface CockpitPanelActionCallbacks {
|
|
30
|
+
readonly openAgentDetail: (agentId: string) => void;
|
|
31
|
+
readonly cancelAgent: (agentId: string) => boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const WORKSPACE_IDS = ['flow', 'governance', 'health', 'domains', 'agents'] as const;
|
|
35
|
+
type WorkspaceId = (typeof WORKSPACE_IDS)[number];
|
|
36
|
+
|
|
37
|
+
function formatCost(cost: number | null): string {
|
|
38
|
+
if (cost === null) return 'n/a';
|
|
39
|
+
return cost < 0.01 ? '<$0.01' : `$${cost.toFixed(2)}`;
|
|
40
|
+
}
|
|
28
41
|
|
|
29
42
|
export class CockpitPanel extends BasePanel {
|
|
30
43
|
private readonly unsub: (() => void) | null;
|
|
44
|
+
private readonly rosterUnsub: (() => void) | null;
|
|
45
|
+
private readonly actionCallbacks: CockpitPanelActionCallbacks;
|
|
31
46
|
private selectedWorkspaceIndex = 0;
|
|
47
|
+
private agentCursorIndex = 0;
|
|
48
|
+
private pendingCancelId: string | null = null;
|
|
32
49
|
|
|
33
|
-
public constructor(
|
|
50
|
+
public constructor(
|
|
51
|
+
private readonly readModel?: UiReadModel<UiCockpitSnapshot>,
|
|
52
|
+
private readonly rosterReadModel?: CockpitRosterReadModel,
|
|
53
|
+
actionCallbacks?: Partial<CockpitPanelActionCallbacks>,
|
|
54
|
+
) {
|
|
34
55
|
super('cockpit', 'Cockpit', 'O', 'monitoring');
|
|
35
56
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
57
|
+
this.rosterUnsub = rosterReadModel ? rosterReadModel.subscribe(() => this.markDirty()) : null;
|
|
58
|
+
this.actionCallbacks = {
|
|
59
|
+
openAgentDetail: actionCallbacks?.openAgentDetail ?? ((_id: string) => { /* noop */ }),
|
|
60
|
+
cancelAgent: actionCallbacks?.cancelAgent ?? ((_id: string) => false),
|
|
61
|
+
};
|
|
36
62
|
}
|
|
37
63
|
|
|
38
64
|
public override onDestroy(): void {
|
|
39
65
|
this.unsub?.();
|
|
66
|
+
this.rosterUnsub?.();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private get selectedWorkspace(): WorkspaceId {
|
|
70
|
+
return WORKSPACE_IDS[this.selectedWorkspaceIndex] ?? 'flow';
|
|
40
71
|
}
|
|
41
72
|
|
|
42
73
|
public handleInput(key: string): boolean {
|
|
74
|
+
// Confirm-cancel absorb: when a cancel is pending, only y/enter confirm, escape/n dismiss, everything else is consumed
|
|
75
|
+
if (this.pendingCancelId !== null) {
|
|
76
|
+
if (key === 'y' || key === 'enter' || key === 'return') {
|
|
77
|
+
this.actionCallbacks.cancelAgent(this.pendingCancelId);
|
|
78
|
+
this.pendingCancelId = null;
|
|
79
|
+
this.markDirty();
|
|
80
|
+
} else if (key === 'escape' || key === 'n') {
|
|
81
|
+
this.pendingCancelId = null;
|
|
82
|
+
this.markDirty();
|
|
83
|
+
}
|
|
84
|
+
// All other keys are consumed while confirm is pending
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
43
87
|
if (key === 'left' || key === 'h') {
|
|
44
88
|
this.selectedWorkspaceIndex = Math.max(0, this.selectedWorkspaceIndex - 1);
|
|
89
|
+
this.agentCursorIndex = 0;
|
|
45
90
|
this.markDirty();
|
|
46
91
|
return true;
|
|
47
92
|
}
|
|
48
93
|
if (key === 'right' || key === 'l') {
|
|
49
94
|
this.selectedWorkspaceIndex = Math.min(WORKSPACE_IDS.length - 1, this.selectedWorkspaceIndex + 1);
|
|
95
|
+
this.agentCursorIndex = 0;
|
|
50
96
|
this.markDirty();
|
|
51
97
|
return true;
|
|
52
98
|
}
|
|
53
99
|
if (key === 'home') {
|
|
54
100
|
this.selectedWorkspaceIndex = 0;
|
|
101
|
+
this.agentCursorIndex = 0;
|
|
55
102
|
this.markDirty();
|
|
56
103
|
return true;
|
|
57
104
|
}
|
|
58
105
|
if (key === 'end') {
|
|
59
106
|
this.selectedWorkspaceIndex = WORKSPACE_IDS.length - 1;
|
|
107
|
+
this.agentCursorIndex = 0;
|
|
60
108
|
this.markDirty();
|
|
61
109
|
return true;
|
|
62
110
|
}
|
|
111
|
+
// Agents workspace: cursor movement, inspect, and cancel-initiation
|
|
112
|
+
if (this.selectedWorkspace === 'agents') {
|
|
113
|
+
const roster = this.rosterReadModel?.getSnapshot().roster ?? [];
|
|
114
|
+
if (key === 'up' || key === 'k') {
|
|
115
|
+
this.agentCursorIndex = Math.max(0, this.agentCursorIndex - 1);
|
|
116
|
+
this.markDirty();
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (key === 'down' || key === 'j') {
|
|
120
|
+
this.agentCursorIndex = Math.min(Math.max(0, roster.length - 1), this.agentCursorIndex + 1);
|
|
121
|
+
this.markDirty();
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (key === 'i' || key === 'enter' || key === 'return') {
|
|
125
|
+
const entry = roster[this.agentCursorIndex];
|
|
126
|
+
if (entry) {
|
|
127
|
+
this.actionCallbacks.openAgentDetail(entry.id);
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (key === 'c') {
|
|
132
|
+
const entry = roster[this.agentCursorIndex];
|
|
133
|
+
if (entry && !this.isTerminal(entry.status)) {
|
|
134
|
+
this.pendingCancelId = entry.id;
|
|
135
|
+
this.markDirty();
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
63
140
|
return false;
|
|
64
141
|
}
|
|
65
142
|
|
|
143
|
+
private isTerminal(status: string): boolean {
|
|
144
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private renderAgentsWorkspace(width: number): Line[] {
|
|
148
|
+
const lines: Line[] = [];
|
|
149
|
+
if (!this.rosterReadModel) {
|
|
150
|
+
lines.push(buildPanelLine(width, [[' Agent roster read model not wired.', C.empty]]));
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
const { roster, stalledAgentCount, totalCost } = this.rosterReadModel.getSnapshot();
|
|
154
|
+
lines.push(buildPanelLine(width, [
|
|
155
|
+
[` ${roster.length} agent${roster.length !== 1 ? 's' : ''}`, C.label],
|
|
156
|
+
stalledAgentCount > 0 ? [` ${stalledAgentCount} stalled`, C.bad] : ['', C.dim],
|
|
157
|
+
[` cost: ${formatCost(totalCost)}`, C.dim],
|
|
158
|
+
]));
|
|
159
|
+
const colors: Record<string, string> = {
|
|
160
|
+
pending: C.dim, running: C.value, completed: C.good, failed: C.bad, cancelled: C.dim, system: C.dim,
|
|
161
|
+
};
|
|
162
|
+
for (let i = 0; i < roster.length; i++) {
|
|
163
|
+
const entry = roster[i]!;
|
|
164
|
+
const cursor = i === this.agentCursorIndex ? '>' : ' ';
|
|
165
|
+
const shortId = entry.id.length > 8 ? entry.id.slice(-8) : entry.id;
|
|
166
|
+
const statusColor = agentStatusColor(entry.status, colors);
|
|
167
|
+
const stalledBadge: [string, string] = entry.stalled ? [' STALLED', C.bad] : ['', C.dim];
|
|
168
|
+
lines.push(buildPanelLine(width, [
|
|
169
|
+
[cursor, i === this.agentCursorIndex ? C.value : C.dim],
|
|
170
|
+
[` ${shortId} `, C.dim],
|
|
171
|
+
[entry.status, statusColor],
|
|
172
|
+
stalledBadge,
|
|
173
|
+
[' ', C.dim],
|
|
174
|
+
[entry.task.slice(0, Math.max(0, width - 30)), C.label],
|
|
175
|
+
]));
|
|
176
|
+
}
|
|
177
|
+
if (roster.length === 0) {
|
|
178
|
+
lines.push(buildPanelLine(width, [[' No agents tracked yet.', C.empty]]));
|
|
179
|
+
}
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
|
|
66
183
|
public render(width: number, height: number): Line[] {
|
|
67
184
|
this.needsRender = false;
|
|
68
185
|
|
|
@@ -153,7 +270,7 @@ export class CockpitPanel extends BasePanel {
|
|
|
153
270
|
], C));
|
|
154
271
|
workspaceLines.push(buildGuidanceLine(width, '/incident latest', 'inspect the latest incident bundle and replay fallout', C));
|
|
155
272
|
workspaceLines.push(buildGuidanceLine(width, '/plugins', 'review errored plugins and provenance posture', C));
|
|
156
|
-
} else {
|
|
273
|
+
} else if (selectedWorkspace === 'domains') {
|
|
157
274
|
workspaceLines.push(buildKeyValueLine(width, [
|
|
158
275
|
{ label: 'tasks', value: String(snapshot.taskCount), valueColor: C.value },
|
|
159
276
|
{ label: 'comms', value: String(snapshot.communicationCount), valueColor: C.value },
|
|
@@ -161,6 +278,9 @@ export class CockpitPanel extends BasePanel {
|
|
|
161
278
|
], C));
|
|
162
279
|
workspaceLines.push(buildGuidanceLine(width, '/mcp', 'inspect trust, quarantine, and risky server posture', C));
|
|
163
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));
|
|
164
284
|
}
|
|
165
285
|
|
|
166
286
|
const sections: PanelWorkspaceSection[] = [
|