pilotswarm-cli 0.1.6 → 0.1.8
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/cli/tui.js +209 -30
- package/package.json +1 -1
package/cli/tui.js
CHANGED
|
@@ -1099,6 +1099,58 @@ function appendActivity(text, orchId) {
|
|
|
1099
1099
|
}
|
|
1100
1100
|
}
|
|
1101
1101
|
|
|
1102
|
+
function formatToolArgValue(value) {
|
|
1103
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1104
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1105
|
+
if (value === null) return "null";
|
|
1106
|
+
if (Array.isArray(value)) return `[${value.map(formatToolArgValue).join(", ")}]`;
|
|
1107
|
+
try {
|
|
1108
|
+
return JSON.stringify(value);
|
|
1109
|
+
} catch {
|
|
1110
|
+
return String(value);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function formatToolArgsSummary(toolName, args) {
|
|
1115
|
+
if (!args || typeof args !== "object") return "";
|
|
1116
|
+
if (toolName === "wait") {
|
|
1117
|
+
const seconds = args.seconds != null ? `${args.seconds}s` : "?";
|
|
1118
|
+
const preserve = args.preserveWorkerAffinity === true ? " preserve=true" : "";
|
|
1119
|
+
const reason = typeof args.reason === "string" && args.reason
|
|
1120
|
+
? ` reason=${JSON.stringify(args.reason)}`
|
|
1121
|
+
: "";
|
|
1122
|
+
return ` ${seconds}${preserve}${reason}`;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const entries = Object.entries(args)
|
|
1126
|
+
.slice(0, 4)
|
|
1127
|
+
.map(([key, value]) => `${key}=${formatToolArgValue(value)}`);
|
|
1128
|
+
if (entries.length === 0) return "";
|
|
1129
|
+
const suffix = Object.keys(args).length > entries.length ? ", ..." : "";
|
|
1130
|
+
return ` ${entries.join(", ")}${suffix}`;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function formatToolActivityLine(timeStr, evt, phase = "start") {
|
|
1134
|
+
const toolName = evt.data?.toolName || evt.data?.name || "tool";
|
|
1135
|
+
const args = evt.data?.arguments || evt.data?.args;
|
|
1136
|
+
const dsid = evt.data?.durableSessionId ? ` {gray-fg}[${shortId(evt.data.durableSessionId)}]{/gray-fg}` : "";
|
|
1137
|
+
const summary = formatToolArgsSummary(toolName, args);
|
|
1138
|
+
if (phase === "start") {
|
|
1139
|
+
return `{white-fg}[${timeStr}]{/white-fg} {yellow-fg}▶ ${toolName}${summary}{/yellow-fg}${dsid}`;
|
|
1140
|
+
}
|
|
1141
|
+
return `{white-fg}[${timeStr}]{/white-fg} {green-fg}✓ ${toolName}{/green-fg}${dsid}`;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function summarizeActivityPreview(text, maxLen = 100) {
|
|
1145
|
+
const compact = String(text || "")
|
|
1146
|
+
.replace(/\s+/g, " ")
|
|
1147
|
+
.trim();
|
|
1148
|
+
if (!compact) return "(no content)";
|
|
1149
|
+
return compact.length > maxLen
|
|
1150
|
+
? `${compact.slice(0, maxLen - 1)}...`
|
|
1151
|
+
: compact;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1102
1154
|
function ensureSessionSplashBuffer(orchId) {
|
|
1103
1155
|
const existing = sessionChatBuffers.get(orchId) || [];
|
|
1104
1156
|
const splashText = systemSplashText.get(orchId);
|
|
@@ -2631,6 +2683,7 @@ function recolorWorkerPanes() {
|
|
|
2631
2683
|
|
|
2632
2684
|
function showCopilotMessage(raw, orchId) {
|
|
2633
2685
|
const _ph = perfStart("showCopilotMessage");
|
|
2686
|
+
stopChatSpinner(orchId);
|
|
2634
2687
|
|
|
2635
2688
|
appendActivity(`{green-fg}[obs] showCopilotMessage called for ${orchId === activeOrchId ? "ACTIVE" : "background"} session, len=${raw?.length || 0}{/green-fg}`, orchId);
|
|
2636
2689
|
|
|
@@ -2907,12 +2960,9 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
2907
2960
|
lines.push("");
|
|
2908
2961
|
}
|
|
2909
2962
|
} else if (type === "tool.execution_start") {
|
|
2910
|
-
|
|
2911
|
-
const dsid = evt.data?.durableSessionId ? ` {gray-fg}[${shortId(evt.data.durableSessionId)}]{/gray-fg}` : "";
|
|
2912
|
-
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {yellow-fg}▶ ${toolName}{/yellow-fg}${dsid}`);
|
|
2963
|
+
activityLines.push(formatToolActivityLine(timeStr, evt, "start"));
|
|
2913
2964
|
} else if (type === "tool.execution_complete") {
|
|
2914
|
-
|
|
2915
|
-
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {green-fg}✓ ${toolName}{/green-fg}`);
|
|
2965
|
+
activityLines.push(formatToolActivityLine(timeStr, evt, "complete"));
|
|
2916
2966
|
} else if (type === "abort" || type === "session.info" || type === "session.idle"
|
|
2917
2967
|
|| type === "session.usage_info" || type === "pending_messages.modified"
|
|
2918
2968
|
|| type === "assistant.usage") {
|
|
@@ -2971,7 +3021,45 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
2971
3021
|
}
|
|
2972
3022
|
|
|
2973
3023
|
const maxRenderedSeq = (events || []).reduce((max, evt) => Math.max(max, evt.seq || 0), 0);
|
|
2974
|
-
|
|
3024
|
+
|
|
3025
|
+
// Append pending question so it survives the history buffer swap.
|
|
3026
|
+
// The observer may have written it into the old buffer, but this
|
|
3027
|
+
// replacement would nuke it without this check.
|
|
3028
|
+
const pendingQ = sessionPendingQuestions.get(orchId);
|
|
3029
|
+
if (pendingQ) {
|
|
3030
|
+
lines.push(`{cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
3031
|
+
const renderedQ = renderMarkdown(pendingQ);
|
|
3032
|
+
for (const line of renderedQ.split("\n")) {
|
|
3033
|
+
lines.push(line);
|
|
3034
|
+
}
|
|
3035
|
+
lines.push("");
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// If CMS history produced no chat-visible lines (only the footer),
|
|
3039
|
+
// but the observer previously wrote real content into the buffer,
|
|
3040
|
+
// keep the existing buffer. This handles the case where assistant
|
|
3041
|
+
// response text comes via the observer (customStatus streaming) but
|
|
3042
|
+
// hasn't been persisted as CMS assistant.message events yet.
|
|
3043
|
+
const chatContentLines = lines.filter(l =>
|
|
3044
|
+
l && !/^{(?:white|gray)-fg}──/.test(l) && l.trim() !== "",
|
|
3045
|
+
);
|
|
3046
|
+
const existing = sessionChatBuffers.get(orchId);
|
|
3047
|
+
const existingHasContent = existing && existing.length > 1
|
|
3048
|
+
&& existing.some(l => l && !/Loading/.test(l) && !/no recent/.test(l));
|
|
3049
|
+
|
|
3050
|
+
if (chatContentLines.length === 0 && existingHasContent) {
|
|
3051
|
+
// CMS has no chat-worthy content but observer buffer does —
|
|
3052
|
+
// append the footer to the existing buffer instead of replacing.
|
|
3053
|
+
if (eventCount > 0) {
|
|
3054
|
+
const footerIdx = lines.findIndex(l => /recent history loaded/.test(l));
|
|
3055
|
+
if (footerIdx >= 0) {
|
|
3056
|
+
existing.push(lines[footerIdx]);
|
|
3057
|
+
existing.push("");
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
} else {
|
|
3061
|
+
sessionChatBuffers.set(orchId, lines);
|
|
3062
|
+
}
|
|
2975
3063
|
sessionActivityBuffers.set(orchId, activityLines);
|
|
2976
3064
|
sessionHistoryLoadedAt.set(orchId, Date.now());
|
|
2977
3065
|
sessionRenderedCmsSeq.set(orchId, maxRenderedSeq);
|
|
@@ -3134,8 +3222,11 @@ if (!isRemote) {
|
|
|
3134
3222
|
...(workerModuleConfig.mcpServers && { mcpServers: workerModuleConfig.mcpServers }),
|
|
3135
3223
|
});
|
|
3136
3224
|
// Register custom tools from worker module
|
|
3137
|
-
|
|
3138
|
-
|
|
3225
|
+
const workerTools = typeof workerModuleConfig.createTools === "function"
|
|
3226
|
+
? await workerModuleConfig.createTools({ workerNodeId: `local-rt-${i}`, workerIndex: i })
|
|
3227
|
+
: workerModuleConfig.tools;
|
|
3228
|
+
if (workerTools?.length) {
|
|
3229
|
+
w.registerTools(workerTools);
|
|
3139
3230
|
}
|
|
3140
3231
|
await w.start();
|
|
3141
3232
|
workers.push(w);
|
|
@@ -3453,6 +3544,60 @@ const sessionRecoveredTurnResult = new Map(); // orchId → normalized completed
|
|
|
3453
3544
|
const sessionObservers = new Map(); // orchId → AbortController
|
|
3454
3545
|
const sessionLiveStatus = new Map(); // orchId → "idle"|"running"|"waiting"|"input_required"
|
|
3455
3546
|
const sessionPendingTurns = new Set(); // orchIds with a locally-sent turn awaiting first live status
|
|
3547
|
+
|
|
3548
|
+
// ─── Inline chat spinner ─────────────────────────────────────────
|
|
3549
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3550
|
+
const sessionSpinnerIndex = new Map(); // orchId → buffer line index where spinner lives
|
|
3551
|
+
let _spinnerFrame = 0;
|
|
3552
|
+
let _spinnerTimer = null;
|
|
3553
|
+
|
|
3554
|
+
function startChatSpinner(orchId) {
|
|
3555
|
+
const buf = sessionChatBuffers.get(orchId);
|
|
3556
|
+
if (!buf) return;
|
|
3557
|
+
// Remove existing spinner if any
|
|
3558
|
+
stopChatSpinner(orchId);
|
|
3559
|
+
const line = `{gray-fg}${SPINNER_FRAMES[0]} Thinking…{/gray-fg}`;
|
|
3560
|
+
buf.push("");
|
|
3561
|
+
buf.push(line);
|
|
3562
|
+
sessionSpinnerIndex.set(orchId, buf.length - 1);
|
|
3563
|
+
if (orchId === activeOrchId) invalidateChat("bottom");
|
|
3564
|
+
// Start global animation timer if not running
|
|
3565
|
+
if (!_spinnerTimer) {
|
|
3566
|
+
_spinnerTimer = setInterval(() => {
|
|
3567
|
+
_spinnerFrame = (_spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
3568
|
+
let anyActive = false;
|
|
3569
|
+
for (const [sid, idx] of sessionSpinnerIndex) {
|
|
3570
|
+
const b = sessionChatBuffers.get(sid);
|
|
3571
|
+
if (b && idx < b.length) {
|
|
3572
|
+
b[idx] = `{gray-fg}${SPINNER_FRAMES[_spinnerFrame]} Thinking…{/gray-fg}`;
|
|
3573
|
+
if (sid === activeOrchId) { invalidateChat(); anyActive = true; }
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
if (!anyActive && sessionSpinnerIndex.size === 0) {
|
|
3577
|
+
clearInterval(_spinnerTimer);
|
|
3578
|
+
_spinnerTimer = null;
|
|
3579
|
+
}
|
|
3580
|
+
}, 80);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
function stopChatSpinner(orchId) {
|
|
3585
|
+
const idx = sessionSpinnerIndex.get(orchId);
|
|
3586
|
+
if (idx == null) return;
|
|
3587
|
+
const buf = sessionChatBuffers.get(orchId);
|
|
3588
|
+
if (buf && idx < buf.length) {
|
|
3589
|
+
// Remove spinner line and the blank line before it
|
|
3590
|
+
const startIdx = (idx > 0 && buf[idx - 1] === "") ? idx - 1 : idx;
|
|
3591
|
+
buf.splice(startIdx, idx - startIdx + 1);
|
|
3592
|
+
}
|
|
3593
|
+
sessionSpinnerIndex.delete(orchId);
|
|
3594
|
+
if (orchId === activeOrchId) invalidateChat();
|
|
3595
|
+
// Stop global timer if no spinners remain
|
|
3596
|
+
if (sessionSpinnerIndex.size === 0 && _spinnerTimer) {
|
|
3597
|
+
clearInterval(_spinnerTimer);
|
|
3598
|
+
_spinnerTimer = null;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3456
3601
|
const sessionPendingQuestions = new Map(); // orchId → latest input-required question awaiting a user answer
|
|
3457
3602
|
const sessionLastSeenResponseVersion = new Map(); // orchId → latest KV-backed response version rendered
|
|
3458
3603
|
const sessionLastSeenCommandVersion = new Map(); // orchId → latest KV-backed command response version rendered
|
|
@@ -3666,6 +3811,33 @@ function handleDbRecovered() {
|
|
|
3666
3811
|
if (_dbOffline) {
|
|
3667
3812
|
appendLog(`{green-fg}Database connection restored.{/green-fg}`);
|
|
3668
3813
|
setStatus("Database connection restored.");
|
|
3814
|
+
|
|
3815
|
+
// The orchestration list poll uses its own management client pool, but
|
|
3816
|
+
// the active session's CMS event stream and reconstructed history may
|
|
3817
|
+
// still be stale after a DB outage. Re-prime the active session view so
|
|
3818
|
+
// chat/activity panes resume updating without requiring a manual switch.
|
|
3819
|
+
const recoveredOrchId = activeOrchId;
|
|
3820
|
+
if (recoveredOrchId) {
|
|
3821
|
+
stopCmsPoller();
|
|
3822
|
+
loadCmsHistory(recoveredOrchId, { force: true })
|
|
3823
|
+
.then(() => {
|
|
3824
|
+
if (recoveredOrchId === activeOrchId) {
|
|
3825
|
+
startCmsPoller(recoveredOrchId);
|
|
3826
|
+
invalidateChat();
|
|
3827
|
+
invalidateActivity();
|
|
3828
|
+
redrawActiveViews();
|
|
3829
|
+
scheduleLightRefresh("dbRecovered", recoveredOrchId);
|
|
3830
|
+
}
|
|
3831
|
+
})
|
|
3832
|
+
.catch(() => {
|
|
3833
|
+
if (recoveredOrchId === activeOrchId) {
|
|
3834
|
+
startCmsPoller(recoveredOrchId);
|
|
3835
|
+
invalidateChat();
|
|
3836
|
+
invalidateActivity();
|
|
3837
|
+
redrawActiveViews();
|
|
3838
|
+
}
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3669
3841
|
}
|
|
3670
3842
|
_dbOffline = false;
|
|
3671
3843
|
_dbNextRetryAt = 0;
|
|
@@ -5115,6 +5287,7 @@ function startObserver(orchId) {
|
|
|
5115
5287
|
|
|
5116
5288
|
function renderResponsePayload(response, cs, source) {
|
|
5117
5289
|
if (!response) return;
|
|
5290
|
+
stopChatSpinner(orchId);
|
|
5118
5291
|
if (response.type === "completed" && response.content) {
|
|
5119
5292
|
appendActivity(`{green-fg}[obs] ✓ SHOWING ${source}: version=${response.version} type=completed content=${response.content.slice(0, 80)}{/green-fg}`, orchId);
|
|
5120
5293
|
renderCompletedContent(response.content);
|
|
@@ -5128,12 +5301,8 @@ function startObserver(orchId) {
|
|
|
5128
5301
|
}
|
|
5129
5302
|
if (response.type === "wait" && response.content) {
|
|
5130
5303
|
appendActivity(`{green-fg}[obs] ✓ SHOWING ${source}: version=${response.version} type=wait content=${response.content.slice(0, 80)}{/green-fg}`, orchId);
|
|
5131
|
-
const
|
|
5132
|
-
appendActivity(
|
|
5133
|
-
const rendered = renderMarkdown(response.content);
|
|
5134
|
-
for (const line of rendered.split("\n")) {
|
|
5135
|
-
appendActivity(line, orchId);
|
|
5136
|
-
}
|
|
5304
|
+
const preview = summarizeActivityPreview(response.content);
|
|
5305
|
+
appendActivity(`{white-fg}[${ts()}]{/white-fg} {gray-fg}[intermediate]{/gray-fg} ${preview}`, orchId);
|
|
5137
5306
|
promoteIntermediateContent(response.content, orchId);
|
|
5138
5307
|
setStatusIfActive(`Waiting (${cs.waitReason || response.waitReason || "timer"})…`);
|
|
5139
5308
|
return;
|
|
@@ -5533,13 +5702,12 @@ function startCmsPoller(orchId) {
|
|
|
5533
5702
|
|
|
5534
5703
|
if (type === "tool.execution_start") {
|
|
5535
5704
|
const toolName = evt.data?.toolName || "tool";
|
|
5536
|
-
const dsid = evt.data?.durableSessionId ? ` {gray-fg}[${shortId(evt.data.durableSessionId)}]{/gray-fg}` : "";
|
|
5537
5705
|
// Track last tool name so we can show it on completion too
|
|
5538
5706
|
sess._lastToolName = toolName;
|
|
5539
|
-
appendActivity(
|
|
5707
|
+
appendActivity(formatToolActivityLine(t, evt, "start"), orchId);
|
|
5540
5708
|
} else if (type === "tool.execution_complete") {
|
|
5541
5709
|
const toolName = evt.data?.toolName || sess._lastToolName || "tool";
|
|
5542
|
-
appendActivity(
|
|
5710
|
+
appendActivity(formatToolActivityLine(t, { ...evt, data: { ...(evt.data || {}), toolName } }, "complete"), orchId);
|
|
5543
5711
|
} else if (type === "assistant.reasoning") {
|
|
5544
5712
|
appendActivity(`{white-fg}[${t}]{/white-fg} {gray-fg}[reasoning]{/gray-fg}`, orchId);
|
|
5545
5713
|
} else if (type === "assistant.turn_start") {
|
|
@@ -6036,6 +6204,7 @@ async function handleInput(text) {
|
|
|
6036
6204
|
}
|
|
6037
6205
|
|
|
6038
6206
|
appendChatRaw(`{white-fg}[${ts()}]{/white-fg} {white-fg}{bold}You:{/bold} ${trimmed}{/white-fg}`, targetOrchId);
|
|
6207
|
+
startChatSpinner(targetOrchId);
|
|
6039
6208
|
inputBar.clearValue();
|
|
6040
6209
|
focusInput();
|
|
6041
6210
|
setSessionPendingTurn(targetOrchId, true);
|
|
@@ -6396,33 +6565,43 @@ screen.on("keypress", (ch, key) => {
|
|
|
6396
6565
|
picker.key(["escape", "q", "a"], closePicker);
|
|
6397
6566
|
|
|
6398
6567
|
picker.on("select", async (_el, idx) => {
|
|
6399
|
-
closePicker();
|
|
6400
6568
|
const art = artifacts[idx];
|
|
6401
6569
|
if (!art) return;
|
|
6402
6570
|
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
if (localPath) {
|
|
6407
|
-
art.downloaded = true;
|
|
6408
|
-
art.localPath = localPath;
|
|
6409
|
-
|
|
6410
|
-
// Open markdown viewer with this file selected
|
|
6571
|
+
if (art.downloaded) {
|
|
6572
|
+
// Already downloaded — close picker and open viewer
|
|
6573
|
+
closePicker();
|
|
6411
6574
|
mdViewActive = true;
|
|
6412
6575
|
refreshMarkdownViewer();
|
|
6413
|
-
|
|
6414
|
-
// Find and select the downloaded file in the viewer
|
|
6415
6576
|
const files = scanExportFiles();
|
|
6416
|
-
const matchIdx = files.findIndex(f => f.localPath === localPath);
|
|
6577
|
+
const matchIdx = files.findIndex(f => f.localPath === art.localPath);
|
|
6417
6578
|
if (matchIdx >= 0) {
|
|
6418
6579
|
mdViewerSelectedIdx = matchIdx;
|
|
6419
6580
|
mdFileListPane.select(matchIdx);
|
|
6420
6581
|
refreshMarkdownViewer();
|
|
6421
6582
|
}
|
|
6422
|
-
|
|
6423
6583
|
screen.realloc();
|
|
6424
6584
|
relayoutAll();
|
|
6425
6585
|
setStatus("Markdown Viewer (v to exit)");
|
|
6586
|
+
screen.render();
|
|
6587
|
+
return;
|
|
6588
|
+
}
|
|
6589
|
+
|
|
6590
|
+
setStatus(`Downloading ${art.filename}...`);
|
|
6591
|
+
screen.render();
|
|
6592
|
+
const localPath = await downloadArtifact(art.sessionId, art.filename);
|
|
6593
|
+
if (localPath) {
|
|
6594
|
+
art.downloaded = true;
|
|
6595
|
+
art.localPath = localPath;
|
|
6596
|
+
|
|
6597
|
+
// Update picker item to show downloaded state
|
|
6598
|
+
const updatedItems = artifacts.map((a) => {
|
|
6599
|
+
const icon = a.downloaded ? "✓" : "↓";
|
|
6600
|
+
return ` ${icon} ${a.filename}`;
|
|
6601
|
+
});
|
|
6602
|
+
picker.setItems(updatedItems);
|
|
6603
|
+
picker.select(idx);
|
|
6604
|
+
setStatus(`Downloaded ${art.filename}`);
|
|
6426
6605
|
} else {
|
|
6427
6606
|
setStatus("Download failed — check logs");
|
|
6428
6607
|
}
|