groundcrew-cli 0.15.9 → 0.15.11

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.
Files changed (3) hide show
  1. package/dist/index.js +56 -12
  2. package/package.json +1 -1
  3. package/src/index.ts +84 -25
package/dist/index.js CHANGED
@@ -426,8 +426,14 @@ function setupInlineSuggestions(rl) {
426
426
  buf.push(`\x1B[${dropdownLines}A`);
427
427
  dropdownLines = 0;
428
428
  }
429
- buf.push("\x1B[K");
430
- ghostLen = 0;
429
+ if (ghostLen > 0) {
430
+ buf.push("\x1B[s");
431
+ buf.push("\x1B[999C");
432
+ buf.push(`\x1B[${ghostLen}D`);
433
+ buf.push("\x1B[K");
434
+ buf.push("\x1B[u");
435
+ ghostLen = 0;
436
+ }
431
437
  if (buf.length) process.stdout.write(buf.join(""));
432
438
  };
433
439
  const showGhost = () => {
@@ -456,13 +462,14 @@ function setupInlineSuggestions(rl) {
456
462
  }
457
463
  dropdownLines = count;
458
464
  buf.push(`\x1B[${count}A`);
459
- const prompt = rl._prompt || "";
460
- const inputLine = rl.line || "";
461
- buf.push(`\r${prompt}${inputLine}`);
465
+ buf.push(`\r`);
462
466
  }
463
467
  process.stdout.write(buf.join(""));
464
- if (dropdownLines > 0 && remainder) {
465
- process.stdout.write(`\x1B[K\x1B[2m${remainder}\x1B[0m\x1B[${remainder.length}D`);
468
+ if (dropdownLines > 0) {
469
+ rl._refreshLine();
470
+ if (remainder) {
471
+ process.stdout.write(`\x1B[K\x1B[2m${remainder}\x1B[0m\x1B[${remainder.length}D`);
472
+ }
466
473
  }
467
474
  };
468
475
  process.stdin.on("keypress", (_ch, key) => {
@@ -474,18 +481,55 @@ function setupInlineSuggestions(rl) {
474
481
  });
475
482
  }
476
483
  async function chat(explicitSession) {
477
- process.stdout.write("\x1B[>1u");
484
+ process.stdout.write("\x1B[?2004h\x1B[>1u");
478
485
  const originalStdinEmit = process.stdin.emit.bind(process.stdin);
479
- let pendingShiftEnter = false;
486
+ let pasteBuffer = "";
487
+ let isPasting = false;
480
488
  process.stdin.emit = function(event, ...args) {
481
489
  if (event === "data") {
482
490
  const data = args[0];
483
- const str = typeof data === "string" ? data : data.toString();
491
+ let str = typeof data === "string" ? data : data.toString();
492
+ const pasteStart = str.indexOf("\x1B[200~");
493
+ if (pasteStart !== -1) {
494
+ isPasting = true;
495
+ if (pasteStart > 0) {
496
+ originalStdinEmit(event, Buffer.from(str.slice(0, pasteStart)));
497
+ }
498
+ str = str.slice(pasteStart + 6);
499
+ }
500
+ if (isPasting) {
501
+ const pasteEnd = str.indexOf("\x1B[201~");
502
+ if (pasteEnd !== -1) {
503
+ pasteBuffer += str.slice(0, pasteEnd);
504
+ const afterPaste = str.slice(pasteEnd + 6);
505
+ isPasting = false;
506
+ const pasted = pasteBuffer.replace(/[\r\n]+$/, "");
507
+ pasteBuffer = "";
508
+ if (pasted.includes("\n") || pasted.includes("\r")) {
509
+ const lines = pasted.split(/\r?\n/);
510
+ for (let i = 0; i < lines.length - 1; i++) {
511
+ originalStdinEmit(event, Buffer.from(lines[i] + "\\\r"));
512
+ }
513
+ originalStdinEmit(event, Buffer.from(lines[lines.length - 1]));
514
+ } else {
515
+ originalStdinEmit(event, Buffer.from(pasted));
516
+ }
517
+ if (afterPaste) {
518
+ return originalStdinEmit(event, Buffer.from(afterPaste));
519
+ }
520
+ return false;
521
+ } else {
522
+ pasteBuffer += str;
523
+ return false;
524
+ }
525
+ }
484
526
  if (str.includes("\x1B[13;2u")) {
485
- pendingShiftEnter = true;
486
527
  const replaced = str.replace(/\x1b\[13;2u/g, "\\\r");
487
528
  return originalStdinEmit(event, Buffer.from(replaced));
488
529
  }
530
+ if (str === "\x1B\r" || str === "\x1B\n") {
531
+ return originalStdinEmit(event, Buffer.from("\\\r"));
532
+ }
489
533
  }
490
534
  return originalStdinEmit(event, ...args);
491
535
  };
@@ -543,7 +587,7 @@ async function chat(explicitSession) {
543
587
  console.log();
544
588
  let continuationBuffer = [];
545
589
  rl.on("close", () => {
546
- process.stdout.write("\x1B[<u");
590
+ process.stdout.write("\x1B[?2004l\x1B[<u");
547
591
  console.log(dim("\nBye."));
548
592
  process.exit(0);
549
593
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundcrew-cli",
3
- "version": "0.15.9",
3
+ "version": "0.15.11",
4
4
  "description": "CLI companion for Groundcrew — queue tasks, send feedback, monitor your Copilot agent from another terminal.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -559,10 +559,17 @@ function setupInlineSuggestions(rl: readline.Interface): void {
559
559
  buf.push(`\x1b[${dropdownLines}A`); // back up to prompt line
560
560
  dropdownLines = 0;
561
561
  }
562
- // Clear inline ghost: readline already repositioned cursor at end of
563
- // typed text after processing the keypress, so just clear to EOL
564
- buf.push("\x1b[K");
565
- ghostLen = 0;
562
+ // Clear inline ghost only if one is showing — save cursor, jump to
563
+ // ghost start (far right), erase it, then restore cursor position.
564
+ // This avoids nuking typed text when cursor is mid-line (left arrow).
565
+ if (ghostLen > 0) {
566
+ buf.push("\x1b[s"); // save cursor position
567
+ buf.push("\x1b[999C"); // jump to far right of line
568
+ buf.push(`\x1b[${ghostLen}D`); // back up to ghost start
569
+ buf.push("\x1b[K"); // clear from ghost start to EOL
570
+ buf.push("\x1b[u"); // restore cursor position
571
+ ghostLen = 0;
572
+ }
566
573
  if (buf.length) process.stdout.write(buf.join(""));
567
574
  };
568
575
 
@@ -604,25 +611,28 @@ function setupInlineSuggestions(rl: readline.Interface): void {
604
611
  }
605
612
  dropdownLines = count;
606
613
 
607
- // Move back up to prompt line
614
+ // Back to prompt line and restore horizontal position
615
+ // Move up to prompt line
608
616
  buf.push(`\x1b[${count}A`);
609
- // Redraw prompt line manually (NOT _refreshLine it clears screen below)
610
- const prompt = (rl as any)._prompt || "";
611
- const inputLine = (rl as any).line || "";
612
- buf.push(`\r${prompt}${inputLine}`);
617
+ // Move to column 1, then rewrite prompt+line to position cursor correctly
618
+ buf.push(`\r`);
619
+ // Let readline handle cursor positioning
613
620
  }
614
621
 
615
622
  process.stdout.write(buf.join(""));
616
- // Re-draw inline ghost after prompt redraw (for dropdown case)
617
- if (dropdownLines > 0 && remainder) {
618
- process.stdout.write(`\x1b[K\x1b[2m${remainder}\x1b[0m\x1b[${remainder.length}D`);
623
+ // After dropdown, force readline to redraw prompt line to fix cursor position
624
+ if (dropdownLines > 0) {
625
+ (rl as any)._refreshLine();
626
+ // Re-draw inline ghost since _refreshLine cleared it
627
+ if (remainder) {
628
+ process.stdout.write(`\x1b[K\x1b[2m${remainder}\x1b[0m\x1b[${remainder.length}D`);
629
+ }
619
630
  }
620
631
  };
621
632
 
622
633
  process.stdin.on("keypress", (_ch: string, key: any) => {
623
634
  if (!key) return;
624
635
 
625
-
626
636
  clearGhost();
627
637
  if (key.name !== "return" && key.name !== "tab") {
628
638
  setImmediate(showGhost);
@@ -631,25 +641,74 @@ function setupInlineSuggestions(rl: readline.Interface): void {
631
641
  }
632
642
 
633
643
  async function chat(explicitSession?: string): Promise<void> {
634
- // Enable Kitty keyboard protocol for Shift+Enter detection
635
- // iTerm2, Kitty, WezTerm, and other modern terminals support this
636
- process.stdout.write("\x1b[>1u");
644
+ // Enable bracketed paste mode + Kitty keyboard protocol (Shift+Enter detection)
645
+ process.stdout.write("\x1b[?2004h\x1b[>1u");
637
646
 
638
- // Intercept raw stdin to detect Shift+Enter (\x1b[13;2u) before readline
639
- // Transform it into backslash + Enter for line continuation
647
+ // Intercept raw stdin for:
648
+ // 1. Bracketed paste (\x1b[200~ ... \x1b[201~) buffer paste, submit as single task
649
+ // 2. Shift+Enter (\x1b[13;2u via Kitty protocol) — line continuation
650
+ // 3. Alt+Enter (\x1b\r — universal fallback) — line continuation
640
651
  const originalStdinEmit = process.stdin.emit.bind(process.stdin);
641
- let pendingShiftEnter = false;
652
+ let pasteBuffer = "";
653
+ let isPasting = false;
654
+
642
655
  process.stdin.emit = function (event: string, ...args: any[]) {
643
656
  if (event === "data") {
644
657
  const data = args[0] as Buffer | string;
645
- const str = typeof data === "string" ? data : data.toString();
658
+ let str = typeof data === "string" ? data : data.toString();
659
+
660
+ // --- Bracketed paste handling ---
661
+ const pasteStart = str.indexOf("\x1b[200~");
662
+ if (pasteStart !== -1) {
663
+ isPasting = true;
664
+ if (pasteStart > 0) {
665
+ originalStdinEmit(event, Buffer.from(str.slice(0, pasteStart)));
666
+ }
667
+ str = str.slice(pasteStart + 6); // skip \x1b[200~
668
+ }
669
+
670
+ if (isPasting) {
671
+ const pasteEnd = str.indexOf("\x1b[201~");
672
+ if (pasteEnd !== -1) {
673
+ pasteBuffer += str.slice(0, pasteEnd);
674
+ const afterPaste = str.slice(pasteEnd + 6);
675
+ isPasting = false;
676
+
677
+ const pasted = pasteBuffer.replace(/[\r\n]+$/, "");
678
+ pasteBuffer = "";
679
+
680
+ if (pasted.includes("\n") || pasted.includes("\r")) {
681
+ // Multi-line paste: use backslash continuation for each internal line
682
+ const lines = pasted.split(/\r?\n/);
683
+ for (let i = 0; i < lines.length - 1; i++) {
684
+ originalStdinEmit(event, Buffer.from(lines[i] + "\\\r"));
685
+ }
686
+ // Last line: insert without auto-submit
687
+ originalStdinEmit(event, Buffer.from(lines[lines.length - 1]));
688
+ } else {
689
+ originalStdinEmit(event, Buffer.from(pasted));
690
+ }
691
+
692
+ if (afterPaste) {
693
+ return originalStdinEmit(event, Buffer.from(afterPaste));
694
+ }
695
+ return false;
696
+ } else {
697
+ pasteBuffer += str;
698
+ return false;
699
+ }
700
+ }
701
+
702
+ // --- Shift+Enter (Kitty protocol: \x1b[13;2u) ---
646
703
  if (str.includes("\x1b[13;2u")) {
647
- // Shift+Enter detected — append backslash and submit for continuation
648
- pendingShiftEnter = true;
649
- // Replace the Kitty sequence with backslash + carriage return
650
704
  const replaced = str.replace(/\x1b\[13;2u/g, "\\\r");
651
705
  return originalStdinEmit(event, Buffer.from(replaced));
652
706
  }
707
+
708
+ // --- Alt+Enter (universal fallback: ESC + CR) ---
709
+ if (str === "\x1b\r" || str === "\x1b\n") {
710
+ return originalStdinEmit(event, Buffer.from("\\\r"));
711
+ }
653
712
  }
654
713
  return originalStdinEmit(event, ...args);
655
714
  } as any;
@@ -714,9 +773,9 @@ async function chat(explicitSession?: string): Promise<void> {
714
773
 
715
774
  let continuationBuffer: string[] = [];
716
775
 
717
- // Handle Ctrl+C gracefully — disable Kitty protocol on exit
776
+ // Handle Ctrl+C gracefully
718
777
  rl.on("close", () => {
719
- process.stdout.write("\x1b[<u"); // disable Kitty keyboard protocol
778
+ process.stdout.write("\x1b[?2004l\x1b[<u"); // disable bracketed paste + Kitty
720
779
  console.log(dim("\nBye."));
721
780
  process.exit(0);
722
781
  });