groundcrew-cli 0.14.2 → 0.15.1

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 +35 -31
  2. package/package.json +1 -1
  3. package/src/index.ts +49 -42
package/dist/index.js CHANGED
@@ -398,7 +398,7 @@ var CHAT_COMMANDS = [
398
398
  { cmd: "/history", desc: "Show completed tasks" },
399
399
  { cmd: "/queue", desc: "Show pending tasks" },
400
400
  { cmd: "/clear", desc: "Clear pending tasks" },
401
- { cmd: "/quit", desc: "Exit chat" }
401
+ { cmd: "/exit", desc: "Exit chat" }
402
402
  ];
403
403
  function chatCompleter(line) {
404
404
  if (!line.startsWith("/")) return [[], line];
@@ -415,50 +415,54 @@ function chatCompleter(line) {
415
415
  return [[], line];
416
416
  }
417
417
  function setupInlineSuggestions(rl) {
418
- let lastGhostLen = 0;
418
+ let ghostLines = 0;
419
419
  const clearGhost = () => {
420
- if (lastGhostLen > 0) {
421
- process.stdout.write("\x1B[" + lastGhostLen + "D");
422
- process.stdout.write("\x1B[0K");
423
- lastGhostLen = 0;
420
+ if (ghostLines > 0) {
421
+ for (let i = 0; i < ghostLines; i++) {
422
+ process.stdout.write("\x1B[B\x1B[2K");
423
+ }
424
+ process.stdout.write(`\x1B[${ghostLines}A`);
425
+ ghostLines = 0;
424
426
  }
427
+ process.stdout.write("\x1B[u\x1B[K");
425
428
  };
426
429
  const showGhost = () => {
427
430
  const line = rl.line;
428
431
  if (!line || !line.startsWith("/") || line.includes(" ")) {
429
- clearGhost();
430
432
  return;
431
433
  }
432
434
  const matches = CHAT_COMMANDS.filter((c) => c.cmd.startsWith(line));
433
- if (matches.length === 0) {
434
- clearGhost();
435
- return;
436
- }
437
- const promptLen = (rl._prompt || "").replace(/\x1b\[[0-9;]*m/g, "").length;
438
- const cols = process.stdout.columns || 80;
439
- const usedCols = promptLen + line.length;
440
- const available = cols - usedCols - 1;
441
- if (available <= 3) {
442
- clearGhost();
443
- return;
444
- }
445
- const best = matches[0];
446
- const ghost = best.cmd.slice(line.length);
447
- let fullGhost = `${ghost} \u2014 ${best.desc}`;
448
- if (fullGhost.length > available) {
449
- fullGhost = fullGhost.slice(0, available - 1) + "\u2026";
435
+ 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`);
457
+ }
458
+ ghostLines = shown.length - 1;
459
+ process.stdout.write("\x1B[u");
450
460
  }
451
- clearGhost();
452
- process.stdout.write(`\x1B[2m${fullGhost}\x1B[0m`);
453
- lastGhostLen = fullGhost.length;
454
- process.stdout.write("\x1B[" + lastGhostLen + "D");
455
461
  };
456
462
  process.stdin.on("keypress", (_ch, key) => {
457
463
  if (!key) return;
458
464
  clearGhost();
459
- if (key.name !== "return" && key.name !== "tab" && key.name !== "backspace") {
460
- setImmediate(showGhost);
461
- } else if (key.name === "backspace") {
465
+ if (key.name !== "return" && key.name !== "tab") {
462
466
  setImmediate(showGhost);
463
467
  }
464
468
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundcrew-cli",
3
- "version": "0.14.2",
3
+ "version": "0.15.1",
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
@@ -518,7 +518,7 @@ const CHAT_COMMANDS: Array<{ cmd: string; desc: string }> = [
518
518
  { cmd: "/history", desc: "Show completed tasks" },
519
519
  { cmd: "/queue", desc: "Show pending tasks" },
520
520
  { cmd: "/clear", desc: "Clear pending tasks" },
521
- { cmd: "/quit", desc: "Exit chat" },
521
+ { cmd: "/exit", desc: "Exit chat" },
522
522
  ];
523
523
 
524
524
  function chatCompleter(line: string): [string[], string] {
@@ -540,68 +540,75 @@ function chatCompleter(line: string): [string[], string] {
540
540
  }
541
541
 
542
542
  /**
543
- * Show inline ghost suggestion as user types / commands.
544
- * Renders dimmed text after cursor, erased on next keystroke.
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).
545
546
  */
546
547
  function setupInlineSuggestions(rl: readline.Interface): void {
547
- let lastGhostLen = 0;
548
+ let ghostLines = 0; // how many extra lines we rendered below prompt
548
549
 
549
550
  const clearGhost = () => {
550
- if (lastGhostLen > 0) {
551
- // Move cursor back to end of typed text, clear ghost
552
- process.stdout.write("\x1b[" + lastGhostLen + "D");
553
- process.stdout.write("\x1b[0K");
554
- lastGhostLen = 0;
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
555
+ }
556
+ // Move back up to prompt line
557
+ process.stdout.write(`\x1b[${ghostLines}A`);
558
+ ghostLines = 0;
555
559
  }
560
+ // Restore cursor and clear to end of line (for inline ghost)
561
+ process.stdout.write("\x1b[u\x1b[K");
556
562
  };
557
563
 
558
564
  const showGhost = () => {
559
565
  const line = (rl as any).line as string;
560
566
  if (!line || !line.startsWith("/") || line.includes(" ")) {
561
- clearGhost();
562
567
  return;
563
568
  }
564
569
 
565
570
  const matches = CHAT_COMMANDS.filter((c) => c.cmd.startsWith(line));
566
- if (matches.length === 0) {
567
- clearGhost();
568
- return;
569
- }
570
-
571
- // Calculate available space to prevent line wrapping
572
- const promptLen = ((rl as any)._prompt || "").replace(/\x1b\[[0-9;]*m/g, "").length;
573
- const cols = process.stdout.columns || 80;
574
- const usedCols = promptLen + line.length;
575
- const available = cols - usedCols - 1; // -1 safety margin
576
-
577
- if (available <= 3) {
578
- clearGhost();
579
- return;
580
- }
581
-
582
- const best = matches[0];
583
- const ghost = best.cmd.slice(line.length);
584
- let fullGhost = `${ghost} \u2014 ${best.desc}`;
585
-
586
- // Truncate to fit
587
- if (fullGhost.length > available) {
588
- fullGhost = fullGhost.slice(0, available - 1) + "\u2026";
571
+ if (matches.length === 0) return;
572
+
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`);
600
+ }
601
+ ghostLines = shown.length - 1;
602
+ // Restore cursor to typing position
603
+ process.stdout.write("\x1b[u");
589
604
  }
590
-
591
- clearGhost();
592
- process.stdout.write(`\x1b[2m${fullGhost}\x1b[0m`);
593
- lastGhostLen = fullGhost.length;
594
- process.stdout.write("\x1b[" + lastGhostLen + "D");
595
605
  };
596
606
 
597
607
  // Listen to keypresses
598
608
  process.stdin.on("keypress", (_ch: string, key: any) => {
599
609
  if (!key) return;
600
- // Clear ghost first, then show updated one on next tick
601
610
  clearGhost();
602
- if (key.name !== "return" && key.name !== "tab" && key.name !== "backspace") {
603
- setImmediate(showGhost);
604
- } else if (key.name === "backspace") {
611
+ if (key.name !== "return" && key.name !== "tab") {
605
612
  setImmediate(showGhost);
606
613
  }
607
614
  });