@kontextmind/kxm 0.6.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/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Box,
|
|
3
|
+
Container,
|
|
4
|
+
HStack,
|
|
5
|
+
Key,
|
|
6
|
+
matchesKey,
|
|
7
|
+
ProcessTerminal,
|
|
8
|
+
ScrollView,
|
|
9
|
+
stripTerminalSequences,
|
|
10
|
+
Text,
|
|
11
|
+
TruncatedText,
|
|
12
|
+
TuiAltScreen,
|
|
13
|
+
VStack,
|
|
14
|
+
type Component,
|
|
15
|
+
type Terminal,
|
|
16
|
+
type TUI,
|
|
17
|
+
} from "@earendil-works/pi-tui";
|
|
18
|
+
import type { AgentRecord } from "./protocol.ts";
|
|
19
|
+
import {
|
|
20
|
+
loadLocalMeshSnapshot,
|
|
21
|
+
summarizeMeshRun,
|
|
22
|
+
type LocalMeshSnapshot,
|
|
23
|
+
type MeshTuiPidClaim,
|
|
24
|
+
type MeshTuiPlan,
|
|
25
|
+
type MeshTuiRun,
|
|
26
|
+
type MeshTuiRunStage,
|
|
27
|
+
} from "./local-snapshot.ts";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
loadLocalMeshSnapshot,
|
|
31
|
+
summarizeMeshRun,
|
|
32
|
+
type LocalMeshSnapshot,
|
|
33
|
+
type MeshTuiPidClaim,
|
|
34
|
+
type MeshTuiPlan,
|
|
35
|
+
type MeshTuiRun,
|
|
36
|
+
type MeshTuiRunStage,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const MESH_TUI_PANELS = ["agents", "tasks", "workflows", "plans", "inbox", "procs", "spend"] as const;
|
|
40
|
+
export type MeshTuiPanel = typeof MESH_TUI_PANELS[number];
|
|
41
|
+
|
|
42
|
+
export const MESH_TUI_TAB_LABELS: Record<MeshTuiPanel, string> = {
|
|
43
|
+
agents: "Agents",
|
|
44
|
+
tasks: "Tasks",
|
|
45
|
+
workflows: "Workflows",
|
|
46
|
+
plans: "Plans",
|
|
47
|
+
inbox: "Inbox",
|
|
48
|
+
procs: "Procs",
|
|
49
|
+
spend: "Spend",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface DashboardAction {
|
|
53
|
+
action: "approve" | "reject" | "degrade" | "signal" | "cancel";
|
|
54
|
+
at: string;
|
|
55
|
+
targetId?: string | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface MeshTuiView {
|
|
59
|
+
tab: MeshTuiPanel;
|
|
60
|
+
selected: number;
|
|
61
|
+
pane: "list" | "detail";
|
|
62
|
+
help: boolean;
|
|
63
|
+
statusMessage?: string | undefined;
|
|
64
|
+
lastAction?: DashboardAction | undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function defaultMeshTuiView(screen?: MeshTuiPanel): MeshTuiView {
|
|
68
|
+
return { tab: screen ?? "agents", selected: 0, pane: "list", help: false };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function applyMeshTuiKey(view: MeshTuiView, key: string, itemCount = 0): MeshTuiView | "quit" {
|
|
72
|
+
if (matchesKey(key, "q") || matchesKey(key, Key.ctrl("c"))) return "quit";
|
|
73
|
+
if (matchesKey(key, Key.escape)) return view.help ? { ...view, help: false } : "quit";
|
|
74
|
+
if (matchesKey(key, "h") || matchesKey(key, "?")) return { ...view, help: !view.help };
|
|
75
|
+
const byNumber: Record<string, MeshTuiPanel> = {
|
|
76
|
+
"1": "agents",
|
|
77
|
+
"2": "tasks",
|
|
78
|
+
"3": "workflows",
|
|
79
|
+
"4": "plans",
|
|
80
|
+
"5": "inbox",
|
|
81
|
+
"6": "procs",
|
|
82
|
+
"7": "spend",
|
|
83
|
+
};
|
|
84
|
+
const tab = byNumber[key];
|
|
85
|
+
if (tab) return { tab, selected: 0, pane: "list", help: false };
|
|
86
|
+
const index = MESH_TUI_PANELS.indexOf(view.tab);
|
|
87
|
+
if (matchesKey(key, Key.tab) || matchesKey(key, "]")) {
|
|
88
|
+
return { tab: MESH_TUI_PANELS[(index + 1) % MESH_TUI_PANELS.length]!, selected: 0, pane: "list", help: false };
|
|
89
|
+
}
|
|
90
|
+
if (matchesKey(key, Key.shift("tab")) || matchesKey(key, "[")) {
|
|
91
|
+
return { tab: MESH_TUI_PANELS[(index - 1 + MESH_TUI_PANELS.length) % MESH_TUI_PANELS.length]!, selected: 0, pane: "list", help: false };
|
|
92
|
+
}
|
|
93
|
+
if (matchesKey(key, Key.left)) return { ...view, pane: "list", help: false };
|
|
94
|
+
if (matchesKey(key, Key.right) || matchesKey(key, Key.enter) || matchesKey(key, Key.space)) {
|
|
95
|
+
return { ...view, pane: "detail", help: false };
|
|
96
|
+
}
|
|
97
|
+
if (matchesKey(key, Key.up)) {
|
|
98
|
+
return { ...view, selected: Math.max(0, view.selected - 1), pane: "list", help: false };
|
|
99
|
+
}
|
|
100
|
+
if (matchesKey(key, Key.down)) {
|
|
101
|
+
return { ...view, selected: Math.min(Math.max(0, itemCount - 1), view.selected + 1), pane: "list", help: false };
|
|
102
|
+
}
|
|
103
|
+
if (matchesKey(key, "a")) {
|
|
104
|
+
return {
|
|
105
|
+
...view,
|
|
106
|
+
statusMessage: `[APPROVE] Queued approval for ${view.tab} item #${view.selected + 1}`,
|
|
107
|
+
lastAction: { action: "approve", at: new Date().toISOString() },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (matchesKey(key, "r")) {
|
|
111
|
+
return {
|
|
112
|
+
...view,
|
|
113
|
+
statusMessage: `[REJECT] Marked ${view.tab} item #${view.selected + 1} for rework`,
|
|
114
|
+
lastAction: { action: "reject", at: new Date().toISOString() },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (matchesKey(key, "d")) {
|
|
118
|
+
return {
|
|
119
|
+
...view,
|
|
120
|
+
statusMessage: `[DEGRADE] Degraded ${view.tab} item #${view.selected + 1} to operator`,
|
|
121
|
+
lastAction: { action: "degrade", at: new Date().toISOString() },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (matchesKey(key, "s")) {
|
|
125
|
+
return {
|
|
126
|
+
...view,
|
|
127
|
+
statusMessage: `[SIGNAL] Signal dispatched for ${view.tab} item #${view.selected + 1}`,
|
|
128
|
+
lastAction: { action: "signal", at: new Date().toISOString() },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (matchesKey(key, "c")) {
|
|
132
|
+
return {
|
|
133
|
+
...view,
|
|
134
|
+
statusMessage: `[CANCEL] Cancellation requested for ${view.tab} item #${view.selected + 1}`,
|
|
135
|
+
lastAction: { action: "cancel", at: new Date().toISOString() },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return view;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface MeshTuiSnapshot extends LocalMeshSnapshot {
|
|
142
|
+
serverUrl: string;
|
|
143
|
+
healthOk: boolean;
|
|
144
|
+
readyOk: boolean;
|
|
145
|
+
storage?: string;
|
|
146
|
+
onlineCount: number;
|
|
147
|
+
transport: "sse" | "snapshot";
|
|
148
|
+
metadataMode?: "ops" | "legacy";
|
|
149
|
+
error?: string;
|
|
150
|
+
fetchedAt: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function age(iso: string, now: number): string {
|
|
154
|
+
const ms = now - Date.parse(iso);
|
|
155
|
+
if (!Number.isFinite(ms) || ms < 0) return "?";
|
|
156
|
+
const s = Math.floor(ms / 1000);
|
|
157
|
+
if (s < 60) return `${s}s`;
|
|
158
|
+
const m = Math.floor(s / 60);
|
|
159
|
+
if (m < 60) return `${m}m`;
|
|
160
|
+
const h = Math.floor(m / 60);
|
|
161
|
+
if (h < 24) return `${h}h`;
|
|
162
|
+
return `${Math.floor(h / 24)}d`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function pad(value: string, width: number): string {
|
|
166
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
167
|
+
if (text.length <= width) return text.padEnd(width);
|
|
168
|
+
return `${text.slice(0, Math.max(0, width - 1))}…`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function progressBar(done: number, total: number, width = 10): string {
|
|
172
|
+
if (total <= 0) return "-".repeat(width);
|
|
173
|
+
const filled = Math.max(0, Math.min(width, Math.round((done / total) * width)));
|
|
174
|
+
return "#".repeat(filled) + "-".repeat(width - filled);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function stageMark(status: string, theme: MeshTuiTheme): string {
|
|
178
|
+
if (status === "passed") return theme.success("[x]");
|
|
179
|
+
if (status === "failed") return theme.error("[F]");
|
|
180
|
+
if (status === "warning") return theme.warning("[!]");
|
|
181
|
+
if (status === "in_progress") return theme.warning("[~]");
|
|
182
|
+
if (status === "waiting") return theme.dim("[.]");
|
|
183
|
+
return theme.dim("[ ]");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
type MeshTuiStyle = (text: string) => string;
|
|
187
|
+
|
|
188
|
+
interface MeshTuiTheme {
|
|
189
|
+
accent: MeshTuiStyle;
|
|
190
|
+
dim: MeshTuiStyle;
|
|
191
|
+
error: MeshTuiStyle;
|
|
192
|
+
success: MeshTuiStyle;
|
|
193
|
+
warning: MeshTuiStyle;
|
|
194
|
+
headerBg: MeshTuiStyle;
|
|
195
|
+
panelBg: MeshTuiStyle;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function meshTuiTheme(color: boolean): MeshTuiTheme {
|
|
199
|
+
const ansi = (code: string): MeshTuiStyle => color ? (text) => `\u001b[${code}m${text}\u001b[0m` : (text) => text;
|
|
200
|
+
return {
|
|
201
|
+
accent: ansi("1;36"),
|
|
202
|
+
dim: ansi("2"),
|
|
203
|
+
error: ansi("1;31"),
|
|
204
|
+
success: ansi("1;32"),
|
|
205
|
+
warning: ansi("1;33"),
|
|
206
|
+
headerBg: ansi("1;97;44"),
|
|
207
|
+
panelBg: ansi("48;5;236"),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function visibleAgents(snapshot: MeshTuiSnapshot): AgentRecord[] {
|
|
212
|
+
return snapshot.agents.filter((agent) => agent.model !== "kxm-tui");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function panelMetric(snapshot: MeshTuiSnapshot, panel: MeshTuiPanel): string {
|
|
216
|
+
if (panel === "agents") {
|
|
217
|
+
const agents = visibleAgents(snapshot);
|
|
218
|
+
return `${agents.filter((agent) => agent.online).length}/${agents.length}`;
|
|
219
|
+
}
|
|
220
|
+
if (panel === "tasks") {
|
|
221
|
+
const active = snapshot.runs.filter((run) => run.status === "running" || run.status === "waiting");
|
|
222
|
+
const total = active.reduce((sum, run) => sum + (run.progress?.total ?? 0), 0);
|
|
223
|
+
const done = active.reduce((sum, run) => sum + (run.progress?.done ?? 0), 0);
|
|
224
|
+
return total > 0 ? `${done}/${total}` : String(active.length);
|
|
225
|
+
}
|
|
226
|
+
if (panel === "workflows") return snapshot.runs.length === snapshot.runTotal ? String(snapshot.runTotal) : `${snapshot.runs.length}/${snapshot.runTotal}`;
|
|
227
|
+
if (panel === "plans") return String(snapshot.plans.length);
|
|
228
|
+
if (panel === "inbox") return snapshot.openMessages.length === snapshot.openMessageTotal ? String(snapshot.openMessageTotal) : `${snapshot.openMessages.length}/${snapshot.openMessageTotal}`;
|
|
229
|
+
if (panel === "spend") {
|
|
230
|
+
const spend = snapshot.spend ?? [];
|
|
231
|
+
let totalCost = 0;
|
|
232
|
+
let hasMetered = false;
|
|
233
|
+
for (const item of spend) {
|
|
234
|
+
const r = item.routing as unknown as Record<string, unknown>;
|
|
235
|
+
const costBasis = (r.costBasis as string | undefined) ?? (typeof r.costUsd === "number" ? "metered" : "unmetered");
|
|
236
|
+
if (costBasis === "metered" && typeof r.costUsd === "number") {
|
|
237
|
+
totalCost += r.costUsd;
|
|
238
|
+
hasMetered = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return hasMetered ? `$${totalCost.toFixed(2)}` : String(spend.length);
|
|
242
|
+
}
|
|
243
|
+
return `${snapshot.pids.filter((claim) => claim.live).length}/${snapshot.pids.length}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function panelRows(snapshot: MeshTuiSnapshot, panel: MeshTuiPanel, theme: MeshTuiTheme): string[] {
|
|
247
|
+
const now = Date.parse(snapshot.fetchedAt);
|
|
248
|
+
if (panel === "agents") {
|
|
249
|
+
const agents = visibleAgents(snapshot);
|
|
250
|
+
return agents.length === 0 ? [] : [
|
|
251
|
+
theme.dim(`${pad("name", 14)} ${pad("on", 3)} ${pad("model", 24)} ${pad("seen", 4)} purpose`),
|
|
252
|
+
...agents.map((agent) => (
|
|
253
|
+
`${pad(agent.name, 14)} ${agent.online ? theme.success(pad("yes", 3)) : theme.dim(pad("no", 3))} ${pad(agent.model ?? "-", 24)} ${pad(age(agent.lastSeenAt, now), 4)} ${agent.purpose}`
|
|
254
|
+
)),
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
if (panel === "tasks") {
|
|
258
|
+
const active = snapshot.runs.filter((run) => run.status === "running" || run.status === "waiting");
|
|
259
|
+
if (active.length === 0) return [];
|
|
260
|
+
const rows: string[] = [];
|
|
261
|
+
for (const run of active) {
|
|
262
|
+
const progress = run.progress;
|
|
263
|
+
const bar = progress ? progressBar(progress.done, progress.total) : "----------";
|
|
264
|
+
const counts = progress ? `${progress.done}/${progress.total}` : "-";
|
|
265
|
+
rows.push(`${pad(run.definitionId, 16)} ${pad(run.status, 9)} ${bar} ${pad(counts, 5)} ${run.currentStage ?? "-"} ${run.id}`);
|
|
266
|
+
for (const stage of run.stages ?? []) {
|
|
267
|
+
const attempts = stage.attempts && stage.attempts > 1 ? ` attempt ${stage.attempts}` : "";
|
|
268
|
+
rows.push(` ${stageMark(stage.status, theme)} ${pad(stage.label ?? stage.id, 22)} ${stage.status}${attempts}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return rows;
|
|
272
|
+
}
|
|
273
|
+
if (panel === "workflows") {
|
|
274
|
+
return [
|
|
275
|
+
...(snapshot.runs.length === 0 ? [] : [theme.dim(`${pad("state", 10)} ${pad("workflow", 16)} ${pad("stage", 14)} ${pad("done", 5)} id`)]),
|
|
276
|
+
...snapshot.runs.map((run) => {
|
|
277
|
+
const counts = run.progress ? `${run.progress.done}/${run.progress.total}` : "-";
|
|
278
|
+
return `${pad(run.status, 10)} ${pad(run.definitionId, 16)} ${pad(run.currentStage ?? "-", 14)} ${pad(counts, 5)} ${run.id}`;
|
|
279
|
+
}),
|
|
280
|
+
...(snapshot.runTotal > snapshot.runs.length ? [theme.dim(`… +${snapshot.runTotal - snapshot.runs.length} more`)] : []),
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
if (panel === "plans") {
|
|
284
|
+
return snapshot.plans.length === 0 ? [] : [
|
|
285
|
+
theme.dim(`${pad("age", 4)} ${pad("run", 12)} ${pad("stage", 14)} plan`),
|
|
286
|
+
...snapshot.plans.map((plan) => (
|
|
287
|
+
`${pad(age(plan.createdAt, now), 4)} ${pad(plan.runId, 12)} ${pad(plan.stageId ?? "-", 14)} ${plan.summary}`
|
|
288
|
+
)),
|
|
289
|
+
];
|
|
290
|
+
}
|
|
291
|
+
if (panel === "inbox") {
|
|
292
|
+
return snapshot.openMessages.length === 0 ? [] : [
|
|
293
|
+
theme.dim(`${pad("state", 9)} ${pad("from", 12)} ${pad("to", 12)} ${pad("mode", 8)} ${pad("age", 4)} id`),
|
|
294
|
+
...snapshot.openMessages.map((message) => (
|
|
295
|
+
`${theme.warning(pad(message.status, 9))} ${pad(message.fromName, 12)} ${pad(message.toName, 12)} ${pad(message.delivery, 8)} ${pad(age(message.createdAt, now), 4)} ${message.id}`
|
|
296
|
+
)),
|
|
297
|
+
...(snapshot.openMessageTotal > snapshot.openMessages.length ? [theme.dim(`… +${snapshot.openMessageTotal - snapshot.openMessages.length} more`)] : []),
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
if (panel === "spend") {
|
|
301
|
+
const spend = snapshot.spend ?? [];
|
|
302
|
+
if (spend.length === 0) return [];
|
|
303
|
+
return [
|
|
304
|
+
theme.dim(`${pad("age", 4)} ${pad("harness/model", 22)} ${pad("cost", 8)} ${pad("tokens", 14)} outcome`),
|
|
305
|
+
...spend.slice().reverse().map((entry) => {
|
|
306
|
+
const r = entry.routing as unknown as Record<string, unknown>;
|
|
307
|
+
const model = (r.effectiveModel as string | undefined) ?? (r.requestedModel as string | undefined) ?? "-";
|
|
308
|
+
const harness = (r.harness as string | undefined) ?? "-";
|
|
309
|
+
const route = `${harness}/${model}`;
|
|
310
|
+
const cost = typeof r.costUsd === "number" ? `$${r.costUsd.toFixed(3)}` : ((r.costBasis as string) ?? "-");
|
|
311
|
+
const tokensIn = typeof r.tokensIn === "number" ? r.tokensIn : 0;
|
|
312
|
+
const tokensOut = typeof r.tokensOut === "number" ? r.tokensOut : 0;
|
|
313
|
+
const tokens = `${tokensIn}+${tokensOut}`;
|
|
314
|
+
const outcome = (r.verifierOutcome as string | undefined) ?? (r.finalOutcome as string | undefined) ?? "-";
|
|
315
|
+
return `${pad(age(entry.recordedAt, now), 4)} ${pad(route, 22)} ${pad(cost, 8)} ${pad(tokens, 14)} ${outcome}`;
|
|
316
|
+
}),
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
return snapshot.pids.map((claim) => `${claim.live ? theme.success("live") : theme.error("dead")} ${pad(claim.role ?? "-", 8)} pid=${claim.pid ?? "-"} ${claim.file}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function tabItemCount(snapshot: MeshTuiSnapshot, tab: MeshTuiPanel): number {
|
|
323
|
+
if (tab === "agents") return visibleAgents(snapshot).length;
|
|
324
|
+
if (tab === "tasks") return snapshot.runs.filter((run) => run.status === "running" || run.status === "waiting").length;
|
|
325
|
+
if (tab === "workflows") return snapshot.runs.length;
|
|
326
|
+
if (tab === "plans") return snapshot.plans.length;
|
|
327
|
+
if (tab === "inbox") return snapshot.openMessages.length;
|
|
328
|
+
if (tab === "spend") return snapshot.spend?.length ?? 0;
|
|
329
|
+
return snapshot.pids.length;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function tabSplits(tab: MeshTuiPanel): boolean {
|
|
333
|
+
return tab !== "procs";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function cursor(theme: MeshTuiTheme, selected: boolean, pane: "list" | "detail", activePane: "list" | "detail"): string {
|
|
337
|
+
if (!selected) return " ";
|
|
338
|
+
return activePane === pane ? theme.accent("› ") : theme.dim("· ");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function listLines(snapshot: MeshTuiSnapshot, view: MeshTuiView, theme: MeshTuiTheme): string[] {
|
|
342
|
+
const now = Date.parse(snapshot.fetchedAt);
|
|
343
|
+
const mark = (index: number) => cursor(theme, index === view.selected, "list", view.pane);
|
|
344
|
+
if (view.tab === "agents") {
|
|
345
|
+
return visibleAgents(snapshot).map((agent, index) => (
|
|
346
|
+
`${mark(index)}${pad(agent.name, 14)} ${agent.online ? theme.success(pad("yes", 3)) : theme.dim(pad("no", 3))} ${pad(agent.model ?? "-", 18)} ${pad(age(agent.lastSeenAt, now), 4)}`
|
|
347
|
+
));
|
|
348
|
+
}
|
|
349
|
+
if (view.tab === "tasks" || view.tab === "workflows") {
|
|
350
|
+
const runs = view.tab === "tasks"
|
|
351
|
+
? snapshot.runs.filter((run) => run.status === "running" || run.status === "waiting")
|
|
352
|
+
: snapshot.runs;
|
|
353
|
+
return runs.map((run, index) => {
|
|
354
|
+
const counts = run.progress ? `${run.progress.done}/${run.progress.total}` : "-";
|
|
355
|
+
const bar = run.progress ? progressBar(run.progress.done, run.progress.total, 8) : "--------";
|
|
356
|
+
return `${mark(index)}${pad(run.definitionId, 14)} ${pad(run.status, 8)} ${bar} ${pad(counts, 5)} ${run.id}`;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (view.tab === "plans") {
|
|
360
|
+
return snapshot.plans.map((plan, index) => (
|
|
361
|
+
`${mark(index)}${pad(age(plan.createdAt, now), 4)} ${pad(plan.stageId ?? "-", 12)} ${plan.summary}`
|
|
362
|
+
));
|
|
363
|
+
}
|
|
364
|
+
if (view.tab === "inbox") {
|
|
365
|
+
return snapshot.openMessages.map((message, index) => (
|
|
366
|
+
`${mark(index)}${pad(message.status, 9)} ${pad(message.fromName, 10)} → ${pad(message.toName, 10)} ${pad(age(message.createdAt, now), 4)}`
|
|
367
|
+
));
|
|
368
|
+
}
|
|
369
|
+
if (view.tab === "spend") {
|
|
370
|
+
const spend = (snapshot.spend ?? []).slice().reverse();
|
|
371
|
+
return spend.map((entry, index) => {
|
|
372
|
+
const r = entry.routing as unknown as Record<string, unknown>;
|
|
373
|
+
const model = (r.effectiveModel as string | undefined) ?? (r.requestedModel as string | undefined) ?? "-";
|
|
374
|
+
const harness = (r.harness as string | undefined) ?? "-";
|
|
375
|
+
const route = `${harness}/${model}`;
|
|
376
|
+
const cost = typeof r.costUsd === "number" ? `$${r.costUsd.toFixed(3)}` : ((r.costBasis as string) ?? "-");
|
|
377
|
+
const outcome = (r.verifierOutcome as string | undefined) ?? (r.finalOutcome as string | undefined) ?? "-";
|
|
378
|
+
return `${mark(index)}${pad(age(entry.recordedAt, now), 4)} ${pad(route, 16)} ${pad(cost, 8)} ${outcome}`;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return snapshot.pids.map((claim, index) => (
|
|
382
|
+
`${mark(index)}${claim.live ? theme.success("live") : theme.error("dead")} ${pad(claim.role ?? "-", 8)} pid=${claim.pid ?? "-"}`
|
|
383
|
+
));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function detailLines(snapshot: MeshTuiSnapshot, view: MeshTuiView, theme: MeshTuiTheme): string[] {
|
|
387
|
+
const now = Date.parse(snapshot.fetchedAt);
|
|
388
|
+
if (view.tab === "agents") {
|
|
389
|
+
const agent = visibleAgents(snapshot)[view.selected];
|
|
390
|
+
if (!agent) return [theme.dim("No agent selected")];
|
|
391
|
+
const related = snapshot.openMessages.filter((message) => message.fromName === agent.name || message.toName === agent.name);
|
|
392
|
+
return [
|
|
393
|
+
theme.accent(agent.name),
|
|
394
|
+
`${agent.online ? theme.success("online") : theme.dim("offline")} ${agent.model ?? "-"}`,
|
|
395
|
+
agent.purpose,
|
|
396
|
+
`seen ${age(agent.lastSeenAt, now)} ago`,
|
|
397
|
+
"",
|
|
398
|
+
theme.dim("Open work"),
|
|
399
|
+
...(related.length === 0 ? [theme.dim("none")] : related.map((message) => `${message.status} ${message.fromName} → ${message.toName} ${age(message.createdAt, now)}`)),
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
if (view.tab === "tasks" || view.tab === "workflows") {
|
|
403
|
+
const runs = view.tab === "tasks"
|
|
404
|
+
? snapshot.runs.filter((run) => run.status === "running" || run.status === "waiting")
|
|
405
|
+
: snapshot.runs;
|
|
406
|
+
const run = runs[view.selected];
|
|
407
|
+
if (!run) return [theme.dim("No run selected")];
|
|
408
|
+
const counts = run.progress ? `${run.progress.done}/${run.progress.total}` : "-";
|
|
409
|
+
return [
|
|
410
|
+
theme.accent(run.definitionId),
|
|
411
|
+
`${run.status} ${counts} ${run.currentStage ?? "-"}`,
|
|
412
|
+
run.id,
|
|
413
|
+
run.targetAgentName ? `owner ${run.targetAgentName}` : "",
|
|
414
|
+
"",
|
|
415
|
+
...(run.stages ?? []).map((stage) => {
|
|
416
|
+
const attempts = stage.attempts && stage.attempts > 1 ? ` attempt ${stage.attempts}` : "";
|
|
417
|
+
return `${stageMark(stage.status, theme)} ${pad(stage.label ?? stage.id, 22)} ${stage.status}${attempts}`;
|
|
418
|
+
}),
|
|
419
|
+
].filter((line) => line !== "");
|
|
420
|
+
}
|
|
421
|
+
if (view.tab === "plans") {
|
|
422
|
+
const plan = snapshot.plans[view.selected];
|
|
423
|
+
if (!plan) return [theme.dim("No plan selected")];
|
|
424
|
+
const run = snapshot.runs.find((candidate) => candidate.id === plan.runId);
|
|
425
|
+
return [
|
|
426
|
+
theme.accent("Plan"),
|
|
427
|
+
plan.summary,
|
|
428
|
+
`run ${plan.runId}`,
|
|
429
|
+
plan.stageId ? `stage ${plan.stageId}` : "",
|
|
430
|
+
`${plan.severity ?? "info"} ${age(plan.createdAt, now)} ago`,
|
|
431
|
+
"",
|
|
432
|
+
...(run?.stages ?? []).map((stage) => `${stageMark(stage.status, theme)} ${stage.label ?? stage.id}`),
|
|
433
|
+
].filter((line) => line !== "");
|
|
434
|
+
}
|
|
435
|
+
if (view.tab === "inbox") {
|
|
436
|
+
const message = snapshot.openMessages[view.selected];
|
|
437
|
+
if (!message) return [theme.dim("No message selected")];
|
|
438
|
+
return [
|
|
439
|
+
theme.accent(message.id),
|
|
440
|
+
`${message.status} ${message.delivery}`,
|
|
441
|
+
`${message.fromName} → ${message.toName}`,
|
|
442
|
+
age(message.createdAt, now),
|
|
443
|
+
message.correlationId ? `corr ${message.correlationId}` : theme.dim("bodies never shown"),
|
|
444
|
+
];
|
|
445
|
+
}
|
|
446
|
+
if (view.tab === "spend") {
|
|
447
|
+
const spend = (snapshot.spend ?? []).slice().reverse();
|
|
448
|
+
const entry = spend[view.selected];
|
|
449
|
+
if (!entry) return [theme.dim("No spend record selected")];
|
|
450
|
+
const r = entry.routing as unknown as Record<string, unknown>;
|
|
451
|
+
const model = (r.effectiveModel as string | undefined) ?? (r.requestedModel as string | undefined) ?? "-";
|
|
452
|
+
const harness = (r.harness as string | undefined) ?? "-";
|
|
453
|
+
const cost = typeof r.costUsd === "number" ? `$${r.costUsd.toFixed(4)}` : ((r.costBasis as string) ?? "-");
|
|
454
|
+
const tokensIn = typeof r.tokensIn === "number" ? r.tokensIn : 0;
|
|
455
|
+
const tokensOut = typeof r.tokensOut === "number" ? r.tokensOut : 0;
|
|
456
|
+
const cacheRead = typeof r.cacheReadTokens === "number" ? r.cacheReadTokens : 0;
|
|
457
|
+
const outcome = (r.verifierOutcome as string | undefined) ?? (r.finalOutcome as string | undefined) ?? "-";
|
|
458
|
+
return [
|
|
459
|
+
theme.accent(`${harness}/${model}`),
|
|
460
|
+
`cost ${cost} (${(r.costBasis as string) ?? "unknown"})`,
|
|
461
|
+
`tokens: in=${tokensIn} out=${tokensOut} cacheRead=${cacheRead}`,
|
|
462
|
+
`outcome: ${outcome}`,
|
|
463
|
+
`recorded ${age(entry.recordedAt, now)} ago (${entry.recordedAt})`,
|
|
464
|
+
...((r.runId || r.workflowRunId) ? [`run ${((r.runId ?? r.workflowRunId) as string)}`] : []),
|
|
465
|
+
...((r.stepId || r.stageId) ? [`stage ${((r.stepId ?? r.stageId) as string)}`] : []),
|
|
466
|
+
];
|
|
467
|
+
}
|
|
468
|
+
const claim = snapshot.pids[view.selected];
|
|
469
|
+
if (!claim) return [theme.dim("No process selected")];
|
|
470
|
+
return [
|
|
471
|
+
claim.live ? theme.success("live") : theme.error("dead"),
|
|
472
|
+
claim.role ?? "-",
|
|
473
|
+
`pid ${claim.pid ?? "-"}`,
|
|
474
|
+
claim.file,
|
|
475
|
+
];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function renderTabBar(snapshot: MeshTuiSnapshot, view: MeshTuiView, theme: MeshTuiTheme): string {
|
|
479
|
+
return MESH_TUI_PANELS.map((panel, index) => {
|
|
480
|
+
const label = `${index + 1} ${MESH_TUI_TAB_LABELS[panel]} ${panelMetric(snapshot, panel)}`;
|
|
481
|
+
return view.tab === panel ? theme.accent(`[${label}]`) : theme.dim(` ${label} `);
|
|
482
|
+
}).join(" ");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function dataPanel(title: string, metric: string, rows: string[], emptyText: string, theme: MeshTuiTheme): Component {
|
|
486
|
+
const panel = new Box(1, 0, theme.panelBg);
|
|
487
|
+
panel.addChild(new Text(`${theme.accent(`▌ ${title}`)} ${theme.dim(metric)}`, 0, 0));
|
|
488
|
+
const table = new Container();
|
|
489
|
+
if (rows.length === 0) {
|
|
490
|
+
table.addChild(new Text(theme.dim(emptyText), 0, 0));
|
|
491
|
+
} else {
|
|
492
|
+
for (const row of rows) table.addChild(new TruncatedText(row, 0, 0));
|
|
493
|
+
}
|
|
494
|
+
panel.addChild(table);
|
|
495
|
+
return panel;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export class KxmDashboard implements Component {
|
|
499
|
+
readonly root = new VStack([], { gap: 0 });
|
|
500
|
+
private readonly listContent = new VStack([], { gap: 0 });
|
|
501
|
+
private readonly listScroll = new ScrollView(this.listContent, { primary: true, overscroll: "contain", scrollbar: "auto" });
|
|
502
|
+
private readonly detailContent = new VStack([], { gap: 0 });
|
|
503
|
+
private readonly detailScroll = new ScrollView(this.detailContent, { primary: true, overscroll: "contain", scrollbar: "auto" });
|
|
504
|
+
private snapshot: MeshTuiSnapshot;
|
|
505
|
+
private view: MeshTuiView;
|
|
506
|
+
private readonly color: boolean;
|
|
507
|
+
private readonly requestRender: () => void;
|
|
508
|
+
private readonly onQuit: () => void;
|
|
509
|
+
private readonly getWidth: () => number;
|
|
510
|
+
private readonly onAction?: ((action: DashboardAction, snapshot: MeshTuiSnapshot, view: MeshTuiView) => Promise<void> | void) | undefined;
|
|
511
|
+
|
|
512
|
+
constructor(
|
|
513
|
+
snapshot: MeshTuiSnapshot,
|
|
514
|
+
view: MeshTuiView,
|
|
515
|
+
color: boolean,
|
|
516
|
+
requestRender: () => void,
|
|
517
|
+
onQuit: () => void,
|
|
518
|
+
getWidth: () => number = () => 120,
|
|
519
|
+
onAction?: ((action: DashboardAction, snapshot: MeshTuiSnapshot, view: MeshTuiView) => Promise<void> | void) | undefined,
|
|
520
|
+
) {
|
|
521
|
+
this.snapshot = snapshot;
|
|
522
|
+
this.view = view;
|
|
523
|
+
this.color = color;
|
|
524
|
+
this.requestRender = requestRender;
|
|
525
|
+
this.onQuit = onQuit;
|
|
526
|
+
this.getWidth = getWidth;
|
|
527
|
+
this.onAction = onAction;
|
|
528
|
+
this.rebuild();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
update(snapshot: MeshTuiSnapshot): void {
|
|
532
|
+
this.snapshot = snapshot;
|
|
533
|
+
this.rebuild();
|
|
534
|
+
this.requestRender();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private activeScroll(): ScrollView {
|
|
538
|
+
return this.view.pane === "detail" ? this.detailScroll : this.listScroll;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private fillPane(target: VStack, rows: string[], empty: string, theme: MeshTuiTheme): void {
|
|
542
|
+
target.clear();
|
|
543
|
+
if (rows.length === 0) target.addChild(new Text(theme.dim(empty), 0, 0));
|
|
544
|
+
else for (const row of rows) target.addChild(new TruncatedText(row, 0, 0));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private rebuild(): void {
|
|
548
|
+
const theme = meshTuiTheme(this.color);
|
|
549
|
+
const title = new TruncatedText(theme.accent("kxm dash"), 0, 0);
|
|
550
|
+
const hub = this.snapshot.healthOk ? theme.success("● hub ok") : theme.error("✗ hub down");
|
|
551
|
+
const ready = this.snapshot.readyOk ? theme.success("● ready") : theme.error("✗ not ready");
|
|
552
|
+
const agents = visibleAgents(this.snapshot);
|
|
553
|
+
const online = agents.filter((agent) => agent.online).length;
|
|
554
|
+
const transport = this.snapshot.transport === "snapshot"
|
|
555
|
+
? "snapshot"
|
|
556
|
+
: this.snapshot.metadataMode === "ops"
|
|
557
|
+
? "live ops"
|
|
558
|
+
: this.snapshot.metadataMode === "legacy"
|
|
559
|
+
? "presence/local"
|
|
560
|
+
: "live";
|
|
561
|
+
const updated = this.snapshot.fetchedAt.slice(11, 19);
|
|
562
|
+
const status = new TruncatedText(
|
|
563
|
+
`${hub} ${ready} ${transport} ${online}/${agents.length} online ${theme.dim(`updated ${updated} UTC · ${this.snapshot.serverUrl}`)}`,
|
|
564
|
+
0,
|
|
565
|
+
0,
|
|
566
|
+
);
|
|
567
|
+
const header = new Box(1, 0, theme.headerBg);
|
|
568
|
+
header.addChild(new HStack([
|
|
569
|
+
{ component: title, basis: 16, shrink: 1, minSize: 10 },
|
|
570
|
+
{ component: status, grow: 1, shrink: 1, minSize: 20 },
|
|
571
|
+
], { gap: 2 }));
|
|
572
|
+
|
|
573
|
+
const tabs = new Box(1, 0, theme.panelBg);
|
|
574
|
+
tabs.addChild(new TruncatedText(renderTabBar(this.snapshot, this.view, theme), 0, 0));
|
|
575
|
+
|
|
576
|
+
this.fillPane(this.listContent, listLines(this.snapshot, this.view, theme), "Nothing here yet", theme);
|
|
577
|
+
this.fillPane(this.detailContent, detailLines(this.snapshot, this.view, theme), "Select an item", theme);
|
|
578
|
+
|
|
579
|
+
const listTitle = this.view.pane === "list" ? theme.accent("list") : theme.dim("list");
|
|
580
|
+
const detailTitle = this.view.pane === "detail" ? theme.accent("detail") : theme.dim("detail");
|
|
581
|
+
const listPane = new Box(1, 0, theme.panelBg);
|
|
582
|
+
listPane.addChild(new Text(listTitle, 0, 0));
|
|
583
|
+
listPane.addChild(this.listScroll);
|
|
584
|
+
const detailPane = new Box(1, 0, theme.panelBg);
|
|
585
|
+
detailPane.addChild(new Text(detailTitle, 0, 0));
|
|
586
|
+
detailPane.addChild(this.detailScroll);
|
|
587
|
+
|
|
588
|
+
const split = tabSplits(this.view.tab);
|
|
589
|
+
const body = this.view.help
|
|
590
|
+
? (() => {
|
|
591
|
+
const help = new Box(1, 0, theme.panelBg);
|
|
592
|
+
help.addChild(new Text(`${theme.accent("kxm dash")}\n1–7 or Tab/[ ] switch tabs · ←→ list/detail · ↑↓ select · PgUp/PgDn scroll · h help · q quit\nAccess Control Plane: [a] approve · [r] reject · [d] degrade · [s] signal · [c] cancel\nAgents, Tasks, Workflows, Plans, Inbox, Procs, Spend. Split pane on wide terminals. No message bodies.`, 0, 0));
|
|
593
|
+
return help;
|
|
594
|
+
})()
|
|
595
|
+
: split
|
|
596
|
+
? new HStack([
|
|
597
|
+
{ component: listPane, basis: 44, shrink: 1, minSize: 28, visible: ({ width }) => width >= 76 || this.view.pane === "list" },
|
|
598
|
+
{ component: detailPane, grow: 1, shrink: 1, minSize: 28, visible: ({ width }) => width >= 76 || this.view.pane === "detail" },
|
|
599
|
+
], { gap: 1 })
|
|
600
|
+
: listPane;
|
|
601
|
+
|
|
602
|
+
const error = this.snapshot.error ? ` ${theme.error(`ERROR ${this.snapshot.error}`)}` : "";
|
|
603
|
+
const feed = this.snapshot.metadataMode === "legacy" ? "presence + local" : "live";
|
|
604
|
+
const actionNotice = this.view.statusMessage ? ` ${theme.accent(this.view.statusMessage)}` : "";
|
|
605
|
+
const footer = new TruncatedText(
|
|
606
|
+
`${theme.dim(`tab ${MESH_TUI_TAB_LABELS[this.view.tab]} · ${this.view.pane} · 1–7 tabs · h help · a/r/d/s/c · q quit · ${feed}`)}${actionNotice}${error}`,
|
|
607
|
+
1,
|
|
608
|
+
0,
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
this.root.clear();
|
|
612
|
+
this.root.addChild(header, { basis: "auto", shrink: 0 });
|
|
613
|
+
this.root.addChild(tabs, { basis: "auto", shrink: 0 });
|
|
614
|
+
this.root.addChild(body, { basis: "auto", grow: 1, shrink: 1, minSize: 1 });
|
|
615
|
+
this.root.addChild(footer, { basis: "auto", shrink: 0 });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
handleInput(data: string): void {
|
|
619
|
+
const viewport = this.activeScroll().viewportHeight;
|
|
620
|
+
const page = viewport > 3 ? viewport - 2 : 10;
|
|
621
|
+
if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.ctrl("u"))) {
|
|
622
|
+
this.activeScroll().scrollBy(-page);
|
|
623
|
+
this.requestRender();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("d"))) {
|
|
627
|
+
this.activeScroll().scrollBy(page);
|
|
628
|
+
this.requestRender();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const next = applyMeshTuiKey(this.view, data, tabItemCount(this.snapshot, this.view.tab));
|
|
632
|
+
if (next === "quit") {
|
|
633
|
+
this.onQuit();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (next !== this.view) {
|
|
637
|
+
this.view = next;
|
|
638
|
+
if (next.lastAction && this.onAction) {
|
|
639
|
+
void this.onAction(next.lastAction, this.snapshot, next);
|
|
640
|
+
}
|
|
641
|
+
this.rebuild();
|
|
642
|
+
this.requestRender();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
invalidate(): void {
|
|
647
|
+
this.root.invalidate();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
render(width: number): string[] {
|
|
651
|
+
return this.root.render(width);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function renderMeshTui(snapshot: MeshTuiSnapshot, view: MeshTuiView = defaultMeshTuiView(), width = 120): string {
|
|
656
|
+
const dashboard = new KxmDashboard(snapshot, view, false, () => undefined, () => undefined);
|
|
657
|
+
return `${dashboard.render(width).map(stripTerminalSequences).join("\n")}\n`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function readJson<T>(response: Response): Promise<T> {
|
|
661
|
+
return await response.json() as T;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function waitForReconnect(signal: AbortSignal, milliseconds = 1_000): Promise<void> {
|
|
665
|
+
if (signal.aborted) return;
|
|
666
|
+
await new Promise<void>((resolve) => {
|
|
667
|
+
const done = () => {
|
|
668
|
+
clearTimeout(timer);
|
|
669
|
+
signal.removeEventListener("abort", done);
|
|
670
|
+
resolve();
|
|
671
|
+
};
|
|
672
|
+
const timer = setTimeout(done, milliseconds);
|
|
673
|
+
timer.unref();
|
|
674
|
+
signal.addEventListener("abort", done, { once: true });
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export async function runMeshTui(input: {
|
|
679
|
+
serverUrl: string;
|
|
680
|
+
dataPath: string;
|
|
681
|
+
stateDir: string;
|
|
682
|
+
project: string;
|
|
683
|
+
authToken?: string;
|
|
684
|
+
fetchImpl: typeof fetch;
|
|
685
|
+
stdout: (text: string) => void;
|
|
686
|
+
stdin?: NodeJS.ReadStream;
|
|
687
|
+
isTty?: boolean;
|
|
688
|
+
now?: () => Date;
|
|
689
|
+
abort?: AbortSignal;
|
|
690
|
+
terminal?: Terminal;
|
|
691
|
+
reconnectMs?: number;
|
|
692
|
+
screen?: MeshTuiPanel;
|
|
693
|
+
env?: Record<string, string | undefined>;
|
|
694
|
+
}): Promise<number> {
|
|
695
|
+
const base = input.serverUrl.replace(/\/$/, "");
|
|
696
|
+
const headers = (identity?: { id: string; key: string }): Record<string, string> => ({
|
|
697
|
+
"content-type": "application/json",
|
|
698
|
+
...(input.authToken ? { authorization: `Bearer ${input.authToken}` } : {}),
|
|
699
|
+
...(identity ? { "x-kxm-agent-id": identity.id, "x-kxm-agent-key": identity.key } : {}),
|
|
700
|
+
});
|
|
701
|
+
const tty = input.isTty ?? Boolean(input.stdin?.isTTY && process.stdout.isTTY);
|
|
702
|
+
let identity: { id: string; key: string } | undefined;
|
|
703
|
+
let useOpsStream = true;
|
|
704
|
+
let interactive: { tui: TUI; dashboard: KxmDashboard } | undefined;
|
|
705
|
+
const name = `kxm-tui-${process.pid}`;
|
|
706
|
+
const view = defaultMeshTuiView(input.screen);
|
|
707
|
+
|
|
708
|
+
const paint = (snapshot: MeshTuiSnapshot) => {
|
|
709
|
+
if (interactive) {
|
|
710
|
+
interactive.dashboard.update(snapshot);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
input.stdout(renderMeshTui(snapshot, view));
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const snapshotFromHub = async (transport: "sse" | "snapshot", extra?: Partial<MeshTuiSnapshot>): Promise<MeshTuiSnapshot> => {
|
|
717
|
+
const fetchedAt = (input.now?.() ?? new Date()).toISOString();
|
|
718
|
+
let healthOk = false;
|
|
719
|
+
let readyOk = false;
|
|
720
|
+
let storage: string | undefined;
|
|
721
|
+
let onlineCount = 0;
|
|
722
|
+
let error: string | undefined;
|
|
723
|
+
try {
|
|
724
|
+
const health = await input.fetchImpl(`${base}/health`);
|
|
725
|
+
const body = await readJson<{ ok?: boolean; agents?: number }>(health);
|
|
726
|
+
healthOk = health.ok && body.ok === true;
|
|
727
|
+
onlineCount = Number.isInteger(body.agents) ? body.agents as number : 0;
|
|
728
|
+
} catch {
|
|
729
|
+
error = "hub_unreachable";
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
const ready = await input.fetchImpl(`${base}/ready`);
|
|
733
|
+
const body = await readJson<{ ok?: boolean; storage?: string }>(ready);
|
|
734
|
+
readyOk = ready.ok && body.ok === true;
|
|
735
|
+
storage = body.storage;
|
|
736
|
+
} catch {
|
|
737
|
+
error = error ?? "hub_unreachable";
|
|
738
|
+
}
|
|
739
|
+
let local = loadLocalMeshSnapshot(input.dataPath, input.stateDir, { env: input.env ?? process.env });
|
|
740
|
+
if (useOpsStream) {
|
|
741
|
+
try {
|
|
742
|
+
const ops = await input.fetchImpl(`${base}/v1/ops/snapshot?project=${encodeURIComponent(input.project)}`, {
|
|
743
|
+
headers: headers(),
|
|
744
|
+
});
|
|
745
|
+
if (ops.ok) {
|
|
746
|
+
const body = await readJson<{
|
|
747
|
+
agents: AgentRecord[];
|
|
748
|
+
openMessages: MeshTuiSnapshot["openMessages"];
|
|
749
|
+
openMessageTotal: number;
|
|
750
|
+
runs: MeshTuiSnapshot["runs"];
|
|
751
|
+
runTotal: number;
|
|
752
|
+
plans?: MeshTuiPlan[];
|
|
753
|
+
}>(ops);
|
|
754
|
+
local = {
|
|
755
|
+
...local,
|
|
756
|
+
agents: body.agents,
|
|
757
|
+
openMessages: body.openMessages,
|
|
758
|
+
openMessageTotal: body.openMessageTotal,
|
|
759
|
+
runs: body.runs,
|
|
760
|
+
runTotal: body.runTotal,
|
|
761
|
+
plans: body.plans ?? local.plans,
|
|
762
|
+
};
|
|
763
|
+
} else if (ops.status === 401 || ops.status === 403 || ops.status === 404 || ops.status === 503) {
|
|
764
|
+
useOpsStream = false;
|
|
765
|
+
} else {
|
|
766
|
+
error = error ?? `ops_snapshot_http_${ops.status}`;
|
|
767
|
+
}
|
|
768
|
+
} catch {
|
|
769
|
+
error = error ?? "ops_snapshot_unreachable";
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (!useOpsStream && identity) {
|
|
773
|
+
try {
|
|
774
|
+
const listed = await input.fetchImpl(`${base}/v1/agents`, { headers: headers(identity) });
|
|
775
|
+
if (listed.ok) {
|
|
776
|
+
const body = await readJson<{ agents: AgentRecord[] }>(listed);
|
|
777
|
+
const byId = new Map(local.agents.map((agent) => [agent.id, agent]));
|
|
778
|
+
for (const agent of body.agents) byId.set(agent.id, agent);
|
|
779
|
+
local = {
|
|
780
|
+
...local,
|
|
781
|
+
agents: [...byId.values()].sort((a, b) => Number(b.online) - Number(a.online) || a.name.localeCompare(b.name)),
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
} catch {
|
|
785
|
+
error = error ?? "agents_unreachable";
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
serverUrl: input.serverUrl,
|
|
790
|
+
healthOk,
|
|
791
|
+
readyOk,
|
|
792
|
+
...(storage ? { storage } : {}),
|
|
793
|
+
onlineCount,
|
|
794
|
+
transport,
|
|
795
|
+
metadataMode: useOpsStream ? "ops" : "legacy",
|
|
796
|
+
fetchedAt,
|
|
797
|
+
...(error ? { error } : {}),
|
|
798
|
+
...local,
|
|
799
|
+
...extra,
|
|
800
|
+
};
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const registerObserver = async () => {
|
|
804
|
+
if (identity) return;
|
|
805
|
+
const registration = await input.fetchImpl(`${base}/v1/agents/register`, {
|
|
806
|
+
method: "POST",
|
|
807
|
+
headers: headers(),
|
|
808
|
+
body: JSON.stringify({
|
|
809
|
+
name,
|
|
810
|
+
purpose: "Read-only mesh observer TUI",
|
|
811
|
+
project: input.project,
|
|
812
|
+
model: "kxm-tui",
|
|
813
|
+
}),
|
|
814
|
+
});
|
|
815
|
+
if (!registration.ok) throw new Error(`observer registration failed with HTTP ${registration.status}`);
|
|
816
|
+
const registered = await readJson<{ agent: AgentRecord; agentKey: string }>(registration);
|
|
817
|
+
identity = { id: registered.agent.id, key: registered.agentKey };
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
const unregister = async () => {
|
|
821
|
+
if (!identity) return;
|
|
822
|
+
const registeredIdentity = identity;
|
|
823
|
+
identity = undefined;
|
|
824
|
+
await input.fetchImpl(`${base}/v1/agents/${encodeURIComponent(registeredIdentity.id)}`, {
|
|
825
|
+
method: "DELETE",
|
|
826
|
+
headers: headers(registeredIdentity),
|
|
827
|
+
}).catch(() => undefined);
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
let snapshot = await snapshotFromHub("sse");
|
|
832
|
+
if (!useOpsStream) {
|
|
833
|
+
try {
|
|
834
|
+
await registerObserver();
|
|
835
|
+
} catch (error) {
|
|
836
|
+
const failed = await snapshotFromHub("snapshot", {
|
|
837
|
+
error: error instanceof Error ? error.message : "observer registration failed",
|
|
838
|
+
});
|
|
839
|
+
paint(failed);
|
|
840
|
+
return 1;
|
|
841
|
+
}
|
|
842
|
+
snapshot = await snapshotFromHub("sse");
|
|
843
|
+
}
|
|
844
|
+
if (!tty) {
|
|
845
|
+
paint(snapshot);
|
|
846
|
+
return snapshot.healthOk ? 0 : 1;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const abort = new AbortController();
|
|
850
|
+
const onAbort = () => abort.abort();
|
|
851
|
+
input.abort?.addEventListener("abort", onAbort);
|
|
852
|
+
if (input.abort?.aborted) abort.abort();
|
|
853
|
+
|
|
854
|
+
const terminal = input.terminal ?? new ProcessTerminal();
|
|
855
|
+
const tui = new TuiAltScreen(terminal, false, undefined, { mouse: true });
|
|
856
|
+
const onAction = async (action: DashboardAction, currentSnapshot: MeshTuiSnapshot, currentView: MeshTuiView) => {
|
|
857
|
+
let targetId: string | undefined;
|
|
858
|
+
if (currentView.tab === "workflows" || currentView.tab === "tasks") {
|
|
859
|
+
const runs = currentView.tab === "tasks"
|
|
860
|
+
? currentSnapshot.runs.filter((r) => r.status === "running" || r.status === "waiting")
|
|
861
|
+
: currentSnapshot.runs;
|
|
862
|
+
targetId = runs[currentView.selected]?.id;
|
|
863
|
+
} else if (currentView.tab === "inbox") {
|
|
864
|
+
targetId = currentSnapshot.openMessages[currentView.selected]?.id;
|
|
865
|
+
}
|
|
866
|
+
if (!targetId) return;
|
|
867
|
+
|
|
868
|
+
try {
|
|
869
|
+
if (action.action === "cancel") {
|
|
870
|
+
await input.fetchImpl(`${base}/v1/runs/${encodeURIComponent(targetId)}/cancel?project=${encodeURIComponent(input.project)}`, {
|
|
871
|
+
method: "POST",
|
|
872
|
+
headers: headers(),
|
|
873
|
+
signal: abort.signal,
|
|
874
|
+
});
|
|
875
|
+
} else if (action.action === "approve" || action.action === "signal") {
|
|
876
|
+
await input.fetchImpl(`${base}/v1/runs/${encodeURIComponent(targetId)}/signal?project=${encodeURIComponent(input.project)}`, {
|
|
877
|
+
method: "POST",
|
|
878
|
+
headers: headers(),
|
|
879
|
+
body: JSON.stringify({ signalKey: "operator-approval", status: "passed", summary: "Interactive operator approval from kxm dash" }),
|
|
880
|
+
signal: abort.signal,
|
|
881
|
+
});
|
|
882
|
+
} else if (action.action === "reject") {
|
|
883
|
+
await input.fetchImpl(`${base}/v1/runs/${encodeURIComponent(targetId)}/signal?project=${encodeURIComponent(input.project)}`, {
|
|
884
|
+
method: "POST",
|
|
885
|
+
headers: headers(),
|
|
886
|
+
body: JSON.stringify({ signalKey: "operator-approval", status: "failed", summary: "Rejected by operator in kxm dash" }),
|
|
887
|
+
signal: abort.signal,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
} catch {
|
|
891
|
+
// fail-soft on dashboard network errors during key press
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const dashboard = new KxmDashboard(
|
|
896
|
+
snapshot,
|
|
897
|
+
view,
|
|
898
|
+
process.env.NO_COLOR === undefined,
|
|
899
|
+
() => tui.requestRender(),
|
|
900
|
+
() => abort.abort(),
|
|
901
|
+
() => terminal.columns,
|
|
902
|
+
onAction,
|
|
903
|
+
);
|
|
904
|
+
interactive = { tui, dashboard };
|
|
905
|
+
tui.setLayoutRoot(dashboard.root);
|
|
906
|
+
tui.setFocus(dashboard);
|
|
907
|
+
tui.start();
|
|
908
|
+
|
|
909
|
+
const applyPresence = (agent: AgentRecord) => {
|
|
910
|
+
const byId = new Map(snapshot.agents.map((row) => [row.id, row]));
|
|
911
|
+
byId.set(agent.id, agent);
|
|
912
|
+
const { error: _staleError, ...healthySnapshot } = snapshot;
|
|
913
|
+
snapshot = {
|
|
914
|
+
...healthySnapshot,
|
|
915
|
+
transport: "sse",
|
|
916
|
+
fetchedAt: (input.now?.() ?? new Date()).toISOString(),
|
|
917
|
+
agents: [...byId.values()].sort((a, b) => Number(b.online) - Number(a.online) || a.name.localeCompare(b.name)),
|
|
918
|
+
onlineCount: [...byId.values()].filter((row) => row.online).length,
|
|
919
|
+
};
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
while (!abort.signal.aborted) {
|
|
924
|
+
let events: Response;
|
|
925
|
+
try {
|
|
926
|
+
if (!useOpsStream && !identity) throw new Error("legacy SSE requires an observer identity");
|
|
927
|
+
const eventUrl = useOpsStream
|
|
928
|
+
? `${base}/v1/ops/events?project=${encodeURIComponent(input.project)}`
|
|
929
|
+
: `${base}/v1/events?agentId=${encodeURIComponent(identity!.id)}&presenceOnly=true`;
|
|
930
|
+
events = await input.fetchImpl(eventUrl, {
|
|
931
|
+
headers: { ...headers(useOpsStream ? undefined : identity!), accept: "text/event-stream" },
|
|
932
|
+
signal: abort.signal,
|
|
933
|
+
});
|
|
934
|
+
} catch (error) {
|
|
935
|
+
if (abort.signal.aborted) break;
|
|
936
|
+
snapshot = await snapshotFromHub("snapshot", { error: error instanceof Error ? `sse_${error.message}` : "sse_unreachable" });
|
|
937
|
+
paint(snapshot);
|
|
938
|
+
await waitForReconnect(abort.signal, input.reconnectMs);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (useOpsStream && (events.status === 401 || events.status === 403 || events.status === 404 || events.status === 503)) {
|
|
942
|
+
useOpsStream = false;
|
|
943
|
+
try {
|
|
944
|
+
await registerObserver();
|
|
945
|
+
} catch {
|
|
946
|
+
snapshot = await snapshotFromHub("snapshot", {
|
|
947
|
+
error: "live metadata access was lost and legacy observer registration failed",
|
|
948
|
+
});
|
|
949
|
+
paint(snapshot);
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
snapshot = await snapshotFromHub("snapshot", {
|
|
953
|
+
error: "admin metadata stream unavailable; using legacy presence and local snapshots",
|
|
954
|
+
});
|
|
955
|
+
paint(snapshot);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (!useOpsStream && events.ok && events.headers.get("x-kxm-events-mode") !== "presence") {
|
|
959
|
+
await events.body?.cancel();
|
|
960
|
+
snapshot = await snapshotFromHub("snapshot", {
|
|
961
|
+
error: "hub does not support metadata-only presence SSE; live fallback disabled",
|
|
962
|
+
});
|
|
963
|
+
paint(snapshot);
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
if (!events.ok || !events.body) {
|
|
967
|
+
snapshot = await snapshotFromHub("snapshot", { error: `sse_http_${events.status}` });
|
|
968
|
+
paint(snapshot);
|
|
969
|
+
await waitForReconnect(abort.signal, input.reconnectMs);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
if (snapshot.error || snapshot.transport !== "sse") {
|
|
973
|
+
const { error: _staleError, ...healthySnapshot } = snapshot;
|
|
974
|
+
snapshot = {
|
|
975
|
+
...healthySnapshot,
|
|
976
|
+
transport: "sse",
|
|
977
|
+
fetchedAt: (input.now?.() ?? new Date()).toISOString(),
|
|
978
|
+
};
|
|
979
|
+
paint(snapshot);
|
|
980
|
+
}
|
|
981
|
+
const reader = events.body.getReader();
|
|
982
|
+
const decoder = new TextDecoder();
|
|
983
|
+
let pending = "";
|
|
984
|
+
while (!abort.signal.aborted) {
|
|
985
|
+
const { done, value } = await reader.read();
|
|
986
|
+
if (done) break;
|
|
987
|
+
pending += decoder.decode(value, { stream: true });
|
|
988
|
+
const parts = pending.split("\n\n");
|
|
989
|
+
pending = parts.pop() ?? "";
|
|
990
|
+
for (const part of parts) {
|
|
991
|
+
if (part.startsWith(":")) continue;
|
|
992
|
+
const lines = part.split("\n");
|
|
993
|
+
const eventLine = lines.find((line) => line.startsWith("event:"));
|
|
994
|
+
if (!useOpsStream && eventLine?.slice(6).trim() !== "presence") continue;
|
|
995
|
+
const dataLine = lines.find((line) => line.startsWith("data:"));
|
|
996
|
+
if (!dataLine) continue;
|
|
997
|
+
let parsed: { type?: "ops" | "presence"; agent?: AgentRecord; project?: string; topic?: string; at?: string };
|
|
998
|
+
try {
|
|
999
|
+
parsed = JSON.parse(dataLine.slice(5).trim()) as typeof parsed;
|
|
1000
|
+
} catch {
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
if ("type" in parsed && parsed.type === "ops") {
|
|
1004
|
+
snapshot = await snapshotFromHub("sse");
|
|
1005
|
+
paint(snapshot);
|
|
1006
|
+
} else if ("type" in parsed && parsed.type === "presence" && parsed.agent) {
|
|
1007
|
+
applyPresence(parsed.agent);
|
|
1008
|
+
const local = loadLocalMeshSnapshot(input.dataPath, input.stateDir, { env: input.env ?? process.env });
|
|
1009
|
+
snapshot = {
|
|
1010
|
+
...snapshot,
|
|
1011
|
+
openMessages: local.openMessages,
|
|
1012
|
+
openMessageTotal: local.openMessageTotal,
|
|
1013
|
+
runs: local.runs,
|
|
1014
|
+
runTotal: local.runTotal,
|
|
1015
|
+
pids: local.pids,
|
|
1016
|
+
spend: local.spend,
|
|
1017
|
+
source: local.source,
|
|
1018
|
+
};
|
|
1019
|
+
paint(snapshot);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (!abort.signal.aborted) {
|
|
1024
|
+
snapshot = await snapshotFromHub("snapshot", { error: "sse_reconnecting" });
|
|
1025
|
+
paint(snapshot);
|
|
1026
|
+
await waitForReconnect(abort.signal, input.reconnectMs);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
if (!abort.signal.aborted) throw error;
|
|
1031
|
+
} finally {
|
|
1032
|
+
input.abort?.removeEventListener("abort", onAbort);
|
|
1033
|
+
tui.stop();
|
|
1034
|
+
interactive = undefined;
|
|
1035
|
+
}
|
|
1036
|
+
return 0;
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
const failed = await snapshotFromHub("snapshot", {
|
|
1039
|
+
error: error instanceof Error ? error.message : "tui_failed",
|
|
1040
|
+
});
|
|
1041
|
+
paint(failed);
|
|
1042
|
+
return 1;
|
|
1043
|
+
} finally {
|
|
1044
|
+
await unregister();
|
|
1045
|
+
}
|
|
1046
|
+
}
|