@pipemd-core/pipemd 1.0.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/AI_SETUP_PIPEMD.md +184 -0
- package/CHANGELOG.md +47 -0
- package/LICENSE +15 -0
- package/README.md +535 -0
- package/dist/index.js +6647 -0
- package/dist/plugins/opencode-server.js +235 -0
- package/dist/plugins/opencode-tui.js +914 -0
- package/dist/templates/agent-decision-tree.md +113 -0
- package/dist/templates/static-rules.md +7 -0
- package/package.json +68 -0
- package/scripts/C-CPP/architecture/arch.sh +229 -0
- package/scripts/C-CPP/lib/limit.sh +146 -0
- package/scripts/C-CPP/project/class-diagram.sh +96 -0
- package/scripts/C-CPP/project/cmake-targets.sh +68 -0
- package/scripts/C-CPP/project/deps.sh +44 -0
- package/scripts/C-CPP/project/find-todos.sh +6 -0
- package/scripts/C-CPP/project/include-graph.sh +110 -0
- package/scripts/C-CPP/project/interfaces.sh +108 -0
- package/scripts/C-CPP/project/tree.sh +5 -0
- package/scripts/C-CPP/quality/lint.sh +14 -0
- package/scripts/C-CPP/quality/test-summary.sh +22 -0
- package/scripts/C-CPP/quality/type-check.sh +26 -0
- package/scripts/DevOps/architecture/arch.sh +186 -0
- package/scripts/DevOps/devops/aws-context.sh +34 -0
- package/scripts/DevOps/devops/docker-stats.sh +42 -0
- package/scripts/DevOps/devops/k8s-unhealthy.sh +41 -0
- package/scripts/DevOps/devops/tf-state.sh +65 -0
- package/scripts/DevOps/lib/limit.sh +143 -0
- package/scripts/Generic/architecture/arch.sh +570 -0
- package/scripts/Generic/lib/limit.sh +140 -0
- package/scripts/Go/architecture/arch.sh +79 -0
- package/scripts/Go/lib/limit.sh +142 -0
- package/scripts/Go/project/deps.sh +35 -0
- package/scripts/Go/project/find-todos.sh +6 -0
- package/scripts/Go/project/go-interfaces.sh +18 -0
- package/scripts/Go/project/go-packages.sh +28 -0
- package/scripts/Go/project/tree.sh +5 -0
- package/scripts/Go/quality/lint.sh +16 -0
- package/scripts/Go/quality/test-summary.sh +16 -0
- package/scripts/Go/quality/type-check.sh +16 -0
- package/scripts/Node-TypeScript/api/express-routes.sh +14 -0
- package/scripts/Node-TypeScript/api/nest-controllers.sh +18 -0
- package/scripts/Node-TypeScript/architecture/arch.sh +174 -0
- package/scripts/Node-TypeScript/frontend/angular-routes.sh +15 -0
- package/scripts/Node-TypeScript/frontend/nextjs-app-router.sh +13 -0
- package/scripts/Node-TypeScript/frontend/react-components.sh +20 -0
- package/scripts/Node-TypeScript/lib/limit.sh +146 -0
- package/scripts/Node-TypeScript/project/deps.sh +15 -0
- package/scripts/Node-TypeScript/project/find-todos.sh +6 -0
- package/scripts/Node-TypeScript/quality/lint.sh +10 -0
- package/scripts/Node-TypeScript/quality/test-summary.sh +39 -0
- package/scripts/Node-TypeScript/quality/type-check.sh +10 -0
- package/scripts/Python/api/fastapi-routes.sh +12 -0
- package/scripts/Python/architecture/arch.sh +220 -0
- package/scripts/Python/db/django-models.sh +12 -0
- package/scripts/Python/db/sqlalchemy.sh +17 -0
- package/scripts/Python/lib/limit.sh +144 -0
- package/scripts/Python/project/deps.sh +28 -0
- package/scripts/Python/project/find-todos.sh +6 -0
- package/scripts/Python/quality/lint.sh +13 -0
- package/scripts/Python/quality/test-summary.sh +11 -0
- package/scripts/Python/quality/type-check.sh +10 -0
- package/scripts/Rust/architecture/arch.sh +176 -0
- package/scripts/Rust/lib/limit.sh +142 -0
- package/scripts/Rust/project/cargo-deps.sh +42 -0
- package/scripts/Rust/project/cargo-features.sh +26 -0
- package/scripts/Rust/project/find-todos.sh +6 -0
- package/scripts/Rust/project/tree.sh +5 -0
- package/scripts/Rust/quality/lint.sh +16 -0
- package/scripts/Rust/quality/test-summary.sh +16 -0
- package/scripts/Rust/quality/type-check.sh +16 -0
- package/scripts/Shared/api/express-routes.sh +11 -0
- package/scripts/Shared/api/fastapi-routes.sh +10 -0
- package/scripts/Shared/api/nest-controllers.sh +22 -0
- package/scripts/Shared/architecture/normalize.sh +178 -0
- package/scripts/Shared/crew/crew.sh +15 -0
- package/scripts/Shared/db/django-models.sh +11 -0
- package/scripts/Shared/db/prisma.sh +33 -0
- package/scripts/Shared/db/sqlalchemy.sh +12 -0
- package/scripts/Shared/frontend/angular-routes.sh +11 -0
- package/scripts/Shared/frontend/nextjs-app-router.sh +13 -0
- package/scripts/Shared/frontend/react-components.sh +11 -0
- package/scripts/Shared/git/diff-stat.sh +6 -0
- package/scripts/Shared/git/git-branch.sh +16 -0
- package/scripts/Shared/git/git-log.sh +6 -0
- package/scripts/Shared/git/git-status.sh +6 -0
- package/scripts/Shared/lib/limit.sh +144 -0
- package/scripts/Shared/project/compose-md.sh +182 -0
- package/scripts/Shared/project/deps.sh +69 -0
- package/scripts/Shared/project/find-todos.sh +6 -0
- package/scripts/Shared/project/tree.sh +5 -0
- package/scripts/Shared/quality/lint.sh +81 -0
- package/scripts/Shared/quality/test-summary.sh +103 -0
- package/scripts/Shared/quality/type-check.sh +114 -0
- package/scripts/copy-plugins.mjs +4 -0
- package/scripts/copy-templates.mjs +5 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
// pmd-crew-tui — PipeMD sidebar panel for OpenCode.
|
|
2
|
+
// Installed by `pmd crew install-hooks`. Safe to delete manually.
|
|
3
|
+
// @pmd-plugin-version ${PLUGIN_VERSION}
|
|
4
|
+
//
|
|
5
|
+
// Renders into the right sidebar showing:
|
|
6
|
+
// PipeMD status, crew sessions, hook log, stats.
|
|
7
|
+
import { createElement as el, createTextNode as txt, spread, insert, useKeyboard } from "@opentui/solid";
|
|
8
|
+
import { createSignal, createMemo, createEffect, onCleanup } from "solid-js";
|
|
9
|
+
import { RGBA } from "@opentui/core";
|
|
10
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
11
|
+
import { join, resolve, dirname } from "node:path";
|
|
12
|
+
|
|
13
|
+
const PROJECT_ROOT = resolve(dirname(import.meta.url.replace("file://", "")), "..");
|
|
14
|
+
const STATS_PATH = join(PROJECT_ROOT, ".pipemd", ".tui-stats.json");
|
|
15
|
+
const STATUS_PATH = join(PROJECT_ROOT, ".pipemd", ".status.json");
|
|
16
|
+
const CREW_STATUS_PATH = join(PROJECT_ROOT, ".pipemd", ".crew-status.json");
|
|
17
|
+
const DASHBOARD_PATH = join(PROJECT_ROOT, ".pipemd", ".dashboard.json");
|
|
18
|
+
const CREW_STATUS_STALE_MS = 90000;
|
|
19
|
+
const CREW_DIR = join(PROJECT_ROOT, ".pipemd", "crew");
|
|
20
|
+
const PID_PATH = join(PROJECT_ROOT, ".pipemd", ".daemon.pid");
|
|
21
|
+
const INJECT_LOG_DIR = join(PROJECT_ROOT, ".pipemd", ".injection-log");
|
|
22
|
+
const ERROR_LOG_PATH = join(PROJECT_ROOT, ".pipemd", ".plugin-errors.log");
|
|
23
|
+
const CONTEXT_FILES = ["AGENTS.md", "AI_CONTEXT.md"];
|
|
24
|
+
const POLL_MS = 2000;
|
|
25
|
+
const MAX_SESSIONS = 20;
|
|
26
|
+
|
|
27
|
+
function estimateTokens(bytes) {
|
|
28
|
+
return Math.round(bytes / 4);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatTokenCount(n) {
|
|
32
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + "k";
|
|
33
|
+
return String(n);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatBytes(b) {
|
|
37
|
+
if (b >= 1024 * 1024) return (b / (1024 * 1024)).toFixed(1) + "MB";
|
|
38
|
+
if (b >= 1024) return (b / 1024).toFixed(1) + "KB";
|
|
39
|
+
return b + "B";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findContextSize() {
|
|
43
|
+
try {
|
|
44
|
+
const st = JSON.parse(readFileSync(STATUS_PATH, "utf-8"));
|
|
45
|
+
if (st && typeof st.renderedBytes === "number" && st.renderedBytes > 0) {
|
|
46
|
+
return { bytes: st.renderedBytes };
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
for (const f of CONTEXT_FILES) {
|
|
50
|
+
try { const s = statSync(f); if (s.isFile() && s.size > 0) return { bytes: s.size }; } catch {}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tryReadJson(p) {
|
|
56
|
+
try { return JSON.parse(readFileSync(p, "utf-8")); } catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function tryReadPid() {
|
|
60
|
+
try {
|
|
61
|
+
const pid = parseInt(readFileSync(PID_PATH, "utf-8").trim(), 10);
|
|
62
|
+
if (pid > 0) { try { process.kill(pid, 0); return pid; } catch (e) { return e.code === "EPERM" ? pid : null; } }
|
|
63
|
+
} catch {}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let savedRoute = null;
|
|
68
|
+
|
|
69
|
+
function readSessions() {
|
|
70
|
+
try {
|
|
71
|
+
return readdirSync(CREW_DIR)
|
|
72
|
+
.filter((f) => f.endsWith(".json"))
|
|
73
|
+
.map((f) => { try { return JSON.parse(readFileSync(join(CREW_DIR, f), "utf-8")); } catch { return null; } })
|
|
74
|
+
.filter((s) => s && s.id);
|
|
75
|
+
} catch { return []; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findConflicts(sessions) {
|
|
79
|
+
const byPath = new Map();
|
|
80
|
+
for (const s of sessions) {
|
|
81
|
+
for (const c of (s.claimedFiles || [])) {
|
|
82
|
+
const set = byPath.get(c.path) || new Set();
|
|
83
|
+
set.add(s.id);
|
|
84
|
+
byPath.set(c.path, set);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const [p, set] of byPath) { if (set.size > 1) out.push({ path: p, sessionIds: [...set] }); }
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readLastError() {
|
|
93
|
+
try {
|
|
94
|
+
const raw = readFileSync(ERROR_LOG_PATH, "utf-8").split("\n").filter(Boolean);
|
|
95
|
+
if (raw.length === 0) return null;
|
|
96
|
+
const last = JSON.parse(raw[raw.length - 1]);
|
|
97
|
+
if (last && typeof last.ts === "number" && Date.now() - last.ts < 300000) {
|
|
98
|
+
return last;
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatTimeAgo(iso) {
|
|
105
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
106
|
+
const s = Math.floor(diff / 1000);
|
|
107
|
+
if (s < 5) return "now";
|
|
108
|
+
if (s < 60) return s + "s";
|
|
109
|
+
const m = Math.floor(s / 60);
|
|
110
|
+
if (m < 60) return m + "m";
|
|
111
|
+
return Math.floor(m / 60) + "h";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function truncStr(s, max) {
|
|
115
|
+
if (!s) return "";
|
|
116
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function basename(p) {
|
|
120
|
+
if (!p) return "";
|
|
121
|
+
const parts = p.replace(/\\/g, "/").split("/");
|
|
122
|
+
return parts[parts.length - 1] || p;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function vbox(children, props) {
|
|
126
|
+
const box = el("box");
|
|
127
|
+
spread(box, { flexDirection: "column", gap: 0, ...props });
|
|
128
|
+
for (const c of children) { if (c) insert(box, c); }
|
|
129
|
+
return box;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hbox(children, props) {
|
|
133
|
+
const box = el("box");
|
|
134
|
+
spread(box, { flexDirection: "row", gap: 1, ...props });
|
|
135
|
+
for (const c of children) { if (c) insert(box, c); }
|
|
136
|
+
return box;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function textNode(content, fg) {
|
|
140
|
+
const t = el("text");
|
|
141
|
+
spread(t, fg ? { fg } : {});
|
|
142
|
+
insert(t, txt(String(content)));
|
|
143
|
+
return t;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function boldNode(content, fg) {
|
|
147
|
+
const b = el("b");
|
|
148
|
+
insert(b, txt(String(content)));
|
|
149
|
+
const t = el("text");
|
|
150
|
+
spread(t, fg ? { fg } : {});
|
|
151
|
+
insert(t, b);
|
|
152
|
+
return t;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function dot(color) {
|
|
156
|
+
const t = el("text");
|
|
157
|
+
spread(t, { fg: color });
|
|
158
|
+
insert(t, txt("\u25CF"));
|
|
159
|
+
return t;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderTraceRoute(api) {
|
|
163
|
+
const theme = () => api.theme.current;
|
|
164
|
+
const [tick, setTick] = createSignal(Date.now());
|
|
165
|
+
const [view, setView] = createSignal("tree");
|
|
166
|
+
const [cursor, setCursor] = createSignal(0);
|
|
167
|
+
const [scrollOffset, setScrollOffset] = createSignal(0);
|
|
168
|
+
const [expandedHook, setExpandedHook] = createSignal(-1);
|
|
169
|
+
const timer = setInterval(() => setTick(Date.now()), POLL_MS);
|
|
170
|
+
onCleanup(() => clearInterval(timer));
|
|
171
|
+
|
|
172
|
+
const stats = createMemo(() => { tick(); return tryReadJson(STATS_PATH); });
|
|
173
|
+
const sessions = createMemo(() => { tick(); return readSessions(); });
|
|
174
|
+
const daemonPid = createMemo(() => { tick(); return tryReadPid(); });
|
|
175
|
+
const allEvents = createMemo(() => (stats()?.events || []).slice().reverse().slice(0, 50));
|
|
176
|
+
const conflictList = createMemo(() => findConflicts(sessions()));
|
|
177
|
+
const dashboard = createMemo(() => { tick(); return tryReadJson(DASHBOARD_PATH); });
|
|
178
|
+
const passiveAgents = createMemo(() => {
|
|
179
|
+
const db = dashboard();
|
|
180
|
+
if (db && db.crew && Array.isArray(db.crew.passiveAgents)
|
|
181
|
+
&& typeof db.ts === "number" && Date.now() - db.ts < 30000) {
|
|
182
|
+
return db.crew.passiveAgents.slice(0, 8);
|
|
183
|
+
}
|
|
184
|
+
const raw = stats()?.passiveAgents || [];
|
|
185
|
+
return raw.slice(0, 8);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const h = () => process.stdout.rows || 24;
|
|
189
|
+
const footerLines = 2;
|
|
190
|
+
const visibleRows = () => h() - footerLines;
|
|
191
|
+
|
|
192
|
+
const dismissTrace = () => {
|
|
193
|
+
const prev = savedRoute;
|
|
194
|
+
savedRoute = null;
|
|
195
|
+
if (prev && prev.name === "session" && prev.params && prev.params.sessionID) {
|
|
196
|
+
api.route.navigate("session", { sessionID: prev.params.sessionID });
|
|
197
|
+
} else {
|
|
198
|
+
api.route.navigate("home");
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
useKeyboard((evt) => {
|
|
203
|
+
if (api.route.current.name !== "pmd-trace") return;
|
|
204
|
+
if (evt.name === "escape" || (evt.alt && evt.name === "p")) {
|
|
205
|
+
evt.preventDefault();
|
|
206
|
+
evt.stopPropagation();
|
|
207
|
+
dismissTrace();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
211
|
+
evt.preventDefault();
|
|
212
|
+
evt.stopPropagation();
|
|
213
|
+
setCursor(Math.max(0, cursor() - 1));
|
|
214
|
+
} else if (evt.name === "down" || evt.name === "j") {
|
|
215
|
+
evt.preventDefault();
|
|
216
|
+
evt.stopPropagation();
|
|
217
|
+
setCursor(cursor() + 1);
|
|
218
|
+
} else if (evt.name === "enter") {
|
|
219
|
+
evt.preventDefault(); evt.stopPropagation();
|
|
220
|
+
if (view() === "hooks") {
|
|
221
|
+
setExpandedHook((prev) => prev === cursor() ? -1 : cursor());
|
|
222
|
+
}
|
|
223
|
+
} else if (evt.name === "right" || evt.name === "l") {
|
|
224
|
+
const views = ["tree", "timeline", "locks", "hooks"];
|
|
225
|
+
const next = (views.indexOf(view()) + 1) % views.length;
|
|
226
|
+
setView(views[next]); setCursor(0); setScrollOffset(0);
|
|
227
|
+
} else if (evt.name === "left" || evt.name === "h") {
|
|
228
|
+
evt.preventDefault(); evt.stopPropagation();
|
|
229
|
+
const views = ["tree", "timeline", "locks", "hooks"];
|
|
230
|
+
const prev = (views.indexOf(view()) - 1 + views.length) % views.length;
|
|
231
|
+
setView(views[prev]); setCursor(0); setScrollOffset(0);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const root = el("box");
|
|
236
|
+
spread(root, {
|
|
237
|
+
position: "absolute",
|
|
238
|
+
zIndex: 3000,
|
|
239
|
+
left: 0,
|
|
240
|
+
top: 0,
|
|
241
|
+
width: process.stdout.columns || 80,
|
|
242
|
+
height: h(),
|
|
243
|
+
backgroundColor: RGBA.fromInts(0, 0, 0, 220),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const panel = el("box");
|
|
247
|
+
spread(panel, {
|
|
248
|
+
flexDirection: "column",
|
|
249
|
+
width: Math.min((process.stdout.columns || 80) - 4, 120),
|
|
250
|
+
paddingLeft: 2,
|
|
251
|
+
paddingRight: 2,
|
|
252
|
+
paddingTop: 1,
|
|
253
|
+
paddingBottom: 1,
|
|
254
|
+
gap: 0,
|
|
255
|
+
});
|
|
256
|
+
insert(root, panel);
|
|
257
|
+
|
|
258
|
+
insert(panel, () => {
|
|
259
|
+
tick();
|
|
260
|
+
view();
|
|
261
|
+
cursor();
|
|
262
|
+
scrollOffset();
|
|
263
|
+
const sess = sessions();
|
|
264
|
+
const evts = allEvents();
|
|
265
|
+
const dPid = daemonPid();
|
|
266
|
+
const conflicts = conflictList();
|
|
267
|
+
const pw = Math.min((process.stdout.columns || 80) - 8, 114);
|
|
268
|
+
|
|
269
|
+
const header = [];
|
|
270
|
+
header.push(hbox([
|
|
271
|
+
boldNode("PipeMD Resolution Trace", theme().primary),
|
|
272
|
+
textNode("\u00B7", theme().textMuted),
|
|
273
|
+
textNode(sess.length + " session" + (sess.length !== 1 ? "s" : ""), theme().text),
|
|
274
|
+
textNode("\u00B7", theme().textMuted),
|
|
275
|
+
textNode(dPid ? "live" : "offline", dPid ? theme().success : theme().error),
|
|
276
|
+
], { gap: 1 }));
|
|
277
|
+
|
|
278
|
+
header.push(hbox([
|
|
279
|
+
textNode(view() === "tree" ? "\u25B6 tree" : " tree", view() === "tree" ? theme().primary : theme().textMuted),
|
|
280
|
+
textNode(view() === "timeline" ? "\u25B6 timeline" : " timeline", view() === "timeline" ? theme().primary : theme().textMuted),
|
|
281
|
+
textNode(view() === "locks" ? "\u25B6 locks" : " locks", view() === "locks" ? theme().primary : theme().textMuted),
|
|
282
|
+
textNode(view() === "hooks" ? "\u25B6 hooks" : " hooks", view() === "hooks" ? theme().primary : theme().textMuted),
|
|
283
|
+
], { gap: 2 }));
|
|
284
|
+
|
|
285
|
+
header.push(textNode("\u2500".repeat(pw), RGBA.fromInts(60, 60, 60, 255)));
|
|
286
|
+
|
|
287
|
+
// ── Agent status (active coordinators + workers + passive) ──
|
|
288
|
+
const pAgents = passiveAgents();
|
|
289
|
+
const coordSessions = sess.filter((s) => s.role !== "worker");
|
|
290
|
+
const workerByCoord = new Map();
|
|
291
|
+
for (const s of sess) {
|
|
292
|
+
if (s.role === "worker" && s.coordinatorId) {
|
|
293
|
+
const list = workerByCoord.get(s.coordinatorId) || [];
|
|
294
|
+
list.push(s);
|
|
295
|
+
workerByCoord.set(s.coordinatorId, list);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const activeCount = sess.length;
|
|
299
|
+
const passiveCount = pAgents.length;
|
|
300
|
+
const totalAgents = activeCount + passiveCount;
|
|
301
|
+
|
|
302
|
+
if (totalAgents > 0) {
|
|
303
|
+
header.push(hbox([
|
|
304
|
+
boldNode("Agents", theme().text),
|
|
305
|
+
dot(theme().success),
|
|
306
|
+
textNode(activeCount + " active", theme().success),
|
|
307
|
+
passiveCount > 0 ? dot(theme().warning) : textNode("", theme().textMuted),
|
|
308
|
+
passiveCount > 0 ? textNode(passiveCount + " passive", theme().warning) : textNode("", theme().textMuted),
|
|
309
|
+
], { gap: 1 }));
|
|
310
|
+
|
|
311
|
+
for (const c of coordSessions) {
|
|
312
|
+
const claimed = (c.claimedFiles || []).map((cl) => cl.path).join(", ");
|
|
313
|
+
header.push(hbox([
|
|
314
|
+
dot(theme().success),
|
|
315
|
+
textNode(truncStr(c.harness, 14), theme().text),
|
|
316
|
+
textNode("coord", theme().textMuted),
|
|
317
|
+
], { gap: 1, paddingLeft: 1 }));
|
|
318
|
+
if (claimed) {
|
|
319
|
+
header.push(textNode(" claimed: " + truncStr(claimed, pw - 16), theme().textMuted));
|
|
320
|
+
}
|
|
321
|
+
const workers = workerByCoord.get(c.id) || [];
|
|
322
|
+
for (let wi = 0; wi < workers.length; wi++) {
|
|
323
|
+
const w = workers[wi];
|
|
324
|
+
const prefix = wi === workers.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
|
|
325
|
+
const wClaimed = (w.claimedFiles || []).map((cl) => cl.path).join(", ");
|
|
326
|
+
const wLabel = w.label ? truncStr(w.label, 16) : truncStr(w.id, 10);
|
|
327
|
+
header.push(hbox([
|
|
328
|
+
textNode(prefix, theme().textMuted),
|
|
329
|
+
dot(theme().primary),
|
|
330
|
+
textNode(truncStr(w.harness || "worker", 12), theme().text),
|
|
331
|
+
textNode(wLabel, theme().textMuted),
|
|
332
|
+
], { gap: 1, paddingLeft: 2 }));
|
|
333
|
+
if (wClaimed) {
|
|
334
|
+
header.push(textNode(" claimed: " + truncStr(wClaimed, pw - 18), theme().textMuted));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const unattached = sess.filter((s) => s.role === "worker" && !s.coordinatorId);
|
|
339
|
+
for (const w of unattached) {
|
|
340
|
+
header.push(hbox([
|
|
341
|
+
dot(theme().warning),
|
|
342
|
+
textNode(truncStr(w.harness || "worker", 12), theme().text),
|
|
343
|
+
textNode("worker", theme().textMuted),
|
|
344
|
+
], { gap: 1, paddingLeft: 1 }));
|
|
345
|
+
}
|
|
346
|
+
for (const pa of pAgents) {
|
|
347
|
+
header.push(hbox([
|
|
348
|
+
dot(theme().warning),
|
|
349
|
+
textNode(truncStr(pa, pw - 6), theme().textMuted),
|
|
350
|
+
], { gap: 1, paddingLeft: 1 }));
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
header.push(textNode("No active agents", theme().textMuted));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
header.push(textNode("\u2500".repeat(pw), RGBA.fromInts(60, 60, 60, 255)));
|
|
357
|
+
|
|
358
|
+
const headerLineCount = header.length;
|
|
359
|
+
|
|
360
|
+
const rows = [];
|
|
361
|
+
|
|
362
|
+
if (view() === "tree") {
|
|
363
|
+
for (const c of coordSessions) {
|
|
364
|
+
const hb = c.lastHeartbeat ? formatTimeAgo(c.lastHeartbeat) : "?";
|
|
365
|
+
const claimed = (c.claimedFiles || []).map((cl) => cl.path).join(", ");
|
|
366
|
+
rows.push(hbox([
|
|
367
|
+
textNode("\u25CF", theme().success),
|
|
368
|
+
textNode(truncStr(c.harness, 14), theme().text),
|
|
369
|
+
textNode(truncStr(c.id, 8), theme().textMuted),
|
|
370
|
+
textNode("pid:" + (c.pid || "?"), theme().textMuted),
|
|
371
|
+
textNode(hb, theme().textMuted),
|
|
372
|
+
], { gap: 1, paddingLeft: 1 }));
|
|
373
|
+
if (claimed) rows.push(textNode(" claimed: " + truncStr(claimed, pw - 16), theme().textMuted));
|
|
374
|
+
const workers = workerByCoord.get(c.id) || [];
|
|
375
|
+
for (let wi = 0; wi < workers.length; wi++) {
|
|
376
|
+
const wk = workers[wi];
|
|
377
|
+
const prefix = wi === workers.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
|
|
378
|
+
const wClaimed = (wk.claimedFiles || []).map((cl) => cl.path).join(", ");
|
|
379
|
+
rows.push(hbox([
|
|
380
|
+
textNode(prefix, theme().textMuted),
|
|
381
|
+
textNode("\u25CB", theme().primary),
|
|
382
|
+
textNode(truncStr(wk.harness || "worker", 12), theme().text),
|
|
383
|
+
textNode(truncStr(wk.id, 8), theme().textMuted),
|
|
384
|
+
textNode("pid:" + (wk.pid || "?"), theme().textMuted),
|
|
385
|
+
], { gap: 1, paddingLeft: 2 }));
|
|
386
|
+
if (wClaimed) rows.push(textNode(" claimed: " + truncStr(wClaimed, pw - 18), theme().textMuted));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const unattached = sess.filter((s) => s.role === "worker" && !s.coordinatorId);
|
|
390
|
+
for (const wk of unattached) {
|
|
391
|
+
rows.push(hbox([
|
|
392
|
+
textNode("\u25CB", theme().warning),
|
|
393
|
+
textNode(truncStr(wk.harness || "worker", 12), theme().text),
|
|
394
|
+
textNode(truncStr(wk.id, 8), theme().textMuted),
|
|
395
|
+
textNode("unattached", theme().warning),
|
|
396
|
+
], { gap: 1, paddingLeft: 1 }));
|
|
397
|
+
}
|
|
398
|
+
} else if (view() === "timeline") {
|
|
399
|
+
if (evts.length === 0) rows.push(textNode("No events recorded", theme().textMuted));
|
|
400
|
+
for (const e of evts) {
|
|
401
|
+
const ts = e.ts ? new Date(e.ts) : null;
|
|
402
|
+
const timeStr = ts ? ts.toTimeString().slice(0, 8) : "?";
|
|
403
|
+
const isInjected = e.result === "injected";
|
|
404
|
+
const isDedup = e.result === "dedup";
|
|
405
|
+
const isHb = e.result === "heartbeat";
|
|
406
|
+
const resultFg = isInjected ? theme().success : isDedup ? theme().warning : isHb ? theme().textMuted : theme().text;
|
|
407
|
+
const resultLabel = isInjected ? "injected" : isDedup ? "dedup" : isHb ? "hb" : "ok";
|
|
408
|
+
const tok = e.tokens || 0;
|
|
409
|
+
const lineChildren = [
|
|
410
|
+
textNode(timeStr, theme().textMuted),
|
|
411
|
+
textNode(truncStr(e.trigger || "?", 10), theme().text),
|
|
412
|
+
textNode(resultLabel, resultFg),
|
|
413
|
+
];
|
|
414
|
+
if (e.file) lineChildren.push(textNode(truncStr(basename(e.file), 16), theme().text));
|
|
415
|
+
if (tok > 0) lineChildren.push(boldNode("+" + formatTokenCount(estimateTokens(tok)) + " tok", theme().primary));
|
|
416
|
+
rows.push(hbox(lineChildren, { gap: 1, paddingLeft: 1 }));
|
|
417
|
+
}
|
|
418
|
+
} else if (view() === "locks") {
|
|
419
|
+
const byFile = new Map();
|
|
420
|
+
for (const s of sess) {
|
|
421
|
+
for (const cl of (s.claimedFiles || [])) {
|
|
422
|
+
const owners = byFile.get(cl.path) || [];
|
|
423
|
+
owners.push(s);
|
|
424
|
+
byFile.set(cl.path, owners);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (byFile.size === 0) rows.push(textNode("No files claimed", theme().textMuted));
|
|
428
|
+
for (const [path, owners] of byFile) {
|
|
429
|
+
const hasConflict = owners.length > 1;
|
|
430
|
+
const fg = hasConflict ? theme().error : theme().text;
|
|
431
|
+
rows.push(hbox([
|
|
432
|
+
textNode(hasConflict ? "\u26A0" : "\u25CF", fg),
|
|
433
|
+
textNode(truncStr(path, pw - 8), fg),
|
|
434
|
+
], { gap: 1, paddingLeft: 1 }));
|
|
435
|
+
for (const o of owners) {
|
|
436
|
+
rows.push(hbox([
|
|
437
|
+
textNode("\u2514\u2500", theme().textMuted),
|
|
438
|
+
textNode(truncStr(o.harness || "?", 10), theme().text),
|
|
439
|
+
textNode(truncStr(o.id, 8), theme().textMuted),
|
|
440
|
+
textNode(o.role || "agent", theme().textMuted),
|
|
441
|
+
], { gap: 1, paddingLeft: 3 }));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else if (view() === "hooks") {
|
|
445
|
+
const evts = allEvents();
|
|
446
|
+
const totalInjected = evts.reduce((s, e) => s + (e.tokens || 0), 0);
|
|
447
|
+
if (totalInjected > 0) {
|
|
448
|
+
rows.push(hbox([
|
|
449
|
+
boldNode(formatTokenCount(estimateTokens(totalInjected)) + " tok", theme().primary),
|
|
450
|
+
textNode("injected", theme().textMuted),
|
|
451
|
+
textNode("\u2139 enter to expand", theme().textMuted),
|
|
452
|
+
], { gap: 1, paddingLeft: 1 }));
|
|
453
|
+
}
|
|
454
|
+
if (evts.length === 0) {
|
|
455
|
+
rows.push(textNode("No hook events recorded", theme().textMuted));
|
|
456
|
+
}
|
|
457
|
+
const expIdx = expandedHook();
|
|
458
|
+
for (let i = 0; i < evts.length; i++) {
|
|
459
|
+
const e = evts[i];
|
|
460
|
+
const ago = formatTimeAgo(e.ts);
|
|
461
|
+
const isInjected = e.result === "injected";
|
|
462
|
+
const isDedup = e.result === "dedup";
|
|
463
|
+
const isClaimed = e.result === "claimed";
|
|
464
|
+
const isHb = e.result === "heartbeat";
|
|
465
|
+
const resultDotFg = isInjected ? theme().primary : isDedup ? theme().textMuted : isClaimed ? theme().warning : isHb ? theme().textMuted : theme().success;
|
|
466
|
+
const triggerStr = truncStr(e.trigger, 10);
|
|
467
|
+
const toolStr = e.tool ? truncStr(e.tool, 8) : "";
|
|
468
|
+
const fileStr = e.file ? truncStr(basename(e.file), 20) : "";
|
|
469
|
+
const tok = e.tokens || 0;
|
|
470
|
+
const tokStr = tok > 0 ? "+" + formatTokenCount(estimateTokens(tok)) + " tok" : "";
|
|
471
|
+
const resultLabel = isInjected ? "injected" : isDedup ? "dedup" : isClaimed ? "claimed" : isHb ? "hb" : "ok";
|
|
472
|
+
|
|
473
|
+
const lineChildren = [
|
|
474
|
+
dot(resultDotFg),
|
|
475
|
+
textNode(ago, theme().textMuted),
|
|
476
|
+
textNode(triggerStr, theme().text),
|
|
477
|
+
];
|
|
478
|
+
if (toolStr) lineChildren.push(textNode(toolStr, theme().text));
|
|
479
|
+
if (fileStr) lineChildren.push(textNode(fileStr, theme().text));
|
|
480
|
+
if (tokStr) lineChildren.push(boldNode(tokStr, theme().primary));
|
|
481
|
+
else lineChildren.push(textNode(resultLabel, resultDotFg));
|
|
482
|
+
if (isInjected) {
|
|
483
|
+
lineChildren.push(textNode(expIdx === i ? "\u25BC" : "\u25B6", theme().textMuted));
|
|
484
|
+
}
|
|
485
|
+
rows.push(hbox(lineChildren, { gap: 1, paddingLeft: 1 }));
|
|
486
|
+
|
|
487
|
+
if (isInjected && expIdx === i) {
|
|
488
|
+
const payloadFile = e.payload;
|
|
489
|
+
if (payloadFile) {
|
|
490
|
+
try {
|
|
491
|
+
const content = readFileSync(join(INJECT_LOG_DIR, payloadFile), "utf-8");
|
|
492
|
+
const plines = content.split("\n").slice(0, 8);
|
|
493
|
+
for (const line of plines) {
|
|
494
|
+
rows.push(hbox([
|
|
495
|
+
textNode("\u2502 ", theme().textMuted),
|
|
496
|
+
textNode(line.length > pw - 8 ? line.slice(0, pw - 11) + "..." : line, theme().textMuted),
|
|
497
|
+
], { paddingLeft: 3 }));
|
|
498
|
+
}
|
|
499
|
+
if (content.split("\n").length > 8) {
|
|
500
|
+
rows.push(hbox([textNode("\u2502 ... (" + content.length + " bytes)", theme().textMuted)], { paddingLeft: 3 }));
|
|
501
|
+
}
|
|
502
|
+
} catch {
|
|
503
|
+
rows.push(hbox([textNode("\u2502 (payload file not found)", theme().textMuted)], { paddingLeft: 3 }));
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
rows.push(hbox([textNode("\u2502 (no payload captured)", theme().textMuted)], { paddingLeft: 3 }));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const totalRows = rows.length;
|
|
513
|
+
const cur = Math.min(cursor(), Math.max(0, totalRows - 1));
|
|
514
|
+
if (cur !== cursor()) setCursor(cur);
|
|
515
|
+
const vr = Math.max(1, visibleRows() - headerLineCount);
|
|
516
|
+
let so = scrollOffset();
|
|
517
|
+
if (cur < so) so = cur;
|
|
518
|
+
else if (cur >= so + vr) so = cur - vr + 1;
|
|
519
|
+
if (so !== scrollOffset()) setScrollOffset(so);
|
|
520
|
+
const sliced = rows.slice(so, so + vr);
|
|
521
|
+
|
|
522
|
+
const cursorBarBg = RGBA.fromInts(60, 60, 120, 80);
|
|
523
|
+
|
|
524
|
+
const body = [];
|
|
525
|
+
for (let i = 0; i < vr; i++) {
|
|
526
|
+
const rowIdx = so + i;
|
|
527
|
+
const isCursorRow = rowIdx === cur && rowIdx < totalRows;
|
|
528
|
+
const rowEl = sliced[i] || textNode("", theme().textMuted);
|
|
529
|
+
if (isCursorRow) {
|
|
530
|
+
const highlight = el("box");
|
|
531
|
+
spread(highlight, {
|
|
532
|
+
backgroundColor: cursorBarBg,
|
|
533
|
+
paddingLeft: 1,
|
|
534
|
+
width: pw,
|
|
535
|
+
});
|
|
536
|
+
insert(highlight, rowEl);
|
|
537
|
+
body.push(highlight);
|
|
538
|
+
} else {
|
|
539
|
+
body.push(rowEl);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const footer = [];
|
|
544
|
+
if (conflicts.length > 0) {
|
|
545
|
+
footer.push(hbox([
|
|
546
|
+
textNode("\u26A0 " + conflicts.length + " conflict" + (conflicts.length > 1 ? "s" : ""), theme().error),
|
|
547
|
+
], { paddingTop: 1 }));
|
|
548
|
+
}
|
|
549
|
+
footer.push(textNode("\u2500".repeat(pw), RGBA.fromInts(60, 60, 60, 255)));
|
|
550
|
+
footer.push(hbox([
|
|
551
|
+
textNode("[esc] close", theme().textMuted),
|
|
552
|
+
textNode("[\u2191\u2193] navigate", theme().textMuted),
|
|
553
|
+
textNode("[\u2190\u2192] views", theme().textMuted),
|
|
554
|
+
view() === "hooks" ? textNode("[enter] expand", theme().textMuted) : textNode("", theme().textMuted),
|
|
555
|
+
], { gap: 2 }));
|
|
556
|
+
|
|
557
|
+
return vbox([...header, ...body, ...footer], {});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
return root;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function renderPmdPanel(api, sessionId) {
|
|
564
|
+
const theme = () => api.theme.current;
|
|
565
|
+
|
|
566
|
+
const [tick, setTick] = createSignal(Date.now());
|
|
567
|
+
const timer = setInterval(() => setTick(Date.now()), POLL_MS);
|
|
568
|
+
onCleanup(() => clearInterval(timer));
|
|
569
|
+
|
|
570
|
+
const [logOpen, setLogOpen] = createSignal(true);
|
|
571
|
+
const [expandedIdx, setExpandedIdx] = createSignal(-1);
|
|
572
|
+
|
|
573
|
+
const stats = createMemo(() => { tick(); return tryReadJson(STATS_PATH); });
|
|
574
|
+
const sessions = createMemo(() => { tick(); return readSessions(); });
|
|
575
|
+
const daemonPid = createMemo(() => { tick(); return tryReadPid(); });
|
|
576
|
+
const conflicts = createMemo(() => findConflicts(sessions()));
|
|
577
|
+
const deliveryMode = createMemo(() => stats()?.deliveryMode || "passive");
|
|
578
|
+
const allEvents = createMemo(() => {
|
|
579
|
+
const raw = stats()?.events || [];
|
|
580
|
+
return raw.slice().reverse();
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const dashboard = createMemo(() => { tick(); return tryReadJson(DASHBOARD_PATH); });
|
|
584
|
+
const passiveAgents = createMemo(() => {
|
|
585
|
+
const db = dashboard();
|
|
586
|
+
if (db && db.crew && Array.isArray(db.crew.passiveAgents)
|
|
587
|
+
&& typeof db.ts === "number" && Date.now() - db.ts < 30000) {
|
|
588
|
+
return db.crew.passiveAgents.slice(0, 8);
|
|
589
|
+
}
|
|
590
|
+
const raw = stats()?.passiveAgents || [];
|
|
591
|
+
return raw.slice(0, 8);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const lastError = createMemo(() => { tick(); return readLastError(); });
|
|
595
|
+
|
|
596
|
+
const root = el("box");
|
|
597
|
+
spread(root, { flexDirection: "column", gap: 0 });
|
|
598
|
+
|
|
599
|
+
insert(root, () => {
|
|
600
|
+
tick();
|
|
601
|
+
logOpen();
|
|
602
|
+
|
|
603
|
+
const daemonOk = daemonPid() !== null;
|
|
604
|
+
const sess = sessions();
|
|
605
|
+
const conflictList = findConflicts(sess);
|
|
606
|
+
const evts = allEvents();
|
|
607
|
+
const dMode = deliveryMode();
|
|
608
|
+
const pAgents = passiveAgents();
|
|
609
|
+
const st = stats();
|
|
610
|
+
const err = lastError();
|
|
611
|
+
const sessionCount = sess.length;
|
|
612
|
+
const passiveCount = pAgents.length;
|
|
613
|
+
const conflictCount = conflictList.length;
|
|
614
|
+
const hooksFired = st?.hooksFired || 0;
|
|
615
|
+
const claimsMade = st?.claimsMade || 0;
|
|
616
|
+
const injectionsDelivered = st?.injectionsDelivered || 0;
|
|
617
|
+
const dedupHits = st?.dedupHits || 0;
|
|
618
|
+
const eventCount = evts.length;
|
|
619
|
+
const ci = findContextSize();
|
|
620
|
+
const contextTokens = ci ? estimateTokens(ci.bytes) : 0;
|
|
621
|
+
const isOpen = logOpen();
|
|
622
|
+
|
|
623
|
+
const children = [];
|
|
624
|
+
|
|
625
|
+
// ── Header: PipeMD ● running active ──
|
|
626
|
+
const headerChildren = [];
|
|
627
|
+
headerChildren.push(boldNode("PipeMD", theme().primary));
|
|
628
|
+
headerChildren.push(dot(daemonOk ? theme().success : theme().error));
|
|
629
|
+
headerChildren.push(textNode(daemonOk ? "running" : "stopped", theme().textMuted));
|
|
630
|
+
if (dMode !== "passive") {
|
|
631
|
+
headerChildren.push(textNode(dMode, theme().textMuted));
|
|
632
|
+
}
|
|
633
|
+
children.push(hbox(headerChildren, { gap: 1 }));
|
|
634
|
+
|
|
635
|
+
// ── Daemon PID ──
|
|
636
|
+
if (daemonOk) {
|
|
637
|
+
children.push(textNode("pid " + daemonPid(), theme().textMuted));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ── Context tokens ──
|
|
641
|
+
if (ci) {
|
|
642
|
+
const tokenFg = contextTokens > 50000 ? theme().error : contextTokens > 20000 ? theme().warning : theme().success;
|
|
643
|
+
children.push(hbox([
|
|
644
|
+
textNode("context", theme().textMuted),
|
|
645
|
+
boldNode(formatTokenCount(contextTokens) + " tok", tokenFg),
|
|
646
|
+
textNode(formatBytes(ci.bytes), theme().textMuted),
|
|
647
|
+
], { gap: 1 }));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ── Plugin error ──
|
|
651
|
+
if (err) {
|
|
652
|
+
const errLines = (err.error || "").split("\n").slice(0, 2);
|
|
653
|
+
children.push(hbox([
|
|
654
|
+
textNode("\u26A0 plugin error", theme().error),
|
|
655
|
+
], { gap: 1, paddingTop: 1 }));
|
|
656
|
+
for (const line of errLines) {
|
|
657
|
+
children.push(hbox([
|
|
658
|
+
textNode(" " + truncStr(line, 36), theme().textMuted),
|
|
659
|
+
]));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ── Conflicts ──
|
|
664
|
+
if (conflictCount > 0) {
|
|
665
|
+
children.push(hbox([
|
|
666
|
+
textNode("\u26A0 " + conflictCount + " conflict" + (conflictCount > 1 ? "s" : ""), theme().error),
|
|
667
|
+
]));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ── Agents (active crew + passive) ──
|
|
671
|
+
const totalAgents = sessionCount + passiveCount;
|
|
672
|
+
if (totalAgents > 0) {
|
|
673
|
+
const agentsHeaderChildren = [];
|
|
674
|
+
agentsHeaderChildren.push(boldNode("Agents", theme().text));
|
|
675
|
+
agentsHeaderChildren.push(dot(theme().success));
|
|
676
|
+
agentsHeaderChildren.push(textNode(sessionCount + " active", theme().success));
|
|
677
|
+
agentsHeaderChildren.push(dot(theme().warning));
|
|
678
|
+
agentsHeaderChildren.push(textNode(passiveCount + " passive", theme().warning));
|
|
679
|
+
children.push(hbox(agentsHeaderChildren, { gap: 1, paddingTop: 1 }));
|
|
680
|
+
|
|
681
|
+
const coordSessions = sess.filter((s) => s.role !== "worker");
|
|
682
|
+
const workerByCoord = new Map();
|
|
683
|
+
for (const w of sess) {
|
|
684
|
+
if (w.role === "worker" && w.coordinatorId) {
|
|
685
|
+
const list = workerByCoord.get(w.coordinatorId) || [];
|
|
686
|
+
list.push(w);
|
|
687
|
+
workerByCoord.set(w.coordinatorId, list);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const attachedWorkerIds = new Set();
|
|
691
|
+
const totalCrew = sess.length;
|
|
692
|
+
let shown = 0;
|
|
693
|
+
for (const c of coordSessions) {
|
|
694
|
+
if (shown >= MAX_SESSIONS) break;
|
|
695
|
+
shown++;
|
|
696
|
+
const claimed = (c.claimedFiles || []).map((cl) => cl.path).join(", ");
|
|
697
|
+
const lineChildren = [
|
|
698
|
+
dot(theme().success),
|
|
699
|
+
textNode(truncStr(c.harness, 14), theme().text),
|
|
700
|
+
textNode("coord", theme().textMuted),
|
|
701
|
+
];
|
|
702
|
+
children.push(hbox(lineChildren, { gap: 1, paddingLeft: 1 }));
|
|
703
|
+
if (claimed) {
|
|
704
|
+
children.push(textNode(" claimed: " + truncStr(claimed, 36), theme().textMuted));
|
|
705
|
+
}
|
|
706
|
+
const workers = workerByCoord.get(c.id) || [];
|
|
707
|
+
const visibleWorkers = workers.slice(0, MAX_SESSIONS - shown);
|
|
708
|
+
for (let wi = 0; wi < visibleWorkers.length; wi++) {
|
|
709
|
+
shown++;
|
|
710
|
+
attachedWorkerIds.add(visibleWorkers[wi].id);
|
|
711
|
+
const w = visibleWorkers[wi];
|
|
712
|
+
const hasMore = workers.length > visibleWorkers.length;
|
|
713
|
+
const isLast = wi === visibleWorkers.length - 1;
|
|
714
|
+
const prefix = isLast && !hasMore ? "\u2514\u2500" : "\u251C\u2500";
|
|
715
|
+
const wClaimed = (w.claimedFiles || []).map((cl) => cl.path).join(", ");
|
|
716
|
+
const wLabel = w.label ? truncStr(w.label, 16) : truncStr(w.id, 10);
|
|
717
|
+
const wLineChildren = [
|
|
718
|
+
textNode(prefix, theme().textMuted),
|
|
719
|
+
dot(theme().primary),
|
|
720
|
+
textNode(truncStr(w.harness || "worker", 12), theme().text),
|
|
721
|
+
textNode(wLabel, theme().textMuted),
|
|
722
|
+
];
|
|
723
|
+
children.push(hbox(wLineChildren, { gap: 1, paddingLeft: 2 }));
|
|
724
|
+
if (wClaimed) {
|
|
725
|
+
children.push(textNode(" claimed: " + truncStr(wClaimed, 34), theme().textMuted));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const hiddenWorkers = workers.length - visibleWorkers.length;
|
|
729
|
+
if (hiddenWorkers > 0) {
|
|
730
|
+
shown += hiddenWorkers;
|
|
731
|
+
children.push(hbox([
|
|
732
|
+
textNode("\u2514\u2500", theme().textMuted),
|
|
733
|
+
textNode("+" + hiddenWorkers + " more worker" + (hiddenWorkers > 1 ? "s" : ""), theme().textMuted),
|
|
734
|
+
], { gap: 1, paddingLeft: 2 }));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const unattached = sess.filter((s) => s.role === "worker" && !attachedWorkerIds.has(s.id));
|
|
738
|
+
for (const w of unattached.slice(0, Math.max(0, MAX_SESSIONS - shown))) {
|
|
739
|
+
shown++;
|
|
740
|
+
const wClaimed = (w.claimedFiles || []).map((cl) => cl.path).join(", ");
|
|
741
|
+
const wLineChildren = [
|
|
742
|
+
dot(theme().warning),
|
|
743
|
+
textNode(truncStr(w.harness || "worker", 12), theme().text),
|
|
744
|
+
textNode("worker", theme().textMuted),
|
|
745
|
+
];
|
|
746
|
+
children.push(hbox(wLineChildren, { gap: 1, paddingLeft: 1 }));
|
|
747
|
+
if (wClaimed) {
|
|
748
|
+
children.push(textNode(" claimed: " + truncStr(wClaimed, 36), theme().textMuted));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const hidden = totalCrew - shown;
|
|
752
|
+
if (hidden > 0) {
|
|
753
|
+
children.push(hbox([
|
|
754
|
+
textNode("\u2022 +" + hidden + " more session" + (hidden > 1 ? "s" : ""), theme().textMuted),
|
|
755
|
+
], { gap: 1, paddingLeft: 1 }));
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
for (const pa of pAgents) {
|
|
759
|
+
const lineChildren = [
|
|
760
|
+
dot(theme().warning),
|
|
761
|
+
textNode(truncStr(pa, 32), theme().text),
|
|
762
|
+
];
|
|
763
|
+
children.push(hbox(lineChildren, { gap: 1, paddingLeft: 1 }));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ── Hook log (retractable) ──
|
|
768
|
+
if (evts.length > 0) {
|
|
769
|
+
const logHeaderChildren = [];
|
|
770
|
+
const arrow = el("text");
|
|
771
|
+
spread(arrow, { fg: theme().text });
|
|
772
|
+
insert(arrow, txt(isOpen ? "\u25BC" : "\u25B6"));
|
|
773
|
+
logHeaderChildren.push(arrow);
|
|
774
|
+
logHeaderChildren.push(boldNode("Hook Log", theme().text));
|
|
775
|
+
logHeaderChildren.push(textNode(evts.length + " events", theme().textMuted));
|
|
776
|
+
if (!isOpen && hooksFired > evts.length) {
|
|
777
|
+
logHeaderChildren.push(textNode("(" + hooksFired + " total)", theme().textMuted));
|
|
778
|
+
}
|
|
779
|
+
children.push(hbox(logHeaderChildren, { gap: 1, paddingTop: 1, onMouseDown: () => setLogOpen((x) => !x) }));
|
|
780
|
+
|
|
781
|
+
if (isOpen) {
|
|
782
|
+
const totalInjected = evts.reduce((s, e) => s + (e.tokens || 0), 0);
|
|
783
|
+
if (totalInjected > 0) {
|
|
784
|
+
children.push(hbox([
|
|
785
|
+
boldNode(formatTokenCount(estimateTokens(totalInjected)) + " tok", theme().primary),
|
|
786
|
+
textNode("injected", theme().textMuted),
|
|
787
|
+
textNode("\u2139 click row", theme().textMuted),
|
|
788
|
+
], { gap: 1, paddingLeft: 1 }));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const expIdx = expandedIdx();
|
|
792
|
+
|
|
793
|
+
for (let i = 0; i < evts.length; i++) {
|
|
794
|
+
const e = evts[i];
|
|
795
|
+
const ago = formatTimeAgo(e.ts);
|
|
796
|
+
const isInjected = e.result === "injected";
|
|
797
|
+
const isDedup = e.result === "dedup";
|
|
798
|
+
const isClaimed = e.result === "claimed";
|
|
799
|
+
const isHeartbeat = e.result === "heartbeat";
|
|
800
|
+
const resultDotFg = isInjected ? theme().primary : isDedup ? theme().textMuted : isClaimed ? theme().warning : isHeartbeat ? theme().textMuted : theme().success;
|
|
801
|
+
const triggerStr = truncStr(e.trigger, 8);
|
|
802
|
+
const toolStr = e.tool ? truncStr(e.tool, 6) : "";
|
|
803
|
+
const fileStr = e.file ? truncStr(basename(e.file), 14) : "";
|
|
804
|
+
const tok = e.tokens || 0;
|
|
805
|
+
const tokStr = tok > 0 ? "+" + formatTokenCount(estimateTokens(tok)) + " tok" : "";
|
|
806
|
+
const resultLabel = isInjected ? "injected" : isDedup ? "dedup" : isClaimed ? "claimed" : isHeartbeat ? "hb" : "ok";
|
|
807
|
+
|
|
808
|
+
const lineChildren = [
|
|
809
|
+
dot(resultDotFg),
|
|
810
|
+
textNode(ago, theme().textMuted),
|
|
811
|
+
textNode(triggerStr, theme().text),
|
|
812
|
+
];
|
|
813
|
+
if (toolStr) lineChildren.push(textNode(toolStr, theme().text));
|
|
814
|
+
if (fileStr) lineChildren.push(textNode(fileStr, theme().text));
|
|
815
|
+
if (tokStr) lineChildren.push(boldNode(tokStr, theme().primary));
|
|
816
|
+
else lineChildren.push(textNode(resultLabel, resultDotFg));
|
|
817
|
+
if (isInjected) {
|
|
818
|
+
lineChildren.push(textNode(expIdx === i ? "\u25BC" : "\u25B6", theme().textMuted));
|
|
819
|
+
}
|
|
820
|
+
children.push(hbox(lineChildren, {
|
|
821
|
+
gap: 1,
|
|
822
|
+
paddingLeft: 1,
|
|
823
|
+
onMouseDown: isInjected ? (() => { const idx = i; return () => setExpandedIdx((prev) => prev === idx ? -1 : idx); })() : undefined,
|
|
824
|
+
}));
|
|
825
|
+
|
|
826
|
+
if (isInjected && expIdx === i) {
|
|
827
|
+
const payloadFile = e.payload;
|
|
828
|
+
if (payloadFile) {
|
|
829
|
+
try {
|
|
830
|
+
const content = readFileSync(join(INJECT_LOG_DIR, payloadFile), "utf-8");
|
|
831
|
+
const lines = content.split("\n").slice(0, 6);
|
|
832
|
+
for (const line of lines) {
|
|
833
|
+
children.push(hbox([
|
|
834
|
+
textNode("\u2502 ", theme().textMuted),
|
|
835
|
+
textNode(line.length > 34 ? line.slice(0, 31) + "..." : line, theme().textMuted),
|
|
836
|
+
], { paddingLeft: 3 }));
|
|
837
|
+
}
|
|
838
|
+
if (content.split("\n").length > 6) {
|
|
839
|
+
children.push(hbox([textNode("\u2502 ... (" + content.length + " bytes)", theme().textMuted)], { paddingLeft: 3 }));
|
|
840
|
+
}
|
|
841
|
+
} catch {
|
|
842
|
+
children.push(hbox([textNode("\u2502 (payload file not found)", theme().textMuted)], { paddingLeft: 3 }));
|
|
843
|
+
}
|
|
844
|
+
} else {
|
|
845
|
+
children.push(hbox([textNode("\u2502 (no payload captured)", theme().textMuted)], { paddingLeft: 3 }));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ── Stats footer ──
|
|
853
|
+
const footerLine1 = [
|
|
854
|
+
textNode(hooksFired + " hooks", theme().textMuted),
|
|
855
|
+
dot(theme().textMuted),
|
|
856
|
+
textNode(claimsMade + " claims", theme().textMuted),
|
|
857
|
+
dot(theme().textMuted),
|
|
858
|
+
textNode(eventCount + " events", theme().textMuted),
|
|
859
|
+
];
|
|
860
|
+
children.push(hbox(footerLine1, { gap: 1, paddingTop: 1 }));
|
|
861
|
+
if (dMode !== "passive") {
|
|
862
|
+
children.push(hbox([
|
|
863
|
+
textNode(injectionsDelivered + " sent", theme().textMuted),
|
|
864
|
+
dot(theme().textMuted),
|
|
865
|
+
textNode(dedupHits + " deduped", theme().textMuted),
|
|
866
|
+
], { gap: 1 }));
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return vbox(children, {});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
return root;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export default {
|
|
876
|
+
id: "pmd-crew-tui",
|
|
877
|
+
tui: async (api) => {
|
|
878
|
+
api.slots.register({
|
|
879
|
+
order: 250,
|
|
880
|
+
slots: {
|
|
881
|
+
sidebar_content(_ctx, props) {
|
|
882
|
+
if (!existsSync(".pipemd/config.yml")) return null;
|
|
883
|
+
return renderPmdPanel(api, props.session_id);
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
api.command.register(() => [{
|
|
889
|
+
title: "PipeMD: Resolution Trace",
|
|
890
|
+
value: "pmd-trace",
|
|
891
|
+
keybind: "alt+p",
|
|
892
|
+
category: "PipeMD",
|
|
893
|
+
onSelect: () => {
|
|
894
|
+
if (api.route.current.name === "pmd-trace") {
|
|
895
|
+
const prev = savedRoute;
|
|
896
|
+
savedRoute = null;
|
|
897
|
+
if (prev && prev.name === "session" && prev.params && prev.params.sessionID) {
|
|
898
|
+
api.route.navigate("session", { sessionID: prev.params.sessionID });
|
|
899
|
+
} else {
|
|
900
|
+
api.route.navigate("home");
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
savedRoute = api.route.current;
|
|
904
|
+
api.route.navigate("pmd-trace");
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
}]);
|
|
908
|
+
|
|
909
|
+
api.route.register([{
|
|
910
|
+
name: "pmd-trace",
|
|
911
|
+
render: () => renderTraceRoute(api),
|
|
912
|
+
}]);
|
|
913
|
+
},
|
|
914
|
+
};
|