agent-sh 0.12.26 → 0.13.0

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 (144) hide show
  1. package/README.md +13 -2
  2. package/dist/agent/agent-loop.d.ts +3 -5
  3. package/dist/agent/agent-loop.js +44 -100
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +38 -1
  6. package/dist/agent/history-file.d.ts +6 -0
  7. package/dist/agent/history-file.js +1 -1
  8. package/dist/agent/host-types.d.ts +125 -0
  9. package/dist/agent/index.d.ts +12 -4
  10. package/dist/agent/index.js +357 -6
  11. package/dist/agent/nuclear-form.d.ts +7 -0
  12. package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
  13. package/dist/{extensions → agent}/providers/deepseek.js +5 -4
  14. package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
  15. package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
  16. package/dist/{extensions → agent}/providers/openai.js +3 -2
  17. package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
  18. package/dist/{extensions → agent}/providers/openrouter.js +4 -3
  19. package/dist/agent/skills.js +51 -7
  20. package/dist/agent/subagent.d.ts +1 -1
  21. package/dist/agent/system-prompt.js +14 -17
  22. package/dist/agent/tool-protocol.d.ts +1 -1
  23. package/dist/agent/tool-protocol.js +5 -3
  24. package/dist/agent/tool-registry.d.ts +9 -4
  25. package/dist/agent/tool-registry.js +27 -4
  26. package/dist/agent/tools/bash.d.ts +1 -1
  27. package/dist/agent/tools/bash.js +3 -2
  28. package/dist/agent/tools/edit-file.js +0 -1
  29. package/dist/agent/tools/glob.js +1 -1
  30. package/dist/agent/tools/grep.js +1 -1
  31. package/dist/agent/tools/pwsh.d.ts +1 -1
  32. package/dist/agent/tools/pwsh.js +1 -2
  33. package/dist/agent/tools/read-file.js +7 -4
  34. package/dist/agent/tools/write-file.js +0 -1
  35. package/dist/agent/types.d.ts +17 -2
  36. package/dist/cli/auth/cli.d.ts +1 -0
  37. package/dist/cli/auth/cli.js +216 -0
  38. package/dist/cli/auth/keys.d.ts +31 -0
  39. package/dist/cli/auth/keys.js +102 -0
  40. package/dist/{index.js → cli/index.js} +29 -32
  41. package/dist/{init.js → cli/init.js} +1 -1
  42. package/dist/{install.js → cli/install.js} +114 -5
  43. package/dist/cli/subcommands.d.ts +1 -0
  44. package/dist/cli/subcommands.js +17 -0
  45. package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
  46. package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
  47. package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
  48. package/dist/{core.d.ts → core/index.d.ts} +18 -15
  49. package/dist/{core.js → core/index.js} +18 -92
  50. package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
  51. package/dist/{settings.js → core/settings.js} +1 -0
  52. package/dist/core/types.d.ts +49 -0
  53. package/dist/core/types.js +1 -0
  54. package/dist/extensions/file-autocomplete.d.ts +1 -1
  55. package/dist/extensions/index.d.ts +7 -14
  56. package/dist/extensions/index.js +2 -19
  57. package/dist/extensions/slash-commands.d.ts +1 -1
  58. package/dist/extensions/slash-commands.js +7 -2
  59. package/dist/shell/host-types.d.ts +114 -0
  60. package/dist/shell/host-types.js +1 -0
  61. package/dist/shell/index.d.ts +8 -7
  62. package/dist/shell/index.js +58 -9
  63. package/dist/shell/input-handler.d.ts +7 -1
  64. package/dist/shell/input-handler.js +5 -2
  65. package/dist/shell/output-parser.d.ts +1 -1
  66. package/dist/{extensions → shell}/shell-context.d.ts +1 -1
  67. package/dist/{extensions → shell}/shell-context.js +18 -12
  68. package/dist/shell/shell.d.ts +6 -4
  69. package/dist/shell/shell.js +33 -109
  70. package/dist/shell/strategies/bash.d.ts +2 -0
  71. package/dist/shell/strategies/bash.js +68 -0
  72. package/dist/shell/strategies/fish.d.ts +2 -0
  73. package/dist/shell/strategies/fish.js +65 -0
  74. package/dist/shell/strategies/index.d.ts +13 -0
  75. package/dist/shell/strategies/index.js +17 -0
  76. package/dist/shell/strategies/types.d.ts +50 -0
  77. package/dist/shell/strategies/types.js +9 -0
  78. package/dist/shell/strategies/zsh.d.ts +2 -0
  79. package/dist/shell/strategies/zsh.js +72 -0
  80. package/dist/shell/tui-input-view.js +14 -3
  81. package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
  82. package/dist/{extensions → shell}/tui-renderer.js +27 -55
  83. package/dist/utils/box-frame.d.ts +4 -0
  84. package/dist/utils/box-frame.js +17 -6
  85. package/dist/utils/compositor.d.ts +1 -1
  86. package/dist/utils/compositor.js +2 -1
  87. package/dist/{executor.js → utils/executor.js} +1 -1
  88. package/dist/utils/floating-panel.d.ts +17 -5
  89. package/dist/utils/floating-panel.js +218 -70
  90. package/dist/utils/llm-facade.d.ts +7 -3
  91. package/dist/utils/stream-transform.d.ts +1 -1
  92. package/dist/utils/terminal-buffer.d.ts +1 -1
  93. package/dist/utils/tool-display.js +4 -0
  94. package/dist/utils/tool-interactive.d.ts +1 -1
  95. package/dist/utils/tty.d.ts +7 -0
  96. package/dist/utils/tty.js +15 -0
  97. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  98. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  99. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  100. package/examples/extensions/ashi/README.md +250 -0
  101. package/examples/extensions/ashi/package.json +60 -0
  102. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  103. package/examples/extensions/ashi/src/capture.ts +34 -0
  104. package/examples/extensions/ashi/src/cli.ts +126 -0
  105. package/examples/extensions/ashi/src/commands.ts +82 -0
  106. package/examples/extensions/ashi/src/compaction.ts +157 -0
  107. package/examples/extensions/ashi/src/components.ts +332 -0
  108. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  109. package/examples/extensions/ashi/src/display-config.ts +62 -0
  110. package/examples/extensions/ashi/src/frontend.ts +735 -0
  111. package/examples/extensions/ashi/src/hooks.ts +136 -0
  112. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  113. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  114. package/examples/extensions/ashi/src/session-store.ts +264 -0
  115. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  116. package/examples/extensions/ashi/src/theme.ts +151 -0
  117. package/examples/extensions/ashi/tsconfig.json +14 -0
  118. package/examples/extensions/emacs-buffer.ts +364 -0
  119. package/examples/extensions/interactive-prompts.ts +114 -69
  120. package/examples/extensions/latex-images.ts +3 -3
  121. package/examples/extensions/opencode-bridge/index.ts +1 -1
  122. package/examples/extensions/overlay-agent.ts +35 -10
  123. package/examples/extensions/peer-mesh.ts +1 -1
  124. package/examples/extensions/pi-bridge/index.ts +0 -1
  125. package/examples/extensions/questionnaire.ts +2 -1
  126. package/examples/extensions/rtk-proxy.ts +3 -3
  127. package/examples/extensions/solarized-theme.ts +3 -3
  128. package/examples/extensions/subagents.ts +6 -6
  129. package/examples/extensions/terminal-buffer.ts +174 -33
  130. package/examples/extensions/tmux-pane.ts +6 -4
  131. package/examples/extensions/tunnel-vision.ts +405 -0
  132. package/examples/extensions/user-shell.ts +1 -1
  133. package/examples/extensions/web-access.ts +8 -113
  134. package/package.json +26 -22
  135. package/dist/extensions/agent-backend.d.ts +0 -14
  136. package/dist/extensions/agent-backend.js +0 -307
  137. package/dist/types.d.ts +0 -227
  138. /package/dist/{types.js → agent/host-types.js} +0 -0
  139. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  140. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  141. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  142. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  143. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  144. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
@@ -0,0 +1,72 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ const OSC7_CMD = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
4
+ const TITLE_CMD = 'printf "\\e]0;⚡ agent-sh: %s\\a" "${PWD/#$HOME/~}"';
5
+ export const zshStrategy = {
6
+ name: "zsh",
7
+ matches(shellPath) {
8
+ return path.basename(shellPath).includes("zsh");
9
+ },
10
+ prepareSpawn(opts) {
11
+ const { tmpDirRoot, instanceTag, showIndicator, env, userHome } = opts;
12
+ // Use ZDOTDIR to source user's real config, then append our hooks via
13
+ // precmd_functions (additive — doesn't clobber p10k/omz).
14
+ const tmpDir = fs.mkdtempSync(path.join(tmpDirRoot, "agent-sh-"));
15
+ const userZdotdir = env.ZDOTDIR || env.HOME || userHome;
16
+ const promptMarker = `printf "\\e]9999;${instanceTag};PROMPT\\a"`;
17
+ const lines = [
18
+ `ZDOTDIR="${userZdotdir}"`,
19
+ `[ -f "${userZdotdir}/.zshrc" ] && source "${userZdotdir}/.zshrc"`,
20
+ "",
21
+ "# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
22
+ "__agent_sh_precmd() {",
23
+ ` ${OSC7_CMD}`,
24
+ ` ${promptMarker}`,
25
+ ...(showIndicator ? [` ${TITLE_CMD}`] : []),
26
+ "}",
27
+ "precmd_functions+=(__agent_sh_precmd)",
28
+ "",
29
+ "# Preexec hook: emit actual command text so agent-sh can track",
30
+ "# history-recalled and tab-completed commands accurately",
31
+ "__agent_sh_preexec() {",
32
+ ` printf "\\e]9997;${instanceTag};%s\\a" "$1"`,
33
+ "}",
34
+ "preexec_functions+=(__agent_sh_preexec)",
35
+ "",
36
+ "# End-of-prompt marker via zle-line-init (fires after prompt is rendered)",
37
+ "# Chain onto existing widget (p10k uses zle-line-init) rather than clobbering",
38
+ 'if (( ${+widgets[zle-line-init]} )); then',
39
+ " zle -A zle-line-init __agent_sh_orig_line_init",
40
+ " __agent_sh_line_init() {",
41
+ " zle __agent_sh_orig_line_init",
42
+ ` printf "\\e]9998;${instanceTag};READY\\a"`,
43
+ " }",
44
+ "else",
45
+ " __agent_sh_line_init() {",
46
+ ` printf "\\e]9998;${instanceTag};READY\\a"`,
47
+ " }",
48
+ "fi",
49
+ "zle -N zle-line-init __agent_sh_line_init",
50
+ "",
51
+ "# Hidden widget to trigger prompt redraw from Node.js side",
52
+ "# Bound to an unused escape sequence that no real key produces",
53
+ "__agent_sh_redraw() {",
54
+ " zle reset-prompt",
55
+ "}",
56
+ "zle -N __agent_sh_redraw",
57
+ "bindkey '\\e[9999~' __agent_sh_redraw",
58
+ ];
59
+ fs.writeFileSync(path.join(tmpDir, ".zshrc"), lines.join("\n") + "\n");
60
+ return {
61
+ args: ["--no-globalrcs"],
62
+ envOverrides: { ZDOTDIR: tmpDir },
63
+ tmpDir,
64
+ };
65
+ },
66
+ envCaptureCommand() {
67
+ return "source ~/.zshrc 2>/dev/null; env -0";
68
+ },
69
+ redrawEscape() {
70
+ return "\x1b[9999~";
71
+ },
72
+ };
@@ -3,7 +3,7 @@
3
3
  * Owns screen state (cursor row/col, autocomplete line count) and the
4
4
  * ANSI redraw. The controller drives it via a small VM shape.
5
5
  */
6
- import { visibleLen } from "../utils/ansi.js";
6
+ import { visibleLen, truncateToWidth } from "../utils/ansi.js";
7
7
  import { palette as p } from "../utils/palette.js";
8
8
  import { StdoutSurface } from "../utils/compositor.js";
9
9
  export class TuiInputView {
@@ -146,15 +146,26 @@ export class TuiInputView {
146
146
  if (vm.items.length === 0)
147
147
  return;
148
148
  this.autoFrame(() => {
149
+ // Truncate descriptions so each row fits one physical line — a wrapped
150
+ // row makes autocompleteLines undercount, clearAutocomplete leaves a
151
+ // residual row, and the next drawPrompt redraws below the original
152
+ // prompt (staircase).
153
+ const termW = this.surface.columns;
149
154
  const lines = [];
150
155
  for (let i = 0; i < vm.items.length; i++) {
151
156
  const item = vm.items[i];
157
+ const nameW = Math.max(12, visibleLen(item.name));
158
+ const overhead = 5 + nameW;
159
+ const descBudget = Math.max(1, termW - overhead);
160
+ const desc = visibleLen(item.description) > descBudget
161
+ ? truncateToWidth(item.description, descBudget)
162
+ : item.description;
152
163
  const selected = i === vm.selected;
153
164
  if (selected) {
154
- lines.push(` \x1b[7m ${p.accent}${item.name.padEnd(12)}${p.reset}\x1b[7m ${item.description} ${p.reset}`);
165
+ lines.push(` \x1b[7m ${p.accent}${item.name.padEnd(12)}${p.reset}\x1b[7m ${desc} ${p.reset}`);
155
166
  }
156
167
  else {
157
- lines.push(` ${p.muted}${item.name.padEnd(12)} ${item.description}${p.reset}`);
168
+ lines.push(` ${p.muted}${item.name.padEnd(12)} ${desc}${p.reset}`);
158
169
  }
159
170
  }
160
171
  this.emit("\n" + lines.join("\n"));
@@ -1,2 +1,2 @@
1
- import type { ExtensionContext } from "../types.js";
1
+ import type { ExtensionContext } from "./host-types.js";
2
2
  export default function activate(ctx: ExtensionContext): void;
@@ -18,7 +18,7 @@ import { palette as p } from "../utils/palette.js";
18
18
  import { renderToolCall, createSpinner, formatElapsed, SPINNER_FRAMES, } from "../utils/tool-display.js";
19
19
  import { renderDiff } from "../utils/diff-renderer.js";
20
20
  import { renderBoxFrame } from "../utils/box-frame.js";
21
- import { getSettings } from "../settings.js";
21
+ import { getSettings } from "../core/settings.js";
22
22
  /** Encode a PNG buffer as a terminal inline image escape sequence. */
23
23
  function encodeImageForTerminal(data) {
24
24
  const b64 = data.toString("base64");
@@ -67,12 +67,11 @@ function createRenderState() {
67
67
  isThinking: false,
68
68
  showThinkingText: false,
69
69
  thinkingPending: false,
70
- previewedDiffPending: false,
71
- previewedDiffToolIds: new Set(),
72
70
  };
73
71
  }
74
72
  export default function activate(ctx) {
75
- const { bus, define, compositor } = ctx;
73
+ const { bus, define } = ctx;
74
+ const compositor = ctx.shell.compositor;
76
75
  const s = createRenderState();
77
76
  /** Track the shell's cwd so path shortening is relative to where the user actually is. */
78
77
  let shellCwd = process.cwd();
@@ -273,10 +272,6 @@ export default function activate(ctx) {
273
272
  s.currentToolKind = e.kind;
274
273
  s.toolStartTime = Date.now();
275
274
  s.orphanContHeaderKind = undefined;
276
- if (s.previewedDiffPending && e.toolCallId) {
277
- s.previewedDiffToolIds.add(e.toolCallId);
278
- }
279
- s.previewedDiffPending = false;
280
275
  if (e.title === "user_shell") {
281
276
  finalizeToolGroup();
282
277
  closeToolLine();
@@ -340,13 +335,7 @@ export default function activate(ctx) {
340
335
  s.toolExitCode = e.exitCode;
341
336
  if (e.exitCode !== 0)
342
337
  s.toolGroupAllOk = false;
343
- let resultDisplay = e.resultDisplay;
344
- if (e.toolCallId && s.previewedDiffToolIds.has(e.toolCallId)) {
345
- s.previewedDiffToolIds.delete(e.toolCallId);
346
- if (resultDisplay?.body?.kind === "diff") {
347
- resultDisplay = { ...resultDisplay, body: undefined };
348
- }
349
- }
338
+ const resultDisplay = e.resultDisplay;
350
339
  if (s.toolGroupKind) {
351
340
  // Grouped tool — track success/failure and summaries, show aggregate on ⎿ line.
352
341
  // Don't restart spinner between grouped tools — it's already running from group start.
@@ -411,46 +400,6 @@ export default function activate(ctx) {
411
400
  s.renderer.writeLine("");
412
401
  drain();
413
402
  });
414
- bus.on("permission:request", (e) => {
415
- if (!shouldRender())
416
- return;
417
- stopCurrentSpinner();
418
- flushCommandOutput();
419
- if (s.renderer) {
420
- s.renderer.flush();
421
- drain();
422
- }
423
- // Diff rendering is handled in the async pipe below so it can yield
424
- // to the event loop between hunks (keeping the spinner responsive).
425
- });
426
- // Async pipe: render diffs via the tui:render-diff handler (extensions can
427
- // advise to customize). Runs after the sync `on` handler above (which
428
- // flushes state) and before shell.ts's pipe (which pauses stdout).
429
- bus.onPipeAsync("permission:request", async (e) => {
430
- if (!shouldRender())
431
- return e;
432
- if (e.kind === "file-write" && e.metadata?.diff) {
433
- showCollapsedThinking();
434
- const lines = ctx.call("tui:render-diff", e.title, e.metadata.diff, cappedW());
435
- if (lines.length > 0) {
436
- if (!s.renderer)
437
- startAgentResponse();
438
- contentGap("diff");
439
- for (const line of lines)
440
- s.renderer.writeLine(line);
441
- drain();
442
- }
443
- // The diff box IS the visual representation of the upcoming tool call.
444
- // Mark lastContentKind as "tool" so the tool call line that follows
445
- // doesn't inject an extra gap between the diff box and the checkmark.
446
- s.lastContentKind = "tool";
447
- s.previewedDiffPending = true;
448
- }
449
- // Don't endAgentResponse() here — permission requests that aren't
450
- // file-write diffs are handled inline (auto-approved or by extensions).
451
- // Closing the response prematurely causes double separator borders.
452
- return e;
453
- });
454
403
  bus.on("input:keypress", (e) => {
455
404
  if (e.key === "\x14")
456
405
  toggleThinkingDisplay(); // Ctrl+T
@@ -661,6 +610,25 @@ export default function activate(ctx) {
661
610
  define("tui:render-diff", (filePath, diff, width) => {
662
611
  return renderDiffBody(diff, filePath, width);
663
612
  });
613
+ /** Display a diff inline above the agent response (e.g. for pre-execute
614
+ * preview from a gating advisor). Formats via `tui:render-diff` so
615
+ * advisors can theme it. */
616
+ define("tui:show-diff", (filePath, diff) => {
617
+ if (!shouldRender())
618
+ return;
619
+ stopCurrentSpinner();
620
+ showCollapsedThinking();
621
+ const lines = ctx.call("tui:render-diff", filePath, diff, cappedW());
622
+ if (lines.length === 0)
623
+ return;
624
+ if (!s.renderer)
625
+ startAgentResponse();
626
+ contentGap("diff");
627
+ for (const line of lines)
628
+ s.renderer.writeLine(line);
629
+ drain();
630
+ s.lastContentKind = "tool";
631
+ });
664
632
  /** Render a diff as framed box lines (pure — no TUI state side effects). */
665
633
  function renderDiffBody(diff, filePath, width) {
666
634
  if (diff.isIdentical)
@@ -716,6 +684,10 @@ export default function activate(ctx) {
716
684
  return "";
717
685
  if (typeof raw.command === "string")
718
686
  return `$ ${raw.command}`;
687
+ if (typeof raw.source === "string") {
688
+ const firstLine = raw.source.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
689
+ return firstLine.length > 80 ? firstLine.slice(0, 77) + "…" : firstLine;
690
+ }
719
691
  if (typeof raw.pattern === "string")
720
692
  return raw.pattern;
721
693
  if (typeof raw.path === "string") {
@@ -12,6 +12,10 @@ export interface BoxFrameOptions {
12
12
  titleRight?: string;
13
13
  /** Footer lines shown below a divider, inside the box. */
14
14
  footer?: string[];
15
+ /** Raw bg ANSI open code (e.g. "\x1b[48;2;40;50;40m"). When set, fills
16
+ * the frame interior with this bg and rewrites internal `\x1b[49m` /
17
+ * `\x1b[0m` resets so per-cell colors in content don't punch holes. */
18
+ bgColor?: string;
15
19
  }
16
20
  /**
17
21
  * Render content lines inside a bordered frame.
@@ -27,6 +27,7 @@ export function renderBoxFrame(content, opts) {
27
27
  const style = opts.style ?? "rounded";
28
28
  const b = BORDERS[style];
29
29
  const bc = borderColor;
30
+ const bg = opts.bgColor ?? "";
30
31
  // Content area width = total - 2 borders - 2 padding spaces
31
32
  const innerW = Math.max(1, width - 4);
32
33
  const output = [];
@@ -47,26 +48,36 @@ export function renderBoxFrame(content, opts) {
47
48
  const leftPart = title ? `${p.reset} ${title} ${bc}` : "";
48
49
  const rightPart = opts.titleRight ? `${p.reset} ${opts.titleRight} ${bc}` : "";
49
50
  const dashCount = Math.max(1, width - 2 - leftVis - rightVis);
50
- output.push(`${bc}${b.tl}${leftPart}${b.h.repeat(dashCount)}${rightPart}${b.tr}${p.reset}`);
51
+ output.push(paintBg(`${bc}${b.tl}${leftPart}${b.h.repeat(dashCount)}${rightPart}${b.tr}${p.reset}`, bg));
51
52
  }
52
53
  else {
53
- output.push(`${bc}${b.tl}${b.h.repeat(width - 2)}${b.tr}${p.reset}`);
54
+ output.push(paintBg(`${bc}${b.tl}${b.h.repeat(width - 2)}${b.tr}${p.reset}`, bg));
54
55
  }
55
56
  // Content lines
56
57
  for (const line of content) {
57
- output.push(boxLine(line, innerW, b.v, bc));
58
+ output.push(paintBg(boxLine(line, innerW, b.v, bc), bg));
58
59
  }
59
60
  // Footer with divider
60
61
  if (opts.footer && opts.footer.length > 0) {
61
- output.push(`${bc}${b.ml}${b.h.repeat(width - 2)}${b.mr}${p.reset}`);
62
+ output.push(paintBg(`${bc}${b.ml}${b.h.repeat(width - 2)}${b.mr}${p.reset}`, bg));
62
63
  for (const line of opts.footer) {
63
- output.push(boxLine(line, innerW, b.v, bc));
64
+ output.push(paintBg(boxLine(line, innerW, b.v, bc), bg));
64
65
  }
65
66
  }
66
67
  // Bottom border
67
- output.push(`${bc}${b.bl}${b.h.repeat(width - 2)}${b.br}${p.reset}`);
68
+ output.push(paintBg(`${bc}${b.bl}${b.h.repeat(width - 2)}${b.br}${p.reset}`, bg));
68
69
  return output;
69
70
  }
71
+ /** Wrap a line with a uniform bg, rewriting internal `\x1b[49m` (bg-default)
72
+ * and `\x1b[0m` (full-reset) so embedded colors don't punch through. */
73
+ function paintBg(line, bg) {
74
+ if (!bg)
75
+ return line;
76
+ const fixed = line
77
+ .replaceAll("\x1b[49m", bg)
78
+ .replaceAll("\x1b[0m", "\x1b[0m" + bg);
79
+ return `${bg}${fixed}\x1b[49m`;
80
+ }
70
81
  // ── Helpers ──────────────────────────────────────────────────────
71
82
  function boxLine(text, innerW, v, bc) {
72
83
  const textWidth = visibleLen(text);
@@ -24,7 +24,7 @@
24
24
  * compositor.redirect("agent:diff", diffPanelSurface);
25
25
  * // "agent:text", "agent:tool" etc. still go to stdout
26
26
  */
27
- import type { EventBus } from "../event-bus.js";
27
+ import type { EventBus } from "../core/event-bus.js";
28
28
  /**
29
29
  * A surface accepts rendered output. Stdout is a surface.
30
30
  * A floating panel's content area is a surface. A test buffer is a surface.
@@ -36,8 +36,9 @@ export const nullSurface = {
36
36
  export class StdoutSurface {
37
37
  write(text) {
38
38
  if (process.stdout.writable) {
39
+ // OPOST is cleared on the TTY; add CR to lone \n so we don't staircase.
39
40
  try {
40
- process.stdout.write(text);
41
+ process.stdout.write(text.replace(/(?<!\r)\n/g, "\r\n"));
41
42
  }
42
43
  catch { /* ignore */ }
43
44
  }
@@ -1,6 +1,6 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
- import { stripAnsi } from "./utils/ansi.js";
3
+ import { stripAnsi } from "./ansi.js";
4
4
  // Node reports a missing cwd as `spawn <binary> ENOENT` — disambiguate.
5
5
  function explainSpawnError(err, cwd) {
6
6
  if (err.code === "ENOENT" && !existsSync(cwd)) {
@@ -1,6 +1,6 @@
1
1
  import { TerminalBuffer } from "./terminal-buffer.js";
2
2
  import { HandlerRegistry } from "./handler-registry.js";
3
- import type { EventBus } from "../event-bus.js";
3
+ import type { EventBus } from "../core/event-bus.js";
4
4
  import type { BorderStyle } from "./box-frame.js";
5
5
  import { type RenderSurface } from "./compositor.js";
6
6
  export interface FloatingPanelConfig {
@@ -147,7 +147,8 @@ export declare class FloatingPanel {
147
147
  * - `{prefix}:render-border-bottom(ctx: FrameContext) -> string`
148
148
  * - `{prefix}:composite-row(content: string, bgLine: string|null, boxLeft: number, boxW: number, cols: number) -> string`
149
149
  * - `{prefix}:submit(query: string) -> void`
150
- * - `{prefix}:dismiss() -> void`
150
+ * - `{prefix}:hide() -> void` (screen down; conversation state preserved)
151
+ * - `{prefix}:reset() -> void` (conversation state cleared)
151
152
  * - `{prefix}:show() -> void`
152
153
  * - `{prefix}:input(data: string) -> boolean`
153
154
  * - `{prefix}:build-row(content: string, width: number) -> string`
@@ -177,6 +178,9 @@ export declare class FloatingPanel {
177
178
  private wrapCacheWidth;
178
179
  private passthroughTimer;
179
180
  private prevSerialized;
181
+ private autocompleteItems;
182
+ private autocompleteIndex;
183
+ private autocompleteActive;
180
184
  constructor(bus: EventBus, config: FloatingPanelConfig, handlers?: HandlerRegistry);
181
185
  private registerDefaultHandlers;
182
186
  private wireEvents;
@@ -196,8 +200,10 @@ export declare class FloatingPanel {
196
200
  hide(): void;
197
201
  /** Show the panel again after hide(), preserving conversation. */
198
202
  show(): void;
199
- /** Fully destroy the panel, resetting all state. */
200
- dismiss(): void;
203
+ /** End the conversation: screen down and all buffered state cleared. */
204
+ reset(): void;
205
+ /** Screen-only teardown; conversation state is left untouched. */
206
+ private teardownToHidden;
201
207
  /** Common screen enter logic shared by open() and show(). */
202
208
  private enterScreen;
203
209
  appendText(text: string): void;
@@ -213,8 +219,14 @@ export declare class FloatingPanel {
213
219
  scrollDown(lines?: number): void;
214
220
  getInput(): string;
215
221
  requestRender(): void;
222
+ private updateAutocomplete;
223
+ private applyAutocomplete;
224
+ private clearAutocomplete;
216
225
  private handleIntercept;
217
- /** Handle scroll input. Returns true if consumed. */
226
+ /**
227
+ * Handle scroll input. Returns true if consumed.
228
+ * Pass `includeArrows=false` in input phase so arrows reach the editor.
229
+ */
218
230
  private handleScroll;
219
231
  private handleInputKey;
220
232
  /** Compute box geometry from config + current viewport. */