pilotswarm-cli 0.1.5 → 0.1.7
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 +164 -49
- package/package.json +1 -1
package/cli/tui.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* npx pilotswarm remote --env .env.remote # client-only (AKS)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { PilotSwarmClient, PilotSwarmWorker, PilotSwarmManagementClient, SessionBlobStore, systemAgentUUID } from "pilotswarm-sdk";
|
|
16
|
+
import { PilotSwarmClient, PilotSwarmWorker, PilotSwarmManagementClient, SessionBlobStore, FilesystemArtifactStore, systemAgentUUID, loadAgentFiles } from "pilotswarm-sdk";
|
|
17
17
|
import { createRequire } from "node:module";
|
|
18
18
|
import { marked } from "marked";
|
|
19
19
|
import { markedTerminal } from "marked-terminal";
|
|
@@ -340,15 +340,19 @@ const sessionArtifacts = new Map();
|
|
|
340
340
|
const _registeredArtifacts = new Set();
|
|
341
341
|
const MAX_ARTIFACT_REGISTRY = 500;
|
|
342
342
|
|
|
343
|
-
/** TUI-level
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
343
|
+
/** TUI-level artifact store for on-demand downloads. Created lazily.
|
|
344
|
+
* Uses Azure Blob when configured, otherwise falls back to local filesystem. */
|
|
345
|
+
let _tuiArtifactStore = null;
|
|
346
|
+
function getTuiArtifactStore() {
|
|
347
|
+
if (_tuiArtifactStore) return _tuiArtifactStore;
|
|
347
348
|
const connStr = process.env.AZURE_STORAGE_CONNECTION_STRING;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
if (connStr) {
|
|
350
|
+
const container = process.env.AZURE_STORAGE_CONTAINER || "copilot-sessions";
|
|
351
|
+
_tuiArtifactStore = new SessionBlobStore(connStr, container);
|
|
352
|
+
} else {
|
|
353
|
+
_tuiArtifactStore = new FilesystemArtifactStore();
|
|
354
|
+
}
|
|
355
|
+
return _tuiArtifactStore;
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
/**
|
|
@@ -381,17 +385,13 @@ function detectArtifactLinks(text, orchId) {
|
|
|
381
385
|
* Returns the local path on success, null on failure.
|
|
382
386
|
*/
|
|
383
387
|
async function downloadArtifact(sessionId, filename) {
|
|
384
|
-
const
|
|
385
|
-
if (!bs) {
|
|
386
|
-
appendLog("{red-fg}📥 No blob storage configured — cannot download artifacts.{/red-fg}");
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
388
|
+
const store = getTuiArtifactStore();
|
|
389
389
|
const sessionDir = path.join(EXPORTS_DIR, sessionId.slice(0, 8));
|
|
390
390
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
391
391
|
const localPath = path.join(sessionDir, filename);
|
|
392
392
|
|
|
393
393
|
try {
|
|
394
|
-
const content = await
|
|
394
|
+
const content = await store.downloadArtifact(sessionId, filename);
|
|
395
395
|
fs.writeFileSync(localPath, content, "utf-8");
|
|
396
396
|
appendLog(`{green-fg}📥 Downloaded: ~/${path.relative(os.homedir(), localPath)} (${(content.length / 1024).toFixed(1)}KB){/green-fg}`);
|
|
397
397
|
return localPath;
|
|
@@ -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);
|
|
@@ -2907,12 +2959,9 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
2907
2959
|
lines.push("");
|
|
2908
2960
|
}
|
|
2909
2961
|
} 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}`);
|
|
2962
|
+
activityLines.push(formatToolActivityLine(timeStr, evt, "start"));
|
|
2913
2963
|
} else if (type === "tool.execution_complete") {
|
|
2914
|
-
|
|
2915
|
-
activityLines.push(`{white-fg}[${timeStr}]{/white-fg} {green-fg}✓ ${toolName}{/green-fg}`);
|
|
2964
|
+
activityLines.push(formatToolActivityLine(timeStr, evt, "complete"));
|
|
2916
2965
|
} else if (type === "abort" || type === "session.info" || type === "session.idle"
|
|
2917
2966
|
|| type === "session.usage_info" || type === "pending_messages.modified"
|
|
2918
2967
|
|| type === "assistant.usage") {
|
|
@@ -2971,7 +3020,45 @@ async function loadCmsHistory(orchId, options = {}) {
|
|
|
2971
3020
|
}
|
|
2972
3021
|
|
|
2973
3022
|
const maxRenderedSeq = (events || []).reduce((max, evt) => Math.max(max, evt.seq || 0), 0);
|
|
2974
|
-
|
|
3023
|
+
|
|
3024
|
+
// Append pending question so it survives the history buffer swap.
|
|
3025
|
+
// The observer may have written it into the old buffer, but this
|
|
3026
|
+
// replacement would nuke it without this check.
|
|
3027
|
+
const pendingQ = sessionPendingQuestions.get(orchId);
|
|
3028
|
+
if (pendingQ) {
|
|
3029
|
+
lines.push(`{cyan-fg}{bold}Copilot:{/bold}{/cyan-fg}`);
|
|
3030
|
+
const renderedQ = renderMarkdown(pendingQ);
|
|
3031
|
+
for (const line of renderedQ.split("\n")) {
|
|
3032
|
+
lines.push(line);
|
|
3033
|
+
}
|
|
3034
|
+
lines.push("");
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// If CMS history produced no chat-visible lines (only the footer),
|
|
3038
|
+
// but the observer previously wrote real content into the buffer,
|
|
3039
|
+
// keep the existing buffer. This handles the case where assistant
|
|
3040
|
+
// response text comes via the observer (customStatus streaming) but
|
|
3041
|
+
// hasn't been persisted as CMS assistant.message events yet.
|
|
3042
|
+
const chatContentLines = lines.filter(l =>
|
|
3043
|
+
l && !/^{(?:white|gray)-fg}──/.test(l) && l.trim() !== "",
|
|
3044
|
+
);
|
|
3045
|
+
const existing = sessionChatBuffers.get(orchId);
|
|
3046
|
+
const existingHasContent = existing && existing.length > 1
|
|
3047
|
+
&& existing.some(l => l && !/Loading/.test(l) && !/no recent/.test(l));
|
|
3048
|
+
|
|
3049
|
+
if (chatContentLines.length === 0 && existingHasContent) {
|
|
3050
|
+
// CMS has no chat-worthy content but observer buffer does —
|
|
3051
|
+
// append the footer to the existing buffer instead of replacing.
|
|
3052
|
+
if (eventCount > 0) {
|
|
3053
|
+
const footerIdx = lines.findIndex(l => /recent history loaded/.test(l));
|
|
3054
|
+
if (footerIdx >= 0) {
|
|
3055
|
+
existing.push(lines[footerIdx]);
|
|
3056
|
+
existing.push("");
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
} else {
|
|
3060
|
+
sessionChatBuffers.set(orchId, lines);
|
|
3061
|
+
}
|
|
2975
3062
|
sessionActivityBuffers.set(orchId, activityLines);
|
|
2976
3063
|
sessionHistoryLoadedAt.set(orchId, Date.now());
|
|
2977
3064
|
sessionRenderedCmsSeq.set(orchId, maxRenderedSeq);
|
|
@@ -3134,8 +3221,11 @@ if (!isRemote) {
|
|
|
3134
3221
|
...(workerModuleConfig.mcpServers && { mcpServers: workerModuleConfig.mcpServers }),
|
|
3135
3222
|
});
|
|
3136
3223
|
// Register custom tools from worker module
|
|
3137
|
-
|
|
3138
|
-
|
|
3224
|
+
const workerTools = typeof workerModuleConfig.createTools === "function"
|
|
3225
|
+
? await workerModuleConfig.createTools({ workerNodeId: `local-rt-${i}`, workerIndex: i })
|
|
3226
|
+
: workerModuleConfig.tools;
|
|
3227
|
+
if (workerTools?.length) {
|
|
3228
|
+
w.registerTools(workerTools);
|
|
3139
3229
|
}
|
|
3140
3230
|
await w.start();
|
|
3141
3231
|
workers.push(w);
|
|
@@ -3276,10 +3366,30 @@ if (!modelProviders) {
|
|
|
3276
3366
|
// Will be populated from mgmt.getModelsByProvider() after mgmt.start()
|
|
3277
3367
|
}
|
|
3278
3368
|
|
|
3279
|
-
// Capture session policy + agent list from the first worker
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3369
|
+
// Capture session policy + agent list from the first worker.
|
|
3370
|
+
// In remote mode (no local workers), load directly from the plugin directory
|
|
3371
|
+
// so the TUI enforces the same session creation restrictions as the backend.
|
|
3372
|
+
let _workerSessionPolicy = workers[0]?.sessionPolicy || null;
|
|
3373
|
+
let _workerAllowedAgentNames = workers[0]?.allowedAgentNames || [];
|
|
3374
|
+
let _workerLoadedAgents = workers[0]?.loadedAgents || [];
|
|
3375
|
+
|
|
3376
|
+
if (workers.length === 0 && process.env.PLUGIN_DIRS) {
|
|
3377
|
+
const pluginDirsArr = process.env.PLUGIN_DIRS.split(",").map(d => d.trim()).filter(Boolean);
|
|
3378
|
+
for (const dir of pluginDirsArr) {
|
|
3379
|
+
const policyFile = path.join(dir, "session-policy.json");
|
|
3380
|
+
if (fs.existsSync(policyFile)) {
|
|
3381
|
+
try { _workerSessionPolicy = JSON.parse(fs.readFileSync(policyFile, "utf-8")); } catch {}
|
|
3382
|
+
}
|
|
3383
|
+
const agentsDir = path.join(dir, "agents");
|
|
3384
|
+
if (fs.existsSync(agentsDir)) {
|
|
3385
|
+
try {
|
|
3386
|
+
const agents = loadAgentFiles(agentsDir).filter(a => !a.system && a.name !== "default");
|
|
3387
|
+
_workerLoadedAgents = agents;
|
|
3388
|
+
_workerAllowedAgentNames = agents.map(a => a.name).filter(Boolean);
|
|
3389
|
+
} catch {}
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3283
3393
|
if (_workerSessionPolicy) {
|
|
3284
3394
|
appendLog(`Session policy: mode=${_workerSessionPolicy.creation?.mode || "open"}, allowGeneric=${_workerSessionPolicy.creation?.allowGeneric ?? true}`);
|
|
3285
3395
|
}
|
|
@@ -5108,12 +5218,8 @@ function startObserver(orchId) {
|
|
|
5108
5218
|
}
|
|
5109
5219
|
if (response.type === "wait" && response.content) {
|
|
5110
5220
|
appendActivity(`{green-fg}[obs] ✓ SHOWING ${source}: version=${response.version} type=wait content=${response.content.slice(0, 80)}{/green-fg}`, orchId);
|
|
5111
|
-
const
|
|
5112
|
-
appendActivity(
|
|
5113
|
-
const rendered = renderMarkdown(response.content);
|
|
5114
|
-
for (const line of rendered.split("\n")) {
|
|
5115
|
-
appendActivity(line, orchId);
|
|
5116
|
-
}
|
|
5221
|
+
const preview = summarizeActivityPreview(response.content);
|
|
5222
|
+
appendActivity(`{white-fg}[${ts()}]{/white-fg} {gray-fg}[intermediate]{/gray-fg} ${preview}`, orchId);
|
|
5117
5223
|
promoteIntermediateContent(response.content, orchId);
|
|
5118
5224
|
setStatusIfActive(`Waiting (${cs.waitReason || response.waitReason || "timer"})…`);
|
|
5119
5225
|
return;
|
|
@@ -5513,13 +5619,12 @@ function startCmsPoller(orchId) {
|
|
|
5513
5619
|
|
|
5514
5620
|
if (type === "tool.execution_start") {
|
|
5515
5621
|
const toolName = evt.data?.toolName || "tool";
|
|
5516
|
-
const dsid = evt.data?.durableSessionId ? ` {gray-fg}[${shortId(evt.data.durableSessionId)}]{/gray-fg}` : "";
|
|
5517
5622
|
// Track last tool name so we can show it on completion too
|
|
5518
5623
|
sess._lastToolName = toolName;
|
|
5519
|
-
appendActivity(
|
|
5624
|
+
appendActivity(formatToolActivityLine(t, evt, "start"), orchId);
|
|
5520
5625
|
} else if (type === "tool.execution_complete") {
|
|
5521
5626
|
const toolName = evt.data?.toolName || sess._lastToolName || "tool";
|
|
5522
|
-
appendActivity(
|
|
5627
|
+
appendActivity(formatToolActivityLine(t, { ...evt, data: { ...(evt.data || {}), toolName } }, "complete"), orchId);
|
|
5523
5628
|
} else if (type === "assistant.reasoning") {
|
|
5524
5629
|
appendActivity(`{white-fg}[${t}]{/white-fg} {gray-fg}[reasoning]{/gray-fg}`, orchId);
|
|
5525
5630
|
} else if (type === "assistant.turn_start") {
|
|
@@ -6376,33 +6481,43 @@ screen.on("keypress", (ch, key) => {
|
|
|
6376
6481
|
picker.key(["escape", "q", "a"], closePicker);
|
|
6377
6482
|
|
|
6378
6483
|
picker.on("select", async (_el, idx) => {
|
|
6379
|
-
closePicker();
|
|
6380
6484
|
const art = artifacts[idx];
|
|
6381
6485
|
if (!art) return;
|
|
6382
6486
|
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
if (localPath) {
|
|
6387
|
-
art.downloaded = true;
|
|
6388
|
-
art.localPath = localPath;
|
|
6389
|
-
|
|
6390
|
-
// Open markdown viewer with this file selected
|
|
6487
|
+
if (art.downloaded) {
|
|
6488
|
+
// Already downloaded — close picker and open viewer
|
|
6489
|
+
closePicker();
|
|
6391
6490
|
mdViewActive = true;
|
|
6392
6491
|
refreshMarkdownViewer();
|
|
6393
|
-
|
|
6394
|
-
// Find and select the downloaded file in the viewer
|
|
6395
6492
|
const files = scanExportFiles();
|
|
6396
|
-
const matchIdx = files.findIndex(f => f.localPath === localPath);
|
|
6493
|
+
const matchIdx = files.findIndex(f => f.localPath === art.localPath);
|
|
6397
6494
|
if (matchIdx >= 0) {
|
|
6398
6495
|
mdViewerSelectedIdx = matchIdx;
|
|
6399
6496
|
mdFileListPane.select(matchIdx);
|
|
6400
6497
|
refreshMarkdownViewer();
|
|
6401
6498
|
}
|
|
6402
|
-
|
|
6403
6499
|
screen.realloc();
|
|
6404
6500
|
relayoutAll();
|
|
6405
6501
|
setStatus("Markdown Viewer (v to exit)");
|
|
6502
|
+
screen.render();
|
|
6503
|
+
return;
|
|
6504
|
+
}
|
|
6505
|
+
|
|
6506
|
+
setStatus(`Downloading ${art.filename}...`);
|
|
6507
|
+
screen.render();
|
|
6508
|
+
const localPath = await downloadArtifact(art.sessionId, art.filename);
|
|
6509
|
+
if (localPath) {
|
|
6510
|
+
art.downloaded = true;
|
|
6511
|
+
art.localPath = localPath;
|
|
6512
|
+
|
|
6513
|
+
// Update picker item to show downloaded state
|
|
6514
|
+
const updatedItems = artifacts.map((a) => {
|
|
6515
|
+
const icon = a.downloaded ? "✓" : "↓";
|
|
6516
|
+
return ` ${icon} ${a.filename}`;
|
|
6517
|
+
});
|
|
6518
|
+
picker.setItems(updatedItems);
|
|
6519
|
+
picker.select(idx);
|
|
6520
|
+
setStatus(`Downloaded ${art.filename}`);
|
|
6406
6521
|
} else {
|
|
6407
6522
|
setStatus("Download failed — check logs");
|
|
6408
6523
|
}
|