agent-sh 0.15.0 → 0.15.2

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 (124) hide show
  1. package/dist/agent/agent-loop.js +11 -8
  2. package/dist/agent/events.d.ts +4 -0
  3. package/docs/README.md +14 -0
  4. package/docs/agent.md +398 -0
  5. package/docs/architecture.md +196 -0
  6. package/docs/context-management.md +200 -0
  7. package/docs/extensions.md +951 -0
  8. package/docs/library.md +84 -0
  9. package/docs/troubleshooting.md +65 -0
  10. package/docs/tui-composition.md +294 -0
  11. package/docs/usage.md +306 -0
  12. package/examples/extensions/ash-scheme/package.json +1 -1
  13. package/examples/extensions/ashi/EXTENDING.md +2 -2
  14. package/examples/extensions/ashi/README.md +2 -2
  15. package/examples/extensions/ashi/docs/ui-surface-protocol.md +1 -1
  16. package/examples/extensions/ashi/package.json +5 -3
  17. package/examples/extensions/ashi/src/chat/tool-group.ts +3 -2
  18. package/examples/extensions/ashi/src/cli.ts +9 -8
  19. package/examples/extensions/ashi/src/dialogs.ts +16 -1
  20. package/examples/extensions/ashi/src/events.ts +1 -0
  21. package/examples/extensions/ashi/src/frontend.ts +26 -6
  22. package/examples/extensions/ashi/src/renderer.ts +24 -4
  23. package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +4 -3
  24. package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +5 -8
  25. package/examples/extensions/ashi/src/ui.ts +11 -0
  26. package/examples/extensions/ashi-ink/package.json +2 -2
  27. package/examples/extensions/claude-code-bridge/package.json +1 -1
  28. package/examples/extensions/opencode-bridge/package.json +1 -1
  29. package/package.json +3 -1
  30. package/src/agent/agent-loop.ts +1566 -0
  31. package/src/agent/entry-format.ts +19 -0
  32. package/src/agent/events.ts +153 -0
  33. package/src/agent/extensions/rolling-history/constants.ts +1 -0
  34. package/src/agent/extensions/rolling-history/index.ts +202 -0
  35. package/src/agent/extensions/rolling-history/recall.ts +131 -0
  36. package/src/agent/extensions/rolling-history/strategy.ts +404 -0
  37. package/src/agent/host-types.ts +192 -0
  38. package/src/agent/index.ts +591 -0
  39. package/src/agent/live-view.ts +279 -0
  40. package/src/agent/llm-client.ts +111 -0
  41. package/src/agent/llm-facade.ts +43 -0
  42. package/src/agent/normalize-args.ts +61 -0
  43. package/src/agent/nuclear-form.ts +382 -0
  44. package/src/agent/providers/deepseek.ts +39 -0
  45. package/src/agent/providers/ollama.ts +92 -0
  46. package/src/agent/providers/openai-compatible.ts +36 -0
  47. package/src/agent/providers/openai.ts +52 -0
  48. package/src/agent/providers/opencode.ts +142 -0
  49. package/src/agent/providers/openrouter.ts +105 -0
  50. package/src/agent/providers/zai-coding-plan.ts +33 -0
  51. package/src/agent/session-store.ts +336 -0
  52. package/src/agent/skills.ts +228 -0
  53. package/src/agent/store.ts +310 -0
  54. package/src/agent/subagent.ts +305 -0
  55. package/src/agent/system-prompt.ts +151 -0
  56. package/src/agent/token-budget.ts +12 -0
  57. package/src/agent/tool-protocol.ts +722 -0
  58. package/src/agent/tool-registry.ts +66 -0
  59. package/src/agent/tools/bash.ts +95 -0
  60. package/src/agent/tools/edit-file.ts +154 -0
  61. package/src/agent/tools/expand-home.ts +7 -0
  62. package/src/agent/tools/glob.ts +108 -0
  63. package/src/agent/tools/grep.ts +228 -0
  64. package/src/agent/tools/list-skills.ts +37 -0
  65. package/src/agent/tools/ls.ts +81 -0
  66. package/src/agent/tools/pwsh.ts +140 -0
  67. package/src/agent/tools/read-file.ts +164 -0
  68. package/src/agent/tools/write-file.ts +72 -0
  69. package/src/agent/types.ts +149 -0
  70. package/src/cli/args.ts +91 -0
  71. package/src/cli/auth/cli.ts +244 -0
  72. package/src/cli/auth/discover.ts +52 -0
  73. package/src/cli/auth/keys.ts +143 -0
  74. package/src/cli/index.ts +295 -0
  75. package/src/cli/init.ts +74 -0
  76. package/src/cli/install.ts +439 -0
  77. package/src/cli/shell-env.ts +68 -0
  78. package/src/cli/subcommands.ts +24 -0
  79. package/src/core/event-bus.ts +252 -0
  80. package/src/core/extension-loader.ts +347 -0
  81. package/src/core/index.ts +152 -0
  82. package/src/core/settings.ts +398 -0
  83. package/src/core/types.ts +61 -0
  84. package/src/extensions/file-autocomplete.ts +71 -0
  85. package/src/extensions/index.ts +38 -0
  86. package/src/extensions/slash-commands/events.ts +14 -0
  87. package/src/extensions/slash-commands/index.ts +269 -0
  88. package/src/shell/events.ts +73 -0
  89. package/src/shell/host-types.ts +150 -0
  90. package/src/shell/index.ts +159 -0
  91. package/src/shell/input-handler.ts +505 -0
  92. package/src/shell/output-parser.ts +156 -0
  93. package/src/shell/shell-context.ts +193 -0
  94. package/src/shell/shell.ts +414 -0
  95. package/src/shell/strategies/bash.ts +83 -0
  96. package/src/shell/strategies/fish.ts +77 -0
  97. package/src/shell/strategies/index.ts +24 -0
  98. package/src/shell/strategies/types.ts +64 -0
  99. package/src/shell/strategies/zsh.ts +92 -0
  100. package/src/shell/terminal.ts +124 -0
  101. package/src/shell/tui-input-view.ts +222 -0
  102. package/src/shell/tui-renderer.ts +1126 -0
  103. package/src/utils/ansi.ts +140 -0
  104. package/src/utils/box-frame.ts +138 -0
  105. package/src/utils/compositor.ts +157 -0
  106. package/src/utils/diff-renderer.ts +829 -0
  107. package/src/utils/diff.ts +244 -0
  108. package/src/utils/executor.ts +305 -0
  109. package/src/utils/file-watcher.ts +110 -0
  110. package/src/utils/floating-panel.ts +1160 -0
  111. package/src/utils/handler-registry.ts +110 -0
  112. package/src/utils/line-editor.ts +636 -0
  113. package/src/utils/markdown.ts +437 -0
  114. package/src/utils/message-utils.ts +113 -0
  115. package/src/utils/package-version.ts +12 -0
  116. package/src/utils/palette.ts +64 -0
  117. package/src/utils/ref-counter.ts +9 -0
  118. package/src/utils/ripgrep-path.ts +17 -0
  119. package/src/utils/shell-output-spill.ts +76 -0
  120. package/src/utils/stream-transform.ts +292 -0
  121. package/src/utils/terminal-buffer.ts +213 -0
  122. package/src/utils/tool-display.ts +315 -0
  123. package/src/utils/tool-interactive.ts +71 -0
  124. package/src/utils/tty.ts +14 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Frontend bootstrap. Loaded directly from src/cli/index.ts (not the
3
+ * built-in extensions manifest) because PTY + stdin raw mode ownership is
4
+ * order-critical.
5
+ */
6
+ import "./events.js"; // augments BusEvents with shell-owned events
7
+ import type { ExtensionContext, RemoteSession, RemoteSessionOptions, ShellSurface } from "./host-types.js";
8
+ import type { EventBus } from "../core/event-bus.js";
9
+ import { Shell } from "./shell.js";
10
+ import { DefaultCompositor } from "../utils/compositor.js";
11
+ import { TerminalBuffer } from "../utils/terminal-buffer.js";
12
+ import { FloatingPanel, type FloatingPanelConfig } from "../utils/floating-panel.js";
13
+ import { setPalette } from "../utils/palette.js";
14
+ import * as streamTransform from "../utils/stream-transform.js";
15
+ import activateShellContext from "./shell-context.js";
16
+ import activateTuiRenderer from "./tui-renderer.js";
17
+ import { type Terminal, processTerminal, surfaceFromTerminal } from "./terminal.js";
18
+
19
+ const SHELL_SURFACE = `You're attached through a terminal shell. It shares the user's working directory, environment, and command history, and you can act on their live session — everything they run at the prompt is visible to you.`;
20
+
21
+ export interface ShellActivateOptions {
22
+ cols: number;
23
+ rows: number;
24
+ /** Path to the shell binary (zsh, bash, etc.). */
25
+ shellPath: string;
26
+ cwd: string;
27
+ /** Optional callback used by the inline status indicator. */
28
+ onShowAgentInfo?: () => { info: string; model?: string };
29
+ /**
30
+ * Host-side I/O endpoint. Defaults to processTerminal() so the CLI
31
+ * works unchanged; headless callers (web hubs, tests) supply their own.
32
+ */
33
+ terminal?: Terminal;
34
+ }
35
+
36
+ export interface ShellHandle {
37
+ /** Terminate the PTY. */
38
+ kill(): void;
39
+ /** Subscribe to PTY exit. The frontend uses this to clean up + exit. */
40
+ onExit(callback: (e: { exitCode: number; signal?: number }) => void): void;
41
+ /** Forward terminal size changes to the PTY. */
42
+ resize(cols: number, rows: number): void;
43
+ }
44
+
45
+ /**
46
+ * Register shell-owned handlers extensions can `ctx.call`, and attach
47
+ * the shell surface to ctx. Must run before `loadExtensions` so user
48
+ * extensions see `ctx.shell` populated.
49
+ */
50
+ export function registerShellHandlers(ctx: ExtensionContext): void {
51
+ const { bus } = ctx;
52
+ const compositor = new DefaultCompositor(bus);
53
+
54
+ ctx.advise("system-prompt:frontend", (next) => {
55
+ const base = (next() as string) ?? "";
56
+ return base ? `${base}\n\n${SHELL_SURFACE}` : SHELL_SURFACE;
57
+ });
58
+
59
+ const shellSurface: ShellSurface = {
60
+ compositor,
61
+ setPalette,
62
+ createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
63
+ createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
64
+ adviseInputMode: (id, advisor) => ctx.advise(`input-mode:${id}:submit`, advisor as Parameters<typeof ctx.advise>[1]),
65
+ createRemoteSession: (sessOpts: RemoteSessionOptions): RemoteSession => {
66
+ const { surface } = sessOpts;
67
+ const cleanups: (() => void)[] = [];
68
+ let active = true;
69
+
70
+ cleanups.push(compositor.redirect("agent", surface));
71
+ cleanups.push(compositor.redirect("query", surface));
72
+ cleanups.push(compositor.redirect("status", surface));
73
+
74
+ // on-processing-done is intentionally not advised — its scope
75
+ // cleanup must always run.
76
+ cleanups.push(ctx.advise("shell:on-processing-start", (next) => active ? undefined : next()));
77
+ cleanups.push(ctx.advise("shell:on-processing-redraw", (next) => active ? undefined : next()));
78
+
79
+ if (sessOpts.suppressBorders !== false) {
80
+ cleanups.push(ctx.advise("tui:response-border", (next, ...a) => active ? null : next(...a)));
81
+ }
82
+ if (sessOpts.suppressQueryBox) {
83
+ cleanups.push(ctx.advise("tui:render-user-query", (next, ...a) => active ? [] : next(...a)));
84
+ }
85
+ if (sessOpts.suppressUsage !== false) {
86
+ cleanups.push(ctx.advise("tui:render-usage", (next, ...a) => active ? "" : next(...a)));
87
+ }
88
+ return {
89
+ submit(query: string) { bus.emit("agent:submit", { query }); },
90
+ get surface() { return surface; },
91
+ get active() { return active; },
92
+ close() {
93
+ if (!active) return;
94
+ active = false;
95
+ for (const fn of cleanups.reverse()) fn();
96
+ cleanups.length = 0;
97
+ },
98
+ };
99
+ },
100
+ };
101
+ (ctx as { shell?: ShellSurface }).shell = shellSurface;
102
+
103
+ let terminalBufferSingleton: TerminalBuffer | null | undefined;
104
+ ctx.define("terminal-buffer", (): TerminalBuffer | null => {
105
+ if (terminalBufferSingleton !== undefined) return terminalBufferSingleton;
106
+ terminalBufferSingleton = TerminalBuffer.createWired(ctx.bus);
107
+ return terminalBufferSingleton;
108
+ });
109
+
110
+ // bus override lets callers pass their scoped bus, so the panel's
111
+ // listeners unwire when the extension reloads.
112
+ ctx.define("floating-panel:create", (config: FloatingPanelConfig, bus?: EventBus): FloatingPanel =>
113
+ new FloatingPanel(bus ?? ctx.bus, config),
114
+ );
115
+
116
+ activateShellContext(ctx);
117
+ activateTuiRenderer(ctx);
118
+ }
119
+
120
+ /**
121
+ * Construct the Shell, wire resize forwarding, and register cleanup with the
122
+ * provided ExtensionContext. Returns a handle the caller (typically
123
+ * `src/cli/index.ts`) uses to drive lifecycle from process-level events.
124
+ */
125
+ export function activateShell(
126
+ ctx: ExtensionContext,
127
+ opts: ShellActivateOptions,
128
+ ): ShellHandle {
129
+ const terminal = opts.terminal ?? processTerminal();
130
+ const surface = surfaceFromTerminal(terminal);
131
+ ctx.shell!.compositor.setDefault("agent", surface);
132
+ ctx.shell!.compositor.setDefault("query", surface);
133
+ ctx.shell!.compositor.setDefault("status", surface);
134
+
135
+ const shell = new Shell({
136
+ bus: ctx.bus,
137
+ handlers: { define: ctx.define, call: ctx.call },
138
+ cols: opts.cols,
139
+ rows: opts.rows,
140
+ shell: opts.shellPath,
141
+ cwd: opts.cwd,
142
+ instanceId: ctx.instanceId,
143
+ onShowAgentInfo: opts.onShowAgentInfo,
144
+ terminal,
145
+ });
146
+
147
+ const offResize = terminal.onResize((cols, rows) => shell.resize(cols, rows));
148
+
149
+ ctx.onDispose(() => {
150
+ offResize();
151
+ shell.kill();
152
+ });
153
+
154
+ return {
155
+ kill: () => shell.kill(),
156
+ onExit: (callback) => shell.onExit(callback),
157
+ resize: (cols, rows) => shell.resize(cols, rows),
158
+ };
159
+ }
@@ -0,0 +1,505 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { LineEditor } from "../utils/line-editor.js";
4
+ import { CONFIG_DIR, getSettings } from "../core/settings.js";
5
+ import type { EventBus } from "../core/event-bus.js";
6
+ import type { InputModeConfig } from "./host-types.js";
7
+ import { TuiInputView } from "./tui-input-view.js";
8
+
9
+ const HISTORY_FILE = path.join(CONFIG_DIR, "input-history");
10
+
11
+ /** Narrow contract between InputHandler and its host (Shell). */
12
+ export interface InputContext {
13
+ isForegroundBusy(): boolean;
14
+ getCwd(): string;
15
+ isAgentActive(): boolean;
16
+ writeToPty(data: string): void;
17
+ onCommandEntered(command: string, cwd: string): void;
18
+ redrawPrompt(): void;
19
+ freshPrompt(): void;
20
+ }
21
+
22
+ /* eslint-disable @typescript-eslint/no-explicit-any */
23
+ export interface InputHandlers {
24
+ define: (name: string, fn: (...a: any[]) => any) => void;
25
+ call: (name: string, ...a: any[]) => any;
26
+ }
27
+
28
+ /** Line editor + shell-passthrough buffer. Delegates rendering to TuiInputView. */
29
+ export class InputHandler {
30
+ private ctx: InputContext;
31
+ private lineBuffer = "";
32
+ private activeMode: InputModeConfig | null = null;
33
+ private pendingReturnMode: string | null = null;
34
+ private modes = new Map<string, InputModeConfig>();
35
+ private modesById = new Map<string, InputModeConfig>();
36
+ private editor = new LineEditor();
37
+ private autocompleteActive = false;
38
+ private autocompleteIndex = 0;
39
+ private autocompleteItems: { name: string; description: string }[] = [];
40
+ private history: string[] = [];
41
+ private historyIndex = -1;
42
+ private savedBuffer = "";
43
+ private escapeTimer: ReturnType<typeof setTimeout> | null = null;
44
+ private bus: EventBus;
45
+ private handlers: InputHandlers;
46
+ private onShowAgentInfo: () => { info: string; model?: string };
47
+ private view: TuiInputView;
48
+
49
+ constructor(opts: {
50
+ ctx: InputContext;
51
+ bus: EventBus;
52
+ handlers: InputHandlers;
53
+ onShowAgentInfo: () => { info: string; model?: string };
54
+ view?: TuiInputView;
55
+ }) {
56
+ this.ctx = opts.ctx;
57
+ this.bus = opts.bus;
58
+ this.handlers = opts.handlers;
59
+ this.onShowAgentInfo = opts.onShowAgentInfo;
60
+ this.view = opts.view ?? new TuiInputView();
61
+ this.loadHistory();
62
+
63
+ this.bus.on("config:changed", () => {
64
+ if (this.activeMode) this.drawPrompt();
65
+ });
66
+
67
+ this.bus.on("input:redraw", () => {
68
+ if (this.activeMode) this.renderModeInput();
69
+ });
70
+
71
+ this.bus.on("input-mode:register", (config) => {
72
+ this.registerMode(config);
73
+ });
74
+ }
75
+
76
+ private registerMode(config: InputModeConfig): void {
77
+ if (this.modes.has(config.trigger)) {
78
+ this.bus.emit("ui:error", {
79
+ message: `Input mode "${config.id}" cannot register trigger "${config.trigger}" — already taken by "${this.modes.get(config.trigger)!.id}"`,
80
+ });
81
+ return;
82
+ }
83
+ this.modes.set(config.trigger, config);
84
+ this.modesById.set(config.id, config);
85
+ this.handlers.define(`input-mode:${config.id}:submit`, config.onSubmit.bind(config));
86
+ }
87
+
88
+ private loadHistory(): void {
89
+ try {
90
+ const data = fs.readFileSync(HISTORY_FILE, "utf-8");
91
+ this.history = data.split("\n").filter(Boolean);
92
+ } catch {
93
+ }
94
+ }
95
+
96
+ private saveHistory(): void {
97
+ try {
98
+ const { historySize } = getSettings();
99
+ fs.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
100
+ const lines = this.history.slice(-historySize);
101
+ fs.writeFileSync(HISTORY_FILE, lines.join("\n") + "\n");
102
+ } catch {
103
+ }
104
+ }
105
+
106
+ private drawPrompt(showBuffer = true): void {
107
+ this.view.drawPrompt({
108
+ showBuffer,
109
+ displayText: this.editor.displayText,
110
+ displayCursor: this.editor.displayCursor,
111
+ indicator: this.activeMode?.indicator ?? "●",
112
+ promptIcon: this.activeMode?.promptIcon ?? "❯",
113
+ agentInfo: this.onShowAgentInfo(),
114
+ });
115
+ }
116
+
117
+ handleInput(data: string): void {
118
+ const intercepted = this.bus.emitPipe("input:intercept", { data, consumed: false });
119
+ if (intercepted.consumed) return;
120
+
121
+ if (this.ctx.isAgentActive()) {
122
+ if (data === "\x03") {
123
+ this.bus.emit("agent:cancel-request", {});
124
+ } else if (data.length === 1 && data.charCodeAt(0) < 32) {
125
+ this.bus.emit("input:keypress", { key: data });
126
+ }
127
+ return;
128
+ }
129
+
130
+ if (data.length === 1 && data.charCodeAt(0) < 32 && !this.activeMode) {
131
+ const code = data.charCodeAt(0);
132
+ if (code === 0x14 || code === 0x0f) { // Ctrl+T, Ctrl+O
133
+ this.bus.emit("input:keypress", { key: data });
134
+ return;
135
+ }
136
+ if (code !== 0x0d && code !== 0x03 && code !== 0x04 && code !== 0x09) {
137
+ this.bus.emit("input:keypress", { key: data });
138
+ }
139
+ }
140
+
141
+ if (this.activeMode) {
142
+ this.handleModeInput(data);
143
+ return;
144
+ }
145
+
146
+ for (let i = 0; i < data.length; i++) {
147
+ const ch = data[i]!;
148
+
149
+ if (ch === "\r") {
150
+ if (this.lineBuffer.trim()) {
151
+ this.ctx.onCommandEntered(this.lineBuffer.trim(), this.ctx.getCwd());
152
+ }
153
+ this.lineBuffer = "";
154
+ this.ctx.writeToPty(ch);
155
+ } else if (ch === "\x7f" || ch === "\b") {
156
+ this.lineBuffer = this.lineBuffer.slice(0, -1);
157
+ this.ctx.writeToPty(ch);
158
+ } else if (ch === "\x03" || ch === "\x04") {
159
+ this.lineBuffer = "";
160
+ this.ctx.writeToPty(ch);
161
+ } else if (ch === "\x0b" || ch === "\x15") {
162
+ // Ctrl-K / Ctrl-U: shell kills the line; mirror so lineBuffer stays in sync.
163
+ this.lineBuffer = "";
164
+ this.ctx.writeToPty(ch);
165
+ } else if (ch === "\x1b") {
166
+ // Forward whole escape sequence as a unit — otherwise payload bytes
167
+ // (e.g. OSC color-query response) can leak into lineBuffer.
168
+ let seq = ch;
169
+ if (i + 1 < data.length) {
170
+ const next = data[i + 1]!;
171
+ if (next === "[") {
172
+ seq += next; i++;
173
+ while (i + 1 < data.length && data[i + 1]!.charCodeAt(0) < 0x40) {
174
+ i++; seq += data[i]!;
175
+ }
176
+ if (i + 1 < data.length) { i++; seq += data[i]!; }
177
+ } else if (next === "O") {
178
+ seq += next; i++;
179
+ if (i + 1 < data.length) { i++; seq += data[i]!; }
180
+ } else if (next === "]" || next === "P" || next === "_" || next === "^") {
181
+ // OSC/DCS/APC/PM — terminated by BEL or ST (ESC \).
182
+ let j = i + 2;
183
+ let termEnd = -1;
184
+ while (j < data.length) {
185
+ const c = data[j]!;
186
+ if (c === "\x07") { termEnd = j; break; }
187
+ if (c === "\x1b" && j + 1 < data.length && data[j + 1] === "\\") {
188
+ termEnd = j + 1; break;
189
+ }
190
+ j++;
191
+ }
192
+ if (termEnd !== -1) {
193
+ seq = data.slice(i, termEnd + 1);
194
+ i = termEnd;
195
+ } else {
196
+ seq += next; i++;
197
+ }
198
+ } else {
199
+ seq += next; i++;
200
+ }
201
+ }
202
+ this.ctx.writeToPty(seq);
203
+ } else if (ch.charCodeAt(0) < 32 && ch !== "\t") {
204
+ this.ctx.writeToPty(ch);
205
+ } else {
206
+ const mode = this.modes.get(ch);
207
+ if (this.lineBuffer === "" && mode && !this.ctx.isForegroundBusy()) {
208
+ this.enterMode(mode);
209
+ return;
210
+ }
211
+ if (!this.ctx.isForegroundBusy()) this.lineBuffer += ch;
212
+ this.ctx.writeToPty(ch);
213
+ }
214
+ }
215
+ }
216
+
217
+ private enterMode(mode: InputModeConfig): void {
218
+ this.activeMode = mode;
219
+ this.editor.clear();
220
+ this.view.enableModeKeys();
221
+ this.drawPrompt(false);
222
+ }
223
+
224
+ private exitMode(): void {
225
+ this.dismissAutocomplete();
226
+ this.activeMode = null;
227
+ this.editor.clear();
228
+ this.view.disableModeKeys();
229
+ this.view.clearPromptArea();
230
+ this.view.resetCursor();
231
+ this.printPrompt();
232
+ }
233
+
234
+ printPrompt(): void {
235
+ this.ctx.redrawPrompt();
236
+ }
237
+
238
+ /** Called when agent processing completes. Returns true if the input
239
+ * handler re-entered a mode (so caller should skip shell prompt). */
240
+ handleProcessingDone(): boolean {
241
+ if (this.pendingReturnMode) {
242
+ const mode = this.modesById.get(this.pendingReturnMode);
243
+ this.pendingReturnMode = null;
244
+ if (mode) {
245
+ this.enterMode(mode);
246
+ return true;
247
+ }
248
+ }
249
+ return false;
250
+ }
251
+
252
+ private renderModeInput(): void {
253
+ this.view.beginFrame();
254
+ try {
255
+ this.view.clearAutocomplete();
256
+ this.drawPrompt();
257
+ this.updateAutocomplete();
258
+ } finally {
259
+ this.view.endFrame();
260
+ }
261
+ }
262
+
263
+ private updateAutocomplete(): void {
264
+ const buf = this.editor.text;
265
+ let command: string | null = null;
266
+ let commandArgs: string | null = null;
267
+ if (buf.startsWith("/")) {
268
+ const spaceIdx = buf.indexOf(" ");
269
+ if (spaceIdx !== -1) {
270
+ command = buf.slice(0, spaceIdx);
271
+ commandArgs = buf.slice(spaceIdx + 1);
272
+ }
273
+ }
274
+ const { items } = this.bus.emitPipe("autocomplete:request", {
275
+ buffer: buf,
276
+ command,
277
+ commandArgs,
278
+ items: [],
279
+ });
280
+ if (items.length > 0) {
281
+ this.autocompleteItems = items;
282
+ this.autocompleteActive = true;
283
+ if (this.autocompleteIndex >= items.length) this.autocompleteIndex = 0;
284
+ this.view.drawAutocomplete({ items: this.autocompleteItems, selected: this.autocompleteIndex });
285
+ } else {
286
+ this.autocompleteActive = false;
287
+ this.autocompleteItems = [];
288
+ }
289
+ }
290
+
291
+ private applyAutocomplete(): void {
292
+ if (!this.autocompleteActive || this.autocompleteItems.length === 0) return;
293
+ const selected = this.autocompleteItems[this.autocompleteIndex];
294
+ if (!selected) return;
295
+
296
+ const atPos = this.editor.text.lastIndexOf("@");
297
+ const isFileAc =
298
+ atPos >= 0 &&
299
+ (atPos === 0 || this.editor.text[atPos - 1] === " ") &&
300
+ !this.editor.text.slice(atPos + 1).includes(" ");
301
+
302
+ if (isFileAc) {
303
+ this.editor.setText(
304
+ this.editor.text.slice(0, atPos) + "@" + selected.name);
305
+ } else {
306
+ this.editor.setText(selected.name);
307
+ }
308
+
309
+ this.view.beginFrame();
310
+ try {
311
+ this.view.clearAutocomplete();
312
+ this.autocompleteActive = false;
313
+ this.autocompleteItems = [];
314
+ this.autocompleteIndex = 0;
315
+
316
+ this.drawPrompt();
317
+ if (isFileAc) this.updateAutocomplete();
318
+ } finally {
319
+ this.view.endFrame();
320
+ }
321
+ }
322
+
323
+ private dismissAutocomplete(): void {
324
+ this.view.clearAutocomplete();
325
+ this.autocompleteActive = false;
326
+ this.autocompleteItems = [];
327
+ this.autocompleteIndex = 0;
328
+ }
329
+
330
+ private handleModeInput(data: string): void {
331
+ if (this.escapeTimer) {
332
+ clearTimeout(this.escapeTimer);
333
+ this.escapeTimer = null;
334
+ }
335
+
336
+ const actions = this.editor.feed(data);
337
+
338
+ if (this.editor.hasPendingEscape()) {
339
+ this.escapeTimer = setTimeout(() => {
340
+ this.escapeTimer = null;
341
+ const flushed = this.editor.flushPendingEscape();
342
+ if (flushed.length > 0) this.processModeActions(flushed);
343
+ }, 50);
344
+ }
345
+
346
+ this.processModeActions(actions);
347
+ }
348
+
349
+ private processModeActions(actions: ReturnType<typeof this.editor.feed>): void {
350
+ for (const act of actions) {
351
+ switch (act.action) {
352
+ case "changed": {
353
+ const switchMode = this.modes.get(this.editor.text);
354
+ if (this.editor.text.length === 1 && switchMode && switchMode !== this.activeMode) {
355
+ this.view.beginFrame();
356
+ try {
357
+ this.dismissAutocomplete();
358
+ this.view.clearPromptArea();
359
+ this.activeMode = switchMode;
360
+ this.editor.clear();
361
+ this.drawPrompt(false);
362
+ } finally {
363
+ this.view.endFrame();
364
+ }
365
+ break;
366
+ }
367
+ this.historyIndex = -1;
368
+ this.autocompleteIndex = 0;
369
+ this.renderModeInput();
370
+ break;
371
+ }
372
+
373
+ case "submit": {
374
+ if (this.autocompleteActive) {
375
+ this.applyAutocomplete();
376
+ }
377
+ // Use editor.text (not act.buffer) so autocomplete selections take effect.
378
+ const query = this.editor.text.trim();
379
+ if (query) {
380
+ if (this.history.length === 0 || this.history[this.history.length - 1] !== query) {
381
+ this.history.push(query);
382
+ this.saveHistory();
383
+ }
384
+ }
385
+ this.historyIndex = -1;
386
+ this.savedBuffer = "";
387
+ this.view.clearAutocomplete();
388
+ this.view.clearPromptArea();
389
+ this.view.disableModeKeys();
390
+ const currentMode = this.activeMode!;
391
+ this.activeMode = null;
392
+ this.editor.clear();
393
+ this.view.resetCursor();
394
+ this.dismissAutocomplete();
395
+ if (query && query.startsWith("/")) {
396
+ const spaceIdx = query.indexOf(" ");
397
+ const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
398
+ const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
399
+ this.bus.emit("command:execute", { name, args });
400
+ if (currentMode.returnToSelf) {
401
+ this.enterMode(currentMode);
402
+ } else {
403
+ this.ctx.freshPrompt();
404
+ }
405
+ } else if (query) {
406
+ this.pendingReturnMode = currentMode.returnToSelf ? currentMode.id : null;
407
+ this.handlers.call(`input-mode:${currentMode.id}:submit`, query, this.bus);
408
+ } else {
409
+ this.exitMode();
410
+ }
411
+ return;
412
+ }
413
+
414
+ case "cancel":
415
+ if (this.autocompleteActive) {
416
+ this.view.beginFrame();
417
+ try {
418
+ this.dismissAutocomplete();
419
+ this.drawPrompt();
420
+ } finally {
421
+ this.view.endFrame();
422
+ }
423
+ } else {
424
+ this.exitMode();
425
+ }
426
+ return;
427
+
428
+ case "delete-empty":
429
+ this.dismissAutocomplete();
430
+ this.exitMode();
431
+ return;
432
+
433
+ case "tab":
434
+ if (this.autocompleteActive) {
435
+ this.applyAutocomplete();
436
+ }
437
+ break;
438
+
439
+ case "arrow-up":
440
+ if (this.autocompleteActive) {
441
+ this.autocompleteIndex =
442
+ this.autocompleteIndex === 0
443
+ ? this.autocompleteItems.length - 1
444
+ : this.autocompleteIndex - 1;
445
+ this.view.beginFrame();
446
+ try {
447
+ this.view.clearAutocomplete();
448
+ this.drawPrompt();
449
+ this.view.drawAutocomplete({ items: this.autocompleteItems, selected: this.autocompleteIndex });
450
+ } finally {
451
+ this.view.endFrame();
452
+ }
453
+ } else if (this.history.length > 0) {
454
+ if (this.historyIndex === -1) {
455
+ this.savedBuffer = this.editor.text;
456
+ this.historyIndex = this.history.length - 1;
457
+ } else if (this.historyIndex > 0) {
458
+ this.historyIndex--;
459
+ }
460
+ this.editor.setText(this.history[this.historyIndex]!);
461
+ this.view.beginFrame();
462
+ try {
463
+ this.view.clearAutocomplete();
464
+ this.drawPrompt();
465
+ } finally {
466
+ this.view.endFrame();
467
+ }
468
+ }
469
+ break;
470
+
471
+ case "arrow-down":
472
+ if (this.autocompleteActive) {
473
+ this.autocompleteIndex =
474
+ this.autocompleteIndex === this.autocompleteItems.length - 1
475
+ ? 0
476
+ : this.autocompleteIndex + 1;
477
+ this.view.beginFrame();
478
+ try {
479
+ this.view.clearAutocomplete();
480
+ this.drawPrompt();
481
+ this.view.drawAutocomplete({ items: this.autocompleteItems, selected: this.autocompleteIndex });
482
+ } finally {
483
+ this.view.endFrame();
484
+ }
485
+ } else if (this.historyIndex !== -1) {
486
+ if (this.historyIndex < this.history.length - 1) {
487
+ this.historyIndex++;
488
+ this.editor.setText(this.history[this.historyIndex]!);
489
+ } else {
490
+ this.historyIndex = -1;
491
+ this.editor.setText(this.savedBuffer);
492
+ }
493
+ this.view.beginFrame();
494
+ try {
495
+ this.view.clearAutocomplete();
496
+ this.drawPrompt();
497
+ } finally {
498
+ this.view.endFrame();
499
+ }
500
+ }
501
+ break;
502
+ }
503
+ }
504
+ }
505
+ }