pilotswarm-cli 0.1.10 → 0.1.12
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/bin/tui.js +6 -3
- package/cli/context-usage.js +80 -0
- package/cli/tui.js +1990 -613
- package/package.json +2 -1
package/cli/tui.js
CHANGED
|
@@ -20,10 +20,18 @@ import { markedTerminal } from "marked-terminal";
|
|
|
20
20
|
import chalk from "chalk";
|
|
21
21
|
import fs from "node:fs";
|
|
22
22
|
import path from "node:path";
|
|
23
|
-
import { spawn } from "node:child_process";
|
|
23
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { performance } from "node:perf_hooks";
|
|
26
26
|
import os from "node:os";
|
|
27
|
+
import {
|
|
28
|
+
computeContextPercent,
|
|
29
|
+
formatTokenCount,
|
|
30
|
+
formatContextHeaderBadge,
|
|
31
|
+
formatContextListBadge,
|
|
32
|
+
formatContextCompactionBadge,
|
|
33
|
+
formatCompactionActivityMarkup,
|
|
34
|
+
} from "./context-usage.js";
|
|
27
35
|
|
|
28
36
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
37
|
|
|
@@ -56,6 +64,102 @@ const BASE_TUI_TITLE = (process.env._TUI_TITLE || "PilotSwarm").trim() || "Pilot
|
|
|
56
64
|
const CUSTOM_TUI_SPLASH = process.env._TUI_SPLASH?.trim() || "";
|
|
57
65
|
const ACTIVE_STARTUP_SPLASH_CONTENT = CUSTOM_TUI_SPLASH || STARTUP_SPLASH_CONTENT;
|
|
58
66
|
const HAS_CUSTOM_TUI_BRANDING = BASE_TUI_TITLE !== "PilotSwarm" || Boolean(CUSTOM_TUI_SPLASH);
|
|
67
|
+
const TERMINAL_RESET_ESCAPE = "\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h";
|
|
68
|
+
const SIGNAL_EXIT_CODES = {
|
|
69
|
+
SIGINT: 130,
|
|
70
|
+
SIGTERM: 143,
|
|
71
|
+
SIGHUP: 129,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let localEmbeddedRuntimeLockPath = null;
|
|
75
|
+
let shutdownInProgress = false;
|
|
76
|
+
|
|
77
|
+
function isProcessAlive(pid) {
|
|
78
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
79
|
+
try {
|
|
80
|
+
process.kill(pid, 0);
|
|
81
|
+
return true;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return err?.code === "EPERM";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getLocalEmbeddedRuntimeLockPath() {
|
|
88
|
+
const sessionStateDir = process.env.SESSION_STATE_DIR || path.join(os.homedir(), ".copilot", "session-state");
|
|
89
|
+
return path.join(path.dirname(sessionStateDir), "embedded-local-runtime.lock.json");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function releaseLocalEmbeddedRuntimeLock() {
|
|
93
|
+
if (!localEmbeddedRuntimeLockPath) return;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const raw = fs.readFileSync(localEmbeddedRuntimeLockPath, "utf-8");
|
|
97
|
+
const owner = JSON.parse(raw);
|
|
98
|
+
if (Number(owner?.pid) && Number(owner.pid) !== process.pid) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
} catch {}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
fs.rmSync(localEmbeddedRuntimeLockPath, { force: true });
|
|
105
|
+
} catch {}
|
|
106
|
+
localEmbeddedRuntimeLockPath = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function acquireLocalEmbeddedRuntimeLock(store) {
|
|
110
|
+
const lockPath = getLocalEmbeddedRuntimeLockPath();
|
|
111
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
112
|
+
|
|
113
|
+
const lockBody = JSON.stringify({
|
|
114
|
+
pid: process.pid,
|
|
115
|
+
mode: "local-embedded",
|
|
116
|
+
cwd: process.cwd(),
|
|
117
|
+
store,
|
|
118
|
+
acquiredAt: new Date().toISOString(),
|
|
119
|
+
}, null, 2);
|
|
120
|
+
|
|
121
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
122
|
+
try {
|
|
123
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
124
|
+
fs.writeFileSync(fd, lockBody, "utf-8");
|
|
125
|
+
fs.closeSync(fd);
|
|
126
|
+
localEmbeddedRuntimeLockPath = lockPath;
|
|
127
|
+
return;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (err?.code !== "EEXIST") throw err;
|
|
130
|
+
|
|
131
|
+
let owner = null;
|
|
132
|
+
try {
|
|
133
|
+
owner = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
|
|
134
|
+
} catch {}
|
|
135
|
+
|
|
136
|
+
const ownerPid = Number(owner?.pid);
|
|
137
|
+
if (ownerPid && ownerPid !== process.pid && isProcessAlive(ownerPid)) {
|
|
138
|
+
const ownerStore = owner?.store ? ` (${owner.store})` : "";
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Another embedded local PilotSwarm TUI is already running (pid ${ownerPid})${ownerStore}. ` +
|
|
141
|
+
`Close it before starting a second local TUI. Remote mode still supports multiple windows.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
fs.rmSync(lockPath, { force: true });
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error("Failed to acquire the embedded local runtime lock.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function exitProcessNow(code) {
|
|
155
|
+
releaseLocalEmbeddedRuntimeLock();
|
|
156
|
+
try { fs.writeSync(1, Buffer.from(TERMINAL_RESET_ESCAPE)); } catch {}
|
|
157
|
+
process.exit(code);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
process.on("exit", () => {
|
|
161
|
+
releaseLocalEmbeddedRuntimeLock();
|
|
162
|
+
});
|
|
59
163
|
|
|
60
164
|
function formatWindowTitle(detail) {
|
|
61
165
|
return detail ? `${BASE_TUI_TITLE} (${detail})` : BASE_TUI_TITLE;
|
|
@@ -79,6 +183,102 @@ function applyWindowTitle(title) {
|
|
|
79
183
|
applyTerminalTitle(title);
|
|
80
184
|
}
|
|
81
185
|
|
|
186
|
+
function enableMouseTracking(screenInstance) {
|
|
187
|
+
if (!screenInstance?.program?.input?.isTTY || !screenInstance?.program?.output?.isTTY) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
// neo-blessed only auto-enables mouse for a narrow set of TERM values.
|
|
193
|
+
// Request the common xterm-compatible modes explicitly so drag selection
|
|
194
|
+
// still works in newer terminals and IDE-integrated terminals.
|
|
195
|
+
screenInstance.enableMouse();
|
|
196
|
+
screenInstance.program.setMouse({
|
|
197
|
+
vt200Mouse: true,
|
|
198
|
+
utfMouse: true,
|
|
199
|
+
sgrMouse: true,
|
|
200
|
+
cellMotion: true,
|
|
201
|
+
allMotion: true,
|
|
202
|
+
}, true);
|
|
203
|
+
} catch {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function openPathInDefaultApp(targetPath) {
|
|
207
|
+
if (!targetPath) return;
|
|
208
|
+
|
|
209
|
+
let command;
|
|
210
|
+
let args;
|
|
211
|
+
|
|
212
|
+
if (process.platform === "darwin") {
|
|
213
|
+
command = "open";
|
|
214
|
+
args = [targetPath];
|
|
215
|
+
} else if (process.platform === "win32") {
|
|
216
|
+
command = "cmd";
|
|
217
|
+
args = ["/c", "start", "", targetPath];
|
|
218
|
+
} else {
|
|
219
|
+
command = "xdg-open";
|
|
220
|
+
args = [targetPath];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
225
|
+
child.once("error", (err) => {
|
|
226
|
+
appendLog(`{red-fg}Open failed: ${err.message}{/red-fg}`);
|
|
227
|
+
setStatus("Open failed — check logs");
|
|
228
|
+
scheduleRender();
|
|
229
|
+
});
|
|
230
|
+
child.unref();
|
|
231
|
+
setStatus(`Opened: ${targetPath}`);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
appendLog(`{red-fg}Open failed: ${err.message}{/red-fg}`);
|
|
234
|
+
setStatus("Open failed — check logs");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let suppressPaneClicksUntil = 0;
|
|
239
|
+
|
|
240
|
+
function copyTextToClipboard(text, { onError } = {}) {
|
|
241
|
+
if (!text) return false;
|
|
242
|
+
|
|
243
|
+
let command;
|
|
244
|
+
let args;
|
|
245
|
+
|
|
246
|
+
if (process.platform === "darwin") {
|
|
247
|
+
command = "pbcopy";
|
|
248
|
+
args = [];
|
|
249
|
+
} else if (process.platform === "win32") {
|
|
250
|
+
command = "clip";
|
|
251
|
+
args = [];
|
|
252
|
+
} else if (process.env.WAYLAND_DISPLAY) {
|
|
253
|
+
command = "wl-copy";
|
|
254
|
+
args = [];
|
|
255
|
+
} else {
|
|
256
|
+
command = "xclip";
|
|
257
|
+
args = ["-selection", "clipboard"];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const result = spawnSync(command, args, {
|
|
262
|
+
input: text,
|
|
263
|
+
encoding: "utf8",
|
|
264
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
265
|
+
});
|
|
266
|
+
if (result.error) throw result.error;
|
|
267
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
268
|
+
throw new Error((result.stderr || "").trim() || `${command} exited with status ${result.status}`);
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
} catch (err) {
|
|
272
|
+
try {
|
|
273
|
+
if (screen?.copyToClipboard?.(text)) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
} catch {}
|
|
277
|
+
onError?.(err);
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
82
282
|
function hideTerminalCursor() {
|
|
83
283
|
try { screen?.program?.hideCursor(); } catch {}
|
|
84
284
|
}
|
|
@@ -137,6 +337,7 @@ const require = createRequire(import.meta.url);
|
|
|
137
337
|
const _origStderr = process.stderr.write.bind(process.stderr);
|
|
138
338
|
process.stderr.write = () => true;
|
|
139
339
|
const blessed = require("neo-blessed");
|
|
340
|
+
const blessedColors = require("neo-blessed/lib/colors");
|
|
140
341
|
process.stderr.write = _origStderr;
|
|
141
342
|
|
|
142
343
|
// ─── Monkey-patch neo-blessed emoji width ────────────────────────
|
|
@@ -212,6 +413,234 @@ function isMarkdownTableLine(line) {
|
|
|
212
413
|
|| /^\s*\|?[:\- ]+\|[:\-| ]+\|?\s*$/.test(line);
|
|
213
414
|
}
|
|
214
415
|
|
|
416
|
+
function getRenderablePaneWidth(pane, fallbackWidth) {
|
|
417
|
+
const numericWidth = typeof pane?.width === "number" ? pane.width : fallbackWidth;
|
|
418
|
+
const innerWidth = numericWidth - (pane?.iwidth || 2) - (pane?.scrollbar ? 1 : 0);
|
|
419
|
+
return Math.max(20, innerWidth);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function splitMarkdownTableCells(line) {
|
|
423
|
+
const trimmed = String(line || "").trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
424
|
+
const cells = [];
|
|
425
|
+
let current = "";
|
|
426
|
+
let escaping = false;
|
|
427
|
+
|
|
428
|
+
for (const ch of trimmed) {
|
|
429
|
+
if (escaping) {
|
|
430
|
+
current += ch;
|
|
431
|
+
escaping = false;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (ch === "\\") {
|
|
435
|
+
escaping = true;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (ch === "|") {
|
|
439
|
+
cells.push(current.trim());
|
|
440
|
+
current = "";
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
current += ch;
|
|
444
|
+
}
|
|
445
|
+
cells.push(current.trim());
|
|
446
|
+
return cells;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function isMarkdownTableSeparatorRow(cells) {
|
|
450
|
+
return cells.length > 0 && cells.every(cell => /^:?-{3,}:?$/.test(String(cell || "").replace(/\s+/g, "")));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function sliceTextToDisplayWidth(text, maxWidth) {
|
|
454
|
+
if (maxWidth <= 0) return "";
|
|
455
|
+
let out = "";
|
|
456
|
+
let used = 0;
|
|
457
|
+
for (const ch of String(text || "")) {
|
|
458
|
+
const chWidth = Math.max(1, displayWidth(ch));
|
|
459
|
+
if (used + chWidth > maxWidth) break;
|
|
460
|
+
out += ch;
|
|
461
|
+
used += chWidth;
|
|
462
|
+
}
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function wrapTableCellText(text, width) {
|
|
467
|
+
const normalized = String(text || "").replace(/\s+/g, " ").trim();
|
|
468
|
+
if (!normalized) return [""];
|
|
469
|
+
|
|
470
|
+
const words = normalized.split(" ");
|
|
471
|
+
const lines = [];
|
|
472
|
+
let current = "";
|
|
473
|
+
|
|
474
|
+
const appendWord = (word) => {
|
|
475
|
+
let remaining = word;
|
|
476
|
+
while (displayWidth(remaining) > width) {
|
|
477
|
+
const chunk = sliceTextToDisplayWidth(remaining, width);
|
|
478
|
+
if (!chunk) break;
|
|
479
|
+
if (current) {
|
|
480
|
+
lines.push(current);
|
|
481
|
+
current = "";
|
|
482
|
+
}
|
|
483
|
+
lines.push(chunk);
|
|
484
|
+
remaining = remaining.slice(chunk.length);
|
|
485
|
+
}
|
|
486
|
+
return remaining;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
for (const word of words) {
|
|
490
|
+
const fittedWord = appendWord(word);
|
|
491
|
+
if (!fittedWord) continue;
|
|
492
|
+
if (!current) {
|
|
493
|
+
current = fittedWord;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const candidate = `${current} ${fittedWord}`;
|
|
497
|
+
if (displayWidth(candidate) <= width) {
|
|
498
|
+
current = candidate;
|
|
499
|
+
} else {
|
|
500
|
+
lines.push(current);
|
|
501
|
+
current = fittedWord;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (current) lines.push(current);
|
|
506
|
+
return lines.length > 0 ? lines : [""];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function computeMarkdownTableColumnWidths(rows, maxWidth, separatorWidth) {
|
|
510
|
+
const columnCount = Math.max(1, ...rows.map(row => row.length));
|
|
511
|
+
const totalSeparatorWidth = typeof separatorWidth === "number"
|
|
512
|
+
? separatorWidth
|
|
513
|
+
: (columnCount - 1) * 3;
|
|
514
|
+
const availableWidth = Math.max(columnCount * 3, maxWidth - totalSeparatorWidth);
|
|
515
|
+
const preferred = Array.from({ length: columnCount }, (_, index) => Math.max(
|
|
516
|
+
3,
|
|
517
|
+
...rows.map(row => displayWidth(row[index] || "")),
|
|
518
|
+
));
|
|
519
|
+
const minimum = Array.from({ length: columnCount }, (_, index) => Math.max(
|
|
520
|
+
3,
|
|
521
|
+
Math.min(displayWidth(rows[0]?.[index] || "") || 3, 18),
|
|
522
|
+
));
|
|
523
|
+
|
|
524
|
+
if (columnCount === 2) {
|
|
525
|
+
const secondMinimum = Math.max(minimum[1], 16);
|
|
526
|
+
const firstCap = Math.max(minimum[0], Math.min(24, Math.floor(availableWidth * 0.32)));
|
|
527
|
+
let firstWidth = Math.min(preferred[0], firstCap);
|
|
528
|
+
let secondWidth = availableWidth - firstWidth;
|
|
529
|
+
if (secondWidth < secondMinimum) {
|
|
530
|
+
firstWidth = Math.max(minimum[0], availableWidth - secondMinimum);
|
|
531
|
+
secondWidth = availableWidth - firstWidth;
|
|
532
|
+
}
|
|
533
|
+
return [firstWidth, secondWidth];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const widths = minimum.slice();
|
|
537
|
+
let remaining = availableWidth - widths.reduce((sum, width) => sum + width, 0);
|
|
538
|
+
if (remaining < 0) {
|
|
539
|
+
const equalShare = Math.max(3, Math.floor(availableWidth / columnCount));
|
|
540
|
+
return widths.map((_, index) => index === columnCount - 1
|
|
541
|
+
? Math.max(3, availableWidth - equalShare * (columnCount - 1))
|
|
542
|
+
: equalShare);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
while (remaining > 0) {
|
|
546
|
+
let bestIndex = 0;
|
|
547
|
+
let bestNeed = -Infinity;
|
|
548
|
+
for (let index = 0; index < columnCount; index++) {
|
|
549
|
+
const need = preferred[index] - widths[index];
|
|
550
|
+
if (need > bestNeed) {
|
|
551
|
+
bestNeed = need;
|
|
552
|
+
bestIndex = index;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
widths[bestIndex] += 1;
|
|
556
|
+
remaining -= 1;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return widths;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function estimateMarkdownTableWidth(rows) {
|
|
563
|
+
const columnCount = Math.max(1, ...rows.map(row => row.length));
|
|
564
|
+
const preferred = Array.from({ length: columnCount }, (_, index) => Math.max(
|
|
565
|
+
3,
|
|
566
|
+
...rows.map(row => displayWidth(row[index] || "")),
|
|
567
|
+
));
|
|
568
|
+
return preferred.reduce((sum, width) => sum + width, 0) + (columnCount * 3 + 1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function renderPlainMarkdownTable(rows, maxWidth) {
|
|
572
|
+
if (!rows || rows.length === 0) return "";
|
|
573
|
+
const columnCount = Math.max(1, ...rows.map(row => row.length));
|
|
574
|
+
const normalizedRows = rows.map(row => Array.from({ length: columnCount }, (_, index) => row[index] || ""));
|
|
575
|
+
const borderOverhead = columnCount * 3 + 1; // | cell | cell |
|
|
576
|
+
const widths = computeMarkdownTableColumnWidths(normalizedRows, maxWidth, borderOverhead);
|
|
577
|
+
const out = [];
|
|
578
|
+
|
|
579
|
+
const topBorder = `┌${widths.map(width => "─".repeat(width + 2)).join("┬")}┐`;
|
|
580
|
+
const middleBorder = `├${widths.map(width => "─".repeat(width + 2)).join("┼")}┤`;
|
|
581
|
+
const bottomBorder = `└${widths.map(width => "─".repeat(width + 2)).join("┴")}┘`;
|
|
582
|
+
|
|
583
|
+
const renderRow = (cells) => {
|
|
584
|
+
const wrapped = cells.map((cell, index) => wrapTableCellText(cell, widths[index]));
|
|
585
|
+
const height = Math.max(...wrapped.map(lines => lines.length));
|
|
586
|
+
for (let lineIndex = 0; lineIndex < height; lineIndex++) {
|
|
587
|
+
const parts = wrapped.map((lines, index) => padToWidth(lines[lineIndex] || "", widths[index]));
|
|
588
|
+
out.push(`│ ${parts.join(" │ ")} │`);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
out.push(topBorder);
|
|
593
|
+
renderRow(normalizedRows[0]);
|
|
594
|
+
out.push(middleBorder);
|
|
595
|
+
for (const row of normalizedRows.slice(1)) {
|
|
596
|
+
renderRow(row);
|
|
597
|
+
}
|
|
598
|
+
out.push(bottomBorder);
|
|
599
|
+
return out.join("\n");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function formatMarkdownTables(md, maxWidth) {
|
|
603
|
+
const lines = String(md || "").split("\n");
|
|
604
|
+
const out = [];
|
|
605
|
+
|
|
606
|
+
for (let index = 0; index < lines.length; index++) {
|
|
607
|
+
const line = lines[index];
|
|
608
|
+
const nextLine = lines[index + 1];
|
|
609
|
+
if (!isMarkdownTableLine(line) || !isMarkdownTableLine(nextLine)) {
|
|
610
|
+
out.push(line);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const headerCells = splitMarkdownTableCells(line);
|
|
615
|
+
const separatorCells = splitMarkdownTableCells(nextLine);
|
|
616
|
+
if (!isMarkdownTableSeparatorRow(separatorCells) || headerCells.length !== separatorCells.length) {
|
|
617
|
+
out.push(line);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const rows = [headerCells];
|
|
622
|
+
const tableLines = [line, nextLine];
|
|
623
|
+
index += 1;
|
|
624
|
+
while (index + 1 < lines.length && isMarkdownTableLine(lines[index + 1])) {
|
|
625
|
+
index += 1;
|
|
626
|
+
tableLines.push(lines[index]);
|
|
627
|
+
const rowCells = splitMarkdownTableCells(lines[index]);
|
|
628
|
+
if (isMarkdownTableSeparatorRow(rowCells)) continue;
|
|
629
|
+
rows.push(rowCells);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (estimateMarkdownTableWidth(rows) <= maxWidth) {
|
|
633
|
+
out.push(...tableLines);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const renderedTable = renderPlainMarkdownTable(rows, maxWidth);
|
|
638
|
+
out.push("```text", renderedTable, "```");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return out.join("\n");
|
|
642
|
+
}
|
|
643
|
+
|
|
215
644
|
function isAsciiArtCandidateLine(line) {
|
|
216
645
|
if (!line || !line.trim()) return false;
|
|
217
646
|
if (isMarkdownTableLine(line)) return false;
|
|
@@ -296,14 +725,14 @@ function preserveAsciiArtBlocks(md) {
|
|
|
296
725
|
return out.join("\n");
|
|
297
726
|
}
|
|
298
727
|
|
|
299
|
-
function renderMarkdown(md) {
|
|
728
|
+
function renderMarkdown(md, widthOverride) {
|
|
300
729
|
const _ph = perfStart("renderMarkdown");
|
|
301
730
|
try {
|
|
302
|
-
|
|
303
|
-
const mdWidth = Math.max(40,
|
|
304
|
-
|
|
731
|
+
const fallbackWidth = Math.max(40, leftW() - 4);
|
|
732
|
+
const mdWidth = Math.max(40, widthOverride || getRenderablePaneWidth(typeof chatBox !== "undefined" ? chatBox : null, fallbackWidth));
|
|
733
|
+
marked.use(markedTerminal({ reflowText: true, width: mdWidth, showSectionPrefix: false, tab: 2, blockquote: chalk.whiteBright.italic, html: chalk.white, codespan: chalk.yellowBright }, { theme: cliHighlightTheme }));
|
|
305
734
|
const unescaped = md.replace(/\\n/g, "\n");
|
|
306
|
-
const preprocessed = preserveAsciiArtBlocks(unescaped);
|
|
735
|
+
const preprocessed = preserveAsciiArtBlocks(formatMarkdownTables(unescaped, mdWidth));
|
|
307
736
|
let rendered = marked(preprocessed).replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
308
737
|
// marked-terminal uses ANSI codes for styling, not blessed tags.
|
|
309
738
|
// Strip curly braces so blessed doesn't misinterpret them as tags.
|
|
@@ -333,6 +762,32 @@ function renderMarkdown(md) {
|
|
|
333
762
|
// Format: artifact://sessionId/filename.md
|
|
334
763
|
const ARTIFACT_URI_RE = /artifact:\/\/([a-f0-9-]+)\/([^\s"'{}]+)/g;
|
|
335
764
|
|
|
765
|
+
function normalizeArtifactFilename(filename) {
|
|
766
|
+
if (!filename) return filename;
|
|
767
|
+
|
|
768
|
+
let normalized = filename;
|
|
769
|
+
while (true) {
|
|
770
|
+
const next = normalized
|
|
771
|
+
// Common markdown wrappers: **artifact://...**, _artifact://..._, `artifact://...`
|
|
772
|
+
.replace(/[*_`]+$/g, "")
|
|
773
|
+
// Common trailing punctuation when the URI appears in prose or markdown links
|
|
774
|
+
.replace(/[)\]}>!,;:?]+$/g, "")
|
|
775
|
+
// Sentence-ending periods: artifact://.../report.md.
|
|
776
|
+
.replace(/(\.[A-Za-z0-9]{1,16})\.+$/g, "$1");
|
|
777
|
+
if (next === normalized) return normalized;
|
|
778
|
+
normalized = next;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function renderArtifactLinks(text) {
|
|
783
|
+
if (!text) return text;
|
|
784
|
+
ARTIFACT_URI_RE.lastIndex = 0;
|
|
785
|
+
return text.replace(ARTIFACT_URI_RE, (_match, _sessionId, rawFilename) => {
|
|
786
|
+
const filename = normalizeArtifactFilename(rawFilename);
|
|
787
|
+
return `📎 **${filename}** _(press 'a' to download)_`;
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
336
791
|
/** Per-session artifact link registry. orchId → [{ sessionId, filename }] */
|
|
337
792
|
const sessionArtifacts = new Map();
|
|
338
793
|
|
|
@@ -341,16 +796,18 @@ const _registeredArtifacts = new Set();
|
|
|
341
796
|
const MAX_ARTIFACT_REGISTRY = 500;
|
|
342
797
|
|
|
343
798
|
/** TUI-level artifact store for on-demand downloads. Created lazily.
|
|
344
|
-
*
|
|
799
|
+
* Remote mode uses Azure Blob when configured; local mode stays on the
|
|
800
|
+
* repo-local filesystem artifact store.
|
|
801
|
+
*/
|
|
345
802
|
let _tuiArtifactStore = null;
|
|
346
803
|
function getTuiArtifactStore() {
|
|
347
804
|
if (_tuiArtifactStore) return _tuiArtifactStore;
|
|
348
805
|
const connStr = process.env.AZURE_STORAGE_CONNECTION_STRING;
|
|
349
|
-
if (connStr) {
|
|
806
|
+
if (isRemote && connStr) {
|
|
350
807
|
const container = process.env.AZURE_STORAGE_CONTAINER || "copilot-sessions";
|
|
351
808
|
_tuiArtifactStore = new SessionBlobStore(connStr, container);
|
|
352
809
|
} else {
|
|
353
|
-
_tuiArtifactStore = new FilesystemArtifactStore();
|
|
810
|
+
_tuiArtifactStore = new FilesystemArtifactStore(process.env.ARTIFACT_DIR);
|
|
354
811
|
}
|
|
355
812
|
return _tuiArtifactStore;
|
|
356
813
|
}
|
|
@@ -364,7 +821,9 @@ function detectArtifactLinks(text, orchId) {
|
|
|
364
821
|
ARTIFACT_URI_RE.lastIndex = 0;
|
|
365
822
|
const matches = [...text.matchAll(ARTIFACT_URI_RE)];
|
|
366
823
|
for (const m of matches) {
|
|
367
|
-
const [, sessionId,
|
|
824
|
+
const [, sessionId, rawFilename] = m;
|
|
825
|
+
const filename = normalizeArtifactFilename(rawFilename);
|
|
826
|
+
if (!filename) continue;
|
|
368
827
|
const key = `${sessionId}/${filename}`;
|
|
369
828
|
if (_registeredArtifacts.has(key)) continue;
|
|
370
829
|
_registeredArtifacts.add(key);
|
|
@@ -386,17 +845,18 @@ function detectArtifactLinks(text, orchId) {
|
|
|
386
845
|
*/
|
|
387
846
|
async function downloadArtifact(sessionId, filename) {
|
|
388
847
|
const store = getTuiArtifactStore();
|
|
848
|
+
const normalizedFilename = normalizeArtifactFilename(filename);
|
|
389
849
|
const sessionDir = path.join(EXPORTS_DIR, sessionId.slice(0, 8));
|
|
390
850
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
391
|
-
const localPath = path.join(sessionDir,
|
|
851
|
+
const localPath = path.join(sessionDir, normalizedFilename);
|
|
392
852
|
|
|
393
853
|
try {
|
|
394
|
-
const content = await store.downloadArtifact(sessionId,
|
|
854
|
+
const content = await store.downloadArtifact(sessionId, normalizedFilename);
|
|
395
855
|
fs.writeFileSync(localPath, content, "utf-8");
|
|
396
856
|
appendLog(`{green-fg}📥 Downloaded: ~/${path.relative(os.homedir(), localPath)} (${(content.length / 1024).toFixed(1)}KB){/green-fg}`);
|
|
397
857
|
return localPath;
|
|
398
858
|
} catch (err) {
|
|
399
|
-
appendLog(`{red-fg}📥 Download error for ${
|
|
859
|
+
appendLog(`{red-fg}📥 Download error for ${normalizedFilename}: ${err.message}{/red-fg}`);
|
|
400
860
|
return null;
|
|
401
861
|
}
|
|
402
862
|
}
|
|
@@ -441,7 +901,7 @@ function showArtifactPicker() {
|
|
|
441
901
|
const items = artifacts.map(a => {
|
|
442
902
|
const icon = a.downloaded ? " ✓" : " ↓";
|
|
443
903
|
const sid = shortId(a.sessionId);
|
|
444
|
-
return `${icon} ${sid}/${a.filename}`;
|
|
904
|
+
return `${icon} ${sid}/${normalizeArtifactFilename(a.filename)}`;
|
|
445
905
|
});
|
|
446
906
|
if (hasMultiple) items.push(" ⬇ Download All");
|
|
447
907
|
return items;
|
|
@@ -635,7 +1095,7 @@ function showFileAttachPrompt() {
|
|
|
635
1095
|
|
|
636
1096
|
// Show a snipped preview in chat (first 3 lines)
|
|
637
1097
|
const lines = content.split("\n");
|
|
638
|
-
const preview = lines.slice(0, 3).map(l => ` ${l
|
|
1098
|
+
const preview = lines.slice(0, 3).map(l => ` ${safeSlice(l, 0, 80)}`).join("\n");
|
|
639
1099
|
const suffix = lines.length > 3 ? `\n {gray-fg}… (${lines.length - 3} more lines){/gray-fg}` : "";
|
|
640
1100
|
appendChatRaw(
|
|
641
1101
|
`{yellow-fg}📄 Attached: ${displayName} (${(content.length / 1024).toFixed(1)}KB){/yellow-fg}\n${preview}${suffix}`,
|
|
@@ -684,9 +1144,57 @@ function ts() {
|
|
|
684
1144
|
return formatDisplayTime(Date.now());
|
|
685
1145
|
}
|
|
686
1146
|
|
|
1147
|
+
function formatHumanDurationSeconds(totalSeconds) {
|
|
1148
|
+
const numeric = Number(totalSeconds);
|
|
1149
|
+
if (!Number.isFinite(numeric)) {
|
|
1150
|
+
return `${asDisplayText(totalSeconds, "?")}s`;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const roundedSeconds = Math.max(0, Math.round(numeric));
|
|
1154
|
+
if (roundedSeconds >= 86400) {
|
|
1155
|
+
const days = Math.floor(roundedSeconds / 86400);
|
|
1156
|
+
const hours = Math.floor((roundedSeconds % 86400) / 3600);
|
|
1157
|
+
const minutes = Math.floor((roundedSeconds % 3600) / 60);
|
|
1158
|
+
const seconds = roundedSeconds % 60;
|
|
1159
|
+
return `${days}d ${hours}h ${minutes}m ${seconds}s`;
|
|
1160
|
+
}
|
|
1161
|
+
if (roundedSeconds >= 3600) {
|
|
1162
|
+
const hours = Math.floor(roundedSeconds / 3600);
|
|
1163
|
+
const minutes = Math.floor((roundedSeconds % 3600) / 60);
|
|
1164
|
+
const seconds = roundedSeconds % 60;
|
|
1165
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
1166
|
+
}
|
|
1167
|
+
if (roundedSeconds >= 60) {
|
|
1168
|
+
const minutes = Math.floor(roundedSeconds / 60);
|
|
1169
|
+
const seconds = roundedSeconds % 60;
|
|
1170
|
+
return `${minutes}m ${seconds}s`;
|
|
1171
|
+
}
|
|
1172
|
+
return `${roundedSeconds}s`;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function asDisplayText(value, fallback = "") {
|
|
1176
|
+
if (typeof value === "string") return value;
|
|
1177
|
+
if (value == null) return fallback;
|
|
1178
|
+
return String(value);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function safeSlice(value, start, end, fallback = "") {
|
|
1182
|
+
return asDisplayText(value, fallback).slice(start, end);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function safeTail(value, count, fallback = "") {
|
|
1186
|
+
return safeSlice(value, -Math.abs(count), undefined, fallback);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function normalizePodName(podName) {
|
|
1190
|
+
const normalized = asDisplayText(podName, "unknown").trim();
|
|
1191
|
+
return normalized || "unknown";
|
|
1192
|
+
}
|
|
1193
|
+
|
|
687
1194
|
/** Extract short display ID (last 8 chars of session UUID) from an orchId or sessionId. */
|
|
688
1195
|
function shortId(id) {
|
|
689
|
-
const
|
|
1196
|
+
const rawId = asDisplayText(id);
|
|
1197
|
+
const sid = rawId.startsWith("session-") ? rawId.slice(8) : rawId;
|
|
690
1198
|
return sid.slice(-8);
|
|
691
1199
|
}
|
|
692
1200
|
|
|
@@ -702,10 +1210,11 @@ const screen = blessed.screen({
|
|
|
702
1210
|
title: BASE_TUI_TITLE,
|
|
703
1211
|
fullUnicode: true,
|
|
704
1212
|
forceUnicode: true,
|
|
705
|
-
mouse:
|
|
1213
|
+
mouse: true,
|
|
706
1214
|
});
|
|
707
1215
|
process.stderr.write = _origStderr;
|
|
708
1216
|
applyWindowTitle(BASE_TUI_TITLE);
|
|
1217
|
+
enableMouseTracking(screen);
|
|
709
1218
|
|
|
710
1219
|
// ─── Coalescing render loop (Option B) ───────────────────────────
|
|
711
1220
|
// Instead of rendering on every screen.render() call (80+ sites),
|
|
@@ -817,6 +1326,10 @@ setInterval(() => {
|
|
|
817
1326
|
_screenDirty = false;
|
|
818
1327
|
const t0 = performance.now();
|
|
819
1328
|
_origRender();
|
|
1329
|
+
const selectionDrawRange = applyPaneSelectionOverlay();
|
|
1330
|
+
if (selectionDrawRange) {
|
|
1331
|
+
screen.draw(selectionDrawRange.startY, selectionDrawRange.endY);
|
|
1332
|
+
}
|
|
820
1333
|
const dur = performance.now() - t0;
|
|
821
1334
|
_perfRenderCount++;
|
|
822
1335
|
_perfRenderTotalMs += dur;
|
|
@@ -832,23 +1345,35 @@ setInterval(() => {
|
|
|
832
1345
|
// ─── Layout calculations ─────────────────────────────────────────
|
|
833
1346
|
// Left column: sessions (top) + chat (bottom). Right column: full-height logs.
|
|
834
1347
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1348
|
+
const DEFAULT_LEFT_PANE_RATIO = 0.70;
|
|
1349
|
+
const MIN_LEFT_PANE_WIDTH = 30;
|
|
1350
|
+
const MIN_RIGHT_PANE_WIDTH = 20;
|
|
1351
|
+
let rightPaneAdjust = 0;
|
|
1352
|
+
|
|
1353
|
+
function baseLeftW() {
|
|
1354
|
+
return Math.floor(screen.width * DEFAULT_LEFT_PANE_RATIO);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function leftW() {
|
|
1358
|
+
const desired = baseLeftW() + rightPaneAdjust;
|
|
1359
|
+
return Math.max(MIN_LEFT_PANE_WIDTH, Math.min(desired, screen.width - MIN_RIGHT_PANE_WIDTH));
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function rightW() {
|
|
1363
|
+
return Math.max(MIN_RIGHT_PANE_WIDTH, screen.width - leftW());
|
|
1364
|
+
}
|
|
838
1365
|
const MIN_PROMPT_EDITOR_ROWS = 1;
|
|
839
1366
|
const MAX_PROMPT_EDITOR_ROWS = 8;
|
|
840
1367
|
let promptValueCache = "";
|
|
1368
|
+
let inputBar = null;
|
|
841
1369
|
|
|
842
1370
|
function promptLineCount(text) {
|
|
843
1371
|
const str = String(text || "");
|
|
844
1372
|
if (!str) return 1;
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
let visualLines = 0;
|
|
848
|
-
for (const line of str.split("\n")) {
|
|
849
|
-
visualLines += Math.max(1, Math.ceil((line.length + 1) / width));
|
|
1373
|
+
if (inputBar?._clines && inputBar._clines.content === str) {
|
|
1374
|
+
return Math.max(1, inputBar._clines.length || 1);
|
|
850
1375
|
}
|
|
851
|
-
return Math.max(1,
|
|
1376
|
+
return Math.max(1, getCursorVisualPosition(str, str.length).row + 1);
|
|
852
1377
|
}
|
|
853
1378
|
|
|
854
1379
|
function promptEditorRows() {
|
|
@@ -917,7 +1442,7 @@ const orchList = blessed.list({
|
|
|
917
1442
|
scrollbar: { style: { bg: "yellow" } },
|
|
918
1443
|
keys: false,
|
|
919
1444
|
vi: false,
|
|
920
|
-
mouse:
|
|
1445
|
+
mouse: true,
|
|
921
1446
|
interactive: true,
|
|
922
1447
|
});
|
|
923
1448
|
|
|
@@ -991,7 +1516,7 @@ const chatBox = blessed.log({
|
|
|
991
1516
|
scrollbar: { style: { bg: "cyan" } },
|
|
992
1517
|
keys: true,
|
|
993
1518
|
vi: true,
|
|
994
|
-
mouse:
|
|
1519
|
+
mouse: true,
|
|
995
1520
|
});
|
|
996
1521
|
|
|
997
1522
|
// ─── Clickable URLs in chat ──────────────────────────────────────
|
|
@@ -1022,6 +1547,8 @@ function extractUrlFromLine(line) {
|
|
|
1022
1547
|
|
|
1023
1548
|
// Mouse click → open URL in browser
|
|
1024
1549
|
chatBox.on("click", function (_mouse) {
|
|
1550
|
+
if (Date.now() < suppressPaneClicksUntil) return;
|
|
1551
|
+
if (paneTextSelection.pane === this && paneTextSelection.active && paneTextSelection.dragging) return;
|
|
1025
1552
|
// Calculate which content line was clicked
|
|
1026
1553
|
// _mouse.y is absolute screen coordinate
|
|
1027
1554
|
const absTop = this.atop != null ? this.atop : this.top;
|
|
@@ -1049,7 +1576,7 @@ const paneColors = ["yellow", "magenta", "green", "blue"];
|
|
|
1049
1576
|
let nextColorIdx = 0;
|
|
1050
1577
|
|
|
1051
1578
|
// Log viewing mode: "workers" | "orchestration" | "sequence" | "nodemap"
|
|
1052
|
-
let logViewMode = "
|
|
1579
|
+
let logViewMode = "sequence";
|
|
1053
1580
|
// Markdown viewer overlay — toggled independently via 'v' key.
|
|
1054
1581
|
// When active, replaces the entire right side (log panes + activity pane).
|
|
1055
1582
|
let mdViewActive = false;
|
|
@@ -1088,25 +1615,20 @@ const orchLogPane = blessed.log({
|
|
|
1088
1615
|
scrollbar: { style: { bg: "cyan" } },
|
|
1089
1616
|
keys: true,
|
|
1090
1617
|
vi: true,
|
|
1091
|
-
mouse:
|
|
1618
|
+
mouse: true,
|
|
1092
1619
|
hidden: true,
|
|
1093
1620
|
});
|
|
1094
1621
|
|
|
1095
1622
|
// ─── Sequence Diagram Mode (swimlane) ────────────────────────────
|
|
1096
1623
|
// Vertical scrolling swimlane: one column per worker node.
|
|
1097
|
-
//
|
|
1098
|
-
// Migration arrows show when affinity resets after dehydrate.
|
|
1624
|
+
// Backed by CMS events (worker_node_id per event, seq-ordered).
|
|
1099
1625
|
|
|
1100
1626
|
const TIME_W = 10; // "HH:MM:SS " — time + trailing space
|
|
1101
1627
|
|
|
1102
|
-
const seqEventBuffers = new Map(); // orchId → [event]
|
|
1103
1628
|
const seqNodes = []; // ordered short node names
|
|
1104
1629
|
const seqNodeSet = new Set();
|
|
1105
1630
|
const MAX_SEQ_RENDER_EVENTS = 120;
|
|
1106
1631
|
|
|
1107
|
-
// Track per-session state for rendering
|
|
1108
|
-
const seqLastActivityNode = new Map(); // orchId → last node that ran activity
|
|
1109
|
-
|
|
1110
1632
|
const seqHeaderBox = blessed.box({
|
|
1111
1633
|
parent: screen,
|
|
1112
1634
|
tags: true,
|
|
@@ -1143,7 +1665,7 @@ const nodeMapPane = blessed.box({
|
|
|
1143
1665
|
scrollbar: { style: { bg: "yellow" } },
|
|
1144
1666
|
keys: true,
|
|
1145
1667
|
vi: true,
|
|
1146
|
-
mouse:
|
|
1668
|
+
mouse: true,
|
|
1147
1669
|
hidden: true,
|
|
1148
1670
|
});
|
|
1149
1671
|
|
|
@@ -1174,7 +1696,7 @@ const mdFileListPane = blessed.list({
|
|
|
1174
1696
|
},
|
|
1175
1697
|
keys: false,
|
|
1176
1698
|
vi: false,
|
|
1177
|
-
mouse:
|
|
1699
|
+
mouse: true,
|
|
1178
1700
|
interactive: true,
|
|
1179
1701
|
hidden: true,
|
|
1180
1702
|
});
|
|
@@ -1199,7 +1721,7 @@ const mdPreviewPane = blessed.box({
|
|
|
1199
1721
|
scrollbar: { style: { bg: "green" } },
|
|
1200
1722
|
keys: true,
|
|
1201
1723
|
vi: true,
|
|
1202
|
-
mouse:
|
|
1724
|
+
mouse: true,
|
|
1203
1725
|
hidden: true,
|
|
1204
1726
|
});
|
|
1205
1727
|
|
|
@@ -1261,7 +1783,7 @@ function refreshMarkdownViewer() {
|
|
|
1261
1783
|
const f = files[mdViewerSelectedIdx];
|
|
1262
1784
|
try {
|
|
1263
1785
|
const raw = fs.readFileSync(f.localPath, "utf-8");
|
|
1264
|
-
const rendered = renderMarkdown(raw);
|
|
1786
|
+
const rendered = renderMarkdown(raw, getRenderablePaneWidth(mdPreviewPane, Math.max(40, leftW() - 4)));
|
|
1265
1787
|
mdPreviewPane.setLabel(` ${f.filename} `);
|
|
1266
1788
|
mdPreviewPane.setContent(rendered);
|
|
1267
1789
|
mdPreviewPane.scrollTo(0);
|
|
@@ -1301,7 +1823,7 @@ function toggleMdViewOff() {
|
|
|
1301
1823
|
|
|
1302
1824
|
// ─── Vim keybindings for markdown preview ────────────────────────
|
|
1303
1825
|
// g = top, G = bottom, Ctrl-d = page down, Ctrl-u = page up
|
|
1304
|
-
// o = open in
|
|
1826
|
+
// o = open in default app, y = copy path
|
|
1305
1827
|
mdPreviewPane.key(["g"], () => { mdPreviewPane.scrollTo(0); scheduleRender(); });
|
|
1306
1828
|
mdPreviewPane.key(["S-g"], () => { mdPreviewPane.setScrollPerc(100); scheduleRender(); });
|
|
1307
1829
|
mdPreviewPane.key(["C-d"], () => {
|
|
@@ -1318,19 +1840,18 @@ mdPreviewPane.key(["o"], () => {
|
|
|
1318
1840
|
const files = scanExportFiles();
|
|
1319
1841
|
const f = files[mdViewerSelectedIdx];
|
|
1320
1842
|
if (!f) return;
|
|
1321
|
-
|
|
1322
|
-
spawn(editor, [f.localPath], { detached: true, stdio: "ignore" }).unref();
|
|
1843
|
+
openPathInDefaultApp(f.localPath);
|
|
1323
1844
|
});
|
|
1324
1845
|
mdPreviewPane.key(["y"], () => {
|
|
1325
1846
|
const files = scanExportFiles();
|
|
1326
1847
|
const f = files[mdViewerSelectedIdx];
|
|
1327
1848
|
if (!f) return;
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1849
|
+
const copied = copyTextToClipboard(f.localPath, {
|
|
1850
|
+
onError: (err) => appendLog(`{red-fg}Copy failed: ${err.message}{/red-fg}`),
|
|
1851
|
+
});
|
|
1852
|
+
setStatus(copied
|
|
1853
|
+
? `{green-fg}Copied: ${f.localPath}{/green-fg}`
|
|
1854
|
+
: "Copy failed — check logs");
|
|
1334
1855
|
});
|
|
1335
1856
|
|
|
1336
1857
|
// ─── Activity pane (sticky, bottom-right) ────────────────────────
|
|
@@ -1356,7 +1877,7 @@ const activityPane = blessed.log({
|
|
|
1356
1877
|
scrollbar: { style: { bg: "gray" } },
|
|
1357
1878
|
keys: true,
|
|
1358
1879
|
vi: true,
|
|
1359
|
-
mouse:
|
|
1880
|
+
mouse: true,
|
|
1360
1881
|
});
|
|
1361
1882
|
|
|
1362
1883
|
// Per-session activity buffers
|
|
@@ -1408,6 +1929,14 @@ function formatToolArgsSummary(toolName, args) {
|
|
|
1408
1929
|
: "";
|
|
1409
1930
|
return ` ${seconds}${preserve}${reason}`;
|
|
1410
1931
|
}
|
|
1932
|
+
if (toolName === "cron") {
|
|
1933
|
+
if (args.action === "cancel") return " cancel";
|
|
1934
|
+
const seconds = args.seconds != null ? `${args.seconds}s` : "?";
|
|
1935
|
+
const reason = typeof args.reason === "string" && args.reason
|
|
1936
|
+
? ` reason=${JSON.stringify(args.reason)}`
|
|
1937
|
+
: "";
|
|
1938
|
+
return ` ${seconds}${reason}`;
|
|
1939
|
+
}
|
|
1411
1940
|
|
|
1412
1941
|
const entries = Object.entries(args)
|
|
1413
1942
|
.slice(0, 4)
|
|
@@ -1456,10 +1985,30 @@ function ensureSessionSplashBuffer(orchId) {
|
|
|
1456
1985
|
* Refresh the node map pane — vertical columns, one per worker node,
|
|
1457
1986
|
* with sessions stacked underneath, color-coded by live status.
|
|
1458
1987
|
*/
|
|
1988
|
+
let _nodeMapTimelineLoadPending = false;
|
|
1989
|
+
|
|
1459
1990
|
function refreshNodeMap() {
|
|
1460
1991
|
const lines = [];
|
|
1461
1992
|
nodeMapPane.setContent("");
|
|
1462
1993
|
|
|
1994
|
+
// Ensure CMS timelines are loaded for all sessions so we can map them to
|
|
1995
|
+
// worker columns. Only the active session's timeline is loaded eagerly;
|
|
1996
|
+
// kick off background loads for every other session the first time the
|
|
1997
|
+
// Node Map is opened (and whenever new sessions appear).
|
|
1998
|
+
const missingTimelines = [];
|
|
1999
|
+
for (const orchId of knownOrchestrationIds) {
|
|
2000
|
+
const sid = orchId.startsWith("session-") ? orchId.slice(8) : orchId;
|
|
2001
|
+
if (!cmsSeqTimelines.has(sid)) missingTimelines.push(sid);
|
|
2002
|
+
}
|
|
2003
|
+
if (missingTimelines.length > 0 && !_nodeMapTimelineLoadPending) {
|
|
2004
|
+
_nodeMapTimelineLoadPending = true;
|
|
2005
|
+
Promise.allSettled(missingTimelines.map(sid => loadCmsSeqTimeline(sid)))
|
|
2006
|
+
.then(() => {
|
|
2007
|
+
_nodeMapTimelineLoadPending = false;
|
|
2008
|
+
if (logViewMode === "nodemap") refreshNodeMap();
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1463
2012
|
// Gather all known nodes from seqNodes (worker pane names)
|
|
1464
2013
|
// Filter out synthetic nodes like "cms" that aren't real workers.
|
|
1465
2014
|
const SYNTHETIC_NODES = new Set(["cms"]);
|
|
@@ -1480,9 +2029,20 @@ function refreshNodeMap() {
|
|
|
1480
2029
|
const UNASSIGNED = "(unknown)";
|
|
1481
2030
|
nodeSessionMap.set(UNASSIGNED, []);
|
|
1482
2031
|
|
|
1483
|
-
// Walk all known orchestrations and assign to their last-known node
|
|
2032
|
+
// Walk all known orchestrations and assign to their last-known node (from CMS events).
|
|
1484
2033
|
for (const orchId of knownOrchestrationIds) {
|
|
1485
|
-
const
|
|
2034
|
+
const sid = orchId.startsWith("session-") ? orchId.slice(8) : orchId;
|
|
2035
|
+
const cmsTimeline = cmsSeqTimelines.get(sid);
|
|
2036
|
+
let node;
|
|
2037
|
+
if (cmsTimeline && cmsTimeline.length > 0) {
|
|
2038
|
+
for (let i = cmsTimeline.length - 1; i >= 0; i--) {
|
|
2039
|
+
const nodeId = getSeqEventNodeId(cmsTimeline[i]);
|
|
2040
|
+
if (nodeId && nodeId !== "(unknown)") {
|
|
2041
|
+
node = safeTail(nodeId, 5);
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
1486
2046
|
const status = getSessionVisualState(orchId);
|
|
1487
2047
|
const uuid4 = shortId(orchId);
|
|
1488
2048
|
const title = sessionHeadings.get(orchId);
|
|
@@ -1507,8 +2067,9 @@ function refreshNodeMap() {
|
|
|
1507
2067
|
|
|
1508
2068
|
// Pad/clip text to column width (plain text, no tags)
|
|
1509
2069
|
const fitCol = (text, w) => {
|
|
1510
|
-
|
|
1511
|
-
|
|
2070
|
+
const displayText = asDisplayText(text);
|
|
2071
|
+
if (displayText.length > w) return safeSlice(displayText, 0, w);
|
|
2072
|
+
return displayText + " ".repeat(w - displayText.length);
|
|
1512
2073
|
};
|
|
1513
2074
|
|
|
1514
2075
|
// Render header row: node names
|
|
@@ -1574,6 +2135,7 @@ function refreshNodeMap() {
|
|
|
1574
2135
|
lines.push("");
|
|
1575
2136
|
lines.push(
|
|
1576
2137
|
"{green-fg}* running{/green-fg} " +
|
|
2138
|
+
"{magenta-fg}~ cron{/magenta-fg} " +
|
|
1577
2139
|
"{yellow-fg}~ waiting{/yellow-fg} " +
|
|
1578
2140
|
"{white-fg}. idle{/white-fg} " +
|
|
1579
2141
|
"{cyan-fg}? input{/cyan-fg} " +
|
|
@@ -1607,10 +2169,233 @@ const seqPane = blessed.log({
|
|
|
1607
2169
|
scrollbar: { style: { bg: "magenta" } },
|
|
1608
2170
|
keys: true,
|
|
1609
2171
|
vi: true,
|
|
1610
|
-
mouse:
|
|
2172
|
+
mouse: true,
|
|
1611
2173
|
hidden: true,
|
|
1612
2174
|
});
|
|
1613
2175
|
|
|
2176
|
+
const selectablePaneLabels = new Map();
|
|
2177
|
+
const selectablePaneMouseBindings = new WeakSet();
|
|
2178
|
+
const SELECTION_BG = blessedColors.convert("yellow");
|
|
2179
|
+
const SELECTION_FG = blessedColors.convert("black");
|
|
2180
|
+
const paneTextSelection = {
|
|
2181
|
+
pane: null,
|
|
2182
|
+
anchor: null,
|
|
2183
|
+
head: null,
|
|
2184
|
+
active: false,
|
|
2185
|
+
dragging: false,
|
|
2186
|
+
visible: false,
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
function selectionInteractionBlocked() {
|
|
2190
|
+
return Boolean(_fileAttachModal || startupLandingVisible || slashPicker);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function registerSelectablePane(pane, label) {
|
|
2194
|
+
selectablePaneLabels.set(pane, label);
|
|
2195
|
+
try {
|
|
2196
|
+
pane.enableMouse?.();
|
|
2197
|
+
} catch {}
|
|
2198
|
+
if (!pane || selectablePaneMouseBindings.has(pane)) return;
|
|
2199
|
+
selectablePaneMouseBindings.add(pane);
|
|
2200
|
+
pane.on("mousedown", (data) => {
|
|
2201
|
+
if (selectionInteractionBlocked()) return;
|
|
2202
|
+
if (paneTextSelection.active && paneTextSelection.pane === pane) return;
|
|
2203
|
+
beginPaneSelection(data, pane);
|
|
2204
|
+
});
|
|
2205
|
+
pane.onScreenEvent("mouse", (data) => {
|
|
2206
|
+
if (selectionInteractionBlocked()) return;
|
|
2207
|
+
if (paneTextSelection.pane !== pane || !paneTextSelection.active) return;
|
|
2208
|
+
if (data.action === "mousemove" || data.action === "mousedown") {
|
|
2209
|
+
updatePaneSelection(data);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (data.action === "mouseup") {
|
|
2213
|
+
updatePaneSelection(data);
|
|
2214
|
+
finalizePaneSelection();
|
|
2215
|
+
}
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function getSelectablePaneLabel(pane) {
|
|
2220
|
+
return selectablePaneLabels.get(pane) || "pane";
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
function clearPaneSelection({ render = true } = {}) {
|
|
2224
|
+
paneTextSelection.pane = null;
|
|
2225
|
+
paneTextSelection.anchor = null;
|
|
2226
|
+
paneTextSelection.head = null;
|
|
2227
|
+
paneTextSelection.active = false;
|
|
2228
|
+
paneTextSelection.dragging = false;
|
|
2229
|
+
paneTextSelection.visible = false;
|
|
2230
|
+
if (render) scheduleRender();
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
function getPaneInnerBounds(pane) {
|
|
2234
|
+
if (!pane || pane.hidden || pane.visible === false || !pane.lpos) return null;
|
|
2235
|
+
|
|
2236
|
+
const xi = pane.lpos.xi + (pane.ileft || 0);
|
|
2237
|
+
const xl = pane.lpos.xl - (pane.iright || 0) - 1;
|
|
2238
|
+
const yi = pane.lpos.yi + (pane.itop || 0);
|
|
2239
|
+
const yl = pane.lpos.yl - (pane.ibottom || 0) - 1;
|
|
2240
|
+
|
|
2241
|
+
if (xi > xl || yi > yl) return null;
|
|
2242
|
+
return { xi, xl, yi, yl };
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
function clampSelectionPoint(bounds, x, y) {
|
|
2246
|
+
return {
|
|
2247
|
+
x: Math.max(bounds.xi, Math.min(bounds.xl, x)),
|
|
2248
|
+
y: Math.max(bounds.yi, Math.min(bounds.yl, y)),
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
function normalizeSelectionRange(anchor, head) {
|
|
2253
|
+
if (head.y < anchor.y || (head.y === anchor.y && head.x < anchor.x)) {
|
|
2254
|
+
return { start: head, end: anchor };
|
|
2255
|
+
}
|
|
2256
|
+
return { start: anchor, end: head };
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
function extractPaneSelectionText(selection = paneTextSelection) {
|
|
2260
|
+
if (!selection.pane || !selection.anchor || !selection.head) return "";
|
|
2261
|
+
|
|
2262
|
+
const bounds = getPaneInnerBounds(selection.pane);
|
|
2263
|
+
if (!bounds) return "";
|
|
2264
|
+
|
|
2265
|
+
const anchor = clampSelectionPoint(bounds, selection.anchor.x, selection.anchor.y);
|
|
2266
|
+
const head = clampSelectionPoint(bounds, selection.head.x, selection.head.y);
|
|
2267
|
+
const { start, end } = normalizeSelectionRange(anchor, head);
|
|
2268
|
+
const rows = [];
|
|
2269
|
+
|
|
2270
|
+
for (let y = start.y; y <= end.y; y++) {
|
|
2271
|
+
const fromX = y === start.y ? start.x : bounds.xi;
|
|
2272
|
+
const toX = y === end.y ? end.x : bounds.xl;
|
|
2273
|
+
const line = screen.lines[y] || [];
|
|
2274
|
+
let row = "";
|
|
2275
|
+
for (let x = fromX; x <= toX; x++) {
|
|
2276
|
+
const ch = line[x]?.[1];
|
|
2277
|
+
row += ch && ch !== "\x00" ? ch : " ";
|
|
2278
|
+
}
|
|
2279
|
+
rows.push(row.replace(/[ \t]+$/u, ""));
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
return rows.join("\n").replace(/\n+$/u, "");
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function applyPaneSelectionOverlay() {
|
|
2286
|
+
if (!paneTextSelection.visible || !paneTextSelection.pane) return null;
|
|
2287
|
+
|
|
2288
|
+
const bounds = getPaneInnerBounds(paneTextSelection.pane);
|
|
2289
|
+
if (!bounds) {
|
|
2290
|
+
clearPaneSelection({ render: false });
|
|
2291
|
+
return null;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
const anchor = clampSelectionPoint(bounds, paneTextSelection.anchor.x, paneTextSelection.anchor.y);
|
|
2295
|
+
const head = clampSelectionPoint(bounds, paneTextSelection.head.x, paneTextSelection.head.y);
|
|
2296
|
+
const { start, end } = normalizeSelectionRange(anchor, head);
|
|
2297
|
+
|
|
2298
|
+
for (let y = start.y; y <= end.y; y++) {
|
|
2299
|
+
const fromX = y === start.y ? start.x : bounds.xi;
|
|
2300
|
+
const toX = y === end.y ? end.x : bounds.xl;
|
|
2301
|
+
const line = screen.lines[y];
|
|
2302
|
+
if (!line) continue;
|
|
2303
|
+
|
|
2304
|
+
for (let x = fromX; x <= toX; x++) {
|
|
2305
|
+
if (!line[x]) continue;
|
|
2306
|
+
const attr = line[x][0];
|
|
2307
|
+
line[x][0] = (attr & ~((0x1ff << 9) | 0x1ff | (8 << 18)))
|
|
2308
|
+
| (1 << 18)
|
|
2309
|
+
| (SELECTION_FG << 9)
|
|
2310
|
+
| SELECTION_BG;
|
|
2311
|
+
}
|
|
2312
|
+
line.dirty = true;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
return { startY: start.y, endY: end.y };
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
function beginPaneSelection(mouse, pane) {
|
|
2319
|
+
const bounds = getPaneInnerBounds(pane);
|
|
2320
|
+
const hadVisibleSelection = paneTextSelection.visible;
|
|
2321
|
+
|
|
2322
|
+
clearPaneSelection({ render: false });
|
|
2323
|
+
|
|
2324
|
+
if (!bounds) {
|
|
2325
|
+
if (hadVisibleSelection) scheduleRender();
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
const point = clampSelectionPoint(bounds, mouse.x, mouse.y);
|
|
2330
|
+
paneTextSelection.pane = pane;
|
|
2331
|
+
paneTextSelection.anchor = point;
|
|
2332
|
+
paneTextSelection.head = point;
|
|
2333
|
+
paneTextSelection.active = true;
|
|
2334
|
+
paneTextSelection.dragging = false;
|
|
2335
|
+
paneTextSelection.visible = false;
|
|
2336
|
+
if (typeof pane.focus === "function" && screen.focused !== pane) {
|
|
2337
|
+
pane.focus();
|
|
2338
|
+
}
|
|
2339
|
+
hideTerminalCursor();
|
|
2340
|
+
setStatus(`Drag to select text in ${getSelectablePaneLabel(pane)}...`);
|
|
2341
|
+
|
|
2342
|
+
scheduleRender();
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
function updatePaneSelection(mouse) {
|
|
2346
|
+
if (!paneTextSelection.active || !paneTextSelection.pane || !paneTextSelection.anchor) return;
|
|
2347
|
+
|
|
2348
|
+
const bounds = getPaneInnerBounds(paneTextSelection.pane);
|
|
2349
|
+
if (!bounds) {
|
|
2350
|
+
clearPaneSelection();
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
const next = clampSelectionPoint(bounds, mouse.x, mouse.y);
|
|
2355
|
+
if (paneTextSelection.head
|
|
2356
|
+
&& paneTextSelection.head.x === next.x
|
|
2357
|
+
&& paneTextSelection.head.y === next.y) {
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
paneTextSelection.head = next;
|
|
2362
|
+
paneTextSelection.dragging = true;
|
|
2363
|
+
paneTextSelection.visible = paneTextSelection.anchor.x !== next.x
|
|
2364
|
+
|| paneTextSelection.anchor.y !== next.y;
|
|
2365
|
+
scheduleRender();
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function finalizePaneSelection() {
|
|
2369
|
+
if (!paneTextSelection.pane) return;
|
|
2370
|
+
paneTextSelection.active = false;
|
|
2371
|
+
|
|
2372
|
+
if (!paneTextSelection.visible || !paneTextSelection.dragging) {
|
|
2373
|
+
clearPaneSelection();
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
const text = extractPaneSelectionText();
|
|
2378
|
+
suppressPaneClicksUntil = Date.now() + 250;
|
|
2379
|
+
|
|
2380
|
+
if (!text) {
|
|
2381
|
+
clearPaneSelection();
|
|
2382
|
+
setStatus("Nothing to copy");
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
const copied = copyTextToClipboard(text, {
|
|
2387
|
+
onError: (err) => appendLog(`{red-fg}Copy failed: ${err.message}{/red-fg}`),
|
|
2388
|
+
});
|
|
2389
|
+
const paneLabel = getSelectablePaneLabel(paneTextSelection.pane);
|
|
2390
|
+
const charCount = text.length;
|
|
2391
|
+
paneTextSelection.dragging = false;
|
|
2392
|
+
paneTextSelection.visible = true;
|
|
2393
|
+
setStatus(copied
|
|
2394
|
+
? `Copied ${charCount} chars from ${paneLabel}`
|
|
2395
|
+
: "Copy failed — check logs");
|
|
2396
|
+
scheduleRender();
|
|
2397
|
+
}
|
|
2398
|
+
|
|
1614
2399
|
// ─── Register focus ring on all panes ────────────────────────────
|
|
1615
2400
|
registerFocusRing(orchList, "yellow");
|
|
1616
2401
|
registerFocusRing(chatBox, "cyan");
|
|
@@ -1629,17 +2414,15 @@ activityPane.on("focus", () => setNavigationStatusForPane("activity"));
|
|
|
1629
2414
|
seqPane.on("focus", () => setNavigationStatusForPane("sequence"));
|
|
1630
2415
|
// Worker panes are created dynamically — registered in getOrCreateWorkerPane()
|
|
1631
2416
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
return short;
|
|
1642
|
-
}
|
|
2417
|
+
registerSelectablePane(orchList, "sessions");
|
|
2418
|
+
registerSelectablePane(chatBox, "chat");
|
|
2419
|
+
registerSelectablePane(orchLogPane, "orchestration logs");
|
|
2420
|
+
registerSelectablePane(seqHeaderBox, "sequence header");
|
|
2421
|
+
registerSelectablePane(seqPane, "sequence");
|
|
2422
|
+
registerSelectablePane(nodeMapPane, "node map");
|
|
2423
|
+
registerSelectablePane(mdFileListPane, "markdown file list");
|
|
2424
|
+
registerSelectablePane(mdPreviewPane, "markdown preview");
|
|
2425
|
+
registerSelectablePane(activityPane, "activity");
|
|
1643
2426
|
|
|
1644
2427
|
// Compute column width dynamically from pane inner width
|
|
1645
2428
|
// Returns an array of per-column widths so remaining pixels are distributed
|
|
@@ -1657,13 +2440,6 @@ function seqColWidths() {
|
|
|
1657
2440
|
return widths;
|
|
1658
2441
|
}
|
|
1659
2442
|
|
|
1660
|
-
// Legacy helper — returns the base column width (used in separator/header)
|
|
1661
|
-
function seqColW() {
|
|
1662
|
-
const innerW = (seqPane.width || 60) - 4;
|
|
1663
|
-
const ncols = seqNodes.length || 1;
|
|
1664
|
-
return Math.max(8, Math.floor((innerW - TIME_W) / ncols));
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
2443
|
// Measure display width of a string (emoji = 2 cells)
|
|
1668
2444
|
// eslint-disable-next-line no-control-regex
|
|
1669
2445
|
const EMOJI_RE = /[\u{1F300}-\u{1FAD6}\u{2600}-\u{27BF}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}\u{E0020}-\u{E007F}]/gu;
|
|
@@ -1697,16 +2473,19 @@ function displayWidth(str) {
|
|
|
1697
2473
|
|
|
1698
2474
|
// Pad string to target display width
|
|
1699
2475
|
function padToWidth(str, targetW) {
|
|
1700
|
-
const
|
|
2476
|
+
const text = asDisplayText(str);
|
|
2477
|
+
const w = displayWidth(text);
|
|
1701
2478
|
const need = Math.max(0, targetW - w);
|
|
1702
|
-
return
|
|
2479
|
+
return text + " ".repeat(need);
|
|
1703
2480
|
}
|
|
1704
2481
|
|
|
1705
2482
|
// Build one swimlane line: place content in a specific column
|
|
1706
|
-
function seqLine(time, colIdx, content, color) {
|
|
2483
|
+
function seqLine(time, colIdx, content, color, options = {}) {
|
|
1707
2484
|
const ncols = seqNodes.length || 1;
|
|
1708
2485
|
const widths = seqColWidths();
|
|
1709
2486
|
const timeStr = (time || "").padEnd(TIME_W);
|
|
2487
|
+
const displayContent = asDisplayText(content);
|
|
2488
|
+
const underline = options.underline === true;
|
|
1710
2489
|
let line = `{white-fg}${timeStr}{/white-fg}`;
|
|
1711
2490
|
|
|
1712
2491
|
for (let i = 0; i < ncols; i++) {
|
|
@@ -1714,11 +2493,15 @@ function seqLine(time, colIdx, content, color) {
|
|
|
1714
2493
|
if (i === colIdx) {
|
|
1715
2494
|
// Content cell — clip to fit
|
|
1716
2495
|
const maxContent = w - 2; // 1 space padding each side
|
|
1717
|
-
const clipped =
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
const
|
|
1721
|
-
|
|
2496
|
+
const clipped = displayContent.length > maxContent
|
|
2497
|
+
? safeSlice(displayContent, 0, maxContent)
|
|
2498
|
+
: displayContent;
|
|
2499
|
+
const remaining = Math.max(0, maxContent - displayWidth(clipped));
|
|
2500
|
+
const leftPadding = " ".repeat(Math.floor(remaining / 2));
|
|
2501
|
+
const rightPadding = " ".repeat(remaining - Math.floor(remaining / 2));
|
|
2502
|
+
let styled = color ? `{${color}-fg}${clipped}{/${color}-fg}` : clipped;
|
|
2503
|
+
if (underline) styled = `{underline}${styled}{/underline}`;
|
|
2504
|
+
line += ` ${leftPadding}${styled}${rightPadding} `;
|
|
1722
2505
|
} else {
|
|
1723
2506
|
// Empty cell — vertical bar for the swimlane (ASCII | avoids
|
|
1724
2507
|
// ambiguous-width issues with Unicode box-drawing characters)
|
|
@@ -1729,17 +2512,6 @@ function seqLine(time, colIdx, content, color) {
|
|
|
1729
2512
|
return line;
|
|
1730
2513
|
}
|
|
1731
2514
|
|
|
1732
|
-
// Full-width separator line for CAN / migration events
|
|
1733
|
-
function seqSeparator(label, color) {
|
|
1734
|
-
const widths = seqColWidths();
|
|
1735
|
-
const totalW = TIME_W + widths.reduce((a, b) => a + b, 0);
|
|
1736
|
-
const labelStr = ` ${label} `;
|
|
1737
|
-
const dashCount = Math.max(0, totalW - labelStr.length);
|
|
1738
|
-
const left = Math.floor(dashCount / 2);
|
|
1739
|
-
const right = dashCount - left;
|
|
1740
|
-
return `{${color}-fg}${"-".repeat(left)}${labelStr}${"-".repeat(right)}{/${color}-fg}`;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
2515
|
function seqHeader() {
|
|
1744
2516
|
const widths = seqColWidths();
|
|
1745
2517
|
let header = "{bold}" + "TIME".padEnd(TIME_W);
|
|
@@ -1758,324 +2530,322 @@ function seqHeader() {
|
|
|
1758
2530
|
}
|
|
1759
2531
|
|
|
1760
2532
|
/**
|
|
1761
|
-
*
|
|
1762
|
-
* Returns null if the line isn't relevant for the sequence diagram.
|
|
2533
|
+
* Update the sticky header box with current node columns.
|
|
1763
2534
|
*/
|
|
1764
|
-
function
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
const orchNode = addSeqNode(podName);
|
|
2535
|
+
function updateSeqHeader() {
|
|
2536
|
+
if (seqNodes.length > 0) {
|
|
2537
|
+
const [header, divider] = seqHeader();
|
|
2538
|
+
seqHeaderBox.setContent(`${header}\n${divider}`);
|
|
2539
|
+
} else {
|
|
2540
|
+
seqHeaderBox.setContent("{bold}TIME (waiting for events){/bold}");
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
1774
2543
|
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2544
|
+
/**
|
|
2545
|
+
* Full re-render of the sequence pane for the active session.
|
|
2546
|
+
*/
|
|
2547
|
+
function refreshSeqPane() {
|
|
2548
|
+
refreshCmsSeqPane();
|
|
2549
|
+
}
|
|
1780
2550
|
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
const promptMatch = plain.match(/prompt="([^"]{0,30})/);
|
|
1785
|
-
return { orchId, time, type: "turn", orchNode, actNode,
|
|
1786
|
-
turn: turnMatch?.[1] || "?",
|
|
1787
|
-
prompt: promptMatch?.[1] || "" };
|
|
1788
|
-
}
|
|
1789
|
-
if (plain.includes("execution start") || plain.includes("[orch] start:")) {
|
|
1790
|
-
const iterMatch = plain.match(/iteration=(\d+)/) || plain.match(/iter=(\d+)/);
|
|
1791
|
-
const hydrate = plain.includes("needsHydration=true") || plain.includes("hydrate=true");
|
|
1792
|
-
return { orchId, time, type: "exec_start", orchNode, actNode,
|
|
1793
|
-
iteration: parseInt(iterMatch?.[1] || "0", 10),
|
|
1794
|
-
hydrate };
|
|
1795
|
-
}
|
|
1796
|
-
if (plain.includes("timer completed")) {
|
|
1797
|
-
const sMatch = plain.match(/seconds=(\d+)/);
|
|
1798
|
-
return { orchId, time, type: "timer_fired", orchNode, actNode,
|
|
1799
|
-
seconds: sMatch?.[1] || "?" };
|
|
1800
|
-
}
|
|
1801
|
-
if (plain.includes("idle timeout")) {
|
|
1802
|
-
return { orchId, time, type: "dehydrate", orchNode, actNode };
|
|
1803
|
-
}
|
|
1804
|
-
if (plain.includes("user responded within idle")) {
|
|
1805
|
-
return { orchId, time, type: "user_idle", orchNode, actNode };
|
|
1806
|
-
}
|
|
1807
|
-
if (plain.includes("wait interrupted")) {
|
|
1808
|
-
return { orchId, time, type: "interrupt", orchNode, actNode };
|
|
1809
|
-
}
|
|
2551
|
+
// ─── CMS-Backed Sequence Diagram ─────────────────────────────────
|
|
2552
|
+
// Replaces log-parsed sequence data with CMS events from the management client.
|
|
2553
|
+
// CMS events have: seq (ordering), created_at (display time), event_type, data, worker_node_id.
|
|
1810
2554
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
}
|
|
1824
|
-
if (plain.includes("activity_name=hydrateSession")) {
|
|
1825
|
-
return { orchId, time, type: "hydrate_act", orchNode, actNode };
|
|
1826
|
-
}
|
|
1827
|
-
if (plain.includes("activity_name=listModels")) {
|
|
1828
|
-
return { orchId, time, type: "listmodels_act", orchNode, actNode };
|
|
1829
|
-
}
|
|
2555
|
+
const cmsSeqTimelines = new Map(); // sessionId → SeqEvent[]
|
|
2556
|
+
const cmsSeqLastSeq = new Map(); // sessionId → last seq seen (for incremental poll)
|
|
2557
|
+
const ORCHESTRATOR_SEQ_TYPES = new Set([
|
|
2558
|
+
"wait",
|
|
2559
|
+
"timer",
|
|
2560
|
+
"cron_start",
|
|
2561
|
+
"cron_fire",
|
|
2562
|
+
"cron_cancel",
|
|
2563
|
+
"spawn",
|
|
2564
|
+
"cmd_recv",
|
|
2565
|
+
"cmd_done",
|
|
2566
|
+
]);
|
|
1830
2567
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
if (cmdMatch) {
|
|
1835
|
-
return { orchId, time, type: "cmd_recv", orchNode, actNode,
|
|
1836
|
-
cmd: cmdMatch[1] };
|
|
1837
|
-
}
|
|
1838
|
-
const modelMatch = plain.match(/model changed: (.+)/);
|
|
1839
|
-
if (modelMatch) {
|
|
1840
|
-
return { orchId, time, type: "cmd_done", orchNode, actNode,
|
|
1841
|
-
detail: modelMatch[1] };
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
2568
|
+
function isOrchestratorSeqEventType(type) {
|
|
2569
|
+
return ORCHESTRATOR_SEQ_TYPES.has(type);
|
|
2570
|
+
}
|
|
1844
2571
|
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
}
|
|
1849
|
-
// explicit dehydrate log from orchestration
|
|
1850
|
-
if (plain.includes("[orch] dehydrating session")) {
|
|
1851
|
-
return { orchId, time, type: "dehydrate", orchNode, actNode };
|
|
1852
|
-
}
|
|
2572
|
+
function getSeqEventNodeId(event) {
|
|
2573
|
+
return event?.displayWorkerNodeId || event?.workerNodeId || "(unknown)";
|
|
2574
|
+
}
|
|
1853
2575
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
const
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
if (plain.includes("[durable-agent] Intermediate content") || plain.includes("[orch] intermediate:")) {
|
|
1861
|
-
const cMatch = plain.match(/(?:Intermediate content|intermediate):\s*(.{0,25})/);
|
|
1862
|
-
return { orchId, time, type: "content", orchNode, actNode,
|
|
1863
|
-
snippet: cMatch?.[1] || "…" };
|
|
1864
|
-
}
|
|
1865
|
-
if (plain.includes("[response]")) {
|
|
1866
|
-
const rMatch = plain.match(/\[response\] (.{0,25})/);
|
|
1867
|
-
return { orchId, time, type: "response", orchNode, actNode,
|
|
1868
|
-
snippet: rMatch?.[1] || "" };
|
|
2576
|
+
function findPriorActivitySeqNodeId(timeline) {
|
|
2577
|
+
for (let i = timeline.length - 1; i >= 0; i--) {
|
|
2578
|
+
const candidate = timeline[i];
|
|
2579
|
+
if (!candidate || isOrchestratorSeqEventType(candidate.type)) continue;
|
|
2580
|
+
const nodeId = getSeqEventNodeId(candidate);
|
|
2581
|
+
if (nodeId && nodeId !== "(unknown)") return nodeId;
|
|
1869
2582
|
}
|
|
1870
|
-
// [runTurn] activity log
|
|
1871
|
-
if (plain.includes("[runTurn]")) {
|
|
1872
|
-
return { orchId, time, type: "activity_start", orchNode, actNode };
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
2583
|
return null;
|
|
1876
2584
|
}
|
|
1877
2585
|
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
if (!lastAct) return; // no nodes yet
|
|
1888
|
-
const col = seqNodes.indexOf(lastAct);
|
|
1889
|
-
if (col < 0) return;
|
|
1890
|
-
|
|
1891
|
-
const synth = { orchId, time: now, type: "user_msg_synth", orchNode: lastAct, actNode: lastAct, label };
|
|
1892
|
-
appendSeqEvent(orchId, synth);
|
|
2586
|
+
function placeCmsSeqEvent(timeline, event) {
|
|
2587
|
+
if (!event) return null;
|
|
2588
|
+
const next = { ...event };
|
|
2589
|
+
if (isOrchestratorSeqEventType(next.type)) {
|
|
2590
|
+
const priorActivityNodeId = findPriorActivitySeqNodeId(timeline);
|
|
2591
|
+
if (priorActivityNodeId) next.displayWorkerNodeId = priorActivityNodeId;
|
|
2592
|
+
next.underline = true;
|
|
2593
|
+
}
|
|
2594
|
+
return next;
|
|
1893
2595
|
}
|
|
1894
2596
|
|
|
1895
2597
|
/**
|
|
1896
|
-
*
|
|
2598
|
+
* Map a CMS event to a sequence diagram event.
|
|
2599
|
+
* Returns null for event types that shouldn't appear in the diagram.
|
|
1897
2600
|
*/
|
|
1898
|
-
function
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2601
|
+
function cmsEventToSeqEvent(evt) {
|
|
2602
|
+
const time = evt.createdAt instanceof Date
|
|
2603
|
+
? formatDisplayTime(evt.createdAt.getTime(), { hour: "2-digit", minute: "2-digit", second: "2-digit" })
|
|
2604
|
+
: "";
|
|
2605
|
+
const node = evt.workerNodeId || "(unknown)";
|
|
2606
|
+
const base = { seq: evt.seq, time, workerNodeId: node };
|
|
2607
|
+
|
|
2608
|
+
switch (evt.eventType) {
|
|
2609
|
+
case "session.turn_started":
|
|
2610
|
+
return { ...base, type: "turn_start", detail: `turn ${evt.data?.iteration ?? "?"}` };
|
|
2611
|
+
case "session.turn_completed":
|
|
2612
|
+
return { ...base, type: "turn_end", detail: `turn ${evt.data?.iteration ?? "?"} done` };
|
|
2613
|
+
case "user.message": {
|
|
2614
|
+
const content = typeof evt.data?.content === "string" ? evt.data.content : "";
|
|
2615
|
+
const snip = summarizeActivityPreview(content, 20);
|
|
2616
|
+
return { ...base, type: "user_msg", detail: snip };
|
|
2617
|
+
}
|
|
2618
|
+
case "assistant.message": {
|
|
2619
|
+
const content = typeof evt.data?.content === "string" ? evt.data.content : "";
|
|
2620
|
+
const snip = summarizeActivityPreview(content, 20);
|
|
2621
|
+
return { ...base, type: "response", detail: snip };
|
|
2622
|
+
}
|
|
2623
|
+
case "session.wait_started":
|
|
2624
|
+
return { ...base, type: "wait", detail: `wait ${evt.data?.seconds ?? "?"}s` };
|
|
2625
|
+
case "session.wait_completed":
|
|
2626
|
+
return { ...base, type: "timer", detail: `${evt.data?.seconds ?? "?"}s up` };
|
|
2627
|
+
case "session.dehydrated":
|
|
2628
|
+
return { ...base, type: "dehydrate", detail: `ZZ ${evt.data?.reason ?? ""}` };
|
|
2629
|
+
case "session.hydrated":
|
|
2630
|
+
return { ...base, type: "hydrate", detail: "^^ rehydrated" };
|
|
2631
|
+
case "session.agent_spawned": {
|
|
2632
|
+
const who = evt.data?.agentId || evt.data?.childSessionId?.slice(0, 8) || "agent";
|
|
2633
|
+
return { ...base, type: "spawn", detail: `spawn ${who}` };
|
|
2634
|
+
}
|
|
2635
|
+
case "session.cron_started":
|
|
2636
|
+
return { ...base, type: "cron_start", detail: `cron ${formatHumanDurationSeconds(evt.data?.intervalSeconds ?? "?")}` };
|
|
2637
|
+
case "session.cron_fired":
|
|
2638
|
+
return { ...base, type: "cron_fire", detail: "cron fired" };
|
|
2639
|
+
case "session.cron_cancelled":
|
|
2640
|
+
return { ...base, type: "cron_cancel", detail: "cron off" };
|
|
2641
|
+
case "session.command_received":
|
|
2642
|
+
return { ...base, type: "cmd_recv", detail: `/${evt.data?.cmd ?? "?"}` };
|
|
2643
|
+
case "session.command_completed":
|
|
2644
|
+
return { ...base, type: "cmd_done", detail: `/${evt.data?.cmd ?? "?"} ok` };
|
|
2645
|
+
case "session.compaction_start":
|
|
2646
|
+
return { ...base, type: "compaction", detail: "compaction…" };
|
|
2647
|
+
case "session.compaction_complete":
|
|
2648
|
+
return { ...base, type: "compaction", detail: "compacted" };
|
|
2649
|
+
case "session.error":
|
|
2650
|
+
return { ...base, type: "error", detail: `err: ${(evt.data?.message || "").slice(0, 20)}` };
|
|
2651
|
+
// Skip purely-data events that don't add visual value
|
|
2652
|
+
case "assistant.usage":
|
|
2653
|
+
case "session.usage_info":
|
|
2654
|
+
return null;
|
|
2655
|
+
default:
|
|
2656
|
+
return null;
|
|
1916
2657
|
}
|
|
1917
2658
|
}
|
|
1918
2659
|
|
|
1919
2660
|
/**
|
|
1920
|
-
* Render a
|
|
2661
|
+
* Render a CMS-derived SeqEvent into the sequence pane.
|
|
1921
2662
|
*/
|
|
1922
|
-
function
|
|
1923
|
-
const
|
|
2663
|
+
function renderCmsSeqEventLine(event) {
|
|
2664
|
+
const col = cmsSeqNodeIndex(getSeqEventNodeId(event));
|
|
2665
|
+
const detail = event.detail || "";
|
|
2666
|
+
const lineStyle = event.underline ? { underline: true } : undefined;
|
|
1924
2667
|
|
|
1925
2668
|
switch (event.type) {
|
|
1926
|
-
case "
|
|
1927
|
-
|
|
1928
|
-
// follows provides enough context. This halves the vertical
|
|
1929
|
-
// density of the diagram.
|
|
2669
|
+
case "turn_start":
|
|
2670
|
+
seqPane.log(seqLine(event.time, col, detail, "gray", lineStyle));
|
|
1930
2671
|
break;
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
const orchCol = seqNodes.indexOf(event.orchNode);
|
|
1934
|
-
seqPane.log(seqLine(event.time, orchCol, `turn ${event.turn}`, "gray"));
|
|
2672
|
+
case "turn_end":
|
|
2673
|
+
// Suppress — turn_start is enough (same as existing log-based behavior)
|
|
1935
2674
|
break;
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
case "activity_start": {
|
|
1939
|
-
const col = seqNodes.indexOf(event.actNode);
|
|
1940
|
-
if (lastAct !== undefined && lastAct !== event.actNode) {
|
|
1941
|
-
seqPane.log(seqLine(event.time, col, `> ${lastAct}->${event.actNode}`, "yellow"));
|
|
1942
|
-
}
|
|
1943
|
-
seqLastActivityNode.set(orchId, event.actNode);
|
|
1944
|
-
seqPane.log(seqLine(event.time, col, "> agent", "cyan"));
|
|
2675
|
+
case "user_msg":
|
|
2676
|
+
seqPane.log(seqLine(event.time, col, `>> ${detail}`, "white", lineStyle));
|
|
1945
2677
|
break;
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
case "resume": {
|
|
1949
|
-
const col = seqNodes.indexOf(event.actNode);
|
|
1950
|
-
if (lastAct !== undefined && lastAct !== event.actNode) {
|
|
1951
|
-
seqPane.log(seqLine(event.time, col, `> ${lastAct}->${event.actNode}`, "yellow"));
|
|
1952
|
-
}
|
|
1953
|
-
seqLastActivityNode.set(orchId, event.actNode);
|
|
1954
|
-
seqPane.log(seqLine(event.time, col, "^ resume", "green"));
|
|
2678
|
+
case "response":
|
|
2679
|
+
seqPane.log(seqLine(event.time, col, `< ${detail}`, "green", lineStyle));
|
|
1955
2680
|
break;
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
case "content": {
|
|
1959
|
-
// Skip verbose streaming-content rows in sequence mode to keep
|
|
1960
|
-
// vertical density high; full content remains in chat pane.
|
|
2681
|
+
case "wait":
|
|
2682
|
+
seqPane.log(seqLine(event.time, col, detail, "yellow", lineStyle));
|
|
1961
2683
|
break;
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
case "response": {
|
|
1965
|
-
const col = seqNodes.indexOf(lastAct || event.orchNode);
|
|
1966
|
-
const colW = seqColW();
|
|
1967
|
-
const maxSnip = Math.max(3, colW - 8);
|
|
1968
|
-
const snip = (event.snippet || "ok").slice(0, maxSnip);
|
|
1969
|
-
seqPane.log(seqLine(event.time, col, `< ${snip}`, "green"));
|
|
2684
|
+
case "timer":
|
|
2685
|
+
seqPane.log(seqLine(event.time, col, detail, "yellow", lineStyle));
|
|
1970
2686
|
break;
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
case "wait": {
|
|
1974
|
-
const col = seqNodes.indexOf(lastAct || event.orchNode);
|
|
1975
|
-
seqPane.log(seqLine(event.time, col, `wait ${event.seconds}s`, "yellow"));
|
|
2687
|
+
case "dehydrate":
|
|
2688
|
+
seqPane.log(seqLine(event.time, col, detail, "red", lineStyle));
|
|
1976
2689
|
break;
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
case "timer_fired": {
|
|
1980
|
-
const col = seqNodes.indexOf(lastAct || event.orchNode);
|
|
1981
|
-
seqPane.log(seqLine(event.time, col, `${event.seconds}s up`, "yellow"));
|
|
2690
|
+
case "hydrate":
|
|
2691
|
+
seqPane.log(seqLine(event.time, col, detail, "green", lineStyle));
|
|
1982
2692
|
break;
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
case "dehydrate": {
|
|
1986
|
-
const col = seqNodes.indexOf(lastAct || event.orchNode);
|
|
1987
|
-
seqPane.log(seqLine(event.time, col, "ZZ dehydrate", "red"));
|
|
2693
|
+
case "spawn":
|
|
2694
|
+
seqPane.log(seqLine(event.time, col, detail, "cyan", lineStyle));
|
|
1988
2695
|
break;
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
case "dehydrate_act": {
|
|
1992
|
-
const col = seqNodes.indexOf(event.actNode);
|
|
1993
|
-
seqPane.log(seqLine(event.time, col, "ZZ > blob", "red"));
|
|
2696
|
+
case "cron_start":
|
|
2697
|
+
seqPane.log(seqLine(event.time, col, detail, "magenta", lineStyle));
|
|
1994
2698
|
break;
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
case "hydrate_act": {
|
|
1998
|
-
const col = seqNodes.indexOf(event.actNode);
|
|
1999
|
-
seqPane.log(seqLine(event.time, col, "^^ < blob", "green"));
|
|
2699
|
+
case "cron_fire":
|
|
2700
|
+
seqPane.log(seqLine(event.time, col, detail, "magenta", lineStyle));
|
|
2000
2701
|
break;
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
case "user_idle": {
|
|
2004
|
-
const col = seqNodes.indexOf(lastAct || event.orchNode);
|
|
2005
|
-
seqPane.log(seqLine(event.time, col, ">> user msg", "cyan"));
|
|
2702
|
+
case "cron_cancel":
|
|
2703
|
+
seqPane.log(seqLine(event.time, col, detail, "magenta", lineStyle));
|
|
2006
2704
|
break;
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
case "interrupt": {
|
|
2010
|
-
const col = seqNodes.indexOf(lastAct || event.orchNode);
|
|
2011
|
-
seqPane.log(seqLine(event.time, col, ">> interrupt", "cyan"));
|
|
2705
|
+
case "cmd_recv":
|
|
2706
|
+
seqPane.log(seqLine(event.time, col, `>> ${detail}`, "magenta", lineStyle));
|
|
2012
2707
|
break;
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
case "user_msg_synth": {
|
|
2016
|
-
const col = seqNodes.indexOf(event.orchNode);
|
|
2017
|
-
const snip = event.label && event.label.length > 12 ? event.label.slice(0, 12) + "…" : (event.label || "msg");
|
|
2018
|
-
seqPane.log(seqLine(event.time, col, `>> ${snip}`, "white"));
|
|
2708
|
+
case "cmd_done":
|
|
2709
|
+
seqPane.log(seqLine(event.time, col, `<< ${detail}`, "magenta", lineStyle));
|
|
2019
2710
|
break;
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
seqPane.log(seqLine(event.time, col,
|
|
2711
|
+
case "compaction":
|
|
2712
|
+
seqPane.log(seqLine(event.time, col, detail, "gray", lineStyle));
|
|
2713
|
+
break;
|
|
2714
|
+
case "error":
|
|
2715
|
+
seqPane.log(seqLine(event.time, col, detail, "red", lineStyle));
|
|
2025
2716
|
break;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
function getSeqAggregationKey(event) {
|
|
2721
|
+
if (!event) return null;
|
|
2722
|
+
const nodeId = getSeqEventNodeId(event);
|
|
2723
|
+
if (event.type === "spawn") {
|
|
2724
|
+
return `spawn:${nodeId}`;
|
|
2725
|
+
}
|
|
2726
|
+
if (isOrchestratorSeqEventType(event.type) && event.detail) {
|
|
2727
|
+
return `${event.type}:${nodeId}:${event.detail}`;
|
|
2728
|
+
}
|
|
2729
|
+
return null;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
function collapseSeqEventsForRender(events) {
|
|
2733
|
+
const collapsed = [];
|
|
2734
|
+
for (const event of events) {
|
|
2735
|
+
const prev = collapsed[collapsed.length - 1];
|
|
2736
|
+
const eventKey = getSeqAggregationKey(event);
|
|
2737
|
+
const prevKey = getSeqAggregationKey(prev);
|
|
2738
|
+
|
|
2739
|
+
if (eventKey && prevKey && eventKey === prevKey) {
|
|
2740
|
+
prev.aggregateCount = (prev.aggregateCount || 1) + 1;
|
|
2741
|
+
if (prev.type === "spawn") {
|
|
2742
|
+
prev.detail = `spawn ×${prev.aggregateCount}`;
|
|
2743
|
+
} else {
|
|
2744
|
+
prev.detail = `${prev.baseDetail || prev.detail} ×${prev.aggregateCount}`;
|
|
2745
|
+
}
|
|
2746
|
+
continue;
|
|
2026
2747
|
}
|
|
2027
2748
|
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2749
|
+
const next = { ...event, aggregateCount: 1, baseDetail: event.detail };
|
|
2750
|
+
collapsed.push(next);
|
|
2751
|
+
}
|
|
2752
|
+
return collapsed;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
/** Ensure a worker node is tracked in the sequence column list. Returns its index. */
|
|
2756
|
+
function cmsSeqNodeIndex(workerNodeId) {
|
|
2757
|
+
const short = workerNodeId ? safeTail(workerNodeId, 5) : "(unk)";
|
|
2758
|
+
if (!seqNodeSet.has(short)) {
|
|
2759
|
+
seqNodeSet.add(short);
|
|
2760
|
+
seqNodes.push(short);
|
|
2761
|
+
if (logViewMode === "sequence") updateSeqHeader();
|
|
2762
|
+
}
|
|
2763
|
+
return seqNodes.indexOf(short);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
/**
|
|
2767
|
+
* Load the full CMS event timeline for a session.
|
|
2768
|
+
* Called on session switch and TUI startup.
|
|
2769
|
+
*/
|
|
2770
|
+
async function loadCmsSeqTimeline(sessionId) {
|
|
2771
|
+
try {
|
|
2772
|
+
const events = await mgmt.getSessionEvents(sessionId);
|
|
2773
|
+
const timeline = [];
|
|
2774
|
+
for (const evt of events) {
|
|
2775
|
+
const seqEvt = placeCmsSeqEvent(timeline, cmsEventToSeqEvent(evt));
|
|
2776
|
+
if (seqEvt) timeline.push(seqEvt);
|
|
2032
2777
|
}
|
|
2778
|
+
cmsSeqTimelines.set(sessionId, timeline);
|
|
2779
|
+
cmsSeqLastSeq.set(sessionId, events.length > 0 ? events[events.length - 1].seq : 0);
|
|
2033
2780
|
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2781
|
+
// Discover worker nodes from events
|
|
2782
|
+
for (const evt of timeline) {
|
|
2783
|
+
const nodeId = getSeqEventNodeId(evt);
|
|
2784
|
+
if (nodeId) cmsSeqNodeIndex(nodeId);
|
|
2038
2785
|
}
|
|
2786
|
+
} catch (err) {
|
|
2787
|
+
// CMS not available — the log-based path will still work as fallback
|
|
2039
2788
|
}
|
|
2040
|
-
// Don't render here — callers batch renders
|
|
2041
2789
|
}
|
|
2042
2790
|
|
|
2043
2791
|
/**
|
|
2044
|
-
*
|
|
2792
|
+
* Incrementally poll for new CMS events for a session.
|
|
2793
|
+
* Appends to existing timeline and renders if sequence mode is active.
|
|
2045
2794
|
*/
|
|
2046
|
-
function
|
|
2047
|
-
|
|
2048
|
-
const
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2795
|
+
async function pollCmsSeqEvents(sessionId) {
|
|
2796
|
+
try {
|
|
2797
|
+
const afterSeq = cmsSeqLastSeq.get(sessionId) || 0;
|
|
2798
|
+
const events = await mgmt.getSessionEvents(sessionId, afterSeq, 100);
|
|
2799
|
+
if (events.length === 0) return;
|
|
2800
|
+
|
|
2801
|
+
const timeline = cmsSeqTimelines.get(sessionId) || [];
|
|
2802
|
+
for (const evt of events) {
|
|
2803
|
+
const seqEvt = placeCmsSeqEvent(timeline, cmsEventToSeqEvent(evt));
|
|
2804
|
+
if (seqEvt) {
|
|
2805
|
+
const nodeId = getSeqEventNodeId(seqEvt);
|
|
2806
|
+
if (nodeId) cmsSeqNodeIndex(nodeId);
|
|
2807
|
+
timeline.push(seqEvt);
|
|
2808
|
+
// Render incrementally if this is the active session in sequence mode
|
|
2809
|
+
const orchId = `session-${sessionId}`;
|
|
2810
|
+
if (logViewMode === "sequence" && orchId === activeOrchId) {
|
|
2811
|
+
refreshCmsSeqPane();
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
cmsSeqTimelines.set(sessionId, timeline);
|
|
2816
|
+
cmsSeqLastSeq.set(sessionId, events[events.length - 1].seq);
|
|
2817
|
+
|
|
2818
|
+
// Cap timeline at 300 events
|
|
2819
|
+
if (timeline.length > 300) timeline.splice(0, timeline.length - 300);
|
|
2820
|
+
|
|
2821
|
+
if (logViewMode === "sequence" && `session-${sessionId}` === activeOrchId) {
|
|
2822
|
+
screen.render();
|
|
2823
|
+
}
|
|
2824
|
+
} catch {}
|
|
2053
2825
|
}
|
|
2054
2826
|
|
|
2055
2827
|
/**
|
|
2056
|
-
* Full re-render of the sequence pane for the active session.
|
|
2828
|
+
* Full re-render of the CMS-backed sequence pane for the active session.
|
|
2057
2829
|
*/
|
|
2058
|
-
function
|
|
2830
|
+
function refreshCmsSeqPane() {
|
|
2059
2831
|
seqPane.setContent("");
|
|
2060
2832
|
const seqShortId = shortId(activeOrchId);
|
|
2061
2833
|
seqPane.setLabel(` Sequence: ${seqShortId} `);
|
|
2062
|
-
|
|
2063
|
-
// Update sticky header
|
|
2064
2834
|
updateSeqHeader();
|
|
2065
2835
|
|
|
2066
|
-
|
|
2067
|
-
|
|
2836
|
+
const sessionId = activeOrchId?.replace("session-", "");
|
|
2837
|
+
const timeline = sessionId ? cmsSeqTimelines.get(sessionId) : null;
|
|
2068
2838
|
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
if (
|
|
2075
|
-
seqPane.log(`{gray-fg}… showing last ${MAX_SEQ_RENDER_EVENTS} of ${
|
|
2839
|
+
if (timeline && timeline.length > 0) {
|
|
2840
|
+
const renderEvents = timeline.length > MAX_SEQ_RENDER_EVENTS
|
|
2841
|
+
? timeline.slice(-MAX_SEQ_RENDER_EVENTS)
|
|
2842
|
+
: timeline;
|
|
2843
|
+
const collapsedEvents = collapseSeqEventsForRender(renderEvents);
|
|
2844
|
+
if (timeline.length > MAX_SEQ_RENDER_EVENTS) {
|
|
2845
|
+
seqPane.log(`{gray-fg}… showing last ${MAX_SEQ_RENDER_EVENTS} of ${timeline.length} events …{/gray-fg}`);
|
|
2076
2846
|
}
|
|
2077
|
-
for (const event of
|
|
2078
|
-
|
|
2847
|
+
for (const event of collapsedEvents) {
|
|
2848
|
+
renderCmsSeqEventLine(event);
|
|
2079
2849
|
}
|
|
2080
2850
|
} else {
|
|
2081
2851
|
seqPane.log("{white-fg}No events yet — interact with this session to populate{/white-fg}");
|
|
@@ -2088,8 +2858,9 @@ function refreshSeqPane() {
|
|
|
2088
2858
|
|
|
2089
2859
|
function appendOrchLog(orchId, podName, text) {
|
|
2090
2860
|
if (!orchLogBuffers.has(orchId)) orchLogBuffers.set(orchId, []);
|
|
2091
|
-
const
|
|
2092
|
-
const
|
|
2861
|
+
const normalizedPodName = normalizePodName(podName);
|
|
2862
|
+
const color = getPodColor(normalizedPodName);
|
|
2863
|
+
const shortPod = safeTail(normalizedPodName, 5);
|
|
2093
2864
|
const coloredLine = `{${color}-fg}[${shortPod}]{/${color}-fg} ${text}`;
|
|
2094
2865
|
orchLogBuffers.get(orchId).push(coloredLine);
|
|
2095
2866
|
// Cap buffer at 500 lines
|
|
@@ -2236,11 +3007,12 @@ function backfillOrchLogs(orchId) {
|
|
|
2236
3007
|
}
|
|
2237
3008
|
|
|
2238
3009
|
function getOrCreateWorkerPane(podName) {
|
|
3010
|
+
podName = normalizePodName(podName);
|
|
2239
3011
|
if (workerPanes.has(podName)) return workerPanes.get(podName);
|
|
2240
3012
|
|
|
2241
3013
|
const color = paneColors[nextColorIdx++ % paneColors.length];
|
|
2242
3014
|
// Short name: last 5 chars of pod name
|
|
2243
|
-
const shortName = podName
|
|
3015
|
+
const shortName = safeTail(podName, 5);
|
|
2244
3016
|
|
|
2245
3017
|
const pane = blessed.log({
|
|
2246
3018
|
parent: screen,
|
|
@@ -2262,17 +3034,18 @@ function getOrCreateWorkerPane(podName) {
|
|
|
2262
3034
|
scrollbar: { style: { bg: color } },
|
|
2263
3035
|
keys: true,
|
|
2264
3036
|
vi: true,
|
|
2265
|
-
mouse:
|
|
3037
|
+
mouse: true,
|
|
2266
3038
|
});
|
|
2267
3039
|
|
|
2268
3040
|
workerPanes.set(podName, pane);
|
|
2269
3041
|
workerPaneOrder.push(podName);
|
|
3042
|
+
registerSelectablePane(pane, `worker ${shortName}`);
|
|
2270
3043
|
registerFocusRing(pane, color);
|
|
2271
3044
|
pane.on("focus", () => setNavigationStatusForPane("workers"));
|
|
2272
3045
|
|
|
2273
3046
|
// Register this pod as a sequence diagram column so all nodes
|
|
2274
3047
|
// appear regardless of whether the active session has used them.
|
|
2275
|
-
|
|
3048
|
+
cmsSeqNodeIndex(podName);
|
|
2276
3049
|
|
|
2277
3050
|
relayoutAll();
|
|
2278
3051
|
return pane;
|
|
@@ -2423,7 +3196,7 @@ function redrawActiveViews() {
|
|
|
2423
3196
|
|
|
2424
3197
|
// ─── Input bar ───────────────────────────────────────────────────
|
|
2425
3198
|
|
|
2426
|
-
|
|
3199
|
+
inputBar = blessed.textarea({
|
|
2427
3200
|
parent: screen,
|
|
2428
3201
|
label: " {bold}you:{/bold} ",
|
|
2429
3202
|
tags: true,
|
|
@@ -2464,13 +3237,69 @@ function clampInputCursor(index, value = inputBar.getValue()) {
|
|
|
2464
3237
|
}
|
|
2465
3238
|
|
|
2466
3239
|
function getInputInnerWidth() {
|
|
3240
|
+
if (!inputBar) return Math.max(1, screen.width - 2);
|
|
2467
3241
|
const numericWidth = typeof inputBar.width === "number" ? inputBar.width : screen.width;
|
|
2468
3242
|
return Math.max(1, numericWidth - (inputBar.iwidth || 2));
|
|
2469
3243
|
}
|
|
2470
3244
|
|
|
3245
|
+
function getInputWrapWidth() {
|
|
3246
|
+
// neo-blessed textarea reserves one extra cell of right margin for the
|
|
3247
|
+
// cursor, so wrapping happens at innerWidth - 1 rather than innerWidth.
|
|
3248
|
+
const margin = 1 + (inputBar?.scrollbar ? 1 : 0);
|
|
3249
|
+
return Math.max(1, getInputInnerWidth() - margin);
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
function getInputStringWidth(text) {
|
|
3253
|
+
if (typeof inputBar?.strWidth === "function") {
|
|
3254
|
+
return inputBar.strWidth(String(text || ""));
|
|
3255
|
+
}
|
|
3256
|
+
return displayWidth(text);
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
function getRawInputCursorPosition(value, cursorIndex) {
|
|
3260
|
+
const text = String(value || "");
|
|
3261
|
+
const target = clampInputCursor(cursorIndex, text);
|
|
3262
|
+
let line = 0;
|
|
3263
|
+
let column = 0;
|
|
3264
|
+
|
|
3265
|
+
for (let i = 0; i < target; i++) {
|
|
3266
|
+
if (text[i] === "\n") {
|
|
3267
|
+
line += 1;
|
|
3268
|
+
column = 0;
|
|
3269
|
+
continue;
|
|
3270
|
+
}
|
|
3271
|
+
column += 1;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
return { line, column };
|
|
3275
|
+
}
|
|
3276
|
+
|
|
2471
3277
|
function getCursorVisualPosition(value, cursorIndex) {
|
|
2472
3278
|
const text = String(value || "");
|
|
2473
|
-
const
|
|
3279
|
+
const clines = inputBar?._clines;
|
|
3280
|
+
if (clines?.ftor && clines.content === text) {
|
|
3281
|
+
const { line: rawLineIndex, column: rawColumn } = getRawInputCursorPosition(text, cursorIndex);
|
|
3282
|
+
const wrappedIndexes = clines.ftor[rawLineIndex] || [];
|
|
3283
|
+
|
|
3284
|
+
if (wrappedIndexes.length > 0) {
|
|
3285
|
+
let remaining = rawColumn;
|
|
3286
|
+
for (let index = 0; index < wrappedIndexes.length; index++) {
|
|
3287
|
+
const wrappedLineIndex = wrappedIndexes[index];
|
|
3288
|
+
const wrappedText = clines[wrappedLineIndex] || "";
|
|
3289
|
+
const segmentLength = wrappedText.length;
|
|
3290
|
+
const isLastSegment = index === wrappedIndexes.length - 1;
|
|
3291
|
+
|
|
3292
|
+
if (remaining <= segmentLength || isLastSegment) {
|
|
3293
|
+
const visiblePrefix = wrappedText.slice(0, Math.min(remaining, segmentLength));
|
|
3294
|
+
return { row: wrappedLineIndex, col: getInputStringWidth(visiblePrefix) };
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
remaining -= segmentLength;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
const width = getInputWrapWidth();
|
|
2474
3303
|
let row = 0;
|
|
2475
3304
|
let col = 0;
|
|
2476
3305
|
|
|
@@ -2481,11 +3310,12 @@ function getCursorVisualPosition(value, cursorIndex) {
|
|
|
2481
3310
|
col = 0;
|
|
2482
3311
|
continue;
|
|
2483
3312
|
}
|
|
2484
|
-
|
|
2485
|
-
if (col
|
|
3313
|
+
const chWidth = Math.max(1, displayWidth(ch));
|
|
3314
|
+
if (col > 0 && col + chWidth > width) {
|
|
2486
3315
|
row += 1;
|
|
2487
3316
|
col = 0;
|
|
2488
3317
|
}
|
|
3318
|
+
col += chWidth;
|
|
2489
3319
|
}
|
|
2490
3320
|
|
|
2491
3321
|
return { row, col };
|
|
@@ -3010,6 +3840,7 @@ function appendLog(text) {
|
|
|
3010
3840
|
}
|
|
3011
3841
|
|
|
3012
3842
|
function appendWorkerLog(podName, text, orchId) {
|
|
3843
|
+
podName = normalizePodName(podName);
|
|
3013
3844
|
const pane = getOrCreateWorkerPane(podName);
|
|
3014
3845
|
// Buffer raw entry for recoloring on session switch
|
|
3015
3846
|
if (!workerLogBuffers.has(podName)) workerLogBuffers.set(podName, []);
|
|
@@ -3058,42 +3889,167 @@ function recolorWorkerPanes() {
|
|
|
3058
3889
|
});
|
|
3059
3890
|
totalLines += Math.min(buf.length, MAX_WORKER_RENDER_LINES);
|
|
3060
3891
|
}
|
|
3061
|
-
screen.render();
|
|
3062
|
-
perfEnd(_ph, { panes: paneCount, lines: totalLines });
|
|
3892
|
+
screen.render();
|
|
3893
|
+
perfEnd(_ph, { panes: paneCount, lines: totalLines });
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
function showCopilotMessage(raw, orchId) {
|
|
3897
|
+
const _ph = perfStart("showCopilotMessage");
|
|
3898
|
+
stopChatSpinner(orchId);
|
|
3899
|
+
const prefix = `{white-fg}[${ts()}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`;
|
|
3900
|
+
const { lines } = buildAssistantChatLines(raw, orchId, prefix);
|
|
3901
|
+
|
|
3902
|
+
// Try to replace the preview lines in-place so the frame loop sees a
|
|
3903
|
+
// single buffer mutation instead of a shrink-then-grow. This avoids
|
|
3904
|
+
// the scroll-position jump that setContent() causes when the total
|
|
3905
|
+
// line count changes between frames.
|
|
3906
|
+
if (!replaceAssistantPreviewInPlace(orchId, raw, lines)) {
|
|
3907
|
+
// No matching preview — append normally.
|
|
3908
|
+
for (const line of lines) {
|
|
3909
|
+
appendChatRaw(line, orchId);
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
perfEnd(_ph, { len: raw?.length || 0 });
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
function isRenderedTableLikeLine(line) {
|
|
3916
|
+
const trimmed = String(line || "").trim();
|
|
3917
|
+
if (!trimmed) return false;
|
|
3918
|
+
if (isMarkdownTableLine(trimmed)) return true;
|
|
3919
|
+
return /^[┌├└│+|]/.test(trimmed);
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
function buildPrefixedRenderedLines(prefix, rendered, options = {}) {
|
|
3923
|
+
const trailingBlank = options.trailingBlank !== false;
|
|
3924
|
+
const renderedLines = String(rendered || "").split("\n");
|
|
3925
|
+
const firstContentLineIndex = renderedLines.findIndex((line) => line.trim() !== "");
|
|
3926
|
+
const startsWithTable = firstContentLineIndex >= 0
|
|
3927
|
+
&& isRenderedTableLikeLine(renderedLines[firstContentLineIndex]);
|
|
3928
|
+
const lines = startsWithTable
|
|
3929
|
+
? [prefix, ...renderedLines]
|
|
3930
|
+
: (() => {
|
|
3931
|
+
const [firstLine = "", ...rest] = renderedLines;
|
|
3932
|
+
return [firstLine ? `${prefix} ${firstLine}` : prefix, ...rest];
|
|
3933
|
+
})();
|
|
3934
|
+
if (trailingBlank) lines.push("");
|
|
3935
|
+
return lines;
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
function stripAssistantHeadingLine(text) {
|
|
3939
|
+
return typeof text === "string"
|
|
3940
|
+
? text.replace(/^HEADING:.*\n?/m, "").trim()
|
|
3941
|
+
: "";
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
function normalizeObserverChatText(text) {
|
|
3945
|
+
if (typeof text !== "string") return "";
|
|
3946
|
+
return text.replace(/\r\n/g, "\n").trim();
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
function normalizeAssistantPreviewText(text) {
|
|
3950
|
+
return normalizeObserverChatText(stripAssistantHeadingLine(text));
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
function removeChatLineSequence(buffer, lines, replacementLines) {
|
|
3954
|
+
if (!Array.isArray(buffer) || !Array.isArray(lines) || lines.length === 0) return false;
|
|
3955
|
+
for (let start = buffer.length - lines.length; start >= 0; start--) {
|
|
3956
|
+
let matches = true;
|
|
3957
|
+
for (let offset = 0; offset < lines.length; offset++) {
|
|
3958
|
+
if (buffer[start + offset] !== lines[offset]) {
|
|
3959
|
+
matches = false;
|
|
3960
|
+
break;
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
if (matches) {
|
|
3964
|
+
if (replacementLines) {
|
|
3965
|
+
buffer.splice(start, lines.length, ...replacementLines);
|
|
3966
|
+
} else {
|
|
3967
|
+
buffer.splice(start, lines.length);
|
|
3968
|
+
}
|
|
3969
|
+
return true;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
return false;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
function stripRenderedChatFormatting(text) {
|
|
3976
|
+
return String(text || "")
|
|
3977
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
3978
|
+
.replace(/\x1b\[[0-9;]*m/g, "")
|
|
3979
|
+
.replace(/\{[^}]*\}/g, "");
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
function prepareAssistantChatRender(raw, orchId, options = {}) {
|
|
3983
|
+
const sourceText = typeof raw === "string" ? raw : "";
|
|
3984
|
+
const bodyText = options.stripHeading === true
|
|
3985
|
+
? stripAssistantHeadingLine(sourceText)
|
|
3986
|
+
: sourceText;
|
|
3987
|
+
|
|
3988
|
+
detectArtifactLinks(sourceText, orchId);
|
|
3989
|
+
|
|
3990
|
+
const renderedText = renderMarkdown(renderArtifactLinks(bodyText));
|
|
3991
|
+
return {
|
|
3992
|
+
sourceText,
|
|
3993
|
+
bodyText,
|
|
3994
|
+
renderedText: options.stripFormatting === true
|
|
3995
|
+
? stripRenderedChatFormatting(renderedText)
|
|
3996
|
+
: renderedText,
|
|
3997
|
+
};
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
function buildAssistantChatLines(raw, orchId, prefix, options = {}) {
|
|
4001
|
+
const { trailingBlank, lineDecorator } = options;
|
|
4002
|
+
const { bodyText, renderedText } = prepareAssistantChatRender(raw, orchId, options);
|
|
4003
|
+
let lines = buildPrefixedRenderedLines(prefix, renderedText, { trailingBlank });
|
|
4004
|
+
if (typeof lineDecorator === "function") {
|
|
4005
|
+
lines = lines.map((line) => lineDecorator(line, bodyText));
|
|
4006
|
+
}
|
|
4007
|
+
return { bodyText, renderedText, lines };
|
|
3063
4008
|
}
|
|
3064
4009
|
|
|
3065
|
-
function
|
|
3066
|
-
const
|
|
3067
|
-
|
|
4010
|
+
function replaceAssistantPreviewInPlace(orchId, matchingText, replacementLines) {
|
|
4011
|
+
const preview = sessionPendingAssistantPreview.get(orchId);
|
|
4012
|
+
if (!preview) return false;
|
|
3068
4013
|
|
|
3069
|
-
|
|
4014
|
+
if (matchingText != null) {
|
|
4015
|
+
const normalized = normalizeAssistantPreviewText(matchingText);
|
|
4016
|
+
if (!normalized || normalized !== preview.normalized) return false;
|
|
4017
|
+
}
|
|
3070
4018
|
|
|
3071
|
-
|
|
3072
|
-
|
|
4019
|
+
const buffer = sessionChatBuffers.get(orchId);
|
|
4020
|
+
if (!buffer) return false;
|
|
3073
4021
|
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
if (
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
"📎 **$1** _(press 'a' to download)_",
|
|
3080
|
-
);
|
|
3081
|
-
}
|
|
4022
|
+
const spliced = removeChatLineSequence(buffer, preview.lines, replacementLines);
|
|
4023
|
+
sessionPendingAssistantPreview.delete(orchId);
|
|
4024
|
+
if (spliced && orchId === activeOrchId) invalidateChat();
|
|
4025
|
+
return spliced;
|
|
4026
|
+
}
|
|
3082
4027
|
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
appendChatRaw(prefix, orchId);
|
|
3086
|
-
// Always show on separate lines for readability
|
|
3087
|
-
for (const line of rendered.split("\n")) {
|
|
3088
|
-
appendChatRaw(line, orchId);
|
|
3089
|
-
}
|
|
3090
|
-
appendChatRaw("", orchId); // blank line after each message
|
|
3091
|
-
perfEnd(_ph, { len: raw?.length || 0 });
|
|
4028
|
+
function clearPendingAssistantPreview(orchId, matchingText) {
|
|
4029
|
+
return replaceAssistantPreviewInPlace(orchId, matchingText, null);
|
|
3092
4030
|
}
|
|
3093
4031
|
|
|
3094
|
-
function
|
|
3095
|
-
|
|
3096
|
-
|
|
4032
|
+
function showLiveAssistantPreview(raw, orchId) {
|
|
4033
|
+
const normalized = normalizeAssistantPreviewText(raw);
|
|
4034
|
+
if (!normalized) return;
|
|
4035
|
+
if (sessionPromotedIntermediate.get(orchId) === normalized) return;
|
|
4036
|
+
if (sessionRecoveredTurnResult.get(orchId) === normalized) return;
|
|
4037
|
+
|
|
4038
|
+
const existing = sessionPendingAssistantPreview.get(orchId);
|
|
4039
|
+
if (existing?.normalized === normalized) return;
|
|
4040
|
+
|
|
4041
|
+
clearPendingAssistantPreview(orchId);
|
|
4042
|
+
|
|
4043
|
+
const { lines: previewLines } = buildAssistantChatLines(raw, orchId, `[${ts()}] Copilot [preview]:`, {
|
|
4044
|
+
stripHeading: true,
|
|
4045
|
+
stripFormatting: true,
|
|
4046
|
+
lineDecorator: (line) => line ? `{gray-fg}${line}{/gray-fg}` : "",
|
|
4047
|
+
});
|
|
4048
|
+
|
|
4049
|
+
for (const line of previewLines) {
|
|
4050
|
+
appendChatRaw(line, orchId);
|
|
4051
|
+
}
|
|
4052
|
+
sessionPendingAssistantPreview.set(orchId, { normalized, lines: previewLines });
|
|
3097
4053
|
}
|
|
3098
4054
|
|
|
3099
4055
|
function promoteIntermediateContent(raw, orchId) {
|
|
@@ -3106,6 +4062,7 @@ function promoteIntermediateContent(raw, orchId) {
|
|
|
3106
4062
|
|
|
3107
4063
|
function shouldSkipCompletedTurnResult(raw, orchId) {
|
|
3108
4064
|
const normalized = normalizeObserverChatText(raw);
|
|
4065
|
+
clearPendingAssistantPreview(orchId);
|
|
3109
4066
|
const promoted = sessionPromotedIntermediate.get(orchId);
|
|
3110
4067
|
sessionPromotedIntermediate.delete(orchId);
|
|
3111
4068
|
const recovered = sessionRecoveredTurnResult.get(orchId);
|
|
@@ -3130,8 +4087,7 @@ function isBootstrapPromptForSession(text, orchId) {
|
|
|
3130
4087
|
});
|
|
3131
4088
|
}
|
|
3132
4089
|
|
|
3133
|
-
|
|
3134
|
-
const seqCmsSeededSessions = new Set();
|
|
4090
|
+
|
|
3135
4091
|
|
|
3136
4092
|
/**
|
|
3137
4093
|
* Load conversation history from CMS and rebuild chat buffer for the session.
|
|
@@ -3203,6 +4159,8 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3203
4159
|
: liveStatus.customStatus;
|
|
3204
4160
|
} catch {}
|
|
3205
4161
|
}
|
|
4162
|
+
updateSessionCronSchedule(orchId, liveCustomStatus);
|
|
4163
|
+
updateSessionContextUsageFromStatus(orchId, liveCustomStatus);
|
|
3206
4164
|
if (liveCustomStatus?.responseVersion) {
|
|
3207
4165
|
liveResponsePayload = await fetchLatestResponsePayload(orchId, dc);
|
|
3208
4166
|
}
|
|
@@ -3218,6 +4176,9 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3218
4176
|
sessionModels.set(orchId, info.model);
|
|
3219
4177
|
if (orchId === activeOrchId) updateChatLabel();
|
|
3220
4178
|
}
|
|
4179
|
+
if (info?.contextUsage) {
|
|
4180
|
+
noteSessionContextUsage(orchId, info.contextUsage);
|
|
4181
|
+
}
|
|
3221
4182
|
|
|
3222
4183
|
if ((!events || events.length === 0) && !liveTurnContent) {
|
|
3223
4184
|
if (sessionHistoryLoadGeneration.get(orchId) !== generation) {
|
|
@@ -3255,7 +4216,10 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3255
4216
|
const stripHostPrefix = (text) => text?.replace(/^\[SYSTEM: Running on host "[^"]*"\.\]\n\n/, "") || text;
|
|
3256
4217
|
|
|
3257
4218
|
// Filter out internal timer continuation prompts — these aren't real user messages
|
|
3258
|
-
const isTimerPrompt = (text) =>
|
|
4219
|
+
const isTimerPrompt = (text) =>
|
|
4220
|
+
/^The \d+ second wait is now complete\./i.test(text)
|
|
4221
|
+
|| /^The wait was partially completed \(/i.test(text)
|
|
4222
|
+
|| /^Resume the wait for the remaining \d+/i.test(text);
|
|
3259
4223
|
|
|
3260
4224
|
const lines = [];
|
|
3261
4225
|
const fmtTime = (value) => {
|
|
@@ -3297,9 +4261,10 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3297
4261
|
const updateType = childMatch[2];
|
|
3298
4262
|
const body = (childMatch[4] || "").trim();
|
|
3299
4263
|
const childTitle = sessionHeadings.get(`session-${childMatch[1]}`) || `Agent ${childId}`;
|
|
3300
|
-
const
|
|
4264
|
+
const typeIcon = updateType === "completed" ? "✅" : updateType === "error" ? "❌" : "📡";
|
|
4265
|
+
const typeColor = updateType === "completed" ? "green" : updateType === "error" ? "red" : "yellow";
|
|
3301
4266
|
lines.push(`{white-fg}[${timeStr}]{/white-fg}`);
|
|
3302
|
-
lines.push(`{${typeColor}-fg}┌─ {bold}${childTitle}{/bold} · ${updateType}
|
|
4267
|
+
lines.push(`{${typeColor}-fg}┌─ ${typeIcon} {bold}${childTitle}{/bold} · ${updateType} ${"─".repeat(Math.max(1, 30 - childTitle.length))}┐{/${typeColor}-fg}`);
|
|
3303
4268
|
if (body) {
|
|
3304
4269
|
const bodyLines = body.split("\n");
|
|
3305
4270
|
for (const bl of bodyLines.slice(0, 8)) {
|
|
@@ -3309,7 +4274,7 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3309
4274
|
lines.push(`{${typeColor}-fg}│{/${typeColor}-fg} {gray-fg}… ${bodyLines.length - 8} more lines{/gray-fg}`);
|
|
3310
4275
|
}
|
|
3311
4276
|
}
|
|
3312
|
-
lines.push(`{${typeColor}-fg}└${"─".repeat(
|
|
4277
|
+
lines.push(`{${typeColor}-fg}└${"─".repeat(40)}┘{/${typeColor}-fg}`);
|
|
3313
4278
|
lines.push("");
|
|
3314
4279
|
} else {
|
|
3315
4280
|
lines.push(`{white-fg}[${timeStr}]{/white-fg} {bold}You:{/bold} ${content}`);
|
|
@@ -3319,7 +4284,6 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3319
4284
|
const content = evt.data?.content;
|
|
3320
4285
|
if (content) {
|
|
3321
4286
|
lastAssistantContent = content;
|
|
3322
|
-
detectArtifactLinks(content, orchId);
|
|
3323
4287
|
if (renderedChars >= MAX_TOTAL_RENDER_CHARS) {
|
|
3324
4288
|
lines.push(`{gray-fg}── additional assistant output omitted to keep session switching fast ──{/gray-fg}`);
|
|
3325
4289
|
lines.push("");
|
|
@@ -3328,17 +4292,12 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3328
4292
|
const clipped = content.length > MAX_ASSISTANT_MESSAGE_CHARS
|
|
3329
4293
|
? content.slice(0, MAX_ASSISTANT_MESSAGE_CHARS) + "\n\n[output truncated in TUI history view]"
|
|
3330
4294
|
: content;
|
|
3331
|
-
const displayClipped = clipped.replace(
|
|
3332
|
-
/artifact:\/\/[a-f0-9-]+\/([^\s"'{}]+)/g,
|
|
3333
|
-
"📎 **$1** _(press 'a' to download)_",
|
|
3334
|
-
);
|
|
3335
|
-
lines.push(`{white-fg}[${timeStr}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
3336
|
-
const rendered = renderMarkdown(displayClipped);
|
|
3337
4295
|
renderedChars += clipped.length;
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
4296
|
+
lines.push(...buildAssistantChatLines(
|
|
4297
|
+
clipped,
|
|
4298
|
+
orchId,
|
|
4299
|
+
`{white-fg}[${timeStr}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`,
|
|
4300
|
+
).lines);
|
|
3342
4301
|
}
|
|
3343
4302
|
} else if (type === "tool.execution_start") {
|
|
3344
4303
|
activityLines.push(formatToolActivityLine(timeStr, evt, "start"));
|
|
@@ -3368,13 +4327,12 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3368
4327
|
const clippedLiveTurn = liveTurnContent.length > MAX_ASSISTANT_MESSAGE_CHARS
|
|
3369
4328
|
? liveTurnContent.slice(0, MAX_ASSISTANT_MESSAGE_CHARS) + "\n\n[output truncated in TUI history view]"
|
|
3370
4329
|
: liveTurnContent;
|
|
3371
|
-
lines.push(`{white-fg}[${fmtTime(Date.now())}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
3372
|
-
const renderedLiveTurn = renderMarkdown(clippedLiveTurn);
|
|
3373
4330
|
renderedChars += clippedLiveTurn.length;
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
4331
|
+
lines.push(...buildAssistantChatLines(
|
|
4332
|
+
clippedLiveTurn,
|
|
4333
|
+
orchId,
|
|
4334
|
+
`{white-fg}[${fmtTime(Date.now())}]{/white-fg} {cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`,
|
|
4335
|
+
).lines);
|
|
3378
4336
|
sessionRecoveredTurnResult.set(orchId, normalizedLiveTurn);
|
|
3379
4337
|
noteSeenResponseVersion(orchId, liveResponsePayload?.version);
|
|
3380
4338
|
} else {
|
|
@@ -3408,12 +4366,8 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3408
4366
|
// replacement would nuke it without this check.
|
|
3409
4367
|
const pendingQ = sessionPendingQuestions.get(orchId);
|
|
3410
4368
|
if (pendingQ) {
|
|
3411
|
-
lines.push(`{cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
3412
4369
|
const renderedQ = renderMarkdown(pendingQ);
|
|
3413
|
-
|
|
3414
|
-
lines.push(line);
|
|
3415
|
-
}
|
|
3416
|
-
lines.push("");
|
|
4370
|
+
lines.push(...buildPrefixedRenderedLines(`{cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`, renderedQ));
|
|
3417
4371
|
}
|
|
3418
4372
|
|
|
3419
4373
|
// If CMS history produced no chat-visible lines (only the footer),
|
|
@@ -3441,7 +4395,19 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3441
4395
|
} else {
|
|
3442
4396
|
sessionChatBuffers.set(orchId, lines);
|
|
3443
4397
|
}
|
|
3444
|
-
|
|
4398
|
+
// Guard: don't replace existing activity with an empty array.
|
|
4399
|
+
// The observer may have already appended live entries; nuking
|
|
4400
|
+
// them with an empty CMS result causes a blank Activity pane.
|
|
4401
|
+
const existingActivity = sessionActivityBuffers.get(orchId);
|
|
4402
|
+
const existingActivityHasContent = existingActivity && existingActivity.length > 0
|
|
4403
|
+
&& existingActivity.some(l => l && !/no recent/.test(l));
|
|
4404
|
+
if (activityLines.length === 0 && existingActivityHasContent) {
|
|
4405
|
+
// keep observer-written activity intact
|
|
4406
|
+
} else if (activityLines.length === 0) {
|
|
4407
|
+
sessionActivityBuffers.set(orchId, ["{gray-fg}(no recent activity yet){/gray-fg}"]);
|
|
4408
|
+
} else {
|
|
4409
|
+
sessionActivityBuffers.set(orchId, activityLines);
|
|
4410
|
+
}
|
|
3445
4411
|
sessionHistoryLoadedAt.set(orchId, Date.now());
|
|
3446
4412
|
sessionRenderedCmsSeq.set(orchId, maxRenderedSeq);
|
|
3447
4413
|
|
|
@@ -3450,33 +4416,6 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
3450
4416
|
invalidateActivity();
|
|
3451
4417
|
}
|
|
3452
4418
|
|
|
3453
|
-
if (!seqCmsSeededSessions.has(orchId)) {
|
|
3454
|
-
const existingSeq = seqEventBuffers.get(orchId) ?? [];
|
|
3455
|
-
if (existingSeq.length === 0) {
|
|
3456
|
-
const cmsNode = addSeqNode("cms");
|
|
3457
|
-
const seeded = [];
|
|
3458
|
-
for (const evt of events) {
|
|
3459
|
-
const t = fmtTime(evt.createdAt);
|
|
3460
|
-
if (evt.eventType === "user.message") {
|
|
3461
|
-
const txt = stripHostPrefix(evt.data?.content || "");
|
|
3462
|
-
if (txt && !isTimerPrompt(txt)) {
|
|
3463
|
-
seeded.push({ type: "user_msg_synth", time: t, orchNode: cmsNode, actNode: cmsNode, label: txt });
|
|
3464
|
-
}
|
|
3465
|
-
} else if (evt.eventType === "assistant.message") {
|
|
3466
|
-
const txt = evt.data?.content || "";
|
|
3467
|
-
if (txt) {
|
|
3468
|
-
seeded.push({ type: "response", time: t, orchNode: cmsNode, actNode: cmsNode, snippet: txt.slice(0, 40) });
|
|
3469
|
-
}
|
|
3470
|
-
} else if (evt.eventType === "tool.execution_start") {
|
|
3471
|
-
seeded.push({ type: "activity_start", time: t, orchNode: cmsNode, actNode: cmsNode });
|
|
3472
|
-
}
|
|
3473
|
-
}
|
|
3474
|
-
if (seeded.length > 0) {
|
|
3475
|
-
seqEventBuffers.set(orchId, seeded);
|
|
3476
|
-
}
|
|
3477
|
-
}
|
|
3478
|
-
seqCmsSeededSessions.add(orchId);
|
|
3479
|
-
}
|
|
3480
4419
|
} catch (err) {
|
|
3481
4420
|
loadFailed = true;
|
|
3482
4421
|
appendLog(`{yellow-fg}CMS history load failed: ${err.message}{/yellow-fg}`);
|
|
@@ -3508,6 +4447,62 @@ const duroxideSchema = process.env.DUROXIDE_SCHEMA || undefined;
|
|
|
3508
4447
|
const numWorkers = parseInt(process.env.WORKERS ?? "4", 10);
|
|
3509
4448
|
const isRemote = numWorkers === 0;
|
|
3510
4449
|
|
|
4450
|
+
// Register startup quit handlers before any embedded worker startup so the
|
|
4451
|
+
// first local run can always be interrupted, even if worker boot blocks.
|
|
4452
|
+
let _startupQuit = false;
|
|
4453
|
+
let _startupPhase = true;
|
|
4454
|
+
let _embeddedWorkerStartupInProgress = false;
|
|
4455
|
+
screen.key(["C-c"], () => {
|
|
4456
|
+
if (_startupPhase) { _startupQuit = true; exitProcessNow(0); }
|
|
4457
|
+
});
|
|
4458
|
+
const _startupQHandler = () => {
|
|
4459
|
+
if (_startupPhase) { _startupQuit = true; exitProcessNow(0); }
|
|
4460
|
+
};
|
|
4461
|
+
screen.key(["q"], _startupQHandler);
|
|
4462
|
+
|
|
4463
|
+
async function failStartupAndExit(message) {
|
|
4464
|
+
console.error(`[PilotSwarm TUI] ${message}`);
|
|
4465
|
+
setStatus(`{red-fg}${message}{/red-fg}`);
|
|
4466
|
+
chatBox.setContent(
|
|
4467
|
+
`${ACTIVE_STARTUP_SPLASH_CONTENT}\n\n {red-fg}${message}{/red-fg}\n\n ` +
|
|
4468
|
+
`{gray-fg}Embedded local mode only supports one TUI per workspace at a time. ` +
|
|
4469
|
+
`Use remote mode for multiple concurrent windows.{/gray-fg}`,
|
|
4470
|
+
);
|
|
4471
|
+
_origRender();
|
|
4472
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
4473
|
+
exitProcessNow(1);
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
function handleTerminationSignal(signal) {
|
|
4477
|
+
if (_startupPhase) {
|
|
4478
|
+
_startupQuit = true;
|
|
4479
|
+
exitProcessNow(SIGNAL_EXIT_CODES[signal] ?? 0);
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
void requestQuit();
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
4486
|
+
process.on(signal, () => {
|
|
4487
|
+
handleTerminationSignal(signal);
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
4490
|
+
|
|
4491
|
+
setStatus(isRemote ? "Connecting to remote DB..." : `Starting ${numWorkers} local workers...`);
|
|
4492
|
+
chatBox.setContent(
|
|
4493
|
+
`${ACTIVE_STARTUP_SPLASH_CONTENT}\n\n {white-fg}${isRemote ? "Connecting..." : `Starting ${numWorkers} embedded workers...`}{/white-fg}`,
|
|
4494
|
+
);
|
|
4495
|
+
_origRender();
|
|
4496
|
+
|
|
4497
|
+
if (!isRemote) {
|
|
4498
|
+
try {
|
|
4499
|
+
acquireLocalEmbeddedRuntimeLock(store);
|
|
4500
|
+
} catch (err) {
|
|
4501
|
+
const message = err?.message || String(err);
|
|
4502
|
+
await failStartupAndExit(message);
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
|
|
3511
4506
|
if (isRemote) {
|
|
3512
4507
|
const title = formatWindowTitle("Scaled — Remote Workers");
|
|
3513
4508
|
screen.title = title;
|
|
@@ -3529,6 +4524,7 @@ appendLog("");
|
|
|
3529
4524
|
const workers = [];
|
|
3530
4525
|
let modelProviders = null;
|
|
3531
4526
|
let logTailInterval = null;
|
|
4527
|
+
let firstLocalWorkerStarted = Promise.resolve();
|
|
3532
4528
|
// Default model — prefer registry defaultModel, env override, then empty (worker picks default).
|
|
3533
4529
|
let currentModel = process.env.COPILOT_MODEL || "";
|
|
3534
4530
|
if (!isRemote) {
|
|
@@ -3550,6 +4546,31 @@ if (!isRemote) {
|
|
|
3550
4546
|
process.stdout.write = () => true;
|
|
3551
4547
|
process.stderr.write = () => true;
|
|
3552
4548
|
|
|
4549
|
+
const restoreBootstrapStdio = () => {
|
|
4550
|
+
process.stdout.write = origStdoutWrite;
|
|
4551
|
+
process.stderr.write = origStderrWrite;
|
|
4552
|
+
};
|
|
4553
|
+
|
|
4554
|
+
let resolveFirstLocalWorkerStarted;
|
|
4555
|
+
let rejectFirstLocalWorkerStarted;
|
|
4556
|
+
let firstLocalWorkerSettled = false;
|
|
4557
|
+
firstLocalWorkerStarted = new Promise((resolve, reject) => {
|
|
4558
|
+
resolveFirstLocalWorkerStarted = resolve;
|
|
4559
|
+
rejectFirstLocalWorkerStarted = reject;
|
|
4560
|
+
});
|
|
4561
|
+
|
|
4562
|
+
const noteFirstLocalWorkerStarted = () => {
|
|
4563
|
+
if (firstLocalWorkerSettled) return;
|
|
4564
|
+
firstLocalWorkerSettled = true;
|
|
4565
|
+
resolveFirstLocalWorkerStarted();
|
|
4566
|
+
};
|
|
4567
|
+
|
|
4568
|
+
const noteAllLocalWorkersFailed = (err) => {
|
|
4569
|
+
if (firstLocalWorkerSettled) return;
|
|
4570
|
+
firstLocalWorkerSettled = true;
|
|
4571
|
+
rejectFirstLocalWorkerStarted(err);
|
|
4572
|
+
};
|
|
4573
|
+
|
|
3553
4574
|
// System message: env override > worker module > default agent (from plugin)
|
|
3554
4575
|
const WORKER_SYSTEM_MESSAGE = process.env._TUI_SYSTEM_MESSAGE || undefined;
|
|
3555
4576
|
|
|
@@ -3583,69 +4604,106 @@ if (!isRemote) {
|
|
|
3583
4604
|
}),
|
|
3584
4605
|
} : undefined;
|
|
3585
4606
|
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
appendLog(`Worker local-rt-${i} started ✓`);
|
|
3615
|
-
}
|
|
3616
|
-
|
|
3617
|
-
// Capture model provider registry from the first worker
|
|
3618
|
-
modelProviders = workers[0]?.modelProviders || null;
|
|
3619
|
-
if (modelProviders) {
|
|
3620
|
-
const byProvider = modelProviders.getModelsByProvider();
|
|
3621
|
-
for (const g of byProvider) {
|
|
3622
|
-
const names = g.models.map(m => m.qualifiedName).join(", ");
|
|
3623
|
-
appendLog(`{bold}${g.providerId}{/bold} (${g.type}): ${names}`);
|
|
4607
|
+
try {
|
|
4608
|
+
setStatus(`Starting ${numWorkers} workers...`);
|
|
4609
|
+
for (let i = 0; i < numWorkers; i++) {
|
|
4610
|
+
const w = new PilotSwarmWorker({
|
|
4611
|
+
store,
|
|
4612
|
+
...(duroxideSchema ? { duroxideSchema } : {}),
|
|
4613
|
+
...(cmsSchema ? { cmsSchema } : {}),
|
|
4614
|
+
githubToken: process.env.GITHUB_TOKEN,
|
|
4615
|
+
logLevel: process.env.LOG_LEVEL || "error",
|
|
4616
|
+
sessionStateDir: process.env.SESSION_STATE_DIR || path.join(os.homedir(), ".copilot", "session-state"),
|
|
4617
|
+
// Embedded local workers always use the filesystem-based session store.
|
|
4618
|
+
// Do not inherit Azure blob config from the shell environment here.
|
|
4619
|
+
workerNodeId: `local-rt-${i}`,
|
|
4620
|
+
systemMessage: workerModuleConfig.systemMessage || WORKER_SYSTEM_MESSAGE || undefined,
|
|
4621
|
+
pluginDirs,
|
|
4622
|
+
...(llmProvider && { provider: llmProvider }),
|
|
4623
|
+
...(workerModuleConfig.skillDirectories && { skillDirectories: workerModuleConfig.skillDirectories }),
|
|
4624
|
+
...(workerModuleConfig.customAgents && { customAgents: workerModuleConfig.customAgents }),
|
|
4625
|
+
...(workerModuleConfig.mcpServers && { mcpServers: workerModuleConfig.mcpServers }),
|
|
4626
|
+
});
|
|
4627
|
+
// Register custom tools from worker module
|
|
4628
|
+
const workerTools = typeof workerModuleConfig.createTools === "function"
|
|
4629
|
+
? await workerModuleConfig.createTools({ workerNodeId: `local-rt-${i}`, workerIndex: i })
|
|
4630
|
+
: workerModuleConfig.tools;
|
|
4631
|
+
if (workerTools?.length) {
|
|
4632
|
+
w.registerTools(workerTools);
|
|
4633
|
+
}
|
|
4634
|
+
workers.push(w);
|
|
3624
4635
|
}
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
4636
|
+
|
|
4637
|
+
// Capture model provider registry from the first worker
|
|
4638
|
+
modelProviders = workers[0]?.modelProviders || null;
|
|
4639
|
+
if (modelProviders) {
|
|
4640
|
+
const byProvider = modelProviders.getModelsByProvider();
|
|
4641
|
+
for (const g of byProvider) {
|
|
4642
|
+
const names = g.models.map(m => m.qualifiedName).join(", ");
|
|
4643
|
+
appendLog(`{bold}${g.providerId}{/bold} (${g.type}): ${names}`);
|
|
4644
|
+
}
|
|
4645
|
+
// Use default model from registry if no explicit override
|
|
4646
|
+
if (modelProviders.defaultModel && !currentModel) {
|
|
4647
|
+
currentModel = modelProviders.defaultModel;
|
|
4648
|
+
}
|
|
3628
4649
|
}
|
|
3629
|
-
}
|
|
3630
4650
|
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
4651
|
+
// Restore stdout/stderr after all workers initialized
|
|
4652
|
+
process.stdout.write = origStdoutWrite;
|
|
4653
|
+
// Keep stderr intercepted — MCP subprocesses (filesystem server, etc.) write
|
|
4654
|
+
// warnings (ExperimentalWarning: SQLite, etc.) that corrupt the TUI.
|
|
4655
|
+
// Route them to the log file instead of the terminal.
|
|
4656
|
+
const logFd = fs.openSync(logFile, "a");
|
|
4657
|
+
process.stderr.write = (chunk, encoding, cb) => {
|
|
4658
|
+
try { fs.appendFileSync(logFd, chunk); } catch {}
|
|
4659
|
+
if (typeof cb === "function") cb();
|
|
4660
|
+
return true;
|
|
4661
|
+
};
|
|
3642
4662
|
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
4663
|
+
const repaintAfterWorkerStartup = () => {
|
|
4664
|
+
if (_embeddedWorkerStartupInProgress) {
|
|
4665
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
4666
|
+
screen.realloc();
|
|
4667
|
+
}
|
|
4668
|
+
screen.render();
|
|
4669
|
+
};
|
|
4670
|
+
|
|
4671
|
+
// Rust native code writes directly to fd 1/2 during init, bypassing Node
|
|
4672
|
+
// and corrupting blessed's alt-screen buffer. Repaint after construction,
|
|
4673
|
+
// then start workers one-by-one in the background to avoid Postgres schema races.
|
|
4674
|
+
_embeddedWorkerStartupInProgress = true;
|
|
4675
|
+
repaintAfterWorkerStartup();
|
|
4676
|
+
|
|
4677
|
+
const startWorkersSequentially = async () => {
|
|
4678
|
+
let startedWorkers = 0;
|
|
4679
|
+
try {
|
|
4680
|
+
for (let i = 0; i < workers.length; i++) {
|
|
4681
|
+
appendLog(`Starting worker local-rt-${i}…`);
|
|
4682
|
+
screen.render();
|
|
4683
|
+
try {
|
|
4684
|
+
await workers[i].start();
|
|
4685
|
+
startedWorkers++;
|
|
4686
|
+
noteFirstLocalWorkerStarted();
|
|
4687
|
+
appendLog(`Worker local-rt-${i} started ✓`);
|
|
4688
|
+
} catch (err) {
|
|
4689
|
+
appendLog(`{red-fg}Worker local-rt-${i} failed: ${err?.message || err}{/red-fg}`);
|
|
4690
|
+
}
|
|
4691
|
+
repaintAfterWorkerStartup();
|
|
4692
|
+
}
|
|
4693
|
+
} finally {
|
|
4694
|
+
repaintAfterWorkerStartup();
|
|
4695
|
+
_embeddedWorkerStartupInProgress = false;
|
|
4696
|
+
}
|
|
4697
|
+
if (startedWorkers === 0) {
|
|
4698
|
+
noteAllLocalWorkersFailed(new Error("All local workers failed to start."));
|
|
4699
|
+
}
|
|
4700
|
+
};
|
|
4701
|
+
|
|
4702
|
+
void startWorkersSequentially();
|
|
4703
|
+
} catch (err) {
|
|
4704
|
+
restoreBootstrapStdio();
|
|
4705
|
+
throw err;
|
|
4706
|
+
}
|
|
3649
4707
|
|
|
3650
4708
|
// Tail the log file into per-worker panes
|
|
3651
4709
|
let tailPos = 0;
|
|
@@ -3724,11 +4782,6 @@ if (!isRemote) {
|
|
|
3724
4782
|
appendOrchLog(orchId, paneName, formatted);
|
|
3725
4783
|
}
|
|
3726
4784
|
|
|
3727
|
-
// Feed sequence diagram
|
|
3728
|
-
const seqEvtLocal = parseSeqEvent(plain, paneName);
|
|
3729
|
-
if (seqEvtLocal) {
|
|
3730
|
-
appendSeqEvent(seqEvtLocal.orchId, seqEvtLocal);
|
|
3731
|
-
}
|
|
3732
4785
|
routed++;
|
|
3733
4786
|
}
|
|
3734
4787
|
tailReads++;
|
|
@@ -3799,22 +4852,25 @@ const mgmt = new PilotSwarmManagementClient({
|
|
|
3799
4852
|
const STARTUP_DB_RETRY_MS = 30_000;
|
|
3800
4853
|
const STARTUP_DB_CONNECT_TIMEOUT_MS = 10_000;
|
|
3801
4854
|
|
|
4855
|
+
async function withStartupTimeout(label, work) {
|
|
4856
|
+
let timer = null;
|
|
4857
|
+
try {
|
|
4858
|
+
return await Promise.race([
|
|
4859
|
+
work(),
|
|
4860
|
+
new Promise((_, reject) => {
|
|
4861
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out (10s deadline)`)), STARTUP_DB_CONNECT_TIMEOUT_MS);
|
|
4862
|
+
}),
|
|
4863
|
+
]);
|
|
4864
|
+
} finally {
|
|
4865
|
+
if (timer) clearTimeout(timer);
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
|
|
3802
4869
|
function isStartupTransientDbError(err) {
|
|
3803
4870
|
const msg = String(err?.message || err || "");
|
|
3804
4871
|
return /ENOTFOUND|ETIMEDOUT|ECONNREFUSED|ECONNRESET|EHOSTUNREACH|socket hang up|getaddrinfo|timeout|network|pool timed out while waiting for an open connection/i.test(msg);
|
|
3805
4872
|
}
|
|
3806
4873
|
|
|
3807
|
-
// Register quit handler BEFORE the connection loop so the user can always exit.
|
|
3808
|
-
let _startupQuit = false;
|
|
3809
|
-
let _startupPhase = true;
|
|
3810
|
-
screen.key(["C-c"], () => {
|
|
3811
|
-
if (_startupPhase) { _startupQuit = true; process.exit(0); }
|
|
3812
|
-
});
|
|
3813
|
-
const _startupQHandler = () => {
|
|
3814
|
-
if (_startupPhase) { _startupQuit = true; process.exit(0); }
|
|
3815
|
-
};
|
|
3816
|
-
screen.key(["q"], _startupQHandler);
|
|
3817
|
-
|
|
3818
4874
|
setStatus(isRemote ? "Connecting to remote DB..." : "Connecting client...");
|
|
3819
4875
|
|
|
3820
4876
|
// Show splash immediately so the user sees something during DB connection
|
|
@@ -3827,41 +4883,35 @@ _origRender();
|
|
|
3827
4883
|
while (true) {
|
|
3828
4884
|
const _startPh = perfStart("startup.clientConnect");
|
|
3829
4885
|
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
Promise.race([mgmt.start(), timeoutPromise]),
|
|
3837
|
-
]);
|
|
3838
|
-
const failure = results.find(r => r.status === "rejected");
|
|
3839
|
-
|
|
3840
|
-
if (!failure) {
|
|
4886
|
+
try {
|
|
4887
|
+
if (!isRemote) {
|
|
4888
|
+
await withStartupTimeout("First local worker start", () => firstLocalWorkerStarted);
|
|
4889
|
+
}
|
|
4890
|
+
await withStartupTimeout("Client start", () => client.start());
|
|
4891
|
+
await withStartupTimeout("Management client start", () => mgmt.start());
|
|
3841
4892
|
perfEnd(_startPh);
|
|
3842
4893
|
break;
|
|
3843
|
-
}
|
|
3844
|
-
|
|
3845
|
-
const err = failure.reason;
|
|
3846
|
-
perfEnd(_startPh, { err: true });
|
|
4894
|
+
} catch (err) {
|
|
4895
|
+
perfEnd(_startPh, { err: true });
|
|
3847
4896
|
|
|
3848
|
-
|
|
3849
|
-
|
|
4897
|
+
try { await client.stop(); } catch {}
|
|
4898
|
+
try { await mgmt.stop(); } catch {}
|
|
3850
4899
|
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
4900
|
+
if (!isStartupTransientDbError(err)) {
|
|
4901
|
+
throw err;
|
|
4902
|
+
}
|
|
3854
4903
|
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
4904
|
+
const msg = String(err?.message || err || "Unknown database error");
|
|
4905
|
+
setStatus(`Database unavailable — retrying in 30s (${safeSlice(msg, 0, 80)})`);
|
|
4906
|
+
chatBox.setContent(`${ACTIVE_STARTUP_SPLASH_CONTENT}\n\n {yellow-fg}Database unavailable.{/yellow-fg}\n {white-fg}${msg}{/white-fg}\n\n {gray-fg}Retrying connection in 30 seconds... (press q or Ctrl+C to quit){/gray-fg}`);
|
|
4907
|
+
_origRender();
|
|
3859
4908
|
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
4909
|
+
// Interruptible sleep — check every 500ms if the user pressed quit
|
|
4910
|
+
const retryEnd = Date.now() + STARTUP_DB_RETRY_MS;
|
|
4911
|
+
while (Date.now() < retryEnd) {
|
|
4912
|
+
if (_startupQuit) process.exit(0);
|
|
4913
|
+
await new Promise(r => setTimeout(r, 500));
|
|
4914
|
+
}
|
|
3865
4915
|
}
|
|
3866
4916
|
}
|
|
3867
4917
|
|
|
@@ -3921,9 +4971,13 @@ const sessionRenderedCmsSeq = new Map(); // orchId → highest CMS seq already i
|
|
|
3921
4971
|
const sessionExpandLevel = new Map(); // orchId → 0 (default) | 1 | 2 (how many times user expanded history)
|
|
3922
4972
|
const sessionSplashApplied = new Set(); // orchIds that have had splash prepended (idempotency guard)
|
|
3923
4973
|
const sessionPromotedIntermediate = new Map(); // orchId → normalized intermediate content already promoted to Chat
|
|
4974
|
+
const sessionPendingAssistantPreview = new Map(); // orchId → { normalized: string, lines: string[] } for provisional assistant.message preview
|
|
3924
4975
|
const sessionRecoveredTurnResult = new Map(); // orchId → normalized completed turn recovered from live status during CMS load
|
|
3925
4976
|
const sessionObservers = new Map(); // orchId → AbortController
|
|
4977
|
+
const sessionObserverPollState = new Map(); // orchId → { version: number }
|
|
3926
4978
|
const sessionLiveStatus = new Map(); // orchId → "idle"|"running"|"waiting"|"input_required"
|
|
4979
|
+
const sessionCronSchedules = new Map(); // orchId → { interval: number, reason?: string }
|
|
4980
|
+
const sessionContextUsage = new Map(); // orchId → latest context usage snapshot
|
|
3927
4981
|
const sessionPendingTurns = new Set(); // orchIds with a locally-sent turn awaiting first live status
|
|
3928
4982
|
|
|
3929
4983
|
// ─── Inline chat spinner ─────────────────────────────────────────
|
|
@@ -4007,6 +5061,153 @@ function isTerminalSessionState(state) {
|
|
|
4007
5061
|
return state === "completed" || state === "failed" || state === "error" || state === "terminated";
|
|
4008
5062
|
}
|
|
4009
5063
|
|
|
5064
|
+
function sessionStateFromOrchestrationStatus(status) {
|
|
5065
|
+
switch (status) {
|
|
5066
|
+
case "Completed": return "completed";
|
|
5067
|
+
case "Failed": return "error";
|
|
5068
|
+
case "Terminated": return "terminated";
|
|
5069
|
+
default: return null;
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
function updateSessionCronSchedule(orchId, customStatus) {
|
|
5074
|
+
if (!orchId || !customStatus) return;
|
|
5075
|
+
if (customStatus.cronActive === true && typeof customStatus.cronInterval === "number") {
|
|
5076
|
+
const next = {
|
|
5077
|
+
interval: customStatus.cronInterval,
|
|
5078
|
+
reason: typeof customStatus.cronReason === "string" ? customStatus.cronReason : undefined,
|
|
5079
|
+
};
|
|
5080
|
+
const prev = sessionCronSchedules.get(orchId);
|
|
5081
|
+
if (prev?.interval === next.interval && prev?.reason === next.reason) return;
|
|
5082
|
+
sessionCronSchedules.set(orchId, next);
|
|
5083
|
+
updateSessionListIcons();
|
|
5084
|
+
return;
|
|
5085
|
+
}
|
|
5086
|
+
if (sessionCronSchedules.delete(orchId)) {
|
|
5087
|
+
updateSessionListIcons();
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
function cloneContextUsageSnapshot(contextUsage) {
|
|
5092
|
+
if (!contextUsage || typeof contextUsage !== "object") return null;
|
|
5093
|
+
return {
|
|
5094
|
+
...contextUsage,
|
|
5095
|
+
...(contextUsage.compaction && typeof contextUsage.compaction === "object"
|
|
5096
|
+
? { compaction: { ...contextUsage.compaction } }
|
|
5097
|
+
: {}),
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
function getContextHeaderBadge(orchId) {
|
|
5102
|
+
return formatContextHeaderBadge(sessionContextUsage.get(orchId));
|
|
5103
|
+
}
|
|
5104
|
+
|
|
5105
|
+
function getContextCompactionBadge(orchId) {
|
|
5106
|
+
return formatContextCompactionBadge(sessionContextUsage.get(orchId));
|
|
5107
|
+
}
|
|
5108
|
+
|
|
5109
|
+
function getSessionContextBadge(orchId) {
|
|
5110
|
+
return formatContextListBadge(sessionContextUsage.get(orchId));
|
|
5111
|
+
}
|
|
5112
|
+
|
|
5113
|
+
function noteSessionContextUsage(orchId, contextUsage) {
|
|
5114
|
+
if (!orchId) return;
|
|
5115
|
+
const snapshot = cloneContextUsageSnapshot(contextUsage);
|
|
5116
|
+
if (!snapshot) return;
|
|
5117
|
+
sessionContextUsage.set(orchId, snapshot);
|
|
5118
|
+
if (orchId === activeOrchId) updateChatLabel();
|
|
5119
|
+
updateSessionListIcons();
|
|
5120
|
+
}
|
|
5121
|
+
|
|
5122
|
+
function updateSessionContextUsageFromStatus(orchId, customStatus) {
|
|
5123
|
+
if (customStatus?.contextUsage && typeof customStatus.contextUsage === "object") {
|
|
5124
|
+
noteSessionContextUsage(orchId, customStatus.contextUsage);
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
|
|
5128
|
+
function applySessionUsageEvent(orchId, eventType, data) {
|
|
5129
|
+
if (!orchId || !eventType || !data || typeof data !== "object") return;
|
|
5130
|
+
const current = cloneContextUsageSnapshot(sessionContextUsage.get(orchId)) || {};
|
|
5131
|
+
|
|
5132
|
+
if (eventType === "session.usage_info") {
|
|
5133
|
+
if (typeof data.tokenLimit !== "number" || typeof data.currentTokens !== "number" || typeof data.messagesLength !== "number") {
|
|
5134
|
+
return;
|
|
5135
|
+
}
|
|
5136
|
+
current.tokenLimit = data.tokenLimit;
|
|
5137
|
+
current.currentTokens = data.currentTokens;
|
|
5138
|
+
current.messagesLength = data.messagesLength;
|
|
5139
|
+
current.utilization = data.tokenLimit > 0 ? data.currentTokens / data.tokenLimit : 0;
|
|
5140
|
+
if (typeof data.systemTokens === "number") current.systemTokens = data.systemTokens;
|
|
5141
|
+
if (typeof data.conversationTokens === "number") current.conversationTokens = data.conversationTokens;
|
|
5142
|
+
if (typeof data.toolDefinitionsTokens === "number") current.toolDefinitionsTokens = data.toolDefinitionsTokens;
|
|
5143
|
+
if (typeof data.isInitial === "boolean") current.isInitial = data.isInitial;
|
|
5144
|
+
noteSessionContextUsage(orchId, current);
|
|
5145
|
+
return;
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
if (eventType === "assistant.usage") {
|
|
5149
|
+
if (typeof data.inputTokens === "number") current.lastInputTokens = data.inputTokens;
|
|
5150
|
+
if (typeof data.outputTokens === "number") current.lastOutputTokens = data.outputTokens;
|
|
5151
|
+
if (typeof data.cacheReadTokens === "number") current.lastCacheReadTokens = data.cacheReadTokens;
|
|
5152
|
+
if (typeof data.cacheWriteTokens === "number") current.lastCacheWriteTokens = data.cacheWriteTokens;
|
|
5153
|
+
noteSessionContextUsage(orchId, current);
|
|
5154
|
+
return;
|
|
5155
|
+
}
|
|
5156
|
+
|
|
5157
|
+
if (eventType === "session.compaction_start") {
|
|
5158
|
+
current.compaction = {
|
|
5159
|
+
...(current.compaction || {}),
|
|
5160
|
+
state: "running",
|
|
5161
|
+
startedAt: Date.now(),
|
|
5162
|
+
completedAt: undefined,
|
|
5163
|
+
error: undefined,
|
|
5164
|
+
};
|
|
5165
|
+
noteSessionContextUsage(orchId, current);
|
|
5166
|
+
return;
|
|
5167
|
+
}
|
|
5168
|
+
|
|
5169
|
+
if (eventType === "session.compaction_complete") {
|
|
5170
|
+
current.compaction = {
|
|
5171
|
+
...(current.compaction || {}),
|
|
5172
|
+
state: data.success === false ? "failed" : "succeeded",
|
|
5173
|
+
completedAt: Date.now(),
|
|
5174
|
+
error: typeof data.error === "string" ? data.error : undefined,
|
|
5175
|
+
preCompactionTokens: typeof data.preCompactionTokens === "number" ? data.preCompactionTokens : undefined,
|
|
5176
|
+
postCompactionTokens: typeof data.postCompactionTokens === "number" ? data.postCompactionTokens : undefined,
|
|
5177
|
+
preCompactionMessagesLength: typeof data.preCompactionMessagesLength === "number" ? data.preCompactionMessagesLength : undefined,
|
|
5178
|
+
messagesRemoved: typeof data.messagesRemoved === "number" ? data.messagesRemoved : undefined,
|
|
5179
|
+
tokensRemoved: typeof data.tokensRemoved === "number" ? data.tokensRemoved : undefined,
|
|
5180
|
+
systemTokens: typeof data.systemTokens === "number" ? data.systemTokens : undefined,
|
|
5181
|
+
conversationTokens: typeof data.conversationTokens === "number" ? data.conversationTokens : undefined,
|
|
5182
|
+
toolDefinitionsTokens: typeof data.toolDefinitionsTokens === "number" ? data.toolDefinitionsTokens : undefined,
|
|
5183
|
+
inputTokens: typeof data.compactionTokensUsed?.input === "number" ? data.compactionTokensUsed.input : undefined,
|
|
5184
|
+
outputTokens: typeof data.compactionTokensUsed?.output === "number" ? data.compactionTokensUsed.output : undefined,
|
|
5185
|
+
cachedInputTokens: typeof data.compactionTokensUsed?.cachedInput === "number" ? data.compactionTokensUsed.cachedInput : undefined,
|
|
5186
|
+
};
|
|
5187
|
+
if (typeof data.postCompactionTokens === "number" && typeof current.tokenLimit === "number" && current.tokenLimit > 0) {
|
|
5188
|
+
current.currentTokens = data.postCompactionTokens;
|
|
5189
|
+
current.utilization = data.postCompactionTokens / current.tokenLimit;
|
|
5190
|
+
}
|
|
5191
|
+
if (typeof data.preCompactionMessagesLength === "number" && typeof data.messagesRemoved === "number") {
|
|
5192
|
+
current.messagesLength = Math.max(0, data.preCompactionMessagesLength - data.messagesRemoved);
|
|
5193
|
+
}
|
|
5194
|
+
if (typeof data.systemTokens === "number") current.systemTokens = data.systemTokens;
|
|
5195
|
+
if (typeof data.conversationTokens === "number") current.conversationTokens = data.conversationTokens;
|
|
5196
|
+
if (typeof data.toolDefinitionsTokens === "number") current.toolDefinitionsTokens = data.toolDefinitionsTokens;
|
|
5197
|
+
noteSessionContextUsage(orchId, current);
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
|
|
5201
|
+
function formatCompactionActivityLine(t, evt) {
|
|
5202
|
+
return formatCompactionActivityMarkup(t, evt.eventType, evt.data || {});
|
|
5203
|
+
}
|
|
5204
|
+
|
|
5205
|
+
function getSessionCronBadge(orchId) {
|
|
5206
|
+
const cron = sessionCronSchedules.get(orchId);
|
|
5207
|
+
if (!cron) return "";
|
|
5208
|
+
return ` {magenta-fg}[cron ${formatHumanDurationSeconds(cron.interval)}]{/magenta-fg}`;
|
|
5209
|
+
}
|
|
5210
|
+
|
|
4010
5211
|
function shouldObserveSession(orchId, cached = orchStatusCache.get(orchId)) {
|
|
4011
5212
|
if (!orchId || sessionObservers.has(orchId)) return false;
|
|
4012
5213
|
const visualState = getSessionVisualState(orchId, cached);
|
|
@@ -4014,8 +5215,15 @@ function shouldObserveSession(orchId, cached = orchStatusCache.get(orchId)) {
|
|
|
4014
5215
|
}
|
|
4015
5216
|
|
|
4016
5217
|
function getSessionVisualState(orchId, cached = orchStatusCache.get(orchId)) {
|
|
5218
|
+
const terminalState = sessionStateFromOrchestrationStatus(cached?.status);
|
|
5219
|
+
if (terminalState) {
|
|
5220
|
+
return terminalState;
|
|
5221
|
+
}
|
|
4017
5222
|
const liveState = sessionLiveStatus.get(orchId) || cached?.liveState;
|
|
4018
|
-
if (liveState)
|
|
5223
|
+
if (liveState) {
|
|
5224
|
+
if (liveState === "waiting" && sessionCronSchedules.has(orchId)) return "cron_waiting";
|
|
5225
|
+
return liveState;
|
|
5226
|
+
}
|
|
4019
5227
|
switch (cached?.status) {
|
|
4020
5228
|
case "Completed": return "completed";
|
|
4021
5229
|
case "Failed": return "failed";
|
|
@@ -4029,13 +5237,14 @@ function getSessionVisualState(orchId, cached = orchStatusCache.get(orchId)) {
|
|
|
4029
5237
|
function getSessionStateColor(state) {
|
|
4030
5238
|
switch (state) {
|
|
4031
5239
|
case "running": return "green";
|
|
5240
|
+
case "cron_waiting": return "magenta";
|
|
4032
5241
|
case "waiting": return "yellow";
|
|
4033
5242
|
case "idle": return "white";
|
|
4034
5243
|
case "input_required": return "cyan";
|
|
4035
5244
|
case "completed": return "gray";
|
|
4036
5245
|
case "failed":
|
|
4037
5246
|
case "error": return "red";
|
|
4038
|
-
case "terminated": return "
|
|
5247
|
+
case "terminated": return "red";
|
|
4039
5248
|
default: return "white";
|
|
4040
5249
|
}
|
|
4041
5250
|
}
|
|
@@ -4043,21 +5252,42 @@ function getSessionStateColor(state) {
|
|
|
4043
5252
|
function getSessionStateIcon(state) {
|
|
4044
5253
|
switch (state) {
|
|
4045
5254
|
case "running": return "{green-fg}*{/green-fg}";
|
|
5255
|
+
case "cron_waiting": return "{magenta-fg}~{/magenta-fg}";
|
|
4046
5256
|
case "waiting": return "{yellow-fg}~{/yellow-fg}";
|
|
4047
5257
|
case "idle": return "{white-fg}.{/white-fg}";
|
|
4048
5258
|
case "input_required": return "{cyan-fg}?{/cyan-fg}";
|
|
4049
5259
|
case "failed":
|
|
4050
|
-
case "error":
|
|
5260
|
+
case "error":
|
|
5261
|
+
case "terminated": return "{red-fg}!{/red-fg}";
|
|
4051
5262
|
default: return "";
|
|
4052
5263
|
}
|
|
4053
5264
|
}
|
|
4054
5265
|
|
|
5266
|
+
function getSessionListStateColor(state) {
|
|
5267
|
+
return state === "cron_waiting" ? "yellow" : getSessionStateColor(state);
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
function getSessionListStateIcon(state) {
|
|
5271
|
+
return state === "cron_waiting"
|
|
5272
|
+
? "{yellow-fg}~{/yellow-fg}"
|
|
5273
|
+
: getSessionStateIcon(state);
|
|
5274
|
+
}
|
|
5275
|
+
|
|
4055
5276
|
function setSessionPendingTurn(orchId, pending) {
|
|
4056
5277
|
if (!orchId) return;
|
|
4057
5278
|
if (pending) sessionPendingTurns.add(orchId);
|
|
4058
5279
|
else sessionPendingTurns.delete(orchId);
|
|
4059
5280
|
}
|
|
4060
5281
|
|
|
5282
|
+
function formatWaitingLabel(statusLike) {
|
|
5283
|
+
const reason = statusLike?.cronActive === true
|
|
5284
|
+
? (statusLike?.cronReason || statusLike?.waitReason || "timer")
|
|
5285
|
+
: (statusLike?.waitReason || "timer");
|
|
5286
|
+
return statusLike?.cronActive === true
|
|
5287
|
+
? `Cron waiting (${reason})…`
|
|
5288
|
+
: `Waiting (${reason})…`;
|
|
5289
|
+
}
|
|
5290
|
+
|
|
4061
5291
|
function isTurnInProgressForSession(orchId) {
|
|
4062
5292
|
if (!orchId) return false;
|
|
4063
5293
|
const liveStatus = sessionLiveStatus.get(orchId);
|
|
@@ -4200,7 +5430,7 @@ function handleDbUnavailable(err) {
|
|
|
4200
5430
|
if (!wasOffline || prevError !== msg) {
|
|
4201
5431
|
appendLog(`{yellow-fg}Database unavailable — retrying in 30s.{/yellow-fg}`);
|
|
4202
5432
|
}
|
|
4203
|
-
setStatus(`Database unavailable — retrying in 30s (${msg
|
|
5433
|
+
setStatus(`Database unavailable — retrying in 30s (${safeSlice(msg, 0, 80)})`);
|
|
4204
5434
|
}
|
|
4205
5435
|
|
|
4206
5436
|
function handleDbRecovered() {
|
|
@@ -4264,8 +5494,8 @@ function updateSessionListIcons() {
|
|
|
4264
5494
|
const id = orchIdOrder[i];
|
|
4265
5495
|
const cached = orchStatusCache.get(id);
|
|
4266
5496
|
const visualState = getSessionVisualState(id, cached);
|
|
4267
|
-
const statusIcon =
|
|
4268
|
-
const color =
|
|
5497
|
+
const statusIcon = getSessionListStateIcon(visualState);
|
|
5498
|
+
const color = getSessionListStateColor(visualState);
|
|
4269
5499
|
|
|
4270
5500
|
// Rebuild just this item's label
|
|
4271
5501
|
const uuid4 = shortId(id);
|
|
@@ -4278,11 +5508,10 @@ function updateSessionListIcons() {
|
|
|
4278
5508
|
})
|
|
4279
5509
|
: "";
|
|
4280
5510
|
|
|
4281
|
-
const hasChanges = orchHasChanges.has(id);
|
|
4282
5511
|
const isActive = id === activeOrchId;
|
|
4283
5512
|
const marker = isActive ? "{bold}▸{/bold}" : " ";
|
|
4284
|
-
const changeSuffix = hasChanges ? " {cyan-fg}{bold}●{/bold}{/cyan-fg}" : "";
|
|
4285
5513
|
const statusIconSlot = statusIcon ? statusIcon + " " : " ";
|
|
5514
|
+
const badgeSuffix = formatSessionListSuffixes(id);
|
|
4286
5515
|
const heading = sessionHeadings.get(id);
|
|
4287
5516
|
// Use cached depth from last full refresh
|
|
4288
5517
|
const depth = orchDepthMap?.get(id) ?? 0;
|
|
@@ -4290,16 +5519,14 @@ function updateSessionListIcons() {
|
|
|
4290
5519
|
|
|
4291
5520
|
// System sessions get special rendering: yellow, ≋ icon
|
|
4292
5521
|
if (systemSessionIds.has(id)) {
|
|
4293
|
-
const collapseBadge = getCollapseBadge(id);
|
|
4294
5522
|
const sysLabel = heading
|
|
4295
|
-
? `${heading} (${uuid4}) ${timeStr}${
|
|
4296
|
-
: `System Agent (${uuid4}) ${timeStr}${
|
|
5523
|
+
? `${heading} (${uuid4}) ${timeStr}${badgeSuffix}`
|
|
5524
|
+
: `System Agent (${uuid4}) ${timeStr}${badgeSuffix}`;
|
|
4297
5525
|
orchList.setItem(i, `${indent}${marker}{bold}{yellow-fg}≋ ${sysLabel}{/yellow-fg}{/bold}`);
|
|
4298
5526
|
} else {
|
|
4299
|
-
const collapseBadge = getCollapseBadge(id);
|
|
4300
5527
|
const label = heading
|
|
4301
|
-
? `${heading} (${uuid4}) ${timeStr}${
|
|
4302
|
-
: `(${uuid4}) ${timeStr}${
|
|
5528
|
+
? `${heading} (${uuid4}) ${timeStr}${badgeSuffix}`
|
|
5529
|
+
: `(${uuid4}) ${timeStr}${badgeSuffix}`;
|
|
4303
5530
|
orchList.setItem(i, `${indent}${marker}${statusIconSlot}{${color}-fg}${label}{/${color}-fg}`);
|
|
4304
5531
|
}
|
|
4305
5532
|
}
|
|
@@ -4331,6 +5558,15 @@ function getCollapseBadge(orchId) {
|
|
|
4331
5558
|
return hidden ? ` {cyan-fg}[+${hidden}]{/cyan-fg}` : "";
|
|
4332
5559
|
}
|
|
4333
5560
|
|
|
5561
|
+
function formatSessionListSuffixes(orchId) {
|
|
5562
|
+
const cronBadge = getSessionCronBadge(orchId);
|
|
5563
|
+
const contextBadge = getSessionContextBadge(orchId);
|
|
5564
|
+
const collapseBadge = getCollapseBadge(orchId);
|
|
5565
|
+
const changeSuffix = orchHasChanges.has(orchId) ? " {cyan-fg}{bold}●{/bold}{/cyan-fg}" : "";
|
|
5566
|
+
// Keep badge ordering stable across lightweight updates and full refreshes.
|
|
5567
|
+
return `${cronBadge}${contextBadge}${collapseBadge}${changeSuffix}`;
|
|
5568
|
+
}
|
|
5569
|
+
|
|
4334
5570
|
function canonicalSystemTitleFromSessionView(sv, orchId) {
|
|
4335
5571
|
const agentId = sv?.agentId || sessionAgentIds.get(orchId) || "";
|
|
4336
5572
|
if (agentId === "pilotswarm") return HAS_CUSTOM_TUI_BRANDING ? BASE_TUI_TITLE : "PilotSwarm Agent";
|
|
@@ -4426,6 +5662,14 @@ async function refreshOrchestrations(force = false) {
|
|
|
4426
5662
|
if (sv.agentId) {
|
|
4427
5663
|
sessionAgentIds.set(id, sv.agentId);
|
|
4428
5664
|
}
|
|
5665
|
+
if (sv.cronActive === true && typeof sv.cronInterval === "number") {
|
|
5666
|
+
sessionCronSchedules.set(id, {
|
|
5667
|
+
interval: sv.cronInterval,
|
|
5668
|
+
reason: typeof sv.cronReason === "string" ? sv.cronReason : undefined,
|
|
5669
|
+
});
|
|
5670
|
+
} else if (sv.cronActive === false) {
|
|
5671
|
+
sessionCronSchedules.delete(id);
|
|
5672
|
+
}
|
|
4429
5673
|
|
|
4430
5674
|
// Splash from CMS — store and pre-populate chat buffer on first discovery
|
|
4431
5675
|
const splashText = brandedSplashForSessionView(sv, id);
|
|
@@ -4443,7 +5687,12 @@ async function refreshOrchestrations(force = false) {
|
|
|
4443
5687
|
|
|
4444
5688
|
// Seed sessionLiveStatus from CMS if no observer has set it yet.
|
|
4445
5689
|
// This ensures status icons show correctly on initial load.
|
|
4446
|
-
|
|
5690
|
+
const terminalState = sessionStateFromOrchestrationStatus(status);
|
|
5691
|
+
if (terminalState) {
|
|
5692
|
+
sessionLiveStatus.set(id, terminalState);
|
|
5693
|
+
setSessionPendingTurn(id, false);
|
|
5694
|
+
clearSessionPendingQuestion(id);
|
|
5695
|
+
} else if (!sessionLiveStatus.has(id) && liveState && liveState !== "pending") {
|
|
4447
5696
|
sessionLiveStatus.set(id, liveState);
|
|
4448
5697
|
}
|
|
4449
5698
|
|
|
@@ -4581,33 +5830,30 @@ async function refreshOrchestrations(force = false) {
|
|
|
4581
5830
|
})
|
|
4582
5831
|
: "";
|
|
4583
5832
|
const visualState = getSessionVisualState(id, orchStatusCache.get(id) || { status, createdAt });
|
|
4584
|
-
const color =
|
|
5833
|
+
const color = getSessionListStateColor(visualState);
|
|
4585
5834
|
|
|
4586
5835
|
// Highlight sessions with unseen changes
|
|
4587
|
-
const hasChanges = orchHasChanges.has(id);
|
|
4588
5836
|
const isActive = id === activeOrchId;
|
|
4589
5837
|
const marker = isActive ? "{bold}▸{/bold}" : " ";
|
|
4590
|
-
const changeSuffix = hasChanges ? " {cyan-fg}{bold}●{/bold}{/cyan-fg}" : "";
|
|
4591
5838
|
|
|
4592
5839
|
// Live status indicator
|
|
4593
|
-
const statusIcon =
|
|
5840
|
+
const statusIcon = getSessionListStateIcon(visualState);
|
|
4594
5841
|
|
|
4595
5842
|
const statusIconSlot = statusIcon ? statusIcon + " " : " ";
|
|
5843
|
+
const badgeSuffix = formatSessionListSuffixes(id);
|
|
4596
5844
|
const heading = sessionHeadings.get(id);
|
|
4597
5845
|
const indent = depth > 0 ? " ".repeat(depth - 1) + " └ " : "";
|
|
4598
5846
|
|
|
4599
5847
|
// System sessions get special rendering: yellow, ≋ icon
|
|
4600
5848
|
if (systemSessionIds.has(id)) {
|
|
4601
|
-
const collapseBadge = getCollapseBadge(id);
|
|
4602
5849
|
const sysLabel = heading
|
|
4603
|
-
? `${heading} (${uuid4}) ${timeStr}${
|
|
4604
|
-
: `System Agent (${uuid4}) ${timeStr}${
|
|
5850
|
+
? `${heading} (${uuid4}) ${timeStr}${badgeSuffix}`
|
|
5851
|
+
: `System Agent (${uuid4}) ${timeStr}${badgeSuffix}`;
|
|
4605
5852
|
orchList.addItem(`${indent}${marker}{bold}{yellow-fg}≋ ${sysLabel}{/yellow-fg}{/bold}`);
|
|
4606
5853
|
} else {
|
|
4607
|
-
const collapseBadge = getCollapseBadge(id);
|
|
4608
5854
|
const label = heading
|
|
4609
|
-
? `${heading} (${uuid4}) ${timeStr}${
|
|
4610
|
-
: `(${uuid4}) ${timeStr}${
|
|
5855
|
+
? `${heading} (${uuid4}) ${timeStr}${badgeSuffix}`
|
|
5856
|
+
: `(${uuid4}) ${timeStr}${badgeSuffix}`;
|
|
4611
5857
|
orchList.addItem(`${indent}${marker}${statusIconSlot}{${color}-fg}${label}{/${color}-fg}`);
|
|
4612
5858
|
}
|
|
4613
5859
|
}
|
|
@@ -4660,6 +5906,14 @@ async function refreshOrchestrations(force = false) {
|
|
|
4660
5906
|
perfEnd(_ph, { sessions: entries.length });
|
|
4661
5907
|
}
|
|
4662
5908
|
|
|
5909
|
+
function renderWaitingForSessionsLanding() {
|
|
5910
|
+
chatBox.setContent(
|
|
5911
|
+
`${ACTIVE_STARTUP_SPLASH_CONTENT}\n\n {white-fg}Waiting for local workers to publish sessions...{/white-fg}\n\n {gray-fg}Press n to create a session immediately.{/gray-fg}`,
|
|
5912
|
+
);
|
|
5913
|
+
chatBox.setScrollPerc(0);
|
|
5914
|
+
screen.render();
|
|
5915
|
+
}
|
|
5916
|
+
|
|
4663
5917
|
// Poll orchestrations every 10 seconds (observers handle live status updates, so
|
|
4664
5918
|
// this only needs to catch new sessions and structural changes like title/parent).
|
|
4665
5919
|
let orchPollTimer = setInterval(() => {
|
|
@@ -4677,7 +5931,7 @@ const perfSummaryInterval = setInterval(() => {
|
|
|
4677
5931
|
for (const l of lines) totalBufferBytes += l.length;
|
|
4678
5932
|
}
|
|
4679
5933
|
let totalSeqEvents = 0;
|
|
4680
|
-
for (const [, evts] of
|
|
5934
|
+
for (const [, evts] of cmsSeqTimelines) totalSeqEvents += evts.length;
|
|
4681
5935
|
perfTrace("periodic_summary", {
|
|
4682
5936
|
heapUsedMB: +(mem.heapUsed / 1024 / 1024).toFixed(1),
|
|
4683
5937
|
heapTotalMB: +(mem.heapTotal / 1024 / 1024).toFixed(1),
|
|
@@ -4685,7 +5939,7 @@ const perfSummaryInterval = setInterval(() => {
|
|
|
4685
5939
|
chatBuffers: sessionChatBuffers.size,
|
|
4686
5940
|
chatBufferLines: totalBufferLines,
|
|
4687
5941
|
chatBufferKB: +(totalBufferBytes / 1024).toFixed(1),
|
|
4688
|
-
seqBuffers:
|
|
5942
|
+
seqBuffers: cmsSeqTimelines.size,
|
|
4689
5943
|
seqEvents: totalSeqEvents,
|
|
4690
5944
|
observers: sessionObservers.size,
|
|
4691
5945
|
renders: _perfRenderCount,
|
|
@@ -4728,6 +5982,11 @@ orchList.key(["c"], async () => {
|
|
|
4728
5982
|
const sessionId = id.startsWith("session-") ? id.slice(8) : id;
|
|
4729
5983
|
try {
|
|
4730
5984
|
await mgmt.cancelSession(sessionId);
|
|
5985
|
+
sessionLiveStatus.set(id, "terminated");
|
|
5986
|
+
setSessionPendingTurn(id, false);
|
|
5987
|
+
clearSessionPendingQuestion(id);
|
|
5988
|
+
sessionCronSchedules.delete(id);
|
|
5989
|
+
updateSessionListIcons();
|
|
4731
5990
|
appendLog(`{yellow-fg}Cancelled ${shortId(id)}{/yellow-fg}`);
|
|
4732
5991
|
await refreshOrchestrations();
|
|
4733
5992
|
} catch (err) {
|
|
@@ -4749,6 +6008,7 @@ orchList.key(["d"], async () => {
|
|
|
4749
6008
|
await mgmt.deleteSession(sessionId);
|
|
4750
6009
|
knownOrchestrationIds.delete(id);
|
|
4751
6010
|
orchStatusCache.delete(id);
|
|
6011
|
+
sessionCronSchedules.delete(id);
|
|
4752
6012
|
appendLog(`{yellow-fg}Deleted ${shortId(id)}{/yellow-fg}`);
|
|
4753
6013
|
await refreshOrchestrations();
|
|
4754
6014
|
} catch (err) {
|
|
@@ -5202,11 +6462,6 @@ function startLogStream() {
|
|
|
5202
6462
|
appendOrchLog(orchId, podName, formatted);
|
|
5203
6463
|
}
|
|
5204
6464
|
|
|
5205
|
-
// Feed sequence diagram
|
|
5206
|
-
const seqEvt = parseSeqEvent(plain, podName);
|
|
5207
|
-
if (seqEvt) {
|
|
5208
|
-
appendSeqEvent(seqEvt.orchId, seqEvt);
|
|
5209
|
-
}
|
|
5210
6465
|
}
|
|
5211
6466
|
screen.render();
|
|
5212
6467
|
});
|
|
@@ -5280,7 +6535,6 @@ if (isRemote) {
|
|
|
5280
6535
|
// Map sessionId → PilotSwarmSession object
|
|
5281
6536
|
const sessions = new Map();
|
|
5282
6537
|
const sessionModels = new Map(); // orchId → model name used for that session
|
|
5283
|
-
let shutdownInProgress = false;
|
|
5284
6538
|
|
|
5285
6539
|
// currentModel is declared earlier (before model providers loading)
|
|
5286
6540
|
|
|
@@ -5549,20 +6803,45 @@ function updateChatLabel() {
|
|
|
5549
6803
|
const model = sessionModels.get(activeOrchId) || "";
|
|
5550
6804
|
const shortModel = model.includes(":") ? model.split(":")[1] : model;
|
|
5551
6805
|
const modelTag = shortModel ? ` {cyan-fg}${shortModel}{/cyan-fg}` : "";
|
|
6806
|
+
const contextTag = getContextHeaderBadge(activeOrchId);
|
|
6807
|
+
const compactionTag = getContextCompactionBadge(activeOrchId);
|
|
5552
6808
|
const collapseBadge = getCollapseBadge(activeOrchId);
|
|
5553
6809
|
const isSweeper = systemSessionIds.has(activeOrchId);
|
|
5554
6810
|
if (isSweeper) {
|
|
5555
6811
|
const sysTitle = sessionHeadings.get(activeOrchId) || "System Agent";
|
|
5556
|
-
chatBox.setLabel(` {bold}{yellow-fg}≋ ${sysTitle}${collapseBadge}{/yellow-fg}{/bold} {white-fg}[${activeSessionShort}]{/white-fg}${modelTag} `);
|
|
6812
|
+
chatBox.setLabel(` {bold}{yellow-fg}≋ ${sysTitle}${collapseBadge}{/yellow-fg}{/bold} {white-fg}[${activeSessionShort}]{/white-fg}${modelTag}${contextTag}${compactionTag} `);
|
|
5557
6813
|
chatBox.style.border.fg = "yellow";
|
|
5558
6814
|
} else {
|
|
5559
6815
|
const title = sessionHeadings.get(activeOrchId) || "Chat";
|
|
5560
|
-
chatBox.setLabel(` {bold}${title}${collapseBadge}{/bold} {white-fg}[${activeSessionShort}]{/white-fg}${modelTag} `);
|
|
6816
|
+
chatBox.setLabel(` {bold}${title}${collapseBadge}{/bold} {white-fg}[${activeSessionShort}]{/white-fg}${modelTag}${contextTag}${compactionTag} `);
|
|
5561
6817
|
chatBox.style.border.fg = "cyan";
|
|
5562
6818
|
}
|
|
5563
6819
|
screen.render();
|
|
5564
6820
|
}
|
|
5565
6821
|
|
|
6822
|
+
function setObserverPollState(orchId, version) {
|
|
6823
|
+
if (!orchId) return;
|
|
6824
|
+
sessionObserverPollState.set(orchId, { version: Math.max(0, version || 0) });
|
|
6825
|
+
if (orchId === activeOrchId) updateActivityLabel();
|
|
6826
|
+
}
|
|
6827
|
+
|
|
6828
|
+
function clearObserverPollState(orchId) {
|
|
6829
|
+
if (!orchId) return;
|
|
6830
|
+
if (sessionObserverPollState.delete(orchId) && orchId === activeOrchId) {
|
|
6831
|
+
updateActivityLabel();
|
|
6832
|
+
}
|
|
6833
|
+
}
|
|
6834
|
+
|
|
6835
|
+
function updateActivityLabel() {
|
|
6836
|
+
const pollState = activeOrchId ? sessionObserverPollState.get(activeOrchId) : null;
|
|
6837
|
+
const isPollingActiveSession = Boolean(activeOrchId && sessionObservers.has(activeOrchId) && pollState);
|
|
6838
|
+
const pollingTag = isPollingActiveSession
|
|
6839
|
+
? ` {gray-fg}[polling current session v${pollState.version}]{/gray-fg}`
|
|
6840
|
+
: "";
|
|
6841
|
+
activityPane.setLabel(` {bold}Activity{/bold}${pollingTag} `);
|
|
6842
|
+
scheduleRender();
|
|
6843
|
+
}
|
|
6844
|
+
|
|
5566
6845
|
/**
|
|
5567
6846
|
* Start observing an orchestration's custom status and pipe turn results
|
|
5568
6847
|
* into the chat buffer. Runs until aborted or the orchestration completes.
|
|
@@ -5578,6 +6857,7 @@ function startObserver(orchId) {
|
|
|
5578
6857
|
|
|
5579
6858
|
const ac = new AbortController();
|
|
5580
6859
|
sessionObservers.set(orchId, ac);
|
|
6860
|
+
setObserverPollState(orchId, 0);
|
|
5581
6861
|
let lastVersion = 0;
|
|
5582
6862
|
let lastIteration = -1;
|
|
5583
6863
|
|
|
@@ -5657,10 +6937,14 @@ function startObserver(orchId) {
|
|
|
5657
6937
|
appendChatRaw("{bold}Session info:{/bold}", orchId);
|
|
5658
6938
|
appendChatRaw(` Model: {cyan-fg}${r.model}{/cyan-fg}`, orchId);
|
|
5659
6939
|
appendChatRaw(` Iteration: ${r.iteration}`, orchId);
|
|
5660
|
-
appendChatRaw(` Session: ${r.sessionId
|
|
6940
|
+
appendChatRaw(` Session: ${safeSlice(r.sessionId, 0, 12)}…`, orchId);
|
|
5661
6941
|
appendChatRaw(` Affinity: ${r.affinityKey}`, orchId);
|
|
5662
6942
|
appendChatRaw(` Hydrated: ${r.needsHydration ? "no (dehydrated)" : "yes"}`, orchId);
|
|
5663
6943
|
appendChatRaw(` Blob: ${r.blobEnabled ? "enabled" : "disabled"}`, orchId);
|
|
6944
|
+
if (r.contextUsage?.tokenLimit > 0 && typeof r.contextUsage?.currentTokens === "number") {
|
|
6945
|
+
const percent = computeContextPercent(r.contextUsage);
|
|
6946
|
+
appendChatRaw(` Context: ${formatTokenCount(r.contextUsage.currentTokens)}/${formatTokenCount(r.contextUsage.tokenLimit)}${percent != null ? ` (${percent}%)` : ""}`, orchId);
|
|
6947
|
+
}
|
|
5664
6948
|
break;
|
|
5665
6949
|
}
|
|
5666
6950
|
case "done": {
|
|
@@ -5685,7 +6969,7 @@ function startObserver(orchId) {
|
|
|
5685
6969
|
if (!response) return;
|
|
5686
6970
|
stopChatSpinner(orchId);
|
|
5687
6971
|
if (response.type === "completed" && response.content) {
|
|
5688
|
-
appendActivity(`{green-fg}[obs] ✓ SHOWING ${source}: version=${response.version} type=completed content=${response.content
|
|
6972
|
+
appendActivity(`{green-fg}[obs] ✓ SHOWING ${source}: version=${response.version} type=completed content=${safeSlice(response.content, 0, 80)}{/green-fg}`, orchId);
|
|
5689
6973
|
renderCompletedContent(response.content);
|
|
5690
6974
|
if (cs.status === "idle" || cs.status === "completed") {
|
|
5691
6975
|
setStatusIfActive(cs.status === "completed" ? "Session completed" : "Ready — type a message");
|
|
@@ -5696,11 +6980,15 @@ function startObserver(orchId) {
|
|
|
5696
6980
|
return;
|
|
5697
6981
|
}
|
|
5698
6982
|
if (response.type === "wait" && response.content) {
|
|
5699
|
-
appendActivity(`{green-fg}[obs] ✓ SHOWING ${source}: version=${response.version} type=wait content=${response.content
|
|
6983
|
+
appendActivity(`{green-fg}[obs] ✓ SHOWING ${source}: version=${response.version} type=wait content=${safeSlice(response.content, 0, 80)}{/green-fg}`, orchId);
|
|
5700
6984
|
const preview = summarizeActivityPreview(response.content);
|
|
5701
6985
|
appendActivity(`{white-fg}[${ts()}]{/white-fg} {gray-fg}[intermediate]{/gray-fg} ${preview}`, orchId);
|
|
5702
6986
|
promoteIntermediateContent(response.content, orchId);
|
|
5703
|
-
setStatusIfActive(
|
|
6987
|
+
setStatusIfActive(formatWaitingLabel({
|
|
6988
|
+
cronActive: cs?.cronActive === true,
|
|
6989
|
+
cronReason: cs?.cronReason,
|
|
6990
|
+
waitReason: cs?.waitReason || response.waitReason,
|
|
6991
|
+
}));
|
|
5704
6992
|
return;
|
|
5705
6993
|
}
|
|
5706
6994
|
if (response.type === "input_required") {
|
|
@@ -5723,6 +7011,8 @@ function startObserver(orchId) {
|
|
|
5723
7011
|
: statusSnapshot.customStatus;
|
|
5724
7012
|
} catch {}
|
|
5725
7013
|
}
|
|
7014
|
+
updateSessionCronSchedule(orchId, cs);
|
|
7015
|
+
updateSessionContextUsageFromStatus(orchId, cs);
|
|
5726
7016
|
if (!cs) return null;
|
|
5727
7017
|
|
|
5728
7018
|
lastVersion = Math.max(lastVersion, statusSnapshot.customStatusVersion || 0);
|
|
@@ -5790,6 +7080,7 @@ function startObserver(orchId) {
|
|
|
5790
7080
|
sessionLoggedTerminalStatus.set(orchId, terminalStatus);
|
|
5791
7081
|
setSessionPendingTurn(orchId, false);
|
|
5792
7082
|
setTurnInProgressIfActive(false);
|
|
7083
|
+
clearObserverPollState(orchId);
|
|
5793
7084
|
sessionObservers.delete(orchId);
|
|
5794
7085
|
return true;
|
|
5795
7086
|
}
|
|
@@ -5804,6 +7095,7 @@ function startObserver(orchId) {
|
|
|
5804
7095
|
try {
|
|
5805
7096
|
const currentStatus = await dc.getStatus(orchId);
|
|
5806
7097
|
if (ac.signal.aborted) return;
|
|
7098
|
+
setObserverPollState(orchId, currentStatus?.customStatusVersion || 0);
|
|
5807
7099
|
|
|
5808
7100
|
// Check for terminal states FIRST — before inspecting customStatus
|
|
5809
7101
|
if (await stopObserverForTerminalStatus(currentStatus, "terminal")) {
|
|
@@ -5817,6 +7109,8 @@ function startObserver(orchId) {
|
|
|
5817
7109
|
? JSON.parse(currentStatus.customStatus) : currentStatus.customStatus;
|
|
5818
7110
|
} catch {}
|
|
5819
7111
|
if (cs) {
|
|
7112
|
+
updateSessionCronSchedule(orchId, cs);
|
|
7113
|
+
updateSessionContextUsageFromStatus(orchId, cs);
|
|
5820
7114
|
lastVersion = currentStatus.customStatusVersion || 0;
|
|
5821
7115
|
if (cs.turnResult && cs.turnResult.type === "completed") {
|
|
5822
7116
|
lastIteration = cs.iteration || 0;
|
|
@@ -5847,7 +7141,7 @@ function startObserver(orchId) {
|
|
|
5847
7141
|
setTurnInProgressIfActive(true);
|
|
5848
7142
|
updateLiveStatus("running");
|
|
5849
7143
|
} else if (cs.status === "waiting") {
|
|
5850
|
-
setStatusIfActive(
|
|
7144
|
+
setStatusIfActive(formatWaitingLabel(cs));
|
|
5851
7145
|
updateLiveStatus("waiting");
|
|
5852
7146
|
} else if (cs.status === "input_required") {
|
|
5853
7147
|
if (cs.turnResult?.type === "input_required") {
|
|
@@ -5888,7 +7182,7 @@ function startObserver(orchId) {
|
|
|
5888
7182
|
const _obsPh = perfStart("observer.waitForStatusChange");
|
|
5889
7183
|
const _waitStart = Date.now();
|
|
5890
7184
|
const statusResult = await dc.waitForStatusChange(
|
|
5891
|
-
orchId, lastVersion,
|
|
7185
|
+
orchId, lastVersion, 1_000, 60_000
|
|
5892
7186
|
);
|
|
5893
7187
|
const _waitMs = Date.now() - _waitStart;
|
|
5894
7188
|
perfEnd(_obsPh, { orchId: orchId.slice(0, 12), ver: statusResult.customStatusVersion });
|
|
@@ -5907,10 +7201,8 @@ function startObserver(orchId) {
|
|
|
5907
7201
|
appendActivity(`{yellow-fg}🔄 [obs] continueAsNew detected in try: v${lastVersion}→v${statusResult.customStatusVersion}, resetting lastIter=${lastIteration}→-1 (${_waitMs}ms){/yellow-fg}`, orchId);
|
|
5908
7202
|
lastVersion = statusResult.customStatusVersion;
|
|
5909
7203
|
lastIteration = -1;
|
|
5910
|
-
} else {
|
|
5911
|
-
// Same version — poll returned without change (shouldn't happen normally)
|
|
5912
|
-
appendActivity(`{gray-fg}[obs] poll returned same version v${lastVersion} (${_waitMs}ms){/gray-fg}`, orchId);
|
|
5913
7204
|
}
|
|
7205
|
+
setObserverPollState(orchId, lastVersion);
|
|
5914
7206
|
|
|
5915
7207
|
let cs = null;
|
|
5916
7208
|
if (statusResult.customStatus) {
|
|
@@ -5919,6 +7211,8 @@ function startObserver(orchId) {
|
|
|
5919
7211
|
? JSON.parse(statusResult.customStatus) : statusResult.customStatus;
|
|
5920
7212
|
} catch {}
|
|
5921
7213
|
}
|
|
7214
|
+
updateSessionCronSchedule(orchId, cs);
|
|
7215
|
+
updateSessionContextUsageFromStatus(orchId, cs);
|
|
5922
7216
|
|
|
5923
7217
|
if (cs) {
|
|
5924
7218
|
// Log every status change with key fields
|
|
@@ -5979,7 +7273,7 @@ function startObserver(orchId) {
|
|
|
5979
7273
|
} else if (cs.turnResult && cs.iteration <= lastIteration) {
|
|
5980
7274
|
appendActivity(`{yellow-fg}[obs] ⚠ SKIPPED turnResult: iter=${cs.iteration} <= lastIter=${lastIteration} (already shown){/yellow-fg}`, orchId);
|
|
5981
7275
|
if (cs.status === "waiting") {
|
|
5982
|
-
setStatusIfActive(
|
|
7276
|
+
setStatusIfActive(formatWaitingLabel(cs));
|
|
5983
7277
|
}
|
|
5984
7278
|
} else if (cs.responseVersion) {
|
|
5985
7279
|
const latestResponse = await consumeLatestResponsePayload(orchId, cs, dc);
|
|
@@ -5999,7 +7293,7 @@ function startObserver(orchId) {
|
|
|
5999
7293
|
setStatusIfActive("Running…");
|
|
6000
7294
|
setTurnInProgressIfActive(true);
|
|
6001
7295
|
} else if (cs.status === "waiting") {
|
|
6002
|
-
setStatusIfActive(
|
|
7296
|
+
setStatusIfActive(formatWaitingLabel(cs));
|
|
6003
7297
|
} else if (cs.status === "input_required") {
|
|
6004
7298
|
setStatusIfActive("Waiting for your answer...");
|
|
6005
7299
|
updateLiveStatus("input_required");
|
|
@@ -6017,7 +7311,7 @@ function startObserver(orchId) {
|
|
|
6017
7311
|
setStatusIfActive("Running…");
|
|
6018
7312
|
setTurnInProgressIfActive(true);
|
|
6019
7313
|
} else if (cs.status === "waiting") {
|
|
6020
|
-
setStatusIfActive(
|
|
7314
|
+
setStatusIfActive(formatWaitingLabel(cs));
|
|
6021
7315
|
}
|
|
6022
7316
|
|
|
6023
7317
|
// Mark session as having unseen changes if not active
|
|
@@ -6029,20 +7323,23 @@ function startObserver(orchId) {
|
|
|
6029
7323
|
} catch (err) {
|
|
6030
7324
|
// waitForStatusChange timed out or failed — check terminal state or continueAsNew
|
|
6031
7325
|
if (ac.signal.aborted) break;
|
|
6032
|
-
|
|
7326
|
+
const errMsg = err?.message || "timeout";
|
|
7327
|
+
const isStatusTimeout = /timed out/i.test(errMsg);
|
|
7328
|
+
if (!isStatusTimeout) {
|
|
7329
|
+
appendActivity(`{yellow-fg}[obs] catch: ${errMsg} lastVersion=${lastVersion} lastIteration=${lastIteration}{/yellow-fg}`, orchId);
|
|
7330
|
+
}
|
|
6033
7331
|
try {
|
|
6034
7332
|
const info = await dc.getStatus(orchId);
|
|
7333
|
+
const currentVersion = info.customStatusVersion || 0;
|
|
7334
|
+
setObserverPollState(orchId, currentVersion);
|
|
6035
7335
|
if (await stopObserverForTerminalStatus(info, "terminal")) {
|
|
6036
7336
|
break;
|
|
6037
7337
|
}
|
|
6038
7338
|
// Detect continueAsNew: customStatusVersion went backwards
|
|
6039
|
-
const currentVersion = info.customStatusVersion || 0;
|
|
6040
7339
|
if (currentVersion < lastVersion) {
|
|
6041
7340
|
appendActivity(`{yellow-fg}🔄 [obs] continueAsNew in catch: v${lastVersion}→v${currentVersion}{/yellow-fg}`, orchId);
|
|
6042
7341
|
lastVersion = 0;
|
|
6043
7342
|
lastIteration = -1;
|
|
6044
|
-
} else {
|
|
6045
|
-
appendActivity(`{gray-fg}[obs] catch: no version reset (v${currentVersion} >= v${lastVersion}){/gray-fg}`, orchId);
|
|
6046
7343
|
}
|
|
6047
7344
|
} catch {}
|
|
6048
7345
|
await new Promise(r => setTimeout(r, 500));
|
|
@@ -6089,11 +7386,42 @@ function startCmsPoller(orchId) {
|
|
|
6089
7386
|
sessionRenderedCmsSeq.set(orchId, evt.seq);
|
|
6090
7387
|
}
|
|
6091
7388
|
|
|
7389
|
+
// ── Update CMS sequence timeline incrementally ────
|
|
7390
|
+
if (evt.seq && evt.eventType) {
|
|
7391
|
+
const timeline = cmsSeqTimelines.get(sid) || [];
|
|
7392
|
+
const seqEvt = placeCmsSeqEvent(timeline, cmsEventToSeqEvent(evt));
|
|
7393
|
+
if (seqEvt) {
|
|
7394
|
+
const nodeId = getSeqEventNodeId(seqEvt);
|
|
7395
|
+
if (nodeId) cmsSeqNodeIndex(nodeId);
|
|
7396
|
+
timeline.push(seqEvt);
|
|
7397
|
+
cmsSeqTimelines.set(sid, timeline);
|
|
7398
|
+
cmsSeqLastSeq.set(sid, evt.seq);
|
|
7399
|
+
if (timeline.length > 300) timeline.splice(0, timeline.length - 300);
|
|
7400
|
+
if (logViewMode === "sequence" && orchId === activeOrchId) {
|
|
7401
|
+
refreshCmsSeqPane();
|
|
7402
|
+
screen.render();
|
|
7403
|
+
}
|
|
7404
|
+
}
|
|
7405
|
+
}
|
|
7406
|
+
|
|
6092
7407
|
const t = formatDisplayTime(Date.now());
|
|
6093
7408
|
const type = evt.eventType;
|
|
6094
7409
|
|
|
6095
|
-
|
|
6096
|
-
|
|
7410
|
+
if (type === "session.usage_info" || type === "assistant.usage"
|
|
7411
|
+
|| type === "session.compaction_start" || type === "session.compaction_complete") {
|
|
7412
|
+
applySessionUsageEvent(orchId, type, evt.data);
|
|
7413
|
+
}
|
|
7414
|
+
|
|
7415
|
+
// assistant.message is shown as a provisional gray chat entry.
|
|
7416
|
+
// The completed turn result clears that provisional copy.
|
|
7417
|
+
if (type === "assistant.message") {
|
|
7418
|
+
if (typeof evt.data?.content === "string" && evt.data.content.trim()) {
|
|
7419
|
+
showLiveAssistantPreview(evt.data.content, orchId);
|
|
7420
|
+
}
|
|
7421
|
+
return;
|
|
7422
|
+
}
|
|
7423
|
+
// user.message is already reflected by the local send path and the
|
|
7424
|
+
// history rebuild path, so keep the live CMS subscription quiet here.
|
|
6097
7425
|
if (type === "user.message") return;
|
|
6098
7426
|
|
|
6099
7427
|
if (type === "tool.execution_start") {
|
|
@@ -6108,6 +7436,8 @@ function startCmsPoller(orchId) {
|
|
|
6108
7436
|
appendActivity(`{white-fg}[${t}]{/white-fg} {gray-fg}[reasoning]{/gray-fg}`, orchId);
|
|
6109
7437
|
} else if (type === "assistant.turn_start") {
|
|
6110
7438
|
appendActivity(`{white-fg}[${t}]{/white-fg} {gray-fg}[turn start]{/gray-fg}`, orchId);
|
|
7439
|
+
} else if (type === "session.compaction_start" || type === "session.compaction_complete") {
|
|
7440
|
+
appendActivity(formatCompactionActivityLine(t, evt), orchId);
|
|
6111
7441
|
} else if (type === "assistant.usage" || type === "session.info" || type === "session.idle"
|
|
6112
7442
|
|| type === "session.usage_info" || type === "pending_messages.modified" || type === "abort") {
|
|
6113
7443
|
// skip internal/noisy events
|
|
@@ -6153,6 +7483,8 @@ async function switchToOrchestration(orchId) {
|
|
|
6153
7483
|
if (!sessionModels.has(orchId) && info?.customStatus) {
|
|
6154
7484
|
try {
|
|
6155
7485
|
const cs = typeof info.customStatus === "string" ? JSON.parse(info.customStatus) : info.customStatus;
|
|
7486
|
+
updateSessionCronSchedule(orchId, cs);
|
|
7487
|
+
updateSessionContextUsageFromStatus(orchId, cs);
|
|
6156
7488
|
const turnResult = cs.turnResult || cs.lastTurnResult;
|
|
6157
7489
|
if (turnResult?.model) {
|
|
6158
7490
|
sessionModels.set(orchId, turnResult.model);
|
|
@@ -6171,6 +7503,8 @@ async function switchToOrchestration(orchId) {
|
|
|
6171
7503
|
if (info?.customStatus) {
|
|
6172
7504
|
try {
|
|
6173
7505
|
const cs = typeof info.customStatus === "string" ? JSON.parse(info.customStatus) : info.customStatus;
|
|
7506
|
+
updateSessionCronSchedule(orchId, cs);
|
|
7507
|
+
updateSessionContextUsageFromStatus(orchId, cs);
|
|
6174
7508
|
if (cs?.status) {
|
|
6175
7509
|
sessionLiveStatus.set(orchId, cs.status);
|
|
6176
7510
|
updateSessionListIcons();
|
|
@@ -6193,6 +7527,7 @@ async function switchToOrchestration(orchId) {
|
|
|
6193
7527
|
// Clear chat and show switch indicator (only when switching to a different session)
|
|
6194
7528
|
if (!isSameSession) {
|
|
6195
7529
|
updateChatLabel();
|
|
7530
|
+
updateActivityLabel();
|
|
6196
7531
|
|
|
6197
7532
|
// Show cached chat buffer instantly if available (no DB wait)
|
|
6198
7533
|
const _cachePh = perfStart("switch.cachedRestore");
|
|
@@ -6248,6 +7583,14 @@ async function switchToOrchestration(orchId) {
|
|
|
6248
7583
|
if (orchId === activeOrchId) startCmsPoller(orchId);
|
|
6249
7584
|
});
|
|
6250
7585
|
|
|
7586
|
+
// Load CMS event timeline for sequence diagram (background, non-blocking)
|
|
7587
|
+
const sessionIdForSeq = orchId.replace("session-", "");
|
|
7588
|
+
loadCmsSeqTimeline(sessionIdForSeq).then(() => {
|
|
7589
|
+
if (orchId === activeOrchId && logViewMode === "sequence") {
|
|
7590
|
+
refreshSeqPane();
|
|
7591
|
+
}
|
|
7592
|
+
}).catch(() => {});
|
|
7593
|
+
|
|
6251
7594
|
// Schedule list refresh in background too
|
|
6252
7595
|
scheduleRefreshOrchestrations();
|
|
6253
7596
|
} else {
|
|
@@ -6271,6 +7614,7 @@ async function switchToOrchestration(orchId) {
|
|
|
6271
7614
|
}
|
|
6272
7615
|
|
|
6273
7616
|
updateChatLabel();
|
|
7617
|
+
updateActivityLabel();
|
|
6274
7618
|
const initialChatLines = ensureSessionSplashBuffer(activeOrchId) || sessionChatBuffers.get(activeOrchId) || [];
|
|
6275
7619
|
if (initialChatLines.length > 0) {
|
|
6276
7620
|
chatBox.setContent(initialChatLines.map(styleUrls).join("\n"));
|
|
@@ -6590,7 +7934,6 @@ async function handleInput(text) {
|
|
|
6590
7934
|
appendActivity(`{cyan-fg}[send] interrupt: session busy, enqueuing to ${targetOrchId?.slice(0,20)}{/cyan-fg}`, targetOrchId);
|
|
6591
7935
|
inputBar.clearValue();
|
|
6592
7936
|
if (targetOrchId === activeOrchId) setStatus("Interrupting...");
|
|
6593
|
-
injectSeqUserEvent(targetOrchId, trimmed);
|
|
6594
7937
|
try {
|
|
6595
7938
|
const dc = getDc();
|
|
6596
7939
|
if (dc) await dc.enqueueEvent(targetOrchId, "messages", JSON.stringify({ prompt: trimmed }));
|
|
@@ -6609,7 +7952,6 @@ async function handleInput(text) {
|
|
|
6609
7952
|
focusInput();
|
|
6610
7953
|
setSessionPendingTurn(targetOrchId, true);
|
|
6611
7954
|
if (targetOrchId === activeOrchId) setStatus("Thinking... (waiting for AKS worker)");
|
|
6612
|
-
injectSeqUserEvent(targetOrchId, trimmed);
|
|
6613
7955
|
screen.render();
|
|
6614
7956
|
|
|
6615
7957
|
try {
|
|
@@ -6624,9 +7966,16 @@ async function handleInput(text) {
|
|
|
6624
7966
|
|| orchStatus.output?.split("\n")[0]
|
|
6625
7967
|
|| "Unknown error")
|
|
6626
7968
|
: orchStatus.status;
|
|
7969
|
+
const terminalState = sessionStateFromOrchestrationStatus(orchStatus.status);
|
|
7970
|
+
if (terminalState) {
|
|
7971
|
+
sessionLiveStatus.set(targetOrchId, terminalState);
|
|
7972
|
+
}
|
|
6627
7973
|
appendChatRaw(`{red-fg}❌ Cannot send — orchestration ${orchStatus.status}: ${reason}{/red-fg}`, targetOrchId);
|
|
6628
7974
|
appendChatRaw(`{white-fg}Create a new session with 'n' to continue.{/white-fg}`, targetOrchId);
|
|
7975
|
+
stopChatSpinner(targetOrchId);
|
|
6629
7976
|
setSessionPendingTurn(targetOrchId, false);
|
|
7977
|
+
clearSessionPendingQuestion(targetOrchId);
|
|
7978
|
+
updateSessionListIcons();
|
|
6630
7979
|
if (targetOrchId === activeOrchId) setStatus(`${orchStatus.status} — session is dead`);
|
|
6631
7980
|
screen.render();
|
|
6632
7981
|
return;
|
|
@@ -6700,6 +8049,7 @@ function showHelpOverlay() {
|
|
|
6700
8049
|
" {yellow-fg}u{/yellow-fg} Dump active session to Markdown file",
|
|
6701
8050
|
" {yellow-fg}a{/yellow-fg} Show artifact picker (download files)",
|
|
6702
8051
|
" {yellow-fg}Ctrl+A{/yellow-fg} Attach local file to prompt",
|
|
8052
|
+
" {yellow-fg}Mouse drag{/yellow-fg} Select visible text inside one pane and copy on release",
|
|
6703
8053
|
"",
|
|
6704
8054
|
"{bold}Sessions Pane{/bold}",
|
|
6705
8055
|
" {yellow-fg}j / k{/yellow-fg} Navigate up / down",
|
|
@@ -6736,7 +8086,7 @@ function showHelpOverlay() {
|
|
|
6736
8086
|
" {yellow-fg}d{/yellow-fg} Delete selected exported file",
|
|
6737
8087
|
" {yellow-fg}g / G{/yellow-fg} Preview top / bottom",
|
|
6738
8088
|
" {yellow-fg}Ctrl+D/U{/yellow-fg} Preview page down / up",
|
|
6739
|
-
" {yellow-fg}o{/yellow-fg} Open selected file in
|
|
8089
|
+
" {yellow-fg}o{/yellow-fg} Open selected file in default app",
|
|
6740
8090
|
" {yellow-fg}y{/yellow-fg} Copy selected file path",
|
|
6741
8091
|
" {yellow-fg}v{/yellow-fg} Exit markdown viewer",
|
|
6742
8092
|
"",
|
|
@@ -6817,8 +8167,8 @@ function showHelpOverlay() {
|
|
|
6817
8167
|
async function cleanup() {
|
|
6818
8168
|
// Force-exit after 15s — don't let a stuck shutdown hang forever
|
|
6819
8169
|
const forceExitTimer = setTimeout(() => {
|
|
6820
|
-
|
|
6821
|
-
try { fs.writeSync(1,
|
|
8170
|
+
releaseLocalEmbeddedRuntimeLock();
|
|
8171
|
+
try { fs.writeSync(1, Buffer.from(TERMINAL_RESET_ESCAPE)); } catch {}
|
|
6822
8172
|
process.exit(0);
|
|
6823
8173
|
}, 15000);
|
|
6824
8174
|
forceExitTimer.unref();
|
|
@@ -6839,6 +8189,7 @@ async function cleanup() {
|
|
|
6839
8189
|
client.stop(),
|
|
6840
8190
|
mgmt.stop(),
|
|
6841
8191
|
]);
|
|
8192
|
+
releaseLocalEmbeddedRuntimeLock();
|
|
6842
8193
|
|
|
6843
8194
|
// Suppress ALL output before destroying — neo-blessed dumps terminfo
|
|
6844
8195
|
// compilation junk (SetUlc) synchronously during destroy().
|
|
@@ -6847,8 +8198,7 @@ async function cleanup() {
|
|
|
6847
8198
|
try { screen.destroy(); } catch {}
|
|
6848
8199
|
// Write terminal reset directly to fd to bypass our suppression
|
|
6849
8200
|
// Disable mouse tracking modes + exit alt-screen + show cursor
|
|
6850
|
-
|
|
6851
|
-
try { fs.writeSync(1, buf); } catch {}
|
|
8201
|
+
try { fs.writeSync(1, Buffer.from(TERMINAL_RESET_ESCAPE)); } catch {}
|
|
6852
8202
|
}
|
|
6853
8203
|
|
|
6854
8204
|
screen.key(["C-c"], async () => {
|
|
@@ -6958,6 +8308,13 @@ screen.on("keypress", (ch, key) => {
|
|
|
6958
8308
|
screen.render();
|
|
6959
8309
|
return;
|
|
6960
8310
|
}
|
|
8311
|
+
if (ch === "o") {
|
|
8312
|
+
const files = scanExportFiles();
|
|
8313
|
+
const f = files[mdViewerSelectedIdx];
|
|
8314
|
+
if (!f) return;
|
|
8315
|
+
openPathInDefaultApp(f.localPath);
|
|
8316
|
+
return;
|
|
8317
|
+
}
|
|
6961
8318
|
// d: delete the selected file
|
|
6962
8319
|
if (ch === "d") {
|
|
6963
8320
|
const files = scanExportFiles();
|
|
@@ -6993,10 +8350,10 @@ screen.on("keypress", (ch, key) => {
|
|
|
6993
8350
|
// [ / ]: resize right pane by 8 chars
|
|
6994
8351
|
} else if ((ch === "[" || ch === "]") && screen.focused !== inputBar) {
|
|
6995
8352
|
if (ch === "[") rightPaneAdjust += 8; // shrink right (grow left)
|
|
6996
|
-
else rightPaneAdjust
|
|
8353
|
+
else rightPaneAdjust -= 8; // grow right (shrink left)
|
|
6997
8354
|
// Clamp: right pane min 20 chars, left pane min 30 chars
|
|
6998
|
-
const maxAdj = screen.width -
|
|
6999
|
-
rightPaneAdjust = Math.max(-(
|
|
8355
|
+
const maxAdj = screen.width - MIN_RIGHT_PANE_WIDTH - baseLeftW();
|
|
8356
|
+
rightPaneAdjust = Math.max(-(baseLeftW() - MIN_LEFT_PANE_WIDTH), Math.min(rightPaneAdjust, maxAdj));
|
|
7000
8357
|
relayoutAll();
|
|
7001
8358
|
redrawActiveViews();
|
|
7002
8359
|
return;
|
|
@@ -7166,6 +8523,7 @@ function buildFocusableList() {
|
|
|
7166
8523
|
}
|
|
7167
8524
|
|
|
7168
8525
|
screen.on("resize", () => {
|
|
8526
|
+
clearPaneSelection({ render: false });
|
|
7169
8527
|
relayoutAll();
|
|
7170
8528
|
if (logViewMode === "sequence") refreshSeqPane();
|
|
7171
8529
|
if (logViewMode === "nodemap") refreshNodeMap();
|
|
@@ -7177,6 +8535,23 @@ await refreshOrchestrations();
|
|
|
7177
8535
|
// Trigger initial right-pane render (orch logs, sequence, etc.)
|
|
7178
8536
|
redrawActiveViews();
|
|
7179
8537
|
|
|
8538
|
+
if (!activeOrchId) {
|
|
8539
|
+
renderWaitingForSessionsLanding();
|
|
8540
|
+
if (!isRemote) {
|
|
8541
|
+
// Lazy worker startup means the first session list refresh can race ahead
|
|
8542
|
+
// of system-agent/session creation. Re-check quickly before the normal
|
|
8543
|
+
// 10s poll so the TUI does not appear stuck on the startup splash.
|
|
8544
|
+
for (const delay of [250, 750, 1500, 3000, 5000]) {
|
|
8545
|
+
setTimeout(() => {
|
|
8546
|
+
if (activeOrchId) return;
|
|
8547
|
+
refreshOrchestrations().then(() => {
|
|
8548
|
+
if (!activeOrchId) renderWaitingForSessionsLanding();
|
|
8549
|
+
}).catch(() => {});
|
|
8550
|
+
}, delay);
|
|
8551
|
+
}
|
|
8552
|
+
}
|
|
8553
|
+
}
|
|
8554
|
+
|
|
7180
8555
|
// ─── Auto-summarize all existing sessions on startup ─────────────
|
|
7181
8556
|
async function summarizeSession(orchId) {
|
|
7182
8557
|
if (sessionSummarized.has(orchId)) return;
|
|
@@ -7219,7 +8594,7 @@ async function summarizeSession(orchId) {
|
|
|
7219
8594
|
let sawRunning = false;
|
|
7220
8595
|
while (Date.now() < deadline) {
|
|
7221
8596
|
try {
|
|
7222
|
-
const result = await dc.waitForStatusChange(orchId, version,
|
|
8597
|
+
const result = await dc.waitForStatusChange(orchId, version, 1_000, 60_000);
|
|
7223
8598
|
if (result.customStatusVersion > version) {
|
|
7224
8599
|
version = result.customStatusVersion;
|
|
7225
8600
|
}
|
|
@@ -7230,6 +8605,8 @@ async function summarizeSession(orchId) {
|
|
|
7230
8605
|
? JSON.parse(result.customStatus) : result.customStatus;
|
|
7231
8606
|
} catch {}
|
|
7232
8607
|
}
|
|
8608
|
+
updateSessionCronSchedule(orchId, cs);
|
|
8609
|
+
updateSessionContextUsageFromStatus(orchId, cs);
|
|
7233
8610
|
if (cs?.status === "running") {
|
|
7234
8611
|
sawRunning = true;
|
|
7235
8612
|
continue; // wait for the completed result
|