groundcrew-cli 0.15.1 → 0.15.3

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 +39 -33
  2. package/package.json +1 -1
  3. package/src/index.ts +58 -47
package/dist/index.js CHANGED
@@ -415,49 +415,54 @@ function chatCompleter(line) {
415
415
  return [[], line];
416
416
  }
417
417
  function setupInlineSuggestions(rl) {
418
- let ghostLines = 0;
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
424
  const clearGhost = () => {
420
- if (ghostLines > 0) {
421
- for (let i = 0; i < ghostLines; i++) {
422
- process.stdout.write("\x1B[B\x1B[2K");
425
+ const buf = [];
426
+ if (dropdownLines > 0) {
427
+ for (let i = 0; i < dropdownLines; i++) {
428
+ buf.push("\x1B[B\x1B[2K");
423
429
  }
424
- process.stdout.write(`\x1B[${ghostLines}A`);
425
- ghostLines = 0;
430
+ buf.push(`\x1B[${dropdownLines}A`);
431
+ dropdownLines = 0;
432
+ }
433
+ if (cursorCol > 0) {
434
+ buf.push(`\x1B[${cursorCol}G\x1B[K`);
426
435
  }
427
- process.stdout.write("\x1B[u\x1B[K");
436
+ if (buf.length) process.stdout.write(buf.join(""));
428
437
  };
429
438
  const showGhost = () => {
430
439
  const line = rl.line;
431
- if (!line || !line.startsWith("/") || line.includes(" ")) {
432
- return;
433
- }
440
+ if (!line || !line.startsWith("/") || line.includes(" ")) return;
434
441
  const matches = CHAT_COMMANDS.filter((c) => c.cmd.startsWith(line));
435
442
  if (matches.length === 0) return;
436
- process.stdout.write("\x1B[s");
437
- if (matches.length === 1) {
438
- const best = matches[0];
439
- const remainder = best.cmd.slice(line.length);
440
- if (!remainder) return;
441
- const ghost = `${remainder} \u2014 ${best.desc}`;
442
- process.stdout.write("\x1B[K");
443
- process.stdout.write(`\x1B[2m${ghost}\x1B[0m`);
444
- process.stdout.write("\x1B[u");
445
- } else {
446
- const shown = matches.slice(0, 5);
447
- process.stdout.write("\x1B[K");
448
- const best = shown[0];
449
- const remainder = best.cmd.slice(line.length);
450
- if (remainder) {
451
- process.stdout.write(`\x1B[2m${remainder} \u2014 ${best.desc}\x1B[0m`);
452
- }
453
- for (let i = 1; i < shown.length; i++) {
454
- const m = shown[i];
455
- process.stdout.write(`
456
- \x1B[2K \x1B[2m${m.cmd.padEnd(12)} \u2014 ${m.desc}\x1B[0m`);
443
+ const shown = matches.slice(0, 5);
444
+ const best = shown[0];
445
+ const remainder = best.cmd.slice(line.length);
446
+ if (!remainder && shown.length === 1) return;
447
+ cursorCol = getPromptLen() + line.length + 1;
448
+ const buf = [];
449
+ buf.push(`\x1B[${cursorCol}G\x1B[K`);
450
+ if (remainder) {
451
+ buf.push(`\x1B[2m${remainder}\x1B[0m`);
452
+ }
453
+ if (shown.length > 1 || shown.length === 1 && remainder) {
454
+ const count = shown.length;
455
+ for (let i = 0; i < count; i++) buf.push("\n");
456
+ buf.push(`\x1B[${count}A`);
457
+ for (let i = 0; i < count; i++) {
458
+ buf.push(`\x1B[B\r\x1B[2K`);
459
+ buf.push(` \x1B[36m${shown[i].cmd.padEnd(14)}\x1B[0m\x1B[2m${shown[i].desc}\x1B[0m`);
457
460
  }
458
- ghostLines = shown.length - 1;
459
- process.stdout.write("\x1B[u");
461
+ dropdownLines = count;
462
+ buf.push(`\x1B[${count}A`);
460
463
  }
464
+ buf.push(`\x1B[${cursorCol}G`);
465
+ process.stdout.write(buf.join(""));
461
466
  };
462
467
  process.stdin.on("keypress", (_ch, key) => {
463
468
  if (!key) return;
@@ -528,6 +533,7 @@ async function chat(explicitSession) {
528
533
  const prompt = () => {
529
534
  const isContinuation = continuationBuffer.length > 0;
530
535
  const prefix = isContinuation ? `${dim(`[${current.id}]`)} ${dim("...")} ` : `${dim(`[${current.id}]`)} ${bold(">")} `;
536
+ rl.setPrompt(prefix);
531
537
  rl.question(prefix, async (line) => {
532
538
  if (line.endsWith("\\")) {
533
539
  continuationBuffer.push(line.slice(0, -1));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundcrew-cli",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
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
@@ -540,71 +540,81 @@ function chatCompleter(line: string): [string[], string] {
540
540
  }
541
541
 
542
542
  /**
543
- * Show inline ghost suggestions as user types / commands.
544
- * Single match: inline ghost after cursor.
545
- * Multiple matches: multi-line dropdown below the prompt (max 5).
543
+ * Show inline ghost + multi-line dropdown as user types / commands.
544
+ * Best match completion shown inline (dimmed) after cursor.
545
+ * All matches (max 5) shown as dropdown lines below the prompt.
546
+ * Uses explicit column + relative cursor movement (no save/restore).
546
547
  */
547
548
  function setupInlineSuggestions(rl: readline.Interface): void {
548
- let ghostLines = 0; // how many extra lines we rendered below prompt
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
556
 
550
557
  const clearGhost = () => {
551
- if (ghostLines > 0) {
552
- // Move down and clear each ghost line, then move back up
553
- for (let i = 0; i < ghostLines; i++) {
554
- process.stdout.write("\x1b[B\x1b[2K"); // down + clear line
558
+ const buf: string[] = [];
559
+ // Clear dropdown lines below prompt
560
+ if (dropdownLines > 0) {
561
+ for (let i = 0; i < dropdownLines; i++) {
562
+ buf.push("\x1b[B\x1b[2K"); // down + clear entire line
555
563
  }
556
- // Move back up to prompt line
557
- process.stdout.write(`\x1b[${ghostLines}A`);
558
- ghostLines = 0;
564
+ buf.push(`\x1b[${dropdownLines}A`); // back up to prompt line
565
+ dropdownLines = 0;
566
+ }
567
+ // Clear inline ghost on prompt line
568
+ if (cursorCol > 0) {
569
+ buf.push(`\x1b[${cursorCol}G\x1b[K`);
559
570
  }
560
- // Restore cursor and clear to end of line (for inline ghost)
561
- process.stdout.write("\x1b[u\x1b[K");
571
+ if (buf.length) process.stdout.write(buf.join(""));
562
572
  };
563
573
 
564
574
  const showGhost = () => {
565
575
  const line = (rl as any).line as string;
566
- if (!line || !line.startsWith("/") || line.includes(" ")) {
567
- return;
568
- }
576
+ if (!line || !line.startsWith("/") || line.includes(" ")) return;
569
577
 
570
578
  const matches = CHAT_COMMANDS.filter((c) => c.cmd.startsWith(line));
571
579
  if (matches.length === 0) return;
572
580
 
573
- // Save cursor position
574
- process.stdout.write("\x1b[s");
575
-
576
- if (matches.length === 1) {
577
- // Single match: inline ghost remainder + description
578
- const best = matches[0];
579
- const remainder = best.cmd.slice(line.length);
580
- if (!remainder) return;
581
- const ghost = `${remainder} \u2014 ${best.desc}`;
582
- process.stdout.write("\x1b[K");
583
- process.stdout.write(`\x1b[2m${ghost}\x1b[0m`);
584
- process.stdout.write("\x1b[u");
585
- } else {
586
- // Multiple matches: show dropdown below prompt (max 5)
587
- const shown = matches.slice(0, 5);
588
- // Clear to end of line first
589
- process.stdout.write("\x1b[K");
590
- // Inline ghost for the best match remainder
591
- const best = shown[0];
592
- const remainder = best.cmd.slice(line.length);
593
- if (remainder) {
594
- process.stdout.write(`\x1b[2m${remainder} \u2014 ${best.desc}\x1b[0m`);
595
- }
596
- // Render dropdown lines below
597
- for (let i = 1; i < shown.length; i++) {
598
- const m = shown[i];
599
- process.stdout.write(`\n\x1b[2K \x1b[2m${m.cmd.padEnd(12)} \u2014 ${m.desc}\x1b[0m`);
581
+ const shown = matches.slice(0, 5);
582
+ const best = shown[0];
583
+ const remainder = best.cmd.slice(line.length);
584
+ if (!remainder && shown.length === 1) return;
585
+
586
+ cursorCol = getPromptLen() + line.length + 1;
587
+ const buf: string[] = [];
588
+
589
+ // Inline ghost: show remainder of best match (dimmed) on prompt line
590
+ buf.push(`\x1b[${cursorCol}G\x1b[K`);
591
+ if (remainder) {
592
+ buf.push(`\x1b[2m${remainder}\x1b[0m`);
593
+ }
594
+
595
+ // Dropdown: show all matches below prompt
596
+ if (shown.length > 1 || (shown.length === 1 && remainder)) {
597
+ // Make room by writing newlines (handles terminal bottom scroll)
598
+ const count = shown.length;
599
+ for (let i = 0; i < count; i++) buf.push("\n");
600
+ buf.push(`\x1b[${count}A`); // back to prompt line
601
+
602
+ // Write each dropdown line: cyan command + dim description
603
+ for (let i = 0; i < count; i++) {
604
+ buf.push(`\x1b[B\r\x1b[2K`); // down + beginning of line + clear
605
+ buf.push(` \x1b[36m${shown[i].cmd.padEnd(14)}\x1b[0m\x1b[2m${shown[i].desc}\x1b[0m`);
600
606
  }
601
- ghostLines = shown.length - 1;
602
- // Restore cursor to typing position
603
- process.stdout.write("\x1b[u");
607
+ dropdownLines = count;
608
+
609
+ // Back to prompt line
610
+ buf.push(`\x1b[${count}A`);
604
611
  }
612
+
613
+ // Restore cursor to typing position on prompt line
614
+ buf.push(`\x1b[${cursorCol}G`);
615
+ process.stdout.write(buf.join(""));
605
616
  };
606
617
 
607
- // Listen to keypresses
608
618
  process.stdin.on("keypress", (_ch: string, key: any) => {
609
619
  if (!key) return;
610
620
  clearGhost();
@@ -687,6 +697,7 @@ async function chat(explicitSession?: string): Promise<void> {
687
697
  ? `${dim(`[${current!.id}]`)} ${dim("...")} `
688
698
  : `${dim(`[${current!.id}]`)} ${bold(">")} `;
689
699
 
700
+ rl.setPrompt(prefix);
690
701
  rl.question(prefix, async (line) => {
691
702
  // Line continuation with backslash
692
703
  if (line.endsWith("\\")) {