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.
Files changed (2) hide show
  1. package/cli/tui.js +209 -30
  2. 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
- const toolName = evt.data?.toolName || "tool";
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
- const toolName = evt.data?.toolName || "tool";
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
- sessionChatBuffers.set(orchId, lines);
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
- if (workerModuleConfig.tools?.length) {
3138
- w.registerTools(workerModuleConfig.tools);
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 prefix = `{white-fg}[${ts()}]{/white-fg} {gray-fg}[intermediate]{/gray-fg}`;
5132
- appendActivity(prefix, orchId);
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(`{white-fg}[${t}]{/white-fg} {yellow-fg}▶ ${toolName}{/yellow-fg}${dsid}`, orchId);
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(`{white-fg}[${t}]{/white-fg} {green-fg} ${toolName}{/green-fg}`, orchId);
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
- setStatus(`Downloading ${art.filename}...`);
6404
- screen.render();
6405
- const localPath = await downloadArtifact(art.sessionId, art.filename);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pilotswarm-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Terminal UI for pilotswarm — interactive durable agent orchestration.",
5
5
  "type": "module",
6
6
  "bin": {