groundcrew-cli 0.15.3 → 0.15.5

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 +19 -11
  2. package/package.json +1 -1
  3. package/src/index.ts +38 -20
package/dist/index.js CHANGED
@@ -416,11 +416,7 @@ function chatCompleter(line) {
416
416
  }
417
417
  function setupInlineSuggestions(rl) {
418
418
  let dropdownLines = 0;
419
- let cursorCol = 0;
420
- const getPromptLen = () => {
421
- const p = rl._prompt || "";
422
- return p.replace(/\x1b\[[0-9;]*m/g, "").length;
423
- };
419
+ let ghostLen = 0;
424
420
  const clearGhost = () => {
425
421
  const buf = [];
426
422
  if (dropdownLines > 0) {
@@ -430,9 +426,8 @@ function setupInlineSuggestions(rl) {
430
426
  buf.push(`\x1B[${dropdownLines}A`);
431
427
  dropdownLines = 0;
432
428
  }
433
- if (cursorCol > 0) {
434
- buf.push(`\x1B[${cursorCol}G\x1B[K`);
435
- }
429
+ buf.push("\x1B[K");
430
+ ghostLen = 0;
436
431
  if (buf.length) process.stdout.write(buf.join(""));
437
432
  };
438
433
  const showGhost = () => {
@@ -444,11 +439,12 @@ function setupInlineSuggestions(rl) {
444
439
  const best = shown[0];
445
440
  const remainder = best.cmd.slice(line.length);
446
441
  if (!remainder && shown.length === 1) return;
447
- cursorCol = getPromptLen() + line.length + 1;
448
442
  const buf = [];
449
- buf.push(`\x1B[${cursorCol}G\x1B[K`);
443
+ buf.push("\x1B[K");
450
444
  if (remainder) {
451
445
  buf.push(`\x1B[2m${remainder}\x1B[0m`);
446
+ ghostLen = remainder.length;
447
+ buf.push(`\x1B[${remainder.length}D`);
452
448
  }
453
449
  if (shown.length > 1 || shown.length === 1 && remainder) {
454
450
  const count = shown.length;
@@ -460,12 +456,24 @@ function setupInlineSuggestions(rl) {
460
456
  }
461
457
  dropdownLines = count;
462
458
  buf.push(`\x1B[${count}A`);
459
+ buf.push(`\r`);
463
460
  }
464
- buf.push(`\x1B[${cursorCol}G`);
465
461
  process.stdout.write(buf.join(""));
462
+ if (dropdownLines > 0) {
463
+ rl._refreshLine();
464
+ if (remainder) {
465
+ process.stdout.write(`\x1B[K\x1B[2m${remainder}\x1B[0m\x1B[${remainder.length}D`);
466
+ }
467
+ }
466
468
  };
467
469
  process.stdin.on("keypress", (_ch, key) => {
468
470
  if (!key) return;
471
+ if (key.name === "return" && key.shift) {
472
+ const line = rl.line;
473
+ rl.line = line + "\\";
474
+ rl.cursor = rl.line.length;
475
+ return;
476
+ }
469
477
  clearGhost();
470
478
  if (key.name !== "return" && key.name !== "tab") {
471
479
  setImmediate(showGhost);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundcrew-cli",
3
- "version": "0.15.3",
3
+ "version": "0.15.5",
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
@@ -543,16 +543,11 @@ function chatCompleter(line: string): [string[], string] {
543
543
  * Show inline ghost + multi-line dropdown as user types / commands.
544
544
  * Best match completion shown inline (dimmed) after cursor.
545
545
  * All matches (max 5) shown as dropdown lines below the prompt.
546
- * Uses explicit column + relative cursor movement (no save/restore).
546
+ * Uses only relative cursor movement no absolute column needed.
547
547
  */
548
548
  function setupInlineSuggestions(rl: readline.Interface): void {
549
- let dropdownLines = 0; // extra lines rendered below prompt
550
- let cursorCol = 0; // 1-based column where typed text ends
551
-
552
- const getPromptLen = (): number => {
553
- const p = (rl as any)._prompt as string || "";
554
- return p.replace(/\x1b\[[0-9;]*m/g, "").length;
555
- };
549
+ let dropdownLines = 0; // extra lines rendered below prompt
550
+ let ghostLen = 0; // length of inline ghost text on prompt line
556
551
 
557
552
  const clearGhost = () => {
558
553
  const buf: string[] = [];
@@ -564,10 +559,10 @@ function setupInlineSuggestions(rl: readline.Interface): void {
564
559
  buf.push(`\x1b[${dropdownLines}A`); // back up to prompt line
565
560
  dropdownLines = 0;
566
561
  }
567
- // Clear inline ghost on prompt line
568
- if (cursorCol > 0) {
569
- buf.push(`\x1b[${cursorCol}G\x1b[K`);
570
- }
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;
571
566
  if (buf.length) process.stdout.write(buf.join(""));
572
567
  };
573
568
 
@@ -583,40 +578,63 @@ function setupInlineSuggestions(rl: readline.Interface): void {
583
578
  const remainder = best.cmd.slice(line.length);
584
579
  if (!remainder && shown.length === 1) return;
585
580
 
586
- cursorCol = getPromptLen() + line.length + 1;
587
581
  const buf: string[] = [];
588
582
 
589
- // Inline ghost: show remainder of best match (dimmed) on prompt line
590
- buf.push(`\x1b[${cursorCol}G\x1b[K`);
583
+ // Inline ghost: cursor is already at end of typed text (readline did that)
584
+ // Just clear rest of line and write dimmed remainder
585
+ buf.push("\x1b[K");
591
586
  if (remainder) {
592
587
  buf.push(`\x1b[2m${remainder}\x1b[0m`);
588
+ ghostLen = remainder.length;
589
+ // Move cursor back to end of typed text
590
+ buf.push(`\x1b[${remainder.length}D`);
593
591
  }
594
592
 
595
593
  // Dropdown: show all matches below prompt
596
594
  if (shown.length > 1 || (shown.length === 1 && remainder)) {
597
- // Make room by writing newlines (handles terminal bottom scroll)
598
595
  const count = shown.length;
596
+ // Make room by writing newlines (handles terminal bottom scroll)
599
597
  for (let i = 0; i < count; i++) buf.push("\n");
600
598
  buf.push(`\x1b[${count}A`); // back to prompt line
601
599
 
602
600
  // Write each dropdown line: cyan command + dim description
603
601
  for (let i = 0; i < count; i++) {
604
- buf.push(`\x1b[B\r\x1b[2K`); // down + beginning of line + clear
602
+ buf.push(`\x1b[B\r\x1b[2K`); // down + col 1 + clear
605
603
  buf.push(` \x1b[36m${shown[i].cmd.padEnd(14)}\x1b[0m\x1b[2m${shown[i].desc}\x1b[0m`);
606
604
  }
607
605
  dropdownLines = count;
608
606
 
609
- // Back to prompt line
607
+ // Back to prompt line and restore horizontal position
608
+ // Move up to prompt line
610
609
  buf.push(`\x1b[${count}A`);
610
+ // Move to column 1, then rewrite prompt+line to position cursor correctly
611
+ buf.push(`\r`);
612
+ // Let readline handle cursor positioning
611
613
  }
612
614
 
613
- // Restore cursor to typing position on prompt line
614
- buf.push(`\x1b[${cursorCol}G`);
615
615
  process.stdout.write(buf.join(""));
616
+ // After dropdown, force readline to redraw prompt line to fix cursor position
617
+ if (dropdownLines > 0) {
618
+ (rl as any)._refreshLine();
619
+ // Re-draw inline ghost since _refreshLine cleared it
620
+ if (remainder) {
621
+ process.stdout.write(`\x1b[K\x1b[2m${remainder}\x1b[0m\x1b[${remainder.length}D`);
622
+ }
623
+ }
616
624
  };
617
625
 
618
626
  process.stdin.on("keypress", (_ch: string, key: any) => {
619
627
  if (!key) return;
628
+
629
+ // Shift+Enter: insert newline marker instead of submitting
630
+ if (key.name === "return" && key.shift) {
631
+ // Append backslash to trigger line continuation, then simulate Enter
632
+ const line = (rl as any).line as string;
633
+ (rl as any).line = line + "\\";
634
+ (rl as any).cursor = (rl as any).line.length;
635
+ return;
636
+ }
637
+
620
638
  clearGhost();
621
639
  if (key.name !== "return" && key.name !== "tab") {
622
640
  setImmediate(showGhost);