groundcrew-cli 0.15.0 → 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 +34 -16
  2. package/package.json +1 -1
  3. package/src/index.ts +48 -24
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,12 +415,16 @@ function chatCompleter(line) {
415
415
  return [[], line];
416
416
  }
417
417
  function setupInlineSuggestions(rl) {
418
- let hasGhost = false;
418
+ let ghostLines = 0;
419
419
  const clearGhost = () => {
420
- if (hasGhost) {
421
- process.stdout.write("\x1B[u\x1B[K");
422
- hasGhost = false;
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;
423
426
  }
427
+ process.stdout.write("\x1B[u\x1B[K");
424
428
  };
425
429
  const showGhost = () => {
426
430
  const line = rl.line;
@@ -429,22 +433,36 @@ function setupInlineSuggestions(rl) {
429
433
  }
430
434
  const matches = CHAT_COMMANDS.filter((c) => c.cmd.startsWith(line));
431
435
  if (matches.length === 0) return;
432
- const best = matches[0];
433
- const remainder = best.cmd.slice(line.length);
434
- if (!remainder && matches.length === 1) return;
435
- const ghost = `${remainder} \u2014 ${best.desc}`;
436
436
  process.stdout.write("\x1B[s");
437
- process.stdout.write("\x1B[K");
438
- process.stdout.write(`\x1B[2m${ghost}\x1B[0m`);
439
- process.stdout.write("\x1B[u");
440
- hasGhost = true;
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");
460
+ }
441
461
  };
442
462
  process.stdin.on("keypress", (_ch, key) => {
443
463
  if (!key) return;
444
464
  clearGhost();
445
- if (key.name !== "return" && key.name !== "tab" && key.name !== "backspace") {
446
- setImmediate(showGhost);
447
- } else if (key.name === "backspace") {
465
+ if (key.name !== "return" && key.name !== "tab") {
448
466
  setImmediate(showGhost);
449
467
  }
450
468
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundcrew-cli",
3
- "version": "0.15.0",
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,18 +540,25 @@ 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 hasGhost = false;
548
+ let ghostLines = 0; // how many extra lines we rendered below prompt
548
549
 
549
550
  const clearGhost = () => {
550
- if (hasGhost) {
551
- // Restore cursor to where user was typing, clear everything after
552
- process.stdout.write("\x1b[u\x1b[K");
553
- hasGhost = false;
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;
554
559
  }
560
+ // Restore cursor and clear to end of line (for inline ghost)
561
+ process.stdout.write("\x1b[u\x1b[K");
555
562
  };
556
563
 
557
564
  const showGhost = () => {
@@ -563,28 +570,45 @@ function setupInlineSuggestions(rl: readline.Interface): void {
563
570
  const matches = CHAT_COMMANDS.filter((c) => c.cmd.startsWith(line));
564
571
  if (matches.length === 0) return;
565
572
 
566
- const best = matches[0];
567
- const remainder = best.cmd.slice(line.length);
568
- if (!remainder && matches.length === 1) return;
569
-
570
- const ghost = `${remainder} \u2014 ${best.desc}`;
571
-
572
- // Save cursor, write dimmed ghost, restore cursor
573
- process.stdout.write("\x1b[s"); // save cursor position
574
- process.stdout.write("\x1b[K"); // clear to end of line (remove old ghost)
575
- process.stdout.write(`\x1b[2m${ghost}\x1b[0m`); // write dimmed ghost
576
- process.stdout.write("\x1b[u"); // restore cursor to typing position
577
- hasGhost = true;
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");
604
+ }
578
605
  };
579
606
 
580
607
  // Listen to keypresses
581
608
  process.stdin.on("keypress", (_ch: string, key: any) => {
582
609
  if (!key) return;
583
- // Clear ghost first, then show updated one on next tick
584
610
  clearGhost();
585
- if (key.name !== "return" && key.name !== "tab" && key.name !== "backspace") {
586
- setImmediate(showGhost);
587
- } else if (key.name === "backspace") {
611
+ if (key.name !== "return" && key.name !== "tab") {
588
612
  setImmediate(showGhost);
589
613
  }
590
614
  });