groundcrew-cli 0.15.15 → 0.15.17
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/dist/index.js +396 -280
- package/package.json +1 -1
- package/src/index.ts +371 -342
package/src/index.ts
CHANGED
|
@@ -565,238 +565,339 @@ const CHAT_COMMANDS: Array<{ cmd: string; desc: string }> = [
|
|
|
565
565
|
{ cmd: "/exit", desc: "Exit chat" },
|
|
566
566
|
];
|
|
567
567
|
|
|
568
|
-
function chatCompleter(line: string): [string[], string] {
|
|
569
|
-
if (!line.startsWith("/")) return [[], line];
|
|
570
|
-
const matches = CHAT_COMMANDS.filter((c) => c.cmd.startsWith(line));
|
|
571
|
-
|
|
572
|
-
if (matches.length === 1) {
|
|
573
|
-
return [[matches[0].cmd + " "], line];
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
if (matches.length > 1) {
|
|
577
|
-
const display = matches.map((c) => `${c.cmd.padEnd(14)} ${c.desc}`);
|
|
578
|
-
console.log();
|
|
579
|
-
display.forEach((d) => console.log(` ${d}`));
|
|
580
|
-
return [matches.map((c) => c.cmd), line];
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
return [[], line];
|
|
584
|
-
}
|
|
585
|
-
|
|
586
568
|
/**
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
569
|
+
* Custom multiline editor — replaces readline for chat input.
|
|
570
|
+
* No `\` line endings, no `...` prefix, clean aligned continuation.
|
|
571
|
+
* Uses ANSI escape sequences for in-place rendering.
|
|
572
|
+
*
|
|
573
|
+
* Submit: Enter
|
|
574
|
+
* Newline: Shift+Enter (Kitty), Alt+Enter, Ctrl+J
|
|
575
|
+
* Clear: Ctrl+C (clears input, or exits if empty)
|
|
576
|
+
* Navigation: Arrow keys, Home/End, Ctrl+A/E
|
|
577
|
+
* Editing: Backspace, Delete, Ctrl+U/K/W/D
|
|
578
|
+
* Tab: slash command completion
|
|
579
|
+
* Paste: bracketed paste with multiline support
|
|
591
580
|
*/
|
|
592
|
-
function
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
//
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
581
|
+
function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
582
|
+
return new Promise((resolve) => {
|
|
583
|
+
const lines: string[] = [""];
|
|
584
|
+
let crow = 0; // cursor row in lines[]
|
|
585
|
+
let ccol = 0; // cursor col in lines[crow]
|
|
586
|
+
|
|
587
|
+
// Visible width of prompt: "[sessionId] > "
|
|
588
|
+
const padWidth = sessionId.length + 5; // [ + id + ] + space + > + space = len+5
|
|
589
|
+
|
|
590
|
+
// Track how many rows up from cursor to top of rendered area (including separator)
|
|
591
|
+
let lastTermRow = 0;
|
|
592
|
+
let pasteBuffer = "";
|
|
593
|
+
let isPasting = false;
|
|
594
|
+
|
|
595
|
+
const fullText = () => lines.join("\n").trim();
|
|
596
|
+
|
|
597
|
+
const render = () => {
|
|
598
|
+
const buf: string[] = [];
|
|
599
|
+
|
|
600
|
+
// Move to start of input area (includes separator line)
|
|
601
|
+
if (lastTermRow > 0) buf.push(`\x1b[${lastTermRow}A`);
|
|
602
|
+
buf.push("\r\x1b[J"); // col 0 + clear to end of screen
|
|
603
|
+
|
|
604
|
+
// Separator line: ─── sessionId ─────────
|
|
605
|
+
const termW = process.stdout.columns || 80;
|
|
606
|
+
const info = ` ${sessionId} `;
|
|
607
|
+
const dashRight = "─".repeat(Math.max(0, termW - 4 - info.length));
|
|
608
|
+
buf.push(dim("───" + info + dashRight));
|
|
609
|
+
|
|
610
|
+
// Draw each input line (below separator)
|
|
611
|
+
for (let i = 0; i < lines.length; i++) {
|
|
612
|
+
buf.push("\n");
|
|
613
|
+
if (i === 0) {
|
|
614
|
+
buf.push(dim(`[${sessionId}]`) + " " + bold(">") + " " + lines[i]);
|
|
615
|
+
} else {
|
|
616
|
+
buf.push(" ".repeat(padWidth) + lines[i]);
|
|
617
|
+
}
|
|
602
618
|
}
|
|
603
|
-
buf.push(`\x1b[${dropdownLines}A`); // back up to prompt line
|
|
604
|
-
dropdownLines = 0;
|
|
605
|
-
}
|
|
606
|
-
// Clear inline ghost only if one is showing — save cursor, jump to
|
|
607
|
-
// ghost start (far right), erase it, then restore cursor position.
|
|
608
|
-
// This avoids nuking typed text when cursor is mid-line (left arrow).
|
|
609
|
-
if (ghostLen > 0) {
|
|
610
|
-
buf.push("\x1b[s"); // save cursor position
|
|
611
|
-
buf.push("\x1b[999C"); // jump to far right of line
|
|
612
|
-
buf.push(`\x1b[${ghostLen}D`); // back up to ghost start
|
|
613
|
-
buf.push("\x1b[K"); // clear from ghost start to EOL
|
|
614
|
-
buf.push("\x1b[u"); // restore cursor position
|
|
615
|
-
ghostLen = 0;
|
|
616
|
-
}
|
|
617
|
-
if (buf.length) process.stdout.write(buf.join(""));
|
|
618
|
-
};
|
|
619
619
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
620
|
+
// Position cursor at (crow, ccol)
|
|
621
|
+
const lastRow = lines.length - 1;
|
|
622
|
+
const rowsUp = lastRow - crow;
|
|
623
|
+
if (rowsUp > 0) buf.push(`\x1b[${rowsUp}A`);
|
|
624
|
+
|
|
625
|
+
buf.push("\r");
|
|
626
|
+
const col = padWidth + ccol;
|
|
627
|
+
if (col > 0) buf.push(`\x1b[${col}C`);
|
|
628
|
+
|
|
629
|
+
// lastTermRow = rows from cursor back to top of separator
|
|
630
|
+
// separator is 1 row, then crow rows of input below it
|
|
631
|
+
lastTermRow = 1 + crow;
|
|
632
|
+
process.stdout.write(buf.join(""));
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const finish = (result: string | null) => {
|
|
636
|
+
process.stdin.removeListener("data", onData);
|
|
637
|
+
resolve(result);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const submit = () => {
|
|
641
|
+
const text = fullText();
|
|
642
|
+
// Move cursor to end of input for clean output
|
|
643
|
+
const lastRow = lines.length - 1;
|
|
644
|
+
const rowsDown = lastRow - crow;
|
|
645
|
+
const buf: string[] = [];
|
|
646
|
+
if (rowsDown > 0) buf.push(`\x1b[${rowsDown}B`);
|
|
647
|
+
buf.push("\r");
|
|
648
|
+
const endCol = padWidth + lines[lastRow].length;
|
|
649
|
+
if (endCol > 0) buf.push(`\x1b[${endCol}C`);
|
|
650
|
+
buf.push("\n");
|
|
651
|
+
process.stdout.write(buf.join(""));
|
|
652
|
+
lastTermRow = 0;
|
|
653
|
+
finish(text || null);
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const insertText = (text: string) => {
|
|
657
|
+
const chunks = text.split(/\r?\n/);
|
|
658
|
+
const before = lines[crow].slice(0, ccol);
|
|
659
|
+
const after = lines[crow].slice(ccol);
|
|
660
|
+
|
|
661
|
+
if (chunks.length === 1) {
|
|
662
|
+
lines[crow] = before + chunks[0] + after;
|
|
663
|
+
ccol += chunks[0].length;
|
|
664
|
+
} else {
|
|
665
|
+
lines[crow] = before + chunks[0];
|
|
666
|
+
const middle = chunks.slice(1, -1);
|
|
667
|
+
const last = chunks[chunks.length - 1];
|
|
668
|
+
lines.splice(crow + 1, 0, ...middle, last + after);
|
|
669
|
+
crow += chunks.length - 1;
|
|
670
|
+
ccol = last.length;
|
|
671
|
+
}
|
|
672
|
+
render();
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const insertNewline = () => {
|
|
676
|
+
const before = lines[crow].slice(0, ccol);
|
|
677
|
+
const after = lines[crow].slice(ccol);
|
|
678
|
+
lines[crow] = before;
|
|
679
|
+
lines.splice(crow + 1, 0, after);
|
|
680
|
+
crow++;
|
|
681
|
+
ccol = 0;
|
|
682
|
+
render();
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const doBackspace = () => {
|
|
686
|
+
if (ccol > 0) {
|
|
687
|
+
lines[crow] = lines[crow].slice(0, ccol - 1) + lines[crow].slice(ccol);
|
|
688
|
+
ccol--;
|
|
689
|
+
} else if (crow > 0) {
|
|
690
|
+
const prevLen = lines[crow - 1].length;
|
|
691
|
+
lines[crow - 1] += lines[crow];
|
|
692
|
+
lines.splice(crow, 1);
|
|
693
|
+
crow--;
|
|
694
|
+
ccol = prevLen;
|
|
695
|
+
}
|
|
696
|
+
render();
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const doDelete = () => {
|
|
700
|
+
if (ccol < lines[crow].length) {
|
|
701
|
+
lines[crow] = lines[crow].slice(0, ccol) + lines[crow].slice(ccol + 1);
|
|
702
|
+
} else if (crow < lines.length - 1) {
|
|
703
|
+
lines[crow] += lines[crow + 1];
|
|
704
|
+
lines.splice(crow + 1, 1);
|
|
705
|
+
}
|
|
706
|
+
render();
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const processKeys = (str: string) => {
|
|
710
|
+
let i = 0;
|
|
711
|
+
while (i < str.length) {
|
|
712
|
+
// Shift+Enter (Kitty: \x1b[13;2u)
|
|
713
|
+
if (str.startsWith("\x1b[13;2u", i)) { insertNewline(); i += 7; continue; }
|
|
714
|
+
|
|
715
|
+
// Alt+Enter (ESC + CR or ESC + LF)
|
|
716
|
+
if (i + 1 < str.length && str[i] === "\x1b" && (str[i + 1] === "\r" || str[i + 1] === "\n")) {
|
|
717
|
+
insertNewline(); i += 2; continue;
|
|
718
|
+
}
|
|
623
719
|
|
|
624
|
-
|
|
625
|
-
|
|
720
|
+
// Arrow Up
|
|
721
|
+
if (str.startsWith("\x1b[A", i)) {
|
|
722
|
+
if (crow > 0) { crow--; ccol = Math.min(ccol, lines[crow].length); render(); }
|
|
723
|
+
i += 3; continue;
|
|
724
|
+
}
|
|
725
|
+
// Arrow Down
|
|
726
|
+
if (str.startsWith("\x1b[B", i)) {
|
|
727
|
+
if (crow < lines.length - 1) { crow++; ccol = Math.min(ccol, lines[crow].length); render(); }
|
|
728
|
+
i += 3; continue;
|
|
729
|
+
}
|
|
730
|
+
// Arrow Right
|
|
731
|
+
if (str.startsWith("\x1b[C", i)) {
|
|
732
|
+
if (ccol < lines[crow].length) ccol++;
|
|
733
|
+
else if (crow < lines.length - 1) { crow++; ccol = 0; }
|
|
734
|
+
render(); i += 3; continue;
|
|
735
|
+
}
|
|
736
|
+
// Arrow Left
|
|
737
|
+
if (str.startsWith("\x1b[D", i)) {
|
|
738
|
+
if (ccol > 0) ccol--;
|
|
739
|
+
else if (crow > 0) { crow--; ccol = lines[crow].length; }
|
|
740
|
+
render(); i += 3; continue;
|
|
741
|
+
}
|
|
626
742
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const remainder = best.cmd.slice(line.length);
|
|
630
|
-
if (!remainder && shown.length === 1) return;
|
|
743
|
+
// Delete key (\x1b[3~)
|
|
744
|
+
if (str.startsWith("\x1b[3~", i)) { doDelete(); i += 4; continue; }
|
|
631
745
|
|
|
632
|
-
|
|
746
|
+
// Home (\x1b[H)
|
|
747
|
+
if (str.startsWith("\x1b[H", i)) { ccol = 0; render(); i += 3; continue; }
|
|
748
|
+
// End (\x1b[F)
|
|
749
|
+
if (str.startsWith("\x1b[F", i)) { ccol = lines[crow].length; render(); i += 3; continue; }
|
|
633
750
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
751
|
+
// Skip unknown CSI sequences
|
|
752
|
+
if (str[i] === "\x1b" && i + 1 < str.length && str[i + 1] === "[") {
|
|
753
|
+
let j = i + 2;
|
|
754
|
+
while (j < str.length && str.charCodeAt(j) >= 0x30 && str.charCodeAt(j) <= 0x3f) j++;
|
|
755
|
+
if (j < str.length) j++; // skip final byte
|
|
756
|
+
i = j; continue;
|
|
757
|
+
}
|
|
758
|
+
// Skip lone ESC
|
|
759
|
+
if (str[i] === "\x1b") { i++; continue; }
|
|
760
|
+
|
|
761
|
+
// Ctrl+C — clear input or exit
|
|
762
|
+
if (str[i] === "\x03") {
|
|
763
|
+
const hasText = fullText();
|
|
764
|
+
if (hasText || lines.length > 1 || lines[0].length > 0) {
|
|
765
|
+
// Move past current rendering to below last line, start fresh
|
|
766
|
+
const lastRow = lines.length - 1;
|
|
767
|
+
const rowsDown = lastRow - crow;
|
|
768
|
+
if (rowsDown > 0) process.stdout.write(`\x1b[${rowsDown}B`);
|
|
769
|
+
process.stdout.write("\r\n");
|
|
770
|
+
lines.length = 0; lines.push("");
|
|
771
|
+
crow = 0; ccol = 0; lastTermRow = 0;
|
|
772
|
+
render();
|
|
773
|
+
} else {
|
|
774
|
+
process.stdout.write("\r\n");
|
|
775
|
+
finish(null); return;
|
|
776
|
+
}
|
|
777
|
+
i++; continue;
|
|
778
|
+
}
|
|
643
779
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
buf.push(`\x1b[${count}A`); // back to prompt line
|
|
650
|
-
|
|
651
|
-
// Write each dropdown line: cyan command + dim description
|
|
652
|
-
for (let i = 0; i < count; i++) {
|
|
653
|
-
buf.push(`\x1b[B\r\x1b[2K`); // down + col 1 + clear
|
|
654
|
-
buf.push(` \x1b[36m${shown[i].cmd.padEnd(14)}\x1b[0m\x1b[2m${shown[i].desc}\x1b[0m`);
|
|
655
|
-
}
|
|
656
|
-
dropdownLines = count;
|
|
657
|
-
|
|
658
|
-
// Back to prompt line and restore horizontal position
|
|
659
|
-
// Move up to prompt line
|
|
660
|
-
buf.push(`\x1b[${count}A`);
|
|
661
|
-
// Move to column 1, then rewrite prompt+line to position cursor correctly
|
|
662
|
-
buf.push(`\r`);
|
|
663
|
-
// Let readline handle cursor positioning
|
|
664
|
-
}
|
|
780
|
+
// Ctrl+D — delete char or exit on empty
|
|
781
|
+
if (str[i] === "\x04") {
|
|
782
|
+
if (fullText()) { doDelete(); } else { process.stdout.write("\n"); finish(null); return; }
|
|
783
|
+
i++; continue;
|
|
784
|
+
}
|
|
665
785
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
786
|
+
// Ctrl+A — home
|
|
787
|
+
if (str[i] === "\x01") { ccol = 0; render(); i++; continue; }
|
|
788
|
+
// Ctrl+E — end
|
|
789
|
+
if (str[i] === "\x05") { ccol = lines[crow].length; render(); i++; continue; }
|
|
790
|
+
// Ctrl+U — clear line before cursor
|
|
791
|
+
if (str[i] === "\x15") {
|
|
792
|
+
lines[crow] = lines[crow].slice(ccol); ccol = 0; render(); i++; continue;
|
|
793
|
+
}
|
|
794
|
+
// Ctrl+K — clear line after cursor
|
|
795
|
+
if (str[i] === "\x0b") {
|
|
796
|
+
lines[crow] = lines[crow].slice(0, ccol); render(); i++; continue;
|
|
797
|
+
}
|
|
798
|
+
// Ctrl+W — delete word before cursor
|
|
799
|
+
if (str[i] === "\x17") {
|
|
800
|
+
const before = lines[crow].slice(0, ccol);
|
|
801
|
+
const stripped = before.replace(/\s+$/, "");
|
|
802
|
+
const sp = stripped.lastIndexOf(" ");
|
|
803
|
+
const newBefore = sp >= 0 ? stripped.slice(0, sp + 1) : "";
|
|
804
|
+
lines[crow] = newBefore + lines[crow].slice(ccol);
|
|
805
|
+
ccol = newBefore.length; render(); i++; continue;
|
|
806
|
+
}
|
|
676
807
|
|
|
677
|
-
|
|
678
|
-
|
|
808
|
+
// Ctrl+J (LF, 0x0A) — newline (cross-terminal)
|
|
809
|
+
if (str[i] === "\n") { insertNewline(); i++; continue; }
|
|
810
|
+
|
|
811
|
+
// Enter (CR, 0x0D) — submit
|
|
812
|
+
if (str[i] === "\r") { submit(); return; }
|
|
813
|
+
|
|
814
|
+
// Backspace (DEL 0x7F or BS 0x08)
|
|
815
|
+
if (str[i] === "\x7f" || str[i] === "\b") { doBackspace(); i++; continue; }
|
|
816
|
+
|
|
817
|
+
// Tab — slash command completion
|
|
818
|
+
if (str[i] === "\t") {
|
|
819
|
+
const currentLine = lines[crow];
|
|
820
|
+
if (lines.length === 1 && currentLine.startsWith("/")) {
|
|
821
|
+
const partial = currentLine.split(" ")[0]; // only match command part
|
|
822
|
+
const matches = CHAT_COMMANDS.filter(c => c.cmd.startsWith(partial));
|
|
823
|
+
if (matches.length === 1) {
|
|
824
|
+
lines[0] = matches[0].cmd + " ";
|
|
825
|
+
ccol = lines[0].length; render();
|
|
826
|
+
} else if (matches.length > 1) {
|
|
827
|
+
// Show matches below, then re-render prompt
|
|
828
|
+
const lastRow = lines.length - 1;
|
|
829
|
+
const rowsDown = lastRow - crow;
|
|
830
|
+
if (rowsDown > 0) process.stdout.write(`\x1b[${rowsDown}B`);
|
|
831
|
+
process.stdout.write("\r\n");
|
|
832
|
+
for (const m of matches) {
|
|
833
|
+
process.stdout.write(` ${cyan(m.cmd.padEnd(14))} ${dim(m.desc)}\n`);
|
|
834
|
+
}
|
|
835
|
+
lastTermRow = 0; render();
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
i++; continue;
|
|
839
|
+
}
|
|
679
840
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
841
|
+
// Regular printable character
|
|
842
|
+
const code = str.charCodeAt(i);
|
|
843
|
+
if (code >= 32) {
|
|
844
|
+
lines[crow] = lines[crow].slice(0, ccol) + str[i] + lines[crow].slice(ccol);
|
|
845
|
+
ccol++; render();
|
|
846
|
+
}
|
|
847
|
+
i++;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
686
850
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
process.stdout.write("\x1b[?2004h\x1b[>1u");
|
|
851
|
+
const onData = (data: Buffer) => {
|
|
852
|
+
let str = data.toString();
|
|
690
853
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
// 3. Alt+Enter (\x1b\r — universal fallback) — line continuation
|
|
695
|
-
const originalStdinEmit = process.stdin.emit.bind(process.stdin);
|
|
696
|
-
let pasteBuffer = "";
|
|
697
|
-
let isPasting = false;
|
|
698
|
-
|
|
699
|
-
process.stdin.emit = function (event: string, ...args: any[]) {
|
|
700
|
-
if (event === "data") {
|
|
701
|
-
const data = args[0] as Buffer | string;
|
|
702
|
-
let str = typeof data === "string" ? data : data.toString();
|
|
703
|
-
|
|
704
|
-
// --- Bracketed paste handling ---
|
|
705
|
-
const pasteStart = str.indexOf("\x1b[200~");
|
|
706
|
-
if (pasteStart !== -1) {
|
|
854
|
+
// Bracketed paste handling
|
|
855
|
+
const ps = str.indexOf("\x1b[200~");
|
|
856
|
+
if (ps !== -1) {
|
|
707
857
|
isPasting = true;
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
str = str.slice(pasteStart + 6); // skip \x1b[200~
|
|
858
|
+
const before = str.slice(0, ps);
|
|
859
|
+
if (before) processKeys(before);
|
|
860
|
+
str = str.slice(ps + 6);
|
|
712
861
|
}
|
|
713
|
-
|
|
714
862
|
if (isPasting) {
|
|
715
|
-
const
|
|
716
|
-
if (
|
|
717
|
-
pasteBuffer += str.slice(0,
|
|
718
|
-
const afterPaste = str.slice(pasteEnd + 6);
|
|
863
|
+
const pe = str.indexOf("\x1b[201~");
|
|
864
|
+
if (pe !== -1) {
|
|
865
|
+
pasteBuffer += str.slice(0, pe);
|
|
719
866
|
isPasting = false;
|
|
720
|
-
|
|
721
867
|
const pasted = pasteBuffer.replace(/[\r\n]+$/, "");
|
|
722
868
|
pasteBuffer = "";
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const lines = pasted.split(/\r?\n/);
|
|
727
|
-
for (let i = 0; i < lines.length - 1; i++) {
|
|
728
|
-
originalStdinEmit(event, Buffer.from(lines[i] + "\\\r"));
|
|
729
|
-
}
|
|
730
|
-
// Last line: insert without auto-submit
|
|
731
|
-
originalStdinEmit(event, Buffer.from(lines[lines.length - 1]));
|
|
732
|
-
} else {
|
|
733
|
-
originalStdinEmit(event, Buffer.from(pasted));
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
if (afterPaste) {
|
|
737
|
-
return originalStdinEmit(event, Buffer.from(afterPaste));
|
|
738
|
-
}
|
|
739
|
-
return false;
|
|
869
|
+
if (pasted) insertText(pasted);
|
|
870
|
+
const after = str.slice(pe + 6);
|
|
871
|
+
if (after) processKeys(after);
|
|
740
872
|
} else {
|
|
741
873
|
pasteBuffer += str;
|
|
742
|
-
return false;
|
|
743
874
|
}
|
|
875
|
+
return;
|
|
744
876
|
}
|
|
745
877
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const replaced = str.replace(/\x1b\[13;2u/g, "\\\r");
|
|
749
|
-
return originalStdinEmit(event, Buffer.from(replaced));
|
|
750
|
-
}
|
|
878
|
+
processKeys(str);
|
|
879
|
+
};
|
|
751
880
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
881
|
+
// Start listening
|
|
882
|
+
process.stdin.on("data", onData);
|
|
883
|
+
render();
|
|
884
|
+
});
|
|
885
|
+
}
|
|
756
886
|
|
|
757
|
-
// --- Ctrl+J (LF, 0x0A) — cross-terminal newline ---
|
|
758
|
-
// In raw mode: Enter sends \r (CR), Ctrl+J sends \n (LF).
|
|
759
|
-
// This is the only reliable way to distinguish "newline" from "submit"
|
|
760
|
-
// across ALL terminal emulators (Terminal.app, iTerm2, Kitty, etc.)
|
|
761
|
-
if (str === "\n") {
|
|
762
|
-
return originalStdinEmit(event, Buffer.from("\\\r"));
|
|
763
|
-
}
|
|
764
887
|
|
|
765
|
-
// --- Backspace at start of continuation → rejoin previous line ---
|
|
766
|
-
// \x7f = DEL (backspace on most terminals), \b = BS (some terminals)
|
|
767
|
-
if ((str === "\x7f" || str === "\b") && continuationBuffer.length > 0) {
|
|
768
|
-
const currentLine = (rl as any).line as string;
|
|
769
|
-
const cursor = (rl as any).cursor as number;
|
|
770
|
-
if (cursor === 0 && currentLine.length === 0) {
|
|
771
|
-
const prevLine = continuationBuffer.pop()!;
|
|
772
|
-
// Inject the previous line's text back into readline
|
|
773
|
-
(rl as any).line = prevLine;
|
|
774
|
-
(rl as any).cursor = prevLine.length;
|
|
775
|
-
// Update the prompt (may no longer be continuation)
|
|
776
|
-
const isCont = continuationBuffer.length > 0;
|
|
777
|
-
const prefix = isCont
|
|
778
|
-
? `${dim(`[${current!.id}]`)} ${dim("...")} `
|
|
779
|
-
: `${dim(`[${current!.id}]`)} ${bold(">")} `;
|
|
780
|
-
rl.setPrompt(prefix);
|
|
781
|
-
(rl as any)._refreshLine();
|
|
782
|
-
return false;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
return originalStdinEmit(event, ...args);
|
|
787
|
-
} as any;
|
|
788
888
|
|
|
889
|
+
async function chat(explicitSession?: string): Promise<void> {
|
|
890
|
+
// Enable bracketed paste + Kitty keyboard protocol
|
|
891
|
+
process.stdout.write("\x1b[?2004h\x1b[>1u");
|
|
892
|
+
|
|
893
|
+
// Use readline ONLY for session picker — then switch to custom multiline editor
|
|
789
894
|
const rl = readline.createInterface({
|
|
790
895
|
input: process.stdin,
|
|
791
896
|
output: process.stdout,
|
|
792
|
-
completer: chatCompleter,
|
|
793
897
|
});
|
|
794
898
|
|
|
795
|
-
setupInlineSuggestions(rl);
|
|
796
|
-
|
|
797
899
|
let current: SessionChoice | null = null;
|
|
798
900
|
|
|
799
|
-
// Resolve initial session
|
|
800
901
|
if (explicitSession) {
|
|
801
902
|
const dir = path.join(SESSIONS_DIR, explicitSession);
|
|
802
903
|
if (!existsSync(dir)) {
|
|
@@ -813,6 +914,9 @@ async function chat(explicitSession?: string): Promise<void> {
|
|
|
813
914
|
}
|
|
814
915
|
}
|
|
815
916
|
|
|
917
|
+
// Done with readline — close it before switching to raw mode
|
|
918
|
+
rl.close();
|
|
919
|
+
|
|
816
920
|
const projectName = path.basename(current.cwd);
|
|
817
921
|
const banner = [
|
|
818
922
|
" \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588 ",
|
|
@@ -846,170 +950,95 @@ async function chat(explicitSession?: string): Promise<void> {
|
|
|
846
950
|
console.log(dim(" \u2570" + "\u2500".repeat(W) + "\u256f"));
|
|
847
951
|
console.log();
|
|
848
952
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
// If line has text → clear it and re-prompt
|
|
853
|
-
// If line is empty → exit
|
|
854
|
-
rl.on("SIGINT", () => {
|
|
855
|
-
const line = (rl as any).line as string;
|
|
856
|
-
if (line || continuationBuffer.length > 0) {
|
|
857
|
-
continuationBuffer = [];
|
|
858
|
-
// Clear the current line visually and re-prompt
|
|
859
|
-
process.stdout.write("\n");
|
|
860
|
-
prompt();
|
|
861
|
-
} else {
|
|
862
|
-
process.stdout.write("\x1b[?2004l\x1b[<u");
|
|
863
|
-
console.log(dim("\nBye."));
|
|
864
|
-
process.exit(0);
|
|
865
|
-
}
|
|
866
|
-
});
|
|
953
|
+
// Enable raw mode for custom multiline editor
|
|
954
|
+
process.stdin.setRawMode(true);
|
|
955
|
+
process.stdin.resume();
|
|
867
956
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
process.
|
|
871
|
-
console.log(dim("
|
|
957
|
+
const exitChat = () => {
|
|
958
|
+
process.stdout.write("\x1b[?2004l\x1b[<u");
|
|
959
|
+
process.stdin.setRawMode(false);
|
|
960
|
+
console.log(dim("Bye."));
|
|
872
961
|
process.exit(0);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
const prompt = () => {
|
|
876
|
-
const isContinuation = continuationBuffer.length > 0;
|
|
877
|
-
const prefix = isContinuation
|
|
878
|
-
? `${dim(`[${current!.id}]`)} ${dim("...")} `
|
|
879
|
-
: `${dim(`[${current!.id}]`)} ${bold(">")} `;
|
|
880
|
-
|
|
881
|
-
rl.setPrompt(prefix);
|
|
882
|
-
rl.question(prefix, async (line) => {
|
|
883
|
-
// Line continuation with backslash
|
|
884
|
-
if (line.endsWith("\\")) {
|
|
885
|
-
continuationBuffer.push(line.slice(0, -1));
|
|
886
|
-
prompt(); return;
|
|
887
|
-
}
|
|
962
|
+
};
|
|
888
963
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const fullText = continuationBuffer.join("\n").trim();
|
|
893
|
-
continuationBuffer = [];
|
|
894
|
-
if (fullText) {
|
|
895
|
-
try {
|
|
896
|
-
if (fullText.startsWith("/")) {
|
|
897
|
-
// Process as command — use first line
|
|
898
|
-
// (multiline commands don't make sense, treat as task)
|
|
899
|
-
await add(fullText, 0, current!.dir);
|
|
900
|
-
} else {
|
|
901
|
-
await add(fullText, 0, current!.dir);
|
|
902
|
-
}
|
|
903
|
-
} catch (err: any) {
|
|
904
|
-
console.error(red(err.message));
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
prompt(); return;
|
|
908
|
-
}
|
|
964
|
+
// ── Main chat loop ─────────────────────────────────────────────────────────────────
|
|
965
|
+
while (true) {
|
|
966
|
+
const text = await readMultilineInput(current.id);
|
|
909
967
|
|
|
910
|
-
|
|
911
|
-
|
|
968
|
+
if (text === null) exitChat();
|
|
969
|
+
if (!text) continue;
|
|
912
970
|
|
|
913
|
-
|
|
914
|
-
if (trimmed === "/quit" || trimmed === "/exit") {
|
|
915
|
-
console.log(dim("Bye."));
|
|
916
|
-
rl.close();
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
971
|
+
const trimmed = text.trim();
|
|
919
972
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if (choices.length === 0) {
|
|
923
|
-
console.log(dim("No active sessions."));
|
|
924
|
-
} else {
|
|
925
|
-
choices.forEach((s, i) => {
|
|
926
|
-
const marker = s.id === current!.id ? green("*") : " ";
|
|
927
|
-
const pName = path.basename(s.cwd);
|
|
928
|
-
console.log(` ${marker} ${bold(String(i + 1))}. ${cyan(s.id)} ${dim(pName)} | ${s.status} | ${s.minutes}min | ${s.tasks} done`);
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
prompt(); return;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
if (trimmed.startsWith("/switch")) {
|
|
935
|
-
const arg = trimmed.slice(7).trim();
|
|
936
|
-
const choices = await listSessionChoices();
|
|
937
|
-
if (choices.length === 0) {
|
|
938
|
-
console.log(red("No active sessions."));
|
|
939
|
-
prompt(); return;
|
|
940
|
-
}
|
|
941
|
-
const idx = parseInt(arg) - 1;
|
|
942
|
-
if (idx >= 0 && idx < choices.length) {
|
|
943
|
-
current = choices[idx];
|
|
944
|
-
console.log(green(`Switched to ${current.id} (${path.basename(current.cwd)})`));
|
|
945
|
-
} else {
|
|
946
|
-
// Show picker
|
|
947
|
-
choices.forEach((s, i) => {
|
|
948
|
-
const marker = s.id === current!.id ? green("*") : " ";
|
|
949
|
-
console.log(` ${marker} ${bold(String(i + 1))}. ${cyan(s.id)} ${dim(path.basename(s.cwd))}`);
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
prompt(); return;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
if (trimmed === "/status") {
|
|
956
|
-
await status(current!.dir);
|
|
957
|
-
prompt(); return;
|
|
958
|
-
}
|
|
973
|
+
try {
|
|
974
|
+
if (trimmed === "/quit" || trimmed === "/exit") exitChat();
|
|
959
975
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
976
|
+
if (trimmed === "/sessions") {
|
|
977
|
+
const choices = await listSessionChoices();
|
|
978
|
+
if (choices.length === 0) {
|
|
979
|
+
console.log(dim("No active sessions."));
|
|
980
|
+
} else {
|
|
981
|
+
choices.forEach((s, i) => {
|
|
982
|
+
const marker = s.id === current!.id ? green("*") : " ";
|
|
983
|
+
const pName = path.basename(s.cwd);
|
|
984
|
+
console.log(` ${marker} ${bold(String(i + 1))}. ${cyan(s.id)} ${dim(pName)} | ${s.status} | ${s.minutes}min | ${s.tasks} done`);
|
|
985
|
+
});
|
|
963
986
|
}
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
964
989
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
prompt(); return;
|
|
990
|
+
if (trimmed.startsWith("/switch")) {
|
|
991
|
+
const arg = trimmed.slice(7).trim();
|
|
992
|
+
const choices = await listSessionChoices();
|
|
993
|
+
if (choices.length === 0) {
|
|
994
|
+
console.log(red("No active sessions."));
|
|
995
|
+
continue;
|
|
973
996
|
}
|
|
974
|
-
|
|
975
|
-
if (
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
997
|
+
const idx = parseInt(arg) - 1;
|
|
998
|
+
if (idx >= 0 && idx < choices.length) {
|
|
999
|
+
current = choices[idx];
|
|
1000
|
+
console.log(green(`Switched to ${current.id} (${path.basename(current.cwd)})`));
|
|
1001
|
+
} else {
|
|
1002
|
+
choices.forEach((s, i) => {
|
|
1003
|
+
const marker = s.id === current!.id ? green("*") : " ";
|
|
1004
|
+
console.log(` ${marker} ${bold(String(i + 1))}. ${cyan(s.id)} ${dim(path.basename(s.cwd))}`);
|
|
1005
|
+
});
|
|
983
1006
|
}
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
984
1009
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
prompt(); return;
|
|
988
|
-
}
|
|
1010
|
+
if (trimmed === "/status") { await status(current.dir); continue; }
|
|
1011
|
+
if (trimmed === "/history") { await history(); continue; }
|
|
989
1012
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1013
|
+
if (trimmed.startsWith("/feedback ")) {
|
|
1014
|
+
const msg = trimmed.slice(10).trim();
|
|
1015
|
+
if (msg) await feedback(msg, current.dir);
|
|
1016
|
+
else console.log(red("Usage: /feedback <message>"));
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
994
1019
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1020
|
+
if (trimmed.startsWith("/priority ")) {
|
|
1021
|
+
const task = trimmed.slice(10).trim();
|
|
1022
|
+
if (task) await add(task, 9, current.dir);
|
|
1023
|
+
else console.log(red("Usage: /priority <task>"));
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1000
1026
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1027
|
+
if (trimmed === "/queue") { await listQueueCmd(current.dir); continue; }
|
|
1028
|
+
if (trimmed === "/clear") { await clear(current.dir); continue; }
|
|
1003
1029
|
|
|
1004
|
-
|
|
1005
|
-
console.
|
|
1030
|
+
if (trimmed.startsWith("/")) {
|
|
1031
|
+
console.log(red(`Unknown command: ${trimmed.split(" ")[0]}`));
|
|
1032
|
+
console.log(dim(" Press Tab to see available commands"));
|
|
1033
|
+
continue;
|
|
1006
1034
|
}
|
|
1007
1035
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1036
|
+
// Default: queue as task
|
|
1037
|
+
await add(trimmed, 0, current.dir);
|
|
1038
|
+
} catch (err: any) {
|
|
1039
|
+
console.error(red(err.message));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1013
1042
|
}
|
|
1014
1043
|
|
|
1015
1044
|
// ── Main ──────────────────────────────────────────────────────────────────────
|