pilotswarm-cli 0.1.8 → 0.1.10
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 +436 -114
- 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
|
}
|
|
@@ -3570,7 +3951,22 @@ function startChatSpinner(orchId) {
|
|
|
3570
3951
|
const b = sessionChatBuffers.get(sid);
|
|
3571
3952
|
if (b && idx < b.length) {
|
|
3572
3953
|
b[idx] = `{gray-fg}${SPINNER_FRAMES[_spinnerFrame]} Thinking…{/gray-fg}`;
|
|
3573
|
-
if (sid === activeOrchId) {
|
|
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
|
+
}
|
|
3574
3970
|
}
|
|
3575
3971
|
}
|
|
3576
3972
|
if (!anyActive && sessionSpinnerIndex.size === 0) {
|
|
@@ -4430,7 +4826,7 @@ orchList.key(["n"], async () => {
|
|
|
4430
4826
|
items: agentChoices.map(a => ` ${a.name || "(generic)"}${a.description ? ` — ${a.description}` : ""}`),
|
|
4431
4827
|
keys: true,
|
|
4432
4828
|
vi: true,
|
|
4433
|
-
mouse:
|
|
4829
|
+
mouse: false,
|
|
4434
4830
|
scrollable: true,
|
|
4435
4831
|
});
|
|
4436
4832
|
|
|
@@ -4574,7 +4970,7 @@ orchList.key(["S-n"], async () => {
|
|
|
4574
4970
|
items,
|
|
4575
4971
|
keys: true,
|
|
4576
4972
|
vi: true,
|
|
4577
|
-
mouse:
|
|
4973
|
+
mouse: false,
|
|
4578
4974
|
scrollable: true,
|
|
4579
4975
|
});
|
|
4580
4976
|
picker.focus();
|
|
@@ -4638,7 +5034,7 @@ orchList.key(["t"], async () => {
|
|
|
4638
5034
|
},
|
|
4639
5035
|
keys: true,
|
|
4640
5036
|
vi: true,
|
|
4641
|
-
mouse:
|
|
5037
|
+
mouse: false,
|
|
4642
5038
|
items: [
|
|
4643
5039
|
" Type a custom title",
|
|
4644
5040
|
" Ask LLM to summarize",
|
|
@@ -5955,7 +6351,11 @@ async function ensureOrchestrationStarted(orchId = activeOrchId) {
|
|
|
5955
6351
|
// ─── Input handling ──────────────────────────────────────────────
|
|
5956
6352
|
|
|
5957
6353
|
async function handleInput(text) {
|
|
5958
|
-
|
|
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();
|
|
5959
6359
|
if (!trimmed) {
|
|
5960
6360
|
inputBar.clearValue();
|
|
5961
6361
|
focusInput();
|
|
@@ -6272,6 +6672,7 @@ async function handleInput(text) {
|
|
|
6272
6672
|
|
|
6273
6673
|
inputBar.on("submit", handleInput);
|
|
6274
6674
|
inputBar.key(["escape"], () => {
|
|
6675
|
+
if (_fileAttachModal) return; // Modal is handling escape
|
|
6275
6676
|
inputBar.clearValue();
|
|
6276
6677
|
// Exit prompt — focus the sessions pane for navigation
|
|
6277
6678
|
orchList.focus();
|
|
@@ -6298,6 +6699,7 @@ function showHelpOverlay() {
|
|
|
6298
6699
|
" {yellow-fg}r{/yellow-fg} Force full screen redraw",
|
|
6299
6700
|
" {yellow-fg}u{/yellow-fg} Dump active session to Markdown file",
|
|
6300
6701
|
" {yellow-fg}a{/yellow-fg} Show artifact picker (download files)",
|
|
6702
|
+
" {yellow-fg}Ctrl+A{/yellow-fg} Attach local file to prompt",
|
|
6301
6703
|
"",
|
|
6302
6704
|
"{bold}Sessions Pane{/bold}",
|
|
6303
6705
|
" {yellow-fg}j / k{/yellow-fg} Navigate up / down",
|
|
@@ -6320,11 +6722,12 @@ function showHelpOverlay() {
|
|
|
6320
6722
|
"",
|
|
6321
6723
|
"{bold}Prompt Editor{/bold}",
|
|
6322
6724
|
" {yellow-fg}Enter{/yellow-fg} Submit prompt",
|
|
6323
|
-
" {yellow-fg}
|
|
6725
|
+
" {yellow-fg}Ctrl+J{/yellow-fg} Insert newline and expand prompt",
|
|
6324
6726
|
" {yellow-fg}← / →{/yellow-fg} Move cursor by character",
|
|
6727
|
+
" {yellow-fg}↑ / ↓{/yellow-fg} Move cursor between lines (multiline)",
|
|
6325
6728
|
" {yellow-fg}Opt+← / →{/yellow-fg} Move cursor by word",
|
|
6326
6729
|
" {yellow-fg}Backspace{/yellow-fg} Delete backward by character",
|
|
6327
|
-
" {yellow-fg}
|
|
6730
|
+
" {yellow-fg}Ctrl+W{/yellow-fg} Delete backward by word",
|
|
6328
6731
|
" {yellow-fg}Esc{/yellow-fg} Return to navigation mode",
|
|
6329
6732
|
"",
|
|
6330
6733
|
"{bold}Markdown Viewer{/bold}",
|
|
@@ -6365,7 +6768,7 @@ function showHelpOverlay() {
|
|
|
6365
6768
|
scrollable: true,
|
|
6366
6769
|
keys: true,
|
|
6367
6770
|
vi: true,
|
|
6368
|
-
mouse:
|
|
6771
|
+
mouse: false,
|
|
6369
6772
|
content: helpContent,
|
|
6370
6773
|
label: " {bold}Help{/bold} ",
|
|
6371
6774
|
});
|
|
@@ -6465,6 +6868,9 @@ let escPressedAt = 0;
|
|
|
6465
6868
|
screen.on("keypress", (ch, key) => {
|
|
6466
6869
|
if (!key) return;
|
|
6467
6870
|
|
|
6871
|
+
// Block all keys when a modal dialog (file attach) is open
|
|
6872
|
+
if (_fileAttachModal) return;
|
|
6873
|
+
|
|
6468
6874
|
if (startupLandingVisible) {
|
|
6469
6875
|
startupLandingVisible = false;
|
|
6470
6876
|
orchList.focus();
|
|
@@ -6485,6 +6891,7 @@ screen.on("keypress", (ch, key) => {
|
|
|
6485
6891
|
orchList.focus();
|
|
6486
6892
|
screen.realloc();
|
|
6487
6893
|
relayoutAll();
|
|
6894
|
+
scheduleLightRefresh("toggleMarkdownView");
|
|
6488
6895
|
setStatus(mdViewActive ? "Markdown Viewer (v to exit)" : `Log mode: ${({ workers: "Per-Worker", orchestration: "Per-Orchestration", sequence: "Sequence Diagram", nodemap: "Node Map" })[logViewMode]}`);
|
|
6489
6896
|
return;
|
|
6490
6897
|
}
|
|
@@ -6520,94 +6927,7 @@ screen.on("keypress", (ch, key) => {
|
|
|
6520
6927
|
|
|
6521
6928
|
// a: open artifact picker for current session — download on selection
|
|
6522
6929
|
if (ch === "a" && screen.focused !== inputBar && !mdViewActive) {
|
|
6523
|
-
|
|
6524
|
-
if (artifacts.length === 0) {
|
|
6525
|
-
setStatus("No artifacts for this session");
|
|
6526
|
-
return;
|
|
6527
|
-
}
|
|
6528
|
-
|
|
6529
|
-
const items = artifacts.map((a, i) => {
|
|
6530
|
-
const icon = a.downloaded ? "✓" : "↓";
|
|
6531
|
-
return ` ${icon} ${a.filename}`;
|
|
6532
|
-
});
|
|
6533
|
-
|
|
6534
|
-
const picker = blessed.list({
|
|
6535
|
-
parent: screen,
|
|
6536
|
-
label: " 📎 Artifacts — Enter to download ",
|
|
6537
|
-
tags: true,
|
|
6538
|
-
left: "center",
|
|
6539
|
-
top: "center",
|
|
6540
|
-
width: Math.min(60, screen.width - 4),
|
|
6541
|
-
height: Math.min(items.length + 2, 16),
|
|
6542
|
-
border: { type: "line" },
|
|
6543
|
-
style: {
|
|
6544
|
-
fg: "white",
|
|
6545
|
-
bg: "black",
|
|
6546
|
-
border: { fg: "cyan" },
|
|
6547
|
-
label: { fg: "cyan" },
|
|
6548
|
-
selected: { bg: "blue", fg: "white" },
|
|
6549
|
-
},
|
|
6550
|
-
keys: true,
|
|
6551
|
-
vi: true,
|
|
6552
|
-
mouse: true,
|
|
6553
|
-
items,
|
|
6554
|
-
});
|
|
6555
|
-
|
|
6556
|
-
picker.focus();
|
|
6557
|
-
screen.render();
|
|
6558
|
-
|
|
6559
|
-
const closePicker = () => {
|
|
6560
|
-
picker.detach();
|
|
6561
|
-
orchList.focus();
|
|
6562
|
-
screen.render();
|
|
6563
|
-
};
|
|
6564
|
-
|
|
6565
|
-
picker.key(["escape", "q", "a"], closePicker);
|
|
6566
|
-
|
|
6567
|
-
picker.on("select", async (_el, idx) => {
|
|
6568
|
-
const art = artifacts[idx];
|
|
6569
|
-
if (!art) return;
|
|
6570
|
-
|
|
6571
|
-
if (art.downloaded) {
|
|
6572
|
-
// Already downloaded — close picker and open viewer
|
|
6573
|
-
closePicker();
|
|
6574
|
-
mdViewActive = true;
|
|
6575
|
-
refreshMarkdownViewer();
|
|
6576
|
-
const files = scanExportFiles();
|
|
6577
|
-
const matchIdx = files.findIndex(f => f.localPath === art.localPath);
|
|
6578
|
-
if (matchIdx >= 0) {
|
|
6579
|
-
mdViewerSelectedIdx = matchIdx;
|
|
6580
|
-
mdFileListPane.select(matchIdx);
|
|
6581
|
-
refreshMarkdownViewer();
|
|
6582
|
-
}
|
|
6583
|
-
screen.realloc();
|
|
6584
|
-
relayoutAll();
|
|
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}`);
|
|
6605
|
-
} else {
|
|
6606
|
-
setStatus("Download failed — check logs");
|
|
6607
|
-
}
|
|
6608
|
-
screen.render();
|
|
6609
|
-
});
|
|
6610
|
-
|
|
6930
|
+
showArtifactPicker();
|
|
6611
6931
|
return;
|
|
6612
6932
|
}
|
|
6613
6933
|
|
|
@@ -6709,7 +7029,6 @@ screen.on("keypress", (ch, key) => {
|
|
|
6709
7029
|
// p from any non-input pane → jump to prompt
|
|
6710
7030
|
if (ch === "p" && screen.focused !== inputBar) {
|
|
6711
7031
|
focusInput();
|
|
6712
|
-
setStatus("Ready — type a message");
|
|
6713
7032
|
screen.render();
|
|
6714
7033
|
return;
|
|
6715
7034
|
}
|
|
@@ -6855,6 +7174,9 @@ screen.on("resize", () => {
|
|
|
6855
7174
|
// Initial orchestration refresh
|
|
6856
7175
|
await refreshOrchestrations();
|
|
6857
7176
|
|
|
7177
|
+
// Trigger initial right-pane render (orch logs, sequence, etc.)
|
|
7178
|
+
redrawActiveViews();
|
|
7179
|
+
|
|
6858
7180
|
// ─── Auto-summarize all existing sessions on startup ─────────────
|
|
6859
7181
|
async function summarizeSession(orchId) {
|
|
6860
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.10",
|
|
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",
|