groundcrew-cli 0.15.16 → 0.16.0
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 +148 -11
- package/package.json +1 -1
- package/src/index.ts +124 -16
package/dist/index.js
CHANGED
|
@@ -5,9 +5,42 @@ import fs from "fs/promises";
|
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import readline from "readline";
|
|
8
|
-
import { execFile } from "child_process";
|
|
8
|
+
import { execFile, execFileSync } from "child_process";
|
|
9
9
|
import { promisify } from "util";
|
|
10
10
|
var execFileAsync = promisify(execFile);
|
|
11
|
+
function getGitContext() {
|
|
12
|
+
try {
|
|
13
|
+
const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
timeout: 500,
|
|
16
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
17
|
+
}).trim();
|
|
18
|
+
let dirty = "";
|
|
19
|
+
try {
|
|
20
|
+
const status2 = execFileSync("git", ["status", "--porcelain", "-uno"], {
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
timeout: 500,
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
24
|
+
}).trim();
|
|
25
|
+
if (status2) {
|
|
26
|
+
const hasStaged = status2.split("\n").some((l) => l[0] !== " " && l[0] !== "?");
|
|
27
|
+
const hasUnstaged = status2.split("\n").some((l) => l[1] === "M" || l[1] === "D");
|
|
28
|
+
if (hasStaged) dirty += "+";
|
|
29
|
+
if (hasUnstaged) dirty += "*";
|
|
30
|
+
}
|
|
31
|
+
const untracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
timeout: 500,
|
|
34
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
35
|
+
}).trim();
|
|
36
|
+
if (untracked) dirty += "%";
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return { branch, dirty };
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
11
44
|
var GROUNDCREW_DIR = ".groundcrew";
|
|
12
45
|
var SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
|
|
13
46
|
var ACTIVE_SESSIONS_FILE = path.join(GROUNDCREW_DIR, "active-sessions.json");
|
|
@@ -429,7 +462,7 @@ var CHAT_COMMANDS = [
|
|
|
429
462
|
{ cmd: "/clear", desc: "Clear pending tasks" },
|
|
430
463
|
{ cmd: "/exit", desc: "Exit chat" }
|
|
431
464
|
];
|
|
432
|
-
function readMultilineInput(sessionId) {
|
|
465
|
+
function readMultilineInput(sessionId, projectName, gitCtx) {
|
|
433
466
|
return new Promise((resolve) => {
|
|
434
467
|
const lines = [""];
|
|
435
468
|
let crow = 0;
|
|
@@ -443,8 +476,18 @@ function readMultilineInput(sessionId) {
|
|
|
443
476
|
const buf = [];
|
|
444
477
|
if (lastTermRow > 0) buf.push(`\x1B[${lastTermRow}A`);
|
|
445
478
|
buf.push("\r\x1B[J");
|
|
479
|
+
const termW = process.stdout.columns || 80;
|
|
480
|
+
let info = ` ${sessionId} `;
|
|
481
|
+
const ctxParts = [];
|
|
482
|
+
if (projectName) ctxParts.push(projectName);
|
|
483
|
+
if (gitCtx) {
|
|
484
|
+
ctxParts.push(`git:(${gitCtx.branch}${gitCtx.dirty})`);
|
|
485
|
+
}
|
|
486
|
+
if (ctxParts.length) info += ` ${ctxParts.join(" ")} `;
|
|
487
|
+
const dashRight = "\u2500".repeat(Math.max(0, termW - 4 - info.length));
|
|
488
|
+
buf.push(dim("\u2500\u2500\u2500" + info + dashRight));
|
|
446
489
|
for (let i = 0; i < lines.length; i++) {
|
|
447
|
-
|
|
490
|
+
buf.push("\n");
|
|
448
491
|
if (i === 0) {
|
|
449
492
|
buf.push(dim(`[${sessionId}]`) + " " + bold(">") + " " + lines[i]);
|
|
450
493
|
} else {
|
|
@@ -457,7 +500,7 @@ function readMultilineInput(sessionId) {
|
|
|
457
500
|
buf.push("\r");
|
|
458
501
|
const col = padWidth + ccol;
|
|
459
502
|
if (col > 0) buf.push(`\x1B[${col}C`);
|
|
460
|
-
lastTermRow = crow;
|
|
503
|
+
lastTermRow = 1 + crow;
|
|
461
504
|
process.stdout.write(buf.join(""));
|
|
462
505
|
};
|
|
463
506
|
const finish = (result) => {
|
|
@@ -595,6 +638,89 @@ function readMultilineInput(sessionId) {
|
|
|
595
638
|
continue;
|
|
596
639
|
}
|
|
597
640
|
if (str[i] === "\x1B" && i + 1 < str.length && str[i + 1] === "[") {
|
|
641
|
+
const csiMatch = str.slice(i).match(/^\x1b\[(\d+);(\d+)u/);
|
|
642
|
+
if (csiMatch) {
|
|
643
|
+
const codepoint = parseInt(csiMatch[1], 10);
|
|
644
|
+
const modifier = parseInt(csiMatch[2], 10);
|
|
645
|
+
const isCtrl = modifier - 1 & 4;
|
|
646
|
+
const seqLen = csiMatch[0].length;
|
|
647
|
+
if (isCtrl) {
|
|
648
|
+
switch (codepoint) {
|
|
649
|
+
case 99:
|
|
650
|
+
if (fullText() || lines.length > 1 || lines[0].length > 0) {
|
|
651
|
+
const lastRow = lines.length - 1;
|
|
652
|
+
const rowsDown = lastRow - crow;
|
|
653
|
+
if (rowsDown > 0) process.stdout.write(`\x1B[${rowsDown}B`);
|
|
654
|
+
process.stdout.write("\r\n");
|
|
655
|
+
lines.length = 0;
|
|
656
|
+
lines.push("");
|
|
657
|
+
crow = 0;
|
|
658
|
+
ccol = 0;
|
|
659
|
+
lastTermRow = 0;
|
|
660
|
+
render();
|
|
661
|
+
} else {
|
|
662
|
+
process.stdout.write("\r\n");
|
|
663
|
+
finish(null);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
i += seqLen;
|
|
667
|
+
continue;
|
|
668
|
+
case 100:
|
|
669
|
+
if (fullText()) {
|
|
670
|
+
doDelete();
|
|
671
|
+
} else {
|
|
672
|
+
process.stdout.write("\n");
|
|
673
|
+
finish(null);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
i += seqLen;
|
|
677
|
+
continue;
|
|
678
|
+
case 97:
|
|
679
|
+
ccol = 0;
|
|
680
|
+
render();
|
|
681
|
+
i += seqLen;
|
|
682
|
+
continue;
|
|
683
|
+
case 101:
|
|
684
|
+
ccol = lines[crow].length;
|
|
685
|
+
render();
|
|
686
|
+
i += seqLen;
|
|
687
|
+
continue;
|
|
688
|
+
case 117:
|
|
689
|
+
lines[crow] = lines[crow].slice(ccol);
|
|
690
|
+
ccol = 0;
|
|
691
|
+
render();
|
|
692
|
+
i += seqLen;
|
|
693
|
+
continue;
|
|
694
|
+
case 107:
|
|
695
|
+
lines[crow] = lines[crow].slice(0, ccol);
|
|
696
|
+
render();
|
|
697
|
+
i += seqLen;
|
|
698
|
+
continue;
|
|
699
|
+
case 119:
|
|
700
|
+
{
|
|
701
|
+
const before = lines[crow].slice(0, ccol);
|
|
702
|
+
const stripped = before.replace(/\s+$/, "");
|
|
703
|
+
const sp = stripped.lastIndexOf(" ");
|
|
704
|
+
const newBefore = sp >= 0 ? stripped.slice(0, sp + 1) : "";
|
|
705
|
+
lines[crow] = newBefore + lines[crow].slice(ccol);
|
|
706
|
+
ccol = newBefore.length;
|
|
707
|
+
render();
|
|
708
|
+
}
|
|
709
|
+
i += seqLen;
|
|
710
|
+
continue;
|
|
711
|
+
case 108:
|
|
712
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
713
|
+
lastTermRow = 0;
|
|
714
|
+
render();
|
|
715
|
+
i += seqLen;
|
|
716
|
+
continue;
|
|
717
|
+
default:
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
i += seqLen;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
598
724
|
let j = i + 2;
|
|
599
725
|
while (j < str.length && str.charCodeAt(j) >= 48 && str.charCodeAt(j) <= 63) j++;
|
|
600
726
|
if (j < str.length) j++;
|
|
@@ -606,11 +732,12 @@ function readMultilineInput(sessionId) {
|
|
|
606
732
|
continue;
|
|
607
733
|
}
|
|
608
734
|
if (str[i] === "") {
|
|
609
|
-
|
|
735
|
+
const hasText = fullText();
|
|
736
|
+
if (hasText || lines.length > 1 || lines[0].length > 0) {
|
|
610
737
|
const lastRow = lines.length - 1;
|
|
611
738
|
const rowsDown = lastRow - crow;
|
|
612
739
|
if (rowsDown > 0) process.stdout.write(`\x1B[${rowsDown}B`);
|
|
613
|
-
process.stdout.write("\n");
|
|
740
|
+
process.stdout.write("\r\n");
|
|
614
741
|
lines.length = 0;
|
|
615
742
|
lines.push("");
|
|
616
743
|
crow = 0;
|
|
@@ -618,7 +745,7 @@ function readMultilineInput(sessionId) {
|
|
|
618
745
|
lastTermRow = 0;
|
|
619
746
|
render();
|
|
620
747
|
} else {
|
|
621
|
-
process.stdout.write("\n");
|
|
748
|
+
process.stdout.write("\r\n");
|
|
622
749
|
finish(null);
|
|
623
750
|
return;
|
|
624
751
|
}
|
|
@@ -672,6 +799,13 @@ function readMultilineInput(sessionId) {
|
|
|
672
799
|
i++;
|
|
673
800
|
continue;
|
|
674
801
|
}
|
|
802
|
+
if (str[i] === "\f") {
|
|
803
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
804
|
+
lastTermRow = 0;
|
|
805
|
+
render();
|
|
806
|
+
i++;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
675
809
|
if (str[i] === "\n") {
|
|
676
810
|
insertNewline();
|
|
677
811
|
i++;
|
|
@@ -687,8 +821,10 @@ function readMultilineInput(sessionId) {
|
|
|
687
821
|
continue;
|
|
688
822
|
}
|
|
689
823
|
if (str[i] === " ") {
|
|
690
|
-
|
|
691
|
-
|
|
824
|
+
const currentLine = lines[crow];
|
|
825
|
+
if (lines.length === 1 && currentLine.startsWith("/")) {
|
|
826
|
+
const partial = currentLine.split(" ")[0];
|
|
827
|
+
const matches = CHAT_COMMANDS.filter((c) => c.cmd.startsWith(partial));
|
|
692
828
|
if (matches.length === 1) {
|
|
693
829
|
lines[0] = matches[0].cmd + " ";
|
|
694
830
|
ccol = lines[0].length;
|
|
@@ -697,7 +833,7 @@ function readMultilineInput(sessionId) {
|
|
|
697
833
|
const lastRow = lines.length - 1;
|
|
698
834
|
const rowsDown = lastRow - crow;
|
|
699
835
|
if (rowsDown > 0) process.stdout.write(`\x1B[${rowsDown}B`);
|
|
700
|
-
process.stdout.write("\n");
|
|
836
|
+
process.stdout.write("\r\n");
|
|
701
837
|
for (const m of matches) {
|
|
702
838
|
process.stdout.write(` ${cyan(m.cmd.padEnd(14))} ${dim(m.desc)}
|
|
703
839
|
`);
|
|
@@ -812,7 +948,8 @@ async function chat(explicitSession) {
|
|
|
812
948
|
process.exit(0);
|
|
813
949
|
};
|
|
814
950
|
while (true) {
|
|
815
|
-
const
|
|
951
|
+
const gitCtx = getGitContext();
|
|
952
|
+
const text = await readMultilineInput(current.id, projectName, gitCtx);
|
|
816
953
|
if (text === null) exitChat();
|
|
817
954
|
if (!text) continue;
|
|
818
955
|
const trimmed = text.trim();
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,11 +2,40 @@ import fs from "fs/promises";
|
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import readline from "readline";
|
|
5
|
-
import { execFile } from "child_process";
|
|
5
|
+
import { execFile, execFileSync } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
7
|
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
|
|
10
|
+
// Git context for separator line (cached, refreshed periodically)
|
|
11
|
+
function getGitContext(): { branch: string; dirty: string } | null {
|
|
12
|
+
try {
|
|
13
|
+
const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
14
|
+
encoding: "utf8", timeout: 500, stdio: ["pipe", "pipe", "pipe"],
|
|
15
|
+
}).trim();
|
|
16
|
+
// Status indicators matching Copilot CLI: * unstaged, + staged, % untracked
|
|
17
|
+
let dirty = "";
|
|
18
|
+
try {
|
|
19
|
+
const status = execFileSync("git", ["status", "--porcelain", "-uno"], {
|
|
20
|
+
encoding: "utf8", timeout: 500, stdio: ["pipe", "pipe", "pipe"],
|
|
21
|
+
}).trim();
|
|
22
|
+
if (status) {
|
|
23
|
+
const hasStaged = status.split("\n").some(l => l[0] !== " " && l[0] !== "?");
|
|
24
|
+
const hasUnstaged = status.split("\n").some(l => l[1] === "M" || l[1] === "D");
|
|
25
|
+
if (hasStaged) dirty += "+";
|
|
26
|
+
if (hasUnstaged) dirty += "*";
|
|
27
|
+
}
|
|
28
|
+
const untracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
29
|
+
encoding: "utf8", timeout: 500, stdio: ["pipe", "pipe", "pipe"],
|
|
30
|
+
}).trim();
|
|
31
|
+
if (untracked) dirty += "%";
|
|
32
|
+
} catch { /* ignore status errors */ }
|
|
33
|
+
return { branch, dirty };
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
10
39
|
// Resolved at startup by resolveRoot() — git-aware project root discovery
|
|
11
40
|
let GROUNDCREW_DIR = ".groundcrew";
|
|
12
41
|
let SESSIONS_DIR = path.join(GROUNDCREW_DIR, "sessions");
|
|
@@ -578,7 +607,7 @@ const CHAT_COMMANDS: Array<{ cmd: string; desc: string }> = [
|
|
|
578
607
|
* Tab: slash command completion
|
|
579
608
|
* Paste: bracketed paste with multiline support
|
|
580
609
|
*/
|
|
581
|
-
function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
610
|
+
function readMultilineInput(sessionId: string, projectName: string, gitCtx: { branch: string; dirty: string } | null): Promise<string | null> {
|
|
582
611
|
return new Promise((resolve) => {
|
|
583
612
|
const lines: string[] = [""];
|
|
584
613
|
let crow = 0; // cursor row in lines[]
|
|
@@ -587,7 +616,7 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
587
616
|
// Visible width of prompt: "[sessionId] > "
|
|
588
617
|
const padWidth = sessionId.length + 5; // [ + id + ] + space + > + space = len+5
|
|
589
618
|
|
|
590
|
-
// Track
|
|
619
|
+
// Track how many rows up from cursor to top of rendered area (including separator)
|
|
591
620
|
let lastTermRow = 0;
|
|
592
621
|
let pasteBuffer = "";
|
|
593
622
|
let isPasting = false;
|
|
@@ -597,13 +626,25 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
597
626
|
const render = () => {
|
|
598
627
|
const buf: string[] = [];
|
|
599
628
|
|
|
600
|
-
// Move to start of input area
|
|
629
|
+
// Move to start of input area (includes separator line)
|
|
601
630
|
if (lastTermRow > 0) buf.push(`\x1b[${lastTermRow}A`);
|
|
602
631
|
buf.push("\r\x1b[J"); // col 0 + clear to end of screen
|
|
603
632
|
|
|
604
|
-
//
|
|
633
|
+
// Separator line: ─── sessionId projectName git:(branch*) ───
|
|
634
|
+
const termW = process.stdout.columns || 80;
|
|
635
|
+
let info = ` ${sessionId} `;
|
|
636
|
+
const ctxParts: string[] = [];
|
|
637
|
+
if (projectName) ctxParts.push(projectName);
|
|
638
|
+
if (gitCtx) {
|
|
639
|
+
ctxParts.push(`git:(${gitCtx.branch}${gitCtx.dirty})`);
|
|
640
|
+
}
|
|
641
|
+
if (ctxParts.length) info += ` ${ctxParts.join(" ")} `;
|
|
642
|
+
const dashRight = "─".repeat(Math.max(0, termW - 4 - info.length));
|
|
643
|
+
buf.push(dim("───" + info + dashRight));
|
|
644
|
+
|
|
645
|
+
// Draw each input line (below separator)
|
|
605
646
|
for (let i = 0; i < lines.length; i++) {
|
|
606
|
-
|
|
647
|
+
buf.push("\n");
|
|
607
648
|
if (i === 0) {
|
|
608
649
|
buf.push(dim(`[${sessionId}]`) + " " + bold(">") + " " + lines[i]);
|
|
609
650
|
} else {
|
|
@@ -620,7 +661,9 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
620
661
|
const col = padWidth + ccol;
|
|
621
662
|
if (col > 0) buf.push(`\x1b[${col}C`);
|
|
622
663
|
|
|
623
|
-
lastTermRow =
|
|
664
|
+
// lastTermRow = rows from cursor back to top of separator
|
|
665
|
+
// separator is 1 row, then crow rows of input below it
|
|
666
|
+
lastTermRow = 1 + crow;
|
|
624
667
|
process.stdout.write(buf.join(""));
|
|
625
668
|
};
|
|
626
669
|
|
|
@@ -740,8 +783,63 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
740
783
|
// End (\x1b[F)
|
|
741
784
|
if (str.startsWith("\x1b[F", i)) { ccol = lines[crow].length; render(); i += 3; continue; }
|
|
742
785
|
|
|
743
|
-
//
|
|
786
|
+
// CSI u (Kitty keyboard protocol) — decode \x1b[{codepoint};{modifier}u
|
|
787
|
+
// modifier bit 3 (value 4) = Ctrl, sent as bits+1 so modifier 5 = Ctrl
|
|
744
788
|
if (str[i] === "\x1b" && i + 1 < str.length && str[i + 1] === "[") {
|
|
789
|
+
const csiMatch = str.slice(i).match(/^\x1b\[(\d+);(\d+)u/);
|
|
790
|
+
if (csiMatch) {
|
|
791
|
+
const codepoint = parseInt(csiMatch[1], 10);
|
|
792
|
+
const modifier = parseInt(csiMatch[2], 10);
|
|
793
|
+
const isCtrl = (modifier - 1) & 4;
|
|
794
|
+
const seqLen = csiMatch[0].length;
|
|
795
|
+
|
|
796
|
+
if (isCtrl) {
|
|
797
|
+
switch (codepoint) {
|
|
798
|
+
case 99: // Ctrl+C
|
|
799
|
+
if (fullText() || lines.length > 1 || lines[0].length > 0) {
|
|
800
|
+
const lastRow = lines.length - 1;
|
|
801
|
+
const rowsDown = lastRow - crow;
|
|
802
|
+
if (rowsDown > 0) process.stdout.write(`\x1b[${rowsDown}B`);
|
|
803
|
+
process.stdout.write("\r\n");
|
|
804
|
+
lines.length = 0; lines.push("");
|
|
805
|
+
crow = 0; ccol = 0; lastTermRow = 0;
|
|
806
|
+
render();
|
|
807
|
+
} else {
|
|
808
|
+
process.stdout.write("\r\n");
|
|
809
|
+
finish(null); return;
|
|
810
|
+
}
|
|
811
|
+
i += seqLen; continue;
|
|
812
|
+
case 100: // Ctrl+D
|
|
813
|
+
if (fullText()) { doDelete(); } else { process.stdout.write("\n"); finish(null); return; }
|
|
814
|
+
i += seqLen; continue;
|
|
815
|
+
case 97: // Ctrl+A — home
|
|
816
|
+
ccol = 0; render(); i += seqLen; continue;
|
|
817
|
+
case 101: // Ctrl+E — end
|
|
818
|
+
ccol = lines[crow].length; render(); i += seqLen; continue;
|
|
819
|
+
case 117: // Ctrl+U — clear before cursor
|
|
820
|
+
lines[crow] = lines[crow].slice(ccol); ccol = 0; render(); i += seqLen; continue;
|
|
821
|
+
case 107: // Ctrl+K — clear after cursor
|
|
822
|
+
lines[crow] = lines[crow].slice(0, ccol); render(); i += seqLen; continue;
|
|
823
|
+
case 119: // Ctrl+W — delete word before cursor
|
|
824
|
+
{ const before = lines[crow].slice(0, ccol);
|
|
825
|
+
const stripped = before.replace(/\s+$/, "");
|
|
826
|
+
const sp = stripped.lastIndexOf(" ");
|
|
827
|
+
const newBefore = sp >= 0 ? stripped.slice(0, sp + 1) : "";
|
|
828
|
+
lines[crow] = newBefore + lines[crow].slice(ccol);
|
|
829
|
+
ccol = newBefore.length; render(); }
|
|
830
|
+
i += seqLen; continue;
|
|
831
|
+
case 108: // Ctrl+L — clear screen
|
|
832
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
833
|
+
lastTermRow = 0; render();
|
|
834
|
+
i += seqLen; continue;
|
|
835
|
+
default: break;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Unhandled CSI u — skip it
|
|
839
|
+
i += seqLen; continue;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Skip unknown CSI sequences (non-u final byte)
|
|
745
843
|
let j = i + 2;
|
|
746
844
|
while (j < str.length && str.charCodeAt(j) >= 0x30 && str.charCodeAt(j) <= 0x3f) j++;
|
|
747
845
|
if (j < str.length) j++; // skip final byte
|
|
@@ -752,17 +850,18 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
752
850
|
|
|
753
851
|
// Ctrl+C — clear input or exit
|
|
754
852
|
if (str[i] === "\x03") {
|
|
755
|
-
|
|
756
|
-
|
|
853
|
+
const hasText = fullText();
|
|
854
|
+
if (hasText || lines.length > 1 || lines[0].length > 0) {
|
|
855
|
+
// Move past current rendering to below last line, start fresh
|
|
757
856
|
const lastRow = lines.length - 1;
|
|
758
857
|
const rowsDown = lastRow - crow;
|
|
759
858
|
if (rowsDown > 0) process.stdout.write(`\x1b[${rowsDown}B`);
|
|
760
|
-
process.stdout.write("\n");
|
|
859
|
+
process.stdout.write("\r\n");
|
|
761
860
|
lines.length = 0; lines.push("");
|
|
762
861
|
crow = 0; ccol = 0; lastTermRow = 0;
|
|
763
862
|
render();
|
|
764
863
|
} else {
|
|
765
|
-
process.stdout.write("\n");
|
|
864
|
+
process.stdout.write("\r\n");
|
|
766
865
|
finish(null); return;
|
|
767
866
|
}
|
|
768
867
|
i++; continue;
|
|
@@ -795,6 +894,11 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
795
894
|
lines[crow] = newBefore + lines[crow].slice(ccol);
|
|
796
895
|
ccol = newBefore.length; render(); i++; continue;
|
|
797
896
|
}
|
|
897
|
+
// Ctrl+L — clear screen (legacy byte)
|
|
898
|
+
if (str[i] === "\x0c") {
|
|
899
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
900
|
+
lastTermRow = 0; render(); i++; continue;
|
|
901
|
+
}
|
|
798
902
|
|
|
799
903
|
// Ctrl+J (LF, 0x0A) — newline (cross-terminal)
|
|
800
904
|
if (str[i] === "\n") { insertNewline(); i++; continue; }
|
|
@@ -807,8 +911,10 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
807
911
|
|
|
808
912
|
// Tab — slash command completion
|
|
809
913
|
if (str[i] === "\t") {
|
|
810
|
-
|
|
811
|
-
|
|
914
|
+
const currentLine = lines[crow];
|
|
915
|
+
if (lines.length === 1 && currentLine.startsWith("/")) {
|
|
916
|
+
const partial = currentLine.split(" ")[0]; // only match command part
|
|
917
|
+
const matches = CHAT_COMMANDS.filter(c => c.cmd.startsWith(partial));
|
|
812
918
|
if (matches.length === 1) {
|
|
813
919
|
lines[0] = matches[0].cmd + " ";
|
|
814
920
|
ccol = lines[0].length; render();
|
|
@@ -817,7 +923,7 @@ function readMultilineInput(sessionId: string): Promise<string | null> {
|
|
|
817
923
|
const lastRow = lines.length - 1;
|
|
818
924
|
const rowsDown = lastRow - crow;
|
|
819
925
|
if (rowsDown > 0) process.stdout.write(`\x1b[${rowsDown}B`);
|
|
820
|
-
process.stdout.write("\n");
|
|
926
|
+
process.stdout.write("\r\n");
|
|
821
927
|
for (const m of matches) {
|
|
822
928
|
process.stdout.write(` ${cyan(m.cmd.padEnd(14))} ${dim(m.desc)}\n`);
|
|
823
929
|
}
|
|
@@ -952,7 +1058,9 @@ async function chat(explicitSession?: string): Promise<void> {
|
|
|
952
1058
|
|
|
953
1059
|
// ── Main chat loop ─────────────────────────────────────────────────────────────────
|
|
954
1060
|
while (true) {
|
|
955
|
-
|
|
1061
|
+
// Refresh git context each turn (branch may change between prompts)
|
|
1062
|
+
const gitCtx = getGitContext();
|
|
1063
|
+
const text = await readMultilineInput(current.id, projectName, gitCtx);
|
|
956
1064
|
|
|
957
1065
|
if (text === null) exitChat();
|
|
958
1066
|
if (!text) continue;
|