pilotswarm-cli 0.1.7 → 0.1.9
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 +519 -113
- package/package.json +2 -2
package/cli/tui.js
CHANGED
|
@@ -401,6 +401,285 @@ async function downloadArtifact(sessionId, filename) {
|
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Gather all artifacts for the active session and its descendants.
|
|
406
|
+
*/
|
|
407
|
+
function gatherAllArtifacts(orchId) {
|
|
408
|
+
const all = [];
|
|
409
|
+
const visited = new Set();
|
|
410
|
+
const queue = [orchId];
|
|
411
|
+
while (queue.length) {
|
|
412
|
+
const id = queue.shift();
|
|
413
|
+
if (visited.has(id)) continue;
|
|
414
|
+
visited.add(id);
|
|
415
|
+
const arts = sessionArtifacts.get(id);
|
|
416
|
+
if (arts) all.push(...arts);
|
|
417
|
+
const children = orchChildrenOf.get(id) || [];
|
|
418
|
+
for (const child of children) queue.push(child);
|
|
419
|
+
}
|
|
420
|
+
return all;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Currently open artifact picker (so we can dismiss/reopen cleanly). */
|
|
424
|
+
let _artifactPicker = null;
|
|
425
|
+
|
|
426
|
+
function showArtifactPicker() {
|
|
427
|
+
// Close existing picker if open
|
|
428
|
+
if (_artifactPicker) {
|
|
429
|
+
_artifactPicker.detach();
|
|
430
|
+
_artifactPicker = null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const artifacts = gatherAllArtifacts(activeOrchId);
|
|
434
|
+
if (artifacts.length === 0) {
|
|
435
|
+
setStatus("No artifacts for this session");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const hasMultiple = artifacts.length > 1;
|
|
440
|
+
const buildItems = () => {
|
|
441
|
+
const items = artifacts.map(a => {
|
|
442
|
+
const icon = a.downloaded ? " ✓" : " ↓";
|
|
443
|
+
const sid = shortId(a.sessionId);
|
|
444
|
+
return `${icon} ${sid}/${a.filename}`;
|
|
445
|
+
});
|
|
446
|
+
if (hasMultiple) items.push(" ⬇ Download All");
|
|
447
|
+
return items;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const picker = blessed.list({
|
|
451
|
+
parent: screen,
|
|
452
|
+
label: " 📎 Artifacts — Enter download · Esc close ",
|
|
453
|
+
tags: true,
|
|
454
|
+
left: "center",
|
|
455
|
+
top: "center",
|
|
456
|
+
width: Math.min(65, screen.width - 4),
|
|
457
|
+
height: Math.min(artifacts.length + (hasMultiple ? 3 : 2), 18),
|
|
458
|
+
border: { type: "line" },
|
|
459
|
+
style: {
|
|
460
|
+
fg: "white",
|
|
461
|
+
bg: "black",
|
|
462
|
+
border: { fg: "cyan" },
|
|
463
|
+
label: { fg: "cyan" },
|
|
464
|
+
selected: { bg: "blue", fg: "white" },
|
|
465
|
+
},
|
|
466
|
+
keys: true,
|
|
467
|
+
vi: true,
|
|
468
|
+
mouse: false,
|
|
469
|
+
items: buildItems(),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
_artifactPicker = picker;
|
|
473
|
+
picker.focus();
|
|
474
|
+
screen.render();
|
|
475
|
+
|
|
476
|
+
const closePicker = () => {
|
|
477
|
+
picker.detach();
|
|
478
|
+
_artifactPicker = null;
|
|
479
|
+
orchList.focus();
|
|
480
|
+
screen.render();
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
picker.key(["escape", "q"], closePicker);
|
|
484
|
+
// 'a' while picker is open → close (toggle behavior)
|
|
485
|
+
picker.key(["a"], closePicker);
|
|
486
|
+
|
|
487
|
+
picker.on("select", async (_el, idx) => {
|
|
488
|
+
// "Download All" option
|
|
489
|
+
if (hasMultiple && idx === artifacts.length) {
|
|
490
|
+
const pending = artifacts.filter(a => !a.downloaded);
|
|
491
|
+
if (pending.length === 0) {
|
|
492
|
+
setStatus("All artifacts already downloaded");
|
|
493
|
+
screen.render();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
setStatus(`Downloading ${pending.length} artifacts...`);
|
|
497
|
+
screen.render();
|
|
498
|
+
let ok = 0;
|
|
499
|
+
for (const art of pending) {
|
|
500
|
+
const localPath = await downloadArtifact(art.sessionId, art.filename);
|
|
501
|
+
if (localPath) { art.downloaded = true; art.localPath = localPath; ok++; }
|
|
502
|
+
}
|
|
503
|
+
picker.setItems(buildItems());
|
|
504
|
+
picker.select(idx);
|
|
505
|
+
setStatus(`Downloaded ${ok}/${pending.length} artifacts`);
|
|
506
|
+
screen.render();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const art = artifacts[idx];
|
|
511
|
+
if (!art) return;
|
|
512
|
+
|
|
513
|
+
if (art.downloaded) {
|
|
514
|
+
// Already downloaded — open viewer at that file
|
|
515
|
+
closePicker();
|
|
516
|
+
mdViewActive = true;
|
|
517
|
+
refreshMarkdownViewer();
|
|
518
|
+
const files = scanExportFiles();
|
|
519
|
+
const matchIdx = files.findIndex(f => f.localPath === art.localPath);
|
|
520
|
+
if (matchIdx >= 0) {
|
|
521
|
+
mdViewerSelectedIdx = matchIdx;
|
|
522
|
+
mdFileListPane.select(matchIdx);
|
|
523
|
+
refreshMarkdownViewer();
|
|
524
|
+
}
|
|
525
|
+
screen.realloc();
|
|
526
|
+
relayoutAll();
|
|
527
|
+
setStatus("Markdown Viewer (v to exit)");
|
|
528
|
+
screen.render();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
setStatus(`Downloading ${art.filename}...`);
|
|
533
|
+
screen.render();
|
|
534
|
+
const localPath = await downloadArtifact(art.sessionId, art.filename);
|
|
535
|
+
if (localPath) {
|
|
536
|
+
art.downloaded = true;
|
|
537
|
+
art.localPath = localPath;
|
|
538
|
+
picker.setItems(buildItems());
|
|
539
|
+
picker.select(idx);
|
|
540
|
+
setStatus(`Downloaded ${art.filename}`);
|
|
541
|
+
} else {
|
|
542
|
+
setStatus("Download failed — check logs");
|
|
543
|
+
}
|
|
544
|
+
screen.render();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ─── File attachment for prompt ──────────────────────────────────
|
|
549
|
+
// Tracks file attachments embedded in the prompt. When the prompt is sent,
|
|
550
|
+
// file contents are prepended to the message text.
|
|
551
|
+
const _promptAttachments = []; // [{ path, displayName, sessionId }]
|
|
552
|
+
|
|
553
|
+
let _fileAttachModal = false;
|
|
554
|
+
|
|
555
|
+
function showFileAttachPrompt() {
|
|
556
|
+
_fileAttachModal = true;
|
|
557
|
+
|
|
558
|
+
// Save the current inputBar value so we can restore it after
|
|
559
|
+
const savedInput = inputBar.getValue() || "";
|
|
560
|
+
const savedCursor = inputCursorIndex;
|
|
561
|
+
|
|
562
|
+
// Modal overlay to block all other input
|
|
563
|
+
const overlay = blessed.box({
|
|
564
|
+
parent: screen,
|
|
565
|
+
left: 0, top: 0, width: "100%", height: "100%",
|
|
566
|
+
style: { bg: "black", transparent: true },
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const pathInput = blessed.textbox({
|
|
570
|
+
parent: overlay,
|
|
571
|
+
label: " 📄 Attach file — paste path, Enter to attach, Esc to cancel ",
|
|
572
|
+
tags: true,
|
|
573
|
+
left: "center",
|
|
574
|
+
top: "center",
|
|
575
|
+
width: Math.min(70, screen.width - 4),
|
|
576
|
+
height: 3,
|
|
577
|
+
border: { type: "line" },
|
|
578
|
+
style: {
|
|
579
|
+
fg: "white",
|
|
580
|
+
bg: "black",
|
|
581
|
+
border: { fg: "yellow" },
|
|
582
|
+
label: { fg: "yellow" },
|
|
583
|
+
},
|
|
584
|
+
inputOnFocus: true,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
pathInput.focus();
|
|
588
|
+
screen.render();
|
|
589
|
+
|
|
590
|
+
const closeModal = () => {
|
|
591
|
+
_fileAttachModal = false;
|
|
592
|
+
overlay.detach();
|
|
593
|
+
// Restore input bar state and refocus
|
|
594
|
+
setInputValue(savedInput, savedCursor);
|
|
595
|
+
focusInput();
|
|
596
|
+
screen.render();
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
pathInput.on("submit", async (filePath) => {
|
|
600
|
+
if (!filePath || !filePath.trim()) {
|
|
601
|
+
closeModal();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const resolved = filePath.trim().replace(/^~/, os.homedir());
|
|
606
|
+
if (!fs.existsSync(resolved)) {
|
|
607
|
+
setStatus(`File not found: ${filePath.trim()}`);
|
|
608
|
+
closeModal();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const displayName = path.basename(resolved);
|
|
613
|
+
const content = fs.readFileSync(resolved, "utf-8");
|
|
614
|
+
|
|
615
|
+
// Upload to artifact store under the active session
|
|
616
|
+
const orchId = activeOrchId;
|
|
617
|
+
const sessionId = orchId ? (orchId.startsWith("session-") ? orchId.slice(8) : orchId) : "local";
|
|
618
|
+
try {
|
|
619
|
+
const store = getTuiArtifactStore();
|
|
620
|
+
await store.uploadArtifact(sessionId, displayName, content);
|
|
621
|
+
|
|
622
|
+
// Register in artifact registry so it shows up in 'v' viewer and 'a' picker
|
|
623
|
+
const artKey = `${sessionId}/${displayName}`;
|
|
624
|
+
if (!_registeredArtifacts.has(artKey)) {
|
|
625
|
+
_registeredArtifacts.add(artKey);
|
|
626
|
+
if (!sessionArtifacts.has(orchId)) sessionArtifacts.set(orchId, []);
|
|
627
|
+
const localDir = path.join(EXPORTS_DIR, sessionId.slice(0, 8));
|
|
628
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
629
|
+
const localPath = path.join(localDir, displayName);
|
|
630
|
+
fs.writeFileSync(localPath, content, "utf-8");
|
|
631
|
+
sessionArtifacts.get(orchId).push({
|
|
632
|
+
sessionId, filename: displayName, downloaded: true, localPath,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Show a snipped preview in chat (first 3 lines)
|
|
637
|
+
const lines = content.split("\n");
|
|
638
|
+
const preview = lines.slice(0, 3).map(l => ` ${l.slice(0, 80)}`).join("\n");
|
|
639
|
+
const suffix = lines.length > 3 ? `\n {gray-fg}… (${lines.length - 3} more lines){/gray-fg}` : "";
|
|
640
|
+
appendChatRaw(
|
|
641
|
+
`{yellow-fg}📄 Attached: ${displayName} (${(content.length / 1024).toFixed(1)}KB){/yellow-fg}\n${preview}${suffix}`,
|
|
642
|
+
orchId,
|
|
643
|
+
);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
appendChatRaw(`{red-fg}Upload failed: ${err.message}{/red-fg}`, orchId);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
_promptAttachments.push({ path: resolved, displayName, sessionId });
|
|
649
|
+
|
|
650
|
+
const token = ` 📎 ${displayName} `;
|
|
651
|
+
closeModal();
|
|
652
|
+
insertInputText(token);
|
|
653
|
+
setStatus(`Attached: ${displayName}`);
|
|
654
|
+
screen.render();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
pathInput.on("cancel", closeModal);
|
|
658
|
+
pathInput.key(["escape"], closeModal);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Process prompt text before sending: expand attachment tokens into file content.
|
|
663
|
+
* Returns the final prompt string with artifact:// references.
|
|
664
|
+
*/
|
|
665
|
+
function expandAttachments(promptText) {
|
|
666
|
+
if (_promptAttachments.length === 0) return promptText;
|
|
667
|
+
|
|
668
|
+
const attachmentRefs = [];
|
|
669
|
+
for (const att of _promptAttachments) {
|
|
670
|
+
attachmentRefs.push(
|
|
671
|
+
`[Attached file: ${att.displayName} — artifact://${att.sessionId}/${att.displayName}]`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Clear attachments after expanding
|
|
676
|
+
_promptAttachments.length = 0;
|
|
677
|
+
|
|
678
|
+
// Remove 📎 filename tokens from the prompt text
|
|
679
|
+
const cleaned = promptText.replace(/\s*📎\s*[^\s]+\s*/g, " ").trim();
|
|
680
|
+
return attachmentRefs.join("\n") + (cleaned ? "\n\n" + cleaned : "");
|
|
681
|
+
}
|
|
682
|
+
|
|
404
683
|
function ts() {
|
|
405
684
|
return formatDisplayTime(Date.now());
|
|
406
685
|
}
|
|
@@ -423,7 +702,7 @@ const screen = blessed.screen({
|
|
|
423
702
|
title: BASE_TUI_TITLE,
|
|
424
703
|
fullUnicode: true,
|
|
425
704
|
forceUnicode: true,
|
|
426
|
-
mouse:
|
|
705
|
+
mouse: false,
|
|
427
706
|
});
|
|
428
707
|
process.stderr.write = _origStderr;
|
|
429
708
|
applyWindowTitle(BASE_TUI_TITLE);
|
|
@@ -561,7 +840,15 @@ const MAX_PROMPT_EDITOR_ROWS = 8;
|
|
|
561
840
|
let promptValueCache = "";
|
|
562
841
|
|
|
563
842
|
function promptLineCount(text) {
|
|
564
|
-
|
|
843
|
+
const str = String(text || "");
|
|
844
|
+
if (!str) return 1;
|
|
845
|
+
// Count visual lines: each \n-delimited line may wrap across multiple visual rows
|
|
846
|
+
const width = Math.max(1, screen.width - 2); // input bar inner width (full width minus borders)
|
|
847
|
+
let visualLines = 0;
|
|
848
|
+
for (const line of str.split("\n")) {
|
|
849
|
+
visualLines += Math.max(1, Math.ceil((line.length + 1) / width));
|
|
850
|
+
}
|
|
851
|
+
return Math.max(1, visualLines);
|
|
565
852
|
}
|
|
566
853
|
|
|
567
854
|
function promptEditorRows() {
|
|
@@ -572,7 +859,7 @@ function inputBarHeight() {
|
|
|
572
859
|
return promptEditorRows() + 2; // border + content rows
|
|
573
860
|
}
|
|
574
861
|
|
|
575
|
-
function bodyH() { return screen.height - inputBarHeight(); } // total body (minus input bar)
|
|
862
|
+
function bodyH() { return screen.height - inputBarHeight() - 1; } // total body (minus input bar + status line)
|
|
576
863
|
function sessH() { return Math.max(5, Math.floor(bodyH() * 0.25)); }
|
|
577
864
|
function chatH() { return bodyH() - sessH(); }
|
|
578
865
|
function activityH() { return Math.max(6, Math.floor(bodyH() * 0.28)); } // sticky Activity pane height
|
|
@@ -630,7 +917,7 @@ const orchList = blessed.list({
|
|
|
630
917
|
scrollbar: { style: { bg: "yellow" } },
|
|
631
918
|
keys: false,
|
|
632
919
|
vi: false,
|
|
633
|
-
mouse:
|
|
920
|
+
mouse: false,
|
|
634
921
|
interactive: true,
|
|
635
922
|
});
|
|
636
923
|
|
|
@@ -704,7 +991,7 @@ const chatBox = blessed.log({
|
|
|
704
991
|
scrollbar: { style: { bg: "cyan" } },
|
|
705
992
|
keys: true,
|
|
706
993
|
vi: true,
|
|
707
|
-
mouse:
|
|
994
|
+
mouse: false,
|
|
708
995
|
});
|
|
709
996
|
|
|
710
997
|
// ─── Clickable URLs in chat ──────────────────────────────────────
|
|
@@ -801,7 +1088,7 @@ const orchLogPane = blessed.log({
|
|
|
801
1088
|
scrollbar: { style: { bg: "cyan" } },
|
|
802
1089
|
keys: true,
|
|
803
1090
|
vi: true,
|
|
804
|
-
mouse:
|
|
1091
|
+
mouse: false,
|
|
805
1092
|
hidden: true,
|
|
806
1093
|
});
|
|
807
1094
|
|
|
@@ -856,7 +1143,7 @@ const nodeMapPane = blessed.box({
|
|
|
856
1143
|
scrollbar: { style: { bg: "yellow" } },
|
|
857
1144
|
keys: true,
|
|
858
1145
|
vi: true,
|
|
859
|
-
mouse:
|
|
1146
|
+
mouse: false,
|
|
860
1147
|
hidden: true,
|
|
861
1148
|
});
|
|
862
1149
|
|
|
@@ -887,7 +1174,7 @@ const mdFileListPane = blessed.list({
|
|
|
887
1174
|
},
|
|
888
1175
|
keys: false,
|
|
889
1176
|
vi: false,
|
|
890
|
-
mouse:
|
|
1177
|
+
mouse: false,
|
|
891
1178
|
interactive: true,
|
|
892
1179
|
hidden: true,
|
|
893
1180
|
});
|
|
@@ -912,7 +1199,7 @@ const mdPreviewPane = blessed.box({
|
|
|
912
1199
|
scrollbar: { style: { bg: "green" } },
|
|
913
1200
|
keys: true,
|
|
914
1201
|
vi: true,
|
|
915
|
-
mouse:
|
|
1202
|
+
mouse: false,
|
|
916
1203
|
hidden: true,
|
|
917
1204
|
});
|
|
918
1205
|
|
|
@@ -1069,7 +1356,7 @@ const activityPane = blessed.log({
|
|
|
1069
1356
|
scrollbar: { style: { bg: "gray" } },
|
|
1070
1357
|
keys: true,
|
|
1071
1358
|
vi: true,
|
|
1072
|
-
mouse:
|
|
1359
|
+
mouse: false,
|
|
1073
1360
|
});
|
|
1074
1361
|
|
|
1075
1362
|
// Per-session activity buffers
|
|
@@ -1320,7 +1607,7 @@ const seqPane = blessed.log({
|
|
|
1320
1607
|
scrollbar: { style: { bg: "magenta" } },
|
|
1321
1608
|
keys: true,
|
|
1322
1609
|
vi: true,
|
|
1323
|
-
mouse:
|
|
1610
|
+
mouse: false,
|
|
1324
1611
|
hidden: true,
|
|
1325
1612
|
});
|
|
1326
1613
|
|
|
@@ -1848,7 +2135,7 @@ function switchLogMode() {
|
|
|
1848
2135
|
// Reset focus to sessions list when panes change
|
|
1849
2136
|
orchList.focus();
|
|
1850
2137
|
// Force full repaint on next tick (same as pressing 'r')
|
|
1851
|
-
|
|
2138
|
+
scheduleLightRefresh("switchLogMode");
|
|
1852
2139
|
}
|
|
1853
2140
|
|
|
1854
2141
|
/**
|
|
@@ -1975,7 +2262,7 @@ function getOrCreateWorkerPane(podName) {
|
|
|
1975
2262
|
scrollbar: { style: { bg: color } },
|
|
1976
2263
|
keys: true,
|
|
1977
2264
|
vi: true,
|
|
1978
|
-
mouse:
|
|
2265
|
+
mouse: false,
|
|
1979
2266
|
});
|
|
1980
2267
|
|
|
1981
2268
|
workerPanes.set(podName, pane);
|
|
@@ -2025,7 +2312,7 @@ function relayoutAll() {
|
|
|
2025
2312
|
if (typeof statusBar !== "undefined" && statusBar) {
|
|
2026
2313
|
statusBar.left = 1;
|
|
2027
2314
|
statusBar.width = lW - 2;
|
|
2028
|
-
statusBar.bottom = iH
|
|
2315
|
+
statusBar.bottom = iH;
|
|
2029
2316
|
}
|
|
2030
2317
|
if (typeof inputBar !== "undefined" && inputBar) {
|
|
2031
2318
|
inputBar.left = 0;
|
|
@@ -2152,7 +2439,7 @@ const inputBar = blessed.textarea({
|
|
|
2152
2439
|
},
|
|
2153
2440
|
inputOnFocus: true,
|
|
2154
2441
|
keys: true,
|
|
2155
|
-
mouse:
|
|
2442
|
+
mouse: false,
|
|
2156
2443
|
});
|
|
2157
2444
|
registerFocusRing(inputBar, "green");
|
|
2158
2445
|
inputBar.on("focus", () => {
|
|
@@ -2252,6 +2539,36 @@ function moveCursorRight() {
|
|
|
2252
2539
|
screen.render();
|
|
2253
2540
|
}
|
|
2254
2541
|
|
|
2542
|
+
function moveCursorUp() {
|
|
2543
|
+
const value = String(inputBar.getValue() || "");
|
|
2544
|
+
// Find the start of the current line and the line above
|
|
2545
|
+
const before = value.slice(0, inputCursorIndex);
|
|
2546
|
+
const currentLineStart = before.lastIndexOf("\n") + 1;
|
|
2547
|
+
if (currentLineStart === 0) return; // already on first line
|
|
2548
|
+
const colInLine = inputCursorIndex - currentLineStart;
|
|
2549
|
+
const prevLineEnd = currentLineStart - 1; // the \n before current line
|
|
2550
|
+
const prevLineStart = value.lastIndexOf("\n", prevLineEnd - 1) + 1;
|
|
2551
|
+
const prevLineLen = prevLineEnd - prevLineStart;
|
|
2552
|
+
inputCursorIndex = clampInputCursor(prevLineStart + Math.min(colInLine, prevLineLen), value);
|
|
2553
|
+
inputBar._updateCursor();
|
|
2554
|
+
screen.render();
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function moveCursorDown() {
|
|
2558
|
+
const value = String(inputBar.getValue() || "");
|
|
2559
|
+
// Find the end of the current line and the line below
|
|
2560
|
+
const currentLineStart = value.lastIndexOf("\n", inputCursorIndex - 1) + 1;
|
|
2561
|
+
const currentLineEnd = value.indexOf("\n", inputCursorIndex);
|
|
2562
|
+
if (currentLineEnd === -1) return; // already on last line
|
|
2563
|
+
const colInLine = inputCursorIndex - currentLineStart;
|
|
2564
|
+
const nextLineStart = currentLineEnd + 1;
|
|
2565
|
+
const nextLineEnd = value.indexOf("\n", nextLineStart);
|
|
2566
|
+
const nextLineLen = (nextLineEnd === -1 ? value.length : nextLineEnd) - nextLineStart;
|
|
2567
|
+
inputCursorIndex = clampInputCursor(nextLineStart + Math.min(colInLine, nextLineLen), value);
|
|
2568
|
+
inputBar._updateCursor();
|
|
2569
|
+
screen.render();
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2255
2572
|
function getPreviousWordBoundary(value, fromIndex) {
|
|
2256
2573
|
let index = clampInputCursor(fromIndex, value);
|
|
2257
2574
|
while (index > 0 && /\s/.test(value[index - 1])) index -= 1;
|
|
@@ -2328,6 +2645,28 @@ inputBar._listener = function promptInputListener(ch, key) {
|
|
|
2328
2645
|
|| key.sequence === "\x1bf";
|
|
2329
2646
|
|
|
2330
2647
|
if (key.name === "escape") {
|
|
2648
|
+
// Alt+Enter (ESC + CR) → insert newline
|
|
2649
|
+
if (key.sequence === "\x1b\r" || key.sequence === "\x1b\n") {
|
|
2650
|
+
insertInputText("\n");
|
|
2651
|
+
screen.render();
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
// Alt+Backspace (ESC + DEL) → delete word backward
|
|
2655
|
+
if (key.sequence === "\x1b\x7f") {
|
|
2656
|
+
deleteInputWordBackward();
|
|
2657
|
+
screen.render();
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
// Alt+Left (ESC + b) → word left
|
|
2661
|
+
if (key.sequence === "\x1bb") {
|
|
2662
|
+
moveCursorWordLeft();
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
// Alt+Right (ESC + f) → word right
|
|
2666
|
+
if (key.sequence === "\x1bf") {
|
|
2667
|
+
moveCursorWordRight();
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2331
2670
|
inputBar._done(null, null);
|
|
2332
2671
|
return;
|
|
2333
2672
|
}
|
|
@@ -2340,6 +2679,12 @@ inputBar._listener = function promptInputListener(ch, key) {
|
|
|
2340
2679
|
inputBar._done(null, value);
|
|
2341
2680
|
return;
|
|
2342
2681
|
}
|
|
2682
|
+
// Ctrl+J — insert newline (Unix standard line feed)
|
|
2683
|
+
if (key.ctrl && key.name === "j") {
|
|
2684
|
+
insertInputText("\n");
|
|
2685
|
+
screen.render();
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2343
2688
|
if (isWordLeft) {
|
|
2344
2689
|
moveCursorWordLeft();
|
|
2345
2690
|
return;
|
|
@@ -2356,11 +2701,25 @@ inputBar._listener = function promptInputListener(ch, key) {
|
|
|
2356
2701
|
moveCursorRight();
|
|
2357
2702
|
return;
|
|
2358
2703
|
}
|
|
2704
|
+
if (key.name === "up") {
|
|
2705
|
+
moveCursorUp();
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
if (key.name === "down") {
|
|
2709
|
+
moveCursorDown();
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2359
2712
|
if (isMetaBackspace) {
|
|
2360
2713
|
deleteInputWordBackward();
|
|
2361
2714
|
screen.render();
|
|
2362
2715
|
return;
|
|
2363
2716
|
}
|
|
2717
|
+
// Ctrl+W — delete word backward (Unix standard)
|
|
2718
|
+
if (key.ctrl && key.name === "w") {
|
|
2719
|
+
deleteInputWordBackward();
|
|
2720
|
+
screen.render();
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2364
2723
|
if (key.name === "backspace") {
|
|
2365
2724
|
deleteInputBackward();
|
|
2366
2725
|
screen.render();
|
|
@@ -2371,6 +2730,28 @@ inputBar._listener = function promptInputListener(ch, key) {
|
|
|
2371
2730
|
screen.render();
|
|
2372
2731
|
return;
|
|
2373
2732
|
}
|
|
2733
|
+
// Ctrl+A — attach file
|
|
2734
|
+
if (key.ctrl && key.name === "a") {
|
|
2735
|
+
// Set modal flag FIRST so subsequent keypresses in this listener are blocked
|
|
2736
|
+
_fileAttachModal = true;
|
|
2737
|
+
// Cancel inputBar's readInput cleanly — _done(null,null) triggers cancel
|
|
2738
|
+
// which we handle with a no-op when _fileAttachModal is set
|
|
2739
|
+
inputBar._done(null, null);
|
|
2740
|
+
setImmediate(() => showFileAttachPrompt());
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
if (key.ctrl && key.name === "e") {
|
|
2744
|
+
inputCursorIndex = clampInputCursor(value.length);
|
|
2745
|
+
inputBar._updateCursor();
|
|
2746
|
+
screen.render();
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
// Ctrl+U — kill line (clear input)
|
|
2750
|
+
if (key.ctrl && key.name === "u") {
|
|
2751
|
+
setInputValue("", 0);
|
|
2752
|
+
screen.render();
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2374
2755
|
if (ch === "/" && value === "") {
|
|
2375
2756
|
insertInputText(ch);
|
|
2376
2757
|
setImmediate(() => {
|
|
@@ -2616,8 +2997,8 @@ function setNavigationStatusForPane(kind) {
|
|
|
2616
2997
|
nodemap: "{yellow-fg}j/k scroll node map · g/G top/bottom · m cycle log mode · p prompt · ? help · Esc then q quit{/yellow-fg}",
|
|
2617
2998
|
markdownList: "{yellow-fg}j/k choose file · Enter preview · d delete file · v exit viewer · ? help · Esc then q quit{/yellow-fg}",
|
|
2618
2999
|
markdownPreview: "{yellow-fg}j/k scroll preview · g/G top/bottom · Ctrl+D/U page · o open · y copy path · v exit viewer · ? help{/yellow-fg}",
|
|
2619
|
-
prompt: "{yellow-fg}Type a message ·
|
|
2620
|
-
answer: "{yellow-fg}Type your answer ·
|
|
3000
|
+
prompt: "{yellow-fg}Type a message · ^J newline · ^W word del · ^A attach file · Esc navigation mode{/yellow-fg}",
|
|
3001
|
+
answer: "{yellow-fg}Type your answer · ^J newline · ^W word del · ^A attach file · Esc navigation mode{/yellow-fg}",
|
|
2621
3002
|
};
|
|
2622
3003
|
setStatus(hints[kind] || hints.chat);
|
|
2623
3004
|
}
|
|
@@ -2683,6 +3064,7 @@ function recolorWorkerPanes() {
|
|
|
2683
3064
|
|
|
2684
3065
|
function showCopilotMessage(raw, orchId) {
|
|
2685
3066
|
const _ph = perfStart("showCopilotMessage");
|
|
3067
|
+
stopChatSpinner(orchId);
|
|
2686
3068
|
|
|
2687
3069
|
appendActivity(`{green-fg}[obs] showCopilotMessage called for ${orchId === activeOrchId ? "ACTIVE" : "background"} session, len=${raw?.length || 0}{/green-fg}`, orchId);
|
|
2688
3070
|
|
|
@@ -3543,6 +3925,75 @@ const sessionRecoveredTurnResult = new Map(); // orchId → normalized completed
|
|
|
3543
3925
|
const sessionObservers = new Map(); // orchId → AbortController
|
|
3544
3926
|
const sessionLiveStatus = new Map(); // orchId → "idle"|"running"|"waiting"|"input_required"
|
|
3545
3927
|
const sessionPendingTurns = new Set(); // orchIds with a locally-sent turn awaiting first live status
|
|
3928
|
+
|
|
3929
|
+
// ─── Inline chat spinner ─────────────────────────────────────────
|
|
3930
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3931
|
+
const sessionSpinnerIndex = new Map(); // orchId → buffer line index where spinner lives
|
|
3932
|
+
let _spinnerFrame = 0;
|
|
3933
|
+
let _spinnerTimer = null;
|
|
3934
|
+
|
|
3935
|
+
function startChatSpinner(orchId) {
|
|
3936
|
+
const buf = sessionChatBuffers.get(orchId);
|
|
3937
|
+
if (!buf) return;
|
|
3938
|
+
// Remove existing spinner if any
|
|
3939
|
+
stopChatSpinner(orchId);
|
|
3940
|
+
const line = `{gray-fg}${SPINNER_FRAMES[0]} Thinking…{/gray-fg}`;
|
|
3941
|
+
buf.push("");
|
|
3942
|
+
buf.push(line);
|
|
3943
|
+
sessionSpinnerIndex.set(orchId, buf.length - 1);
|
|
3944
|
+
if (orchId === activeOrchId) invalidateChat("bottom");
|
|
3945
|
+
// Start global animation timer if not running
|
|
3946
|
+
if (!_spinnerTimer) {
|
|
3947
|
+
_spinnerTimer = setInterval(() => {
|
|
3948
|
+
_spinnerFrame = (_spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
3949
|
+
let anyActive = false;
|
|
3950
|
+
for (const [sid, idx] of sessionSpinnerIndex) {
|
|
3951
|
+
const b = sessionChatBuffers.get(sid);
|
|
3952
|
+
if (b && idx < b.length) {
|
|
3953
|
+
b[idx] = `{gray-fg}${SPINNER_FRAMES[_spinnerFrame]} Thinking…{/gray-fg}`;
|
|
3954
|
+
if (sid === activeOrchId) {
|
|
3955
|
+
anyActive = true;
|
|
3956
|
+
// Only repaint if the spinner line is visible in the
|
|
3957
|
+
// viewport — avoids fighting j/k and PgUp/PgDn scroll.
|
|
3958
|
+
const scrollTop = chatBox.childBase || 0;
|
|
3959
|
+
const visibleRows = chatBox.height - 2; // minus border
|
|
3960
|
+
if (idx >= scrollTop && idx < scrollTop + visibleRows) {
|
|
3961
|
+
// Update chatBox content in-place (no setContent
|
|
3962
|
+
// which resets scroll position). Re-join the full
|
|
3963
|
+
// buffer but restore exact scroll afterward.
|
|
3964
|
+
const prevBase = chatBox.childBase || 0;
|
|
3965
|
+
chatBox.setContent(b.map(styleUrls).join("\n"));
|
|
3966
|
+
chatBox.childBase = prevBase;
|
|
3967
|
+
_screenDirty = true;
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
if (!anyActive && sessionSpinnerIndex.size === 0) {
|
|
3973
|
+
clearInterval(_spinnerTimer);
|
|
3974
|
+
_spinnerTimer = null;
|
|
3975
|
+
}
|
|
3976
|
+
}, 80);
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
function stopChatSpinner(orchId) {
|
|
3981
|
+
const idx = sessionSpinnerIndex.get(orchId);
|
|
3982
|
+
if (idx == null) return;
|
|
3983
|
+
const buf = sessionChatBuffers.get(orchId);
|
|
3984
|
+
if (buf && idx < buf.length) {
|
|
3985
|
+
// Remove spinner line and the blank line before it
|
|
3986
|
+
const startIdx = (idx > 0 && buf[idx - 1] === "") ? idx - 1 : idx;
|
|
3987
|
+
buf.splice(startIdx, idx - startIdx + 1);
|
|
3988
|
+
}
|
|
3989
|
+
sessionSpinnerIndex.delete(orchId);
|
|
3990
|
+
if (orchId === activeOrchId) invalidateChat();
|
|
3991
|
+
// Stop global timer if no spinners remain
|
|
3992
|
+
if (sessionSpinnerIndex.size === 0 && _spinnerTimer) {
|
|
3993
|
+
clearInterval(_spinnerTimer);
|
|
3994
|
+
_spinnerTimer = null;
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3546
3997
|
const sessionPendingQuestions = new Map(); // orchId → latest input-required question awaiting a user answer
|
|
3547
3998
|
const sessionLastSeenResponseVersion = new Map(); // orchId → latest KV-backed response version rendered
|
|
3548
3999
|
const sessionLastSeenCommandVersion = new Map(); // orchId → latest KV-backed command response version rendered
|
|
@@ -3756,6 +4207,33 @@ function handleDbRecovered() {
|
|
|
3756
4207
|
if (_dbOffline) {
|
|
3757
4208
|
appendLog(`{green-fg}Database connection restored.{/green-fg}`);
|
|
3758
4209
|
setStatus("Database connection restored.");
|
|
4210
|
+
|
|
4211
|
+
// The orchestration list poll uses its own management client pool, but
|
|
4212
|
+
// the active session's CMS event stream and reconstructed history may
|
|
4213
|
+
// still be stale after a DB outage. Re-prime the active session view so
|
|
4214
|
+
// chat/activity panes resume updating without requiring a manual switch.
|
|
4215
|
+
const recoveredOrchId = activeOrchId;
|
|
4216
|
+
if (recoveredOrchId) {
|
|
4217
|
+
stopCmsPoller();
|
|
4218
|
+
loadCmsHistory(recoveredOrchId, { force: true })
|
|
4219
|
+
.then(() => {
|
|
4220
|
+
if (recoveredOrchId === activeOrchId) {
|
|
4221
|
+
startCmsPoller(recoveredOrchId);
|
|
4222
|
+
invalidateChat();
|
|
4223
|
+
invalidateActivity();
|
|
4224
|
+
redrawActiveViews();
|
|
4225
|
+
scheduleLightRefresh("dbRecovered", recoveredOrchId);
|
|
4226
|
+
}
|
|
4227
|
+
})
|
|
4228
|
+
.catch(() => {
|
|
4229
|
+
if (recoveredOrchId === activeOrchId) {
|
|
4230
|
+
startCmsPoller(recoveredOrchId);
|
|
4231
|
+
invalidateChat();
|
|
4232
|
+
invalidateActivity();
|
|
4233
|
+
redrawActiveViews();
|
|
4234
|
+
}
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
3759
4237
|
}
|
|
3760
4238
|
_dbOffline = false;
|
|
3761
4239
|
_dbNextRetryAt = 0;
|
|
@@ -4348,7 +4826,7 @@ orchList.key(["n"], async () => {
|
|
|
4348
4826
|
items: agentChoices.map(a => ` ${a.name || "(generic)"}${a.description ? ` — ${a.description}` : ""}`),
|
|
4349
4827
|
keys: true,
|
|
4350
4828
|
vi: true,
|
|
4351
|
-
mouse:
|
|
4829
|
+
mouse: false,
|
|
4352
4830
|
scrollable: true,
|
|
4353
4831
|
});
|
|
4354
4832
|
|
|
@@ -4492,7 +4970,7 @@ orchList.key(["S-n"], async () => {
|
|
|
4492
4970
|
items,
|
|
4493
4971
|
keys: true,
|
|
4494
4972
|
vi: true,
|
|
4495
|
-
mouse:
|
|
4973
|
+
mouse: false,
|
|
4496
4974
|
scrollable: true,
|
|
4497
4975
|
});
|
|
4498
4976
|
picker.focus();
|
|
@@ -4556,7 +5034,7 @@ orchList.key(["t"], async () => {
|
|
|
4556
5034
|
},
|
|
4557
5035
|
keys: true,
|
|
4558
5036
|
vi: true,
|
|
4559
|
-
mouse:
|
|
5037
|
+
mouse: false,
|
|
4560
5038
|
items: [
|
|
4561
5039
|
" Type a custom title",
|
|
4562
5040
|
" Ask LLM to summarize",
|
|
@@ -5205,6 +5683,7 @@ function startObserver(orchId) {
|
|
|
5205
5683
|
|
|
5206
5684
|
function renderResponsePayload(response, cs, source) {
|
|
5207
5685
|
if (!response) return;
|
|
5686
|
+
stopChatSpinner(orchId);
|
|
5208
5687
|
if (response.type === "completed" && response.content) {
|
|
5209
5688
|
appendActivity(`{green-fg}[obs] ✓ SHOWING ${source}: version=${response.version} type=completed content=${response.content.slice(0, 80)}{/green-fg}`, orchId);
|
|
5210
5689
|
renderCompletedContent(response.content);
|
|
@@ -5872,7 +6351,11 @@ async function ensureOrchestrationStarted(orchId = activeOrchId) {
|
|
|
5872
6351
|
// ─── Input handling ──────────────────────────────────────────────
|
|
5873
6352
|
|
|
5874
6353
|
async function handleInput(text) {
|
|
5875
|
-
|
|
6354
|
+
// If file attach modal triggered cancellation, ignore this call
|
|
6355
|
+
if (_fileAttachModal) return;
|
|
6356
|
+
// Expand file attachments before trimming — replaces 📎tokens with file content
|
|
6357
|
+
const expanded = expandAttachments(text || "");
|
|
6358
|
+
const trimmed = expanded.trim();
|
|
5876
6359
|
if (!trimmed) {
|
|
5877
6360
|
inputBar.clearValue();
|
|
5878
6361
|
focusInput();
|
|
@@ -6121,6 +6604,7 @@ async function handleInput(text) {
|
|
|
6121
6604
|
}
|
|
6122
6605
|
|
|
6123
6606
|
appendChatRaw(`{white-fg}[${ts()}]{/white-fg} {white-fg}{bold}You:{/bold} ${trimmed}{/white-fg}`, targetOrchId);
|
|
6607
|
+
startChatSpinner(targetOrchId);
|
|
6124
6608
|
inputBar.clearValue();
|
|
6125
6609
|
focusInput();
|
|
6126
6610
|
setSessionPendingTurn(targetOrchId, true);
|
|
@@ -6188,6 +6672,7 @@ async function handleInput(text) {
|
|
|
6188
6672
|
|
|
6189
6673
|
inputBar.on("submit", handleInput);
|
|
6190
6674
|
inputBar.key(["escape"], () => {
|
|
6675
|
+
if (_fileAttachModal) return; // Modal is handling escape
|
|
6191
6676
|
inputBar.clearValue();
|
|
6192
6677
|
// Exit prompt — focus the sessions pane for navigation
|
|
6193
6678
|
orchList.focus();
|
|
@@ -6214,6 +6699,7 @@ function showHelpOverlay() {
|
|
|
6214
6699
|
" {yellow-fg}r{/yellow-fg} Force full screen redraw",
|
|
6215
6700
|
" {yellow-fg}u{/yellow-fg} Dump active session to Markdown file",
|
|
6216
6701
|
" {yellow-fg}a{/yellow-fg} Show artifact picker (download files)",
|
|
6702
|
+
" {yellow-fg}Ctrl+A{/yellow-fg} Attach local file to prompt",
|
|
6217
6703
|
"",
|
|
6218
6704
|
"{bold}Sessions Pane{/bold}",
|
|
6219
6705
|
" {yellow-fg}j / k{/yellow-fg} Navigate up / down",
|
|
@@ -6236,11 +6722,12 @@ function showHelpOverlay() {
|
|
|
6236
6722
|
"",
|
|
6237
6723
|
"{bold}Prompt Editor{/bold}",
|
|
6238
6724
|
" {yellow-fg}Enter{/yellow-fg} Submit prompt",
|
|
6239
|
-
" {yellow-fg}
|
|
6725
|
+
" {yellow-fg}Ctrl+J{/yellow-fg} Insert newline and expand prompt",
|
|
6240
6726
|
" {yellow-fg}← / →{/yellow-fg} Move cursor by character",
|
|
6727
|
+
" {yellow-fg}↑ / ↓{/yellow-fg} Move cursor between lines (multiline)",
|
|
6241
6728
|
" {yellow-fg}Opt+← / →{/yellow-fg} Move cursor by word",
|
|
6242
6729
|
" {yellow-fg}Backspace{/yellow-fg} Delete backward by character",
|
|
6243
|
-
" {yellow-fg}
|
|
6730
|
+
" {yellow-fg}Ctrl+W{/yellow-fg} Delete backward by word",
|
|
6244
6731
|
" {yellow-fg}Esc{/yellow-fg} Return to navigation mode",
|
|
6245
6732
|
"",
|
|
6246
6733
|
"{bold}Markdown Viewer{/bold}",
|
|
@@ -6281,7 +6768,7 @@ function showHelpOverlay() {
|
|
|
6281
6768
|
scrollable: true,
|
|
6282
6769
|
keys: true,
|
|
6283
6770
|
vi: true,
|
|
6284
|
-
mouse:
|
|
6771
|
+
mouse: false,
|
|
6285
6772
|
content: helpContent,
|
|
6286
6773
|
label: " {bold}Help{/bold} ",
|
|
6287
6774
|
});
|
|
@@ -6381,6 +6868,9 @@ let escPressedAt = 0;
|
|
|
6381
6868
|
screen.on("keypress", (ch, key) => {
|
|
6382
6869
|
if (!key) return;
|
|
6383
6870
|
|
|
6871
|
+
// Block all keys when a modal dialog (file attach) is open
|
|
6872
|
+
if (_fileAttachModal) return;
|
|
6873
|
+
|
|
6384
6874
|
if (startupLandingVisible) {
|
|
6385
6875
|
startupLandingVisible = false;
|
|
6386
6876
|
orchList.focus();
|
|
@@ -6401,6 +6891,7 @@ screen.on("keypress", (ch, key) => {
|
|
|
6401
6891
|
orchList.focus();
|
|
6402
6892
|
screen.realloc();
|
|
6403
6893
|
relayoutAll();
|
|
6894
|
+
scheduleLightRefresh("toggleMarkdownView");
|
|
6404
6895
|
setStatus(mdViewActive ? "Markdown Viewer (v to exit)" : `Log mode: ${({ workers: "Per-Worker", orchestration: "Per-Orchestration", sequence: "Sequence Diagram", nodemap: "Node Map" })[logViewMode]}`);
|
|
6405
6896
|
return;
|
|
6406
6897
|
}
|
|
@@ -6436,94 +6927,7 @@ screen.on("keypress", (ch, key) => {
|
|
|
6436
6927
|
|
|
6437
6928
|
// a: open artifact picker for current session — download on selection
|
|
6438
6929
|
if (ch === "a" && screen.focused !== inputBar && !mdViewActive) {
|
|
6439
|
-
|
|
6440
|
-
if (artifacts.length === 0) {
|
|
6441
|
-
setStatus("No artifacts for this session");
|
|
6442
|
-
return;
|
|
6443
|
-
}
|
|
6444
|
-
|
|
6445
|
-
const items = artifacts.map((a, i) => {
|
|
6446
|
-
const icon = a.downloaded ? "✓" : "↓";
|
|
6447
|
-
return ` ${icon} ${a.filename}`;
|
|
6448
|
-
});
|
|
6449
|
-
|
|
6450
|
-
const picker = blessed.list({
|
|
6451
|
-
parent: screen,
|
|
6452
|
-
label: " 📎 Artifacts — Enter to download ",
|
|
6453
|
-
tags: true,
|
|
6454
|
-
left: "center",
|
|
6455
|
-
top: "center",
|
|
6456
|
-
width: Math.min(60, screen.width - 4),
|
|
6457
|
-
height: Math.min(items.length + 2, 16),
|
|
6458
|
-
border: { type: "line" },
|
|
6459
|
-
style: {
|
|
6460
|
-
fg: "white",
|
|
6461
|
-
bg: "black",
|
|
6462
|
-
border: { fg: "cyan" },
|
|
6463
|
-
label: { fg: "cyan" },
|
|
6464
|
-
selected: { bg: "blue", fg: "white" },
|
|
6465
|
-
},
|
|
6466
|
-
keys: true,
|
|
6467
|
-
vi: true,
|
|
6468
|
-
mouse: true,
|
|
6469
|
-
items,
|
|
6470
|
-
});
|
|
6471
|
-
|
|
6472
|
-
picker.focus();
|
|
6473
|
-
screen.render();
|
|
6474
|
-
|
|
6475
|
-
const closePicker = () => {
|
|
6476
|
-
picker.detach();
|
|
6477
|
-
orchList.focus();
|
|
6478
|
-
screen.render();
|
|
6479
|
-
};
|
|
6480
|
-
|
|
6481
|
-
picker.key(["escape", "q", "a"], closePicker);
|
|
6482
|
-
|
|
6483
|
-
picker.on("select", async (_el, idx) => {
|
|
6484
|
-
const art = artifacts[idx];
|
|
6485
|
-
if (!art) return;
|
|
6486
|
-
|
|
6487
|
-
if (art.downloaded) {
|
|
6488
|
-
// Already downloaded — close picker and open viewer
|
|
6489
|
-
closePicker();
|
|
6490
|
-
mdViewActive = true;
|
|
6491
|
-
refreshMarkdownViewer();
|
|
6492
|
-
const files = scanExportFiles();
|
|
6493
|
-
const matchIdx = files.findIndex(f => f.localPath === art.localPath);
|
|
6494
|
-
if (matchIdx >= 0) {
|
|
6495
|
-
mdViewerSelectedIdx = matchIdx;
|
|
6496
|
-
mdFileListPane.select(matchIdx);
|
|
6497
|
-
refreshMarkdownViewer();
|
|
6498
|
-
}
|
|
6499
|
-
screen.realloc();
|
|
6500
|
-
relayoutAll();
|
|
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}`);
|
|
6521
|
-
} else {
|
|
6522
|
-
setStatus("Download failed — check logs");
|
|
6523
|
-
}
|
|
6524
|
-
screen.render();
|
|
6525
|
-
});
|
|
6526
|
-
|
|
6930
|
+
showArtifactPicker();
|
|
6527
6931
|
return;
|
|
6528
6932
|
}
|
|
6529
6933
|
|
|
@@ -6625,7 +7029,6 @@ screen.on("keypress", (ch, key) => {
|
|
|
6625
7029
|
// p from any non-input pane → jump to prompt
|
|
6626
7030
|
if (ch === "p" && screen.focused !== inputBar) {
|
|
6627
7031
|
focusInput();
|
|
6628
|
-
setStatus("Ready — type a message");
|
|
6629
7032
|
screen.render();
|
|
6630
7033
|
return;
|
|
6631
7034
|
}
|
|
@@ -6771,6 +7174,9 @@ screen.on("resize", () => {
|
|
|
6771
7174
|
// Initial orchestration refresh
|
|
6772
7175
|
await refreshOrchestrations();
|
|
6773
7176
|
|
|
7177
|
+
// Trigger initial right-pane render (orch logs, sequence, etc.)
|
|
7178
|
+
redrawActiveViews();
|
|
7179
|
+
|
|
6774
7180
|
// ─── Auto-summarize all existing sessions on startup ─────────────
|
|
6775
7181
|
async function summarizeSession(orchId) {
|
|
6776
7182
|
if (sessionSummarized.has(orchId)) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pilotswarm-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Terminal UI for pilotswarm — interactive durable agent orchestration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"url": "https://github.com/affandar/PilotSwarm/issues"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"pilotswarm-sdk": "^0.1.
|
|
33
|
+
"pilotswarm-sdk": "^0.1.9",
|
|
34
34
|
"@bradygaster/squad-cli": "^0.8.25",
|
|
35
35
|
"chalk": "^5.6.2",
|
|
36
36
|
"marked": "^15.0.12",
|