@pellux/goodvibes-tui 0.21.0 → 0.22.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 +23 -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/parser.ts +17 -0
- package/src/cli/types.ts +2 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/core/context-auto-compact.ts +77 -0
- 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/control-room-runtime.ts +5 -5
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands.ts +0 -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 +36 -28
- 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/context-status-hint.ts +54 -0
- 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 +24 -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/version.ts +1 -1
- package/src/panels/knowledge-panel.ts +0 -343
|
@@ -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[] = [
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// cockpit-read-model.ts
|
|
3
|
+
//
|
|
4
|
+
// TASK-046: Cockpit agent roster slice + cost/token aggregates + stalledAgentCount
|
|
5
|
+
//
|
|
6
|
+
// Provides a thin read-model over AgentManager.list() / getStatus() so the
|
|
7
|
+
// CockpitPanel can display an agent roster without depending on the full
|
|
8
|
+
// AgentInspectorPanel object.
|
|
9
|
+
//
|
|
10
|
+
// Design notes:
|
|
11
|
+
// - Per-agent cost delegates to calcSessionCost() from cost-utils.ts (canonical
|
|
12
|
+
// billing formula) and requires real usage data from AgentRecord.usage. When
|
|
13
|
+
// usage is absent (agent spawned but not yet completed), cost/tokens show as n/a
|
|
14
|
+
// rather than fabricated values (39327f86 honest-UX standard).
|
|
15
|
+
// - stalledAgentCount delegates to countStalledAgents() from agent-inspector-shared.ts
|
|
16
|
+
// (canonical stall-count function extracted from TASK-046 review) — no reimplementation.
|
|
17
|
+
// - The read-model is a plain object (snapshot + subscribe) so it can be
|
|
18
|
+
// wired in tests without a full runtime.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
import type { AgentRecord } from '@pellux/goodvibes-sdk/platform/tools';
|
|
22
|
+
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
23
|
+
import {
|
|
24
|
+
AGENT_TERMINAL_STATUSES,
|
|
25
|
+
AGENT_STALL_THRESHOLD_MS,
|
|
26
|
+
countStalledAgents,
|
|
27
|
+
} from './agent-inspector-shared.ts';
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Public types
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** Status of a single agent in the cockpit roster. */
|
|
34
|
+
export type CockpitAgentStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
35
|
+
|
|
36
|
+
/** A single row in the cockpit agent roster. */
|
|
37
|
+
export interface CockpitAgentRosterEntry {
|
|
38
|
+
/** Full agent id. */
|
|
39
|
+
readonly id: string;
|
|
40
|
+
/** Short task description (truncated at 50 chars). */
|
|
41
|
+
readonly task: string;
|
|
42
|
+
/** Model identifier, or 'unknown' when not yet resolved. */
|
|
43
|
+
readonly model: string;
|
|
44
|
+
/** Agent lifecycle status. */
|
|
45
|
+
readonly status: CockpitAgentStatus;
|
|
46
|
+
/** True when the agent is non-terminal and has exceeded AGENT_STALL_THRESHOLD_MS. */
|
|
47
|
+
readonly stalled: boolean;
|
|
48
|
+
/** Input tokens consumed (including cache read+write), or null when unavailable. */
|
|
49
|
+
readonly inputTokens: number | null;
|
|
50
|
+
/** Output tokens produced, or null when unavailable. */
|
|
51
|
+
readonly outputTokens: number | null;
|
|
52
|
+
/** Estimated cost in USD, or null when token data is unavailable. */
|
|
53
|
+
readonly cost: number | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Aggregate snapshot produced by the cockpit roster read-model. */
|
|
57
|
+
export interface CockpitRosterSnapshot {
|
|
58
|
+
/** All agents in the manager, newest-first by startedAt. */
|
|
59
|
+
readonly roster: readonly CockpitAgentRosterEntry[];
|
|
60
|
+
/** Number of non-terminal agents running past AGENT_STALL_THRESHOLD_MS. */
|
|
61
|
+
readonly stalledAgentCount: number;
|
|
62
|
+
/**
|
|
63
|
+
* Sum of all input tokens across agents with real usage data.
|
|
64
|
+
* null when NO agent has usage data yet (avoids showing 0 when data is simply absent).
|
|
65
|
+
*/
|
|
66
|
+
readonly totalInputTokens: number | null;
|
|
67
|
+
/**
|
|
68
|
+
* Sum of all output tokens across agents with real usage data.
|
|
69
|
+
* null when NO agent has usage data yet.
|
|
70
|
+
*/
|
|
71
|
+
readonly totalOutputTokens: number | null;
|
|
72
|
+
/**
|
|
73
|
+
* Total estimated cost in USD across agents with real pricing data.
|
|
74
|
+
* null when NO agent has priceable data yet.
|
|
75
|
+
*/
|
|
76
|
+
readonly totalCost: number | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// AgentManager minimal interface (subset used here)
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export interface CockpitRosterAgentManager {
|
|
84
|
+
list(): AgentRecord[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Snapshot builder — pure, testable
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build a CockpitRosterSnapshot from a raw AgentRecord list.
|
|
93
|
+
* Exported so unit tests can drive it directly without a manager stub.
|
|
94
|
+
*/
|
|
95
|
+
export function buildCockpitRosterSnapshot(
|
|
96
|
+
records: AgentRecord[],
|
|
97
|
+
now: number = Date.now(),
|
|
98
|
+
): CockpitRosterSnapshot {
|
|
99
|
+
// Sort newest-first by startedAt
|
|
100
|
+
const sorted = [...records].sort((a, b) => b.startedAt - a.startedAt);
|
|
101
|
+
|
|
102
|
+
let hasUsage = false;
|
|
103
|
+
let totalInputTokens = 0;
|
|
104
|
+
let totalOutputTokens = 0;
|
|
105
|
+
let totalCost = 0;
|
|
106
|
+
|
|
107
|
+
const roster: CockpitAgentRosterEntry[] = sorted.map((rec) => {
|
|
108
|
+
const isTerminal = AGENT_TERMINAL_STATUSES.has(rec.status);
|
|
109
|
+
const elapsed = now - rec.startedAt;
|
|
110
|
+
const stalled = !isTerminal && elapsed >= AGENT_STALL_THRESHOLD_MS;
|
|
111
|
+
|
|
112
|
+
let inputTokens: number | null = null;
|
|
113
|
+
let outputTokens: number | null = null;
|
|
114
|
+
let cost: number | null = null;
|
|
115
|
+
|
|
116
|
+
if (rec.usage) {
|
|
117
|
+
hasUsage = true;
|
|
118
|
+
const inp =
|
|
119
|
+
rec.usage.inputTokens +
|
|
120
|
+
(rec.usage.cacheReadTokens ?? 0) +
|
|
121
|
+
(rec.usage.cacheWriteTokens ?? 0);
|
|
122
|
+
const out = rec.usage.outputTokens;
|
|
123
|
+
const agentCost = calcSessionCost(
|
|
124
|
+
rec.usage.inputTokens,
|
|
125
|
+
rec.usage.outputTokens,
|
|
126
|
+
rec.usage.cacheReadTokens ?? 0,
|
|
127
|
+
rec.usage.cacheWriteTokens ?? 0,
|
|
128
|
+
rec.model ?? 'unknown',
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
inputTokens = inp;
|
|
132
|
+
outputTokens = out;
|
|
133
|
+
cost = agentCost;
|
|
134
|
+
|
|
135
|
+
totalInputTokens += inp;
|
|
136
|
+
totalOutputTokens += out;
|
|
137
|
+
totalCost += agentCost;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const task = rec.task.length > 50 ? rec.task.slice(0, 47) + '...' : rec.task;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
id: rec.id,
|
|
144
|
+
task,
|
|
145
|
+
model: rec.model ?? 'unknown',
|
|
146
|
+
status: rec.status,
|
|
147
|
+
stalled,
|
|
148
|
+
inputTokens,
|
|
149
|
+
outputTokens,
|
|
150
|
+
cost,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
roster,
|
|
156
|
+
stalledAgentCount: countStalledAgents(sorted, now),
|
|
157
|
+
totalInputTokens: hasUsage ? totalInputTokens : null,
|
|
158
|
+
totalOutputTokens: hasUsage ? totalOutputTokens : null,
|
|
159
|
+
totalCost: hasUsage ? totalCost : null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Read-model factory
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/** Minimal read-model interface matching the existing UiReadModel shape. */
|
|
168
|
+
export interface CockpitRosterReadModel {
|
|
169
|
+
getSnapshot(): CockpitRosterSnapshot;
|
|
170
|
+
/**
|
|
171
|
+
* Notify all subscribers that the roster has changed and the cockpit panel
|
|
172
|
+
* should re-render. Wire this to an AgentManager event feed so live agent
|
|
173
|
+
* state changes propagate:
|
|
174
|
+
*
|
|
175
|
+
* const roster = createCockpitRosterReadModel(agentManager);
|
|
176
|
+
* agentEvents.subscribe(() => roster.markDirty());
|
|
177
|
+
*
|
|
178
|
+
* For static/test fixtures the implementation is a no-op.
|
|
179
|
+
*/
|
|
180
|
+
markDirty(): void;
|
|
181
|
+
/**
|
|
182
|
+
* Subscribe to changes. The listener is called whenever markDirty() is
|
|
183
|
+
* invoked. Returns an unsubscribe function.
|
|
184
|
+
*
|
|
185
|
+
* For static/test fixtures, returns a no-op unsubscribe.
|
|
186
|
+
*/
|
|
187
|
+
subscribe(listener: () => void): () => void;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Create a live CockpitRosterReadModel backed by an AgentManager.
|
|
192
|
+
*
|
|
193
|
+
* The returned read-model re-derives its snapshot on every getSnapshot() call
|
|
194
|
+
* so it always reflects the current agent state without needing an event bus.
|
|
195
|
+
* Callers that want reactive updates should poll or wire in their own
|
|
196
|
+
* event-driven markDirty() path.
|
|
197
|
+
*/
|
|
198
|
+
export function createCockpitRosterReadModel(
|
|
199
|
+
agentManager: CockpitRosterAgentManager,
|
|
200
|
+
): CockpitRosterReadModel {
|
|
201
|
+
const listeners = new Set<() => void>();
|
|
202
|
+
|
|
203
|
+
function markDirty(): void {
|
|
204
|
+
for (const listener of listeners) {
|
|
205
|
+
listener();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
getSnapshot(): CockpitRosterSnapshot {
|
|
211
|
+
return buildCockpitRosterSnapshot(agentManager.list());
|
|
212
|
+
},
|
|
213
|
+
markDirty,
|
|
214
|
+
subscribe(listener: () => void): () => void {
|
|
215
|
+
listeners.add(listener);
|
|
216
|
+
return () => listeners.delete(listener);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a static CockpitRosterReadModel for testing.
|
|
223
|
+
*/
|
|
224
|
+
export function createStaticCockpitRosterReadModel(
|
|
225
|
+
snapshot: CockpitRosterSnapshot,
|
|
226
|
+
): CockpitRosterReadModel {
|
|
227
|
+
return {
|
|
228
|
+
getSnapshot: () => snapshot,
|
|
229
|
+
markDirty: () => {},
|
|
230
|
+
subscribe: () => () => {},
|
|
231
|
+
};
|
|
232
|
+
}
|
package/src/panels/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ export { SecurityPanel } from './security-panel.ts';
|
|
|
40
40
|
export { MarketplacePanel } from './marketplace-panel.ts';
|
|
41
41
|
export { SandboxPanel } from './sandbox-panel.ts';
|
|
42
42
|
export { ApprovalPanel } from './approval-panel.ts';
|
|
43
|
-
export {
|
|
43
|
+
export { KnowledgeGraphPanel } from './knowledge-graph-panel.ts';
|
|
44
44
|
export { SystemMessagesPanel } from './system-messages-panel.ts';
|
|
45
45
|
export { PanelListPanel } from './panel-list-panel.ts';
|
|
46
46
|
export type { SystemMessageEntry, SystemMessagePriority } from './system-messages-panel.ts';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KnowledgeGraphPanel — SDK knowledge graph front-door.
|
|
3
|
+
*
|
|
4
|
+
* TASK-040: The 'knowledge' panel id is repointed here (the SDK graph), fixing
|
|
5
|
+
* the naming inversion where the former panel named 'Knowledge' was actually
|
|
6
|
+
* rendering memory records.
|
|
7
|
+
*
|
|
8
|
+
* This panel is a thin information surface that explains the graph's capabilities
|
|
9
|
+
* and routes the user to the /knowledge command suite for ingest/RAG operations.
|
|
10
|
+
* The full graph UI is command-driven (/knowledge ask, ingest-url, list, search…).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Line } from '../types/grid.ts';
|
|
14
|
+
import { BasePanel } from './base-panel.ts';
|
|
15
|
+
import {
|
|
16
|
+
buildBodyText,
|
|
17
|
+
buildGuidanceLine,
|
|
18
|
+
buildPanelLine,
|
|
19
|
+
buildPanelWorkspace,
|
|
20
|
+
DEFAULT_PANEL_PALETTE,
|
|
21
|
+
} from './polish.ts';
|
|
22
|
+
|
|
23
|
+
const C = {
|
|
24
|
+
...DEFAULT_PANEL_PALETTE,
|
|
25
|
+
header: '#94a3b8',
|
|
26
|
+
headerBg: '#1e293b',
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export class KnowledgeGraphPanel extends BasePanel {
|
|
30
|
+
constructor() {
|
|
31
|
+
super('knowledge', 'Knowledge', 'K', 'agent');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
handleInput(_key: string): boolean {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
render(width: number, height: number): Line[] {
|
|
39
|
+
const sections = [
|
|
40
|
+
{
|
|
41
|
+
title: 'SDK Knowledge Graph',
|
|
42
|
+
lines: [
|
|
43
|
+
...buildBodyText(
|
|
44
|
+
width,
|
|
45
|
+
'The knowledge graph stores ingested URLs, bookmarks, and structured facts as nodes and edges. ' +
|
|
46
|
+
'Use /knowledge commands to ingest sources, search the graph, and build task-context packets.',
|
|
47
|
+
C,
|
|
48
|
+
C.value,
|
|
49
|
+
),
|
|
50
|
+
buildPanelLine(width, [['', C.dim]]),
|
|
51
|
+
buildGuidanceLine(width, '/knowledge status', 'check the graph status and source counts', C),
|
|
52
|
+
buildGuidanceLine(width, '/knowledge ask <query>', 'ask a question against the ingested knowledge', C),
|
|
53
|
+
buildGuidanceLine(width, '/knowledge ingest-url <url>', 'ingest a URL as a knowledge source', C),
|
|
54
|
+
buildGuidanceLine(width, '/knowledge list', 'list ingested sources or graph nodes', C),
|
|
55
|
+
buildGuidanceLine(width, '/knowledge search <query>', 'search the graph for nodes and sources', C),
|
|
56
|
+
buildGuidanceLine(width, '/knowledge packet <task>', 'build a compact prompt packet for a task', C),
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
title: 'Project Memory',
|
|
61
|
+
lines: [
|
|
62
|
+
...buildBodyText(
|
|
63
|
+
width,
|
|
64
|
+
'For durable decisions, risks, runbooks, incidents, and architecture records, use the Memory panel ' +
|
|
65
|
+
'or the /recall command surface. Durable memory is a sub-namespace of the knowledge graph.',
|
|
66
|
+
C,
|
|
67
|
+
C.dim,
|
|
68
|
+
),
|
|
69
|
+
buildPanelLine(width, [['', C.dim]]),
|
|
70
|
+
buildGuidanceLine(width, '/recall add <class> <summary>', 'capture a new memory record', C),
|
|
71
|
+
buildGuidanceLine(width, '/recall queue', 'show the operator review queue', C),
|
|
72
|
+
buildGuidanceLine(width, '/project-memory (pmem)', 'project-memory alias for /recall front-door', C),
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
return buildPanelWorkspace(width, height, {
|
|
78
|
+
title: 'Knowledge Graph',
|
|
79
|
+
intro: 'Ingested sources, graph nodes, and the durable memory bridge.',
|
|
80
|
+
sections,
|
|
81
|
+
palette: C,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|