agent-sh 0.3.1 → 0.5.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 (78) hide show
  1. package/README.md +66 -96
  2. package/dist/agent/agent-loop.d.ts +85 -0
  3. package/dist/agent/agent-loop.js +611 -0
  4. package/dist/agent/conversation-state.d.ts +27 -0
  5. package/dist/agent/conversation-state.js +59 -0
  6. package/dist/agent/index.d.ts +11 -0
  7. package/dist/agent/index.js +9 -0
  8. package/dist/agent/skills.d.ts +25 -0
  9. package/dist/agent/skills.js +186 -0
  10. package/dist/agent/subagent.d.ts +37 -0
  11. package/dist/agent/subagent.js +117 -0
  12. package/dist/agent/system-prompt.d.ts +14 -0
  13. package/dist/agent/system-prompt.js +98 -0
  14. package/dist/agent/tool-registry.d.ts +15 -0
  15. package/dist/agent/tool-registry.js +30 -0
  16. package/dist/agent/tools/bash.d.ts +7 -0
  17. package/dist/agent/tools/bash.js +62 -0
  18. package/dist/agent/tools/edit-file.d.ts +2 -0
  19. package/dist/agent/tools/edit-file.js +95 -0
  20. package/dist/agent/tools/glob.d.ts +2 -0
  21. package/dist/agent/tools/glob.js +55 -0
  22. package/dist/agent/tools/grep.d.ts +2 -0
  23. package/dist/agent/tools/grep.js +77 -0
  24. package/dist/agent/tools/list-skills.d.ts +2 -0
  25. package/dist/agent/tools/list-skills.js +28 -0
  26. package/dist/agent/tools/ls.d.ts +2 -0
  27. package/dist/agent/tools/ls.js +43 -0
  28. package/dist/agent/tools/read-file.d.ts +2 -0
  29. package/dist/agent/tools/read-file.js +55 -0
  30. package/dist/agent/tools/user-shell.d.ts +13 -0
  31. package/dist/agent/tools/user-shell.js +57 -0
  32. package/dist/agent/tools/write-file.d.ts +2 -0
  33. package/dist/agent/tools/write-file.js +74 -0
  34. package/dist/agent/types.d.ts +44 -0
  35. package/dist/agent/types.js +1 -0
  36. package/dist/core.d.ts +24 -14
  37. package/dist/core.js +260 -36
  38. package/dist/event-bus.d.ts +84 -14
  39. package/dist/event-bus.js +10 -1
  40. package/dist/extension-loader.js +12 -1
  41. package/dist/extensions/command-suggest.d.ts +10 -0
  42. package/dist/extensions/command-suggest.js +41 -0
  43. package/dist/extensions/slash-commands.d.ts +1 -1
  44. package/dist/extensions/slash-commands.js +161 -64
  45. package/dist/extensions/tui-renderer.js +111 -53
  46. package/dist/index.js +124 -120
  47. package/dist/input-handler.d.ts +17 -8
  48. package/dist/input-handler.js +152 -45
  49. package/dist/output-parser.d.ts +7 -0
  50. package/dist/output-parser.js +27 -0
  51. package/dist/settings.d.ts +53 -2
  52. package/dist/settings.js +45 -2
  53. package/dist/shell.js +36 -27
  54. package/dist/types.d.ts +46 -6
  55. package/dist/utils/box-frame.d.ts +3 -1
  56. package/dist/utils/box-frame.js +12 -5
  57. package/dist/utils/line-editor.js +4 -0
  58. package/dist/utils/llm-client.d.ts +45 -0
  59. package/dist/utils/llm-client.js +60 -0
  60. package/dist/utils/markdown.js +2 -2
  61. package/dist/utils/stream-transform.js +20 -47
  62. package/dist/utils/tool-display.js +15 -5
  63. package/examples/extensions/claude-code-bridge/README.md +35 -0
  64. package/examples/extensions/claude-code-bridge/index.ts +198 -0
  65. package/examples/extensions/claude-code-bridge/package.json +11 -0
  66. package/examples/extensions/openrouter.ts +87 -0
  67. package/examples/extensions/pi-bridge/README.md +35 -0
  68. package/examples/extensions/pi-bridge/index.ts +265 -0
  69. package/examples/extensions/pi-bridge/package.json +13 -0
  70. package/examples/extensions/subagents.ts +87 -0
  71. package/package.json +3 -5
  72. package/dist/acp-client.d.ts +0 -100
  73. package/dist/acp-client.js +0 -656
  74. package/dist/extensions/shell-exec.d.ts +0 -24
  75. package/dist/extensions/shell-exec.js +0 -188
  76. package/dist/mcp-server.d.ts +0 -13
  77. package/dist/mcp-server.js +0 -234
  78. package/examples/pi-agent-sh.ts +0 -166
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
+ import * as path from "node:path";
3
4
  import { Shell } from "./shell.js";
4
5
  import { createCore } from "./core.js";
5
6
  import { palette as p } from "./utils/palette.js";
@@ -7,19 +8,23 @@ import tuiRenderer from "./extensions/tui-renderer.js";
7
8
  import slashCommands from "./extensions/slash-commands.js";
8
9
  import fileAutocomplete from "./extensions/file-autocomplete.js";
9
10
  import shellRecall from "./extensions/shell-recall.js";
10
- import shellExec from "./extensions/shell-exec.js";
11
+ import commandSuggest from "./extensions/command-suggest.js";
11
12
  import { loadExtensions } from "./extension-loader.js";
13
+ import { getSettings } from "./settings.js";
12
14
  /**
13
- * Capture the user's full shell environment asynchronously.
15
+ * Capture the user's full shell environment.
14
16
  * This picks up env vars exported in .zshrc/.bashrc that the
15
- * Node.js process doesn't have.
16
- *
17
- * Uses -l (login shell) instead of -i to avoid TTY blocking issues.
17
+ * Node.js process doesn't have (e.g. when launched from an IDE).
18
18
  */
19
19
  async function captureShellEnvAsync(shell) {
20
20
  return new Promise((resolve) => {
21
21
  try {
22
- const child = spawn(shell, ["-l", "-c", "env -0"], {
22
+ const shellName = path.basename(shell);
23
+ const isZsh = shellName.includes("zsh");
24
+ const sourceRc = isZsh
25
+ ? 'source ~/.zshrc 2>/dev/null;'
26
+ : '[ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null;';
27
+ const child = spawn(shell, ["-l", "-c", `${sourceRc} env -0`], {
23
28
  stdio: ["ignore", "pipe", "ignore"],
24
29
  timeout: 5000,
25
30
  });
@@ -29,7 +34,7 @@ async function captureShellEnvAsync(shell) {
29
34
  });
30
35
  child.on("close", (code) => {
31
36
  if (code !== 0 || !output) {
32
- resolve({}); // Return empty to trigger fallback
37
+ resolve({});
33
38
  return;
34
39
  }
35
40
  const env = {};
@@ -41,9 +46,8 @@ async function captureShellEnvAsync(shell) {
41
46
  resolve(env);
42
47
  });
43
48
  child.on("error", () => {
44
- resolve({}); // Return empty to trigger fallback
49
+ resolve({});
45
50
  });
46
- // Safety timeout
47
51
  setTimeout(() => {
48
52
  child.kill("SIGTERM");
49
53
  resolve({});
@@ -54,14 +58,9 @@ async function captureShellEnvAsync(shell) {
54
58
  }
55
59
  });
56
60
  }
57
- /**
58
- * Merge captured shell env into base env, only adding keys that don't exist.
59
- * This preserves any runtime modifications while adding missing shell vars.
60
- */
61
61
  function mergeShellEnv(baseEnv, shellEnv) {
62
62
  const merged = { ...baseEnv };
63
63
  for (const [key, value] of Object.entries(shellEnv)) {
64
- // Only add if key doesn't exist or is empty in base env
65
64
  if (!(key in merged) || !merged[key]) {
66
65
  merged[key] = value;
67
66
  }
@@ -69,127 +68,110 @@ function mergeShellEnv(baseEnv, shellEnv) {
69
68
  return merged;
70
69
  }
71
70
  function parseArgs(argv) {
72
- // Priority: CLI args > Environment variables > Config file > Defaults
73
- const defaultAgent = process.env.AGENT_SH_AGENT || "pi-acp";
74
- let agentCommand = defaultAgent;
75
- let agentArgs = [];
76
71
  let model;
77
72
  let extensions;
73
+ let provider;
78
74
  const shell = process.env.SHELL || "/bin/bash";
75
+ let apiKey = process.env.OPENAI_API_KEY;
76
+ let baseURL = process.env.OPENAI_BASE_URL;
79
77
  for (let i = 0; i < argv.length; i++) {
80
78
  const arg = argv[i];
81
- if (arg === "--agent" && argv[i + 1]) {
82
- agentCommand = argv[++i];
79
+ if (arg === "--model" && argv[i + 1]) {
80
+ model = argv[++i];
83
81
  }
84
- else if (arg === "--agent-args" && argv[i + 1]) {
85
- const argsString = argv[++i];
86
- agentArgs = argsString.split(" ");
87
- // Extract model from agent args if provided
88
- const modelArgIndex = agentArgs.findIndex(a => a === "--model" || a === "-m");
89
- if (modelArgIndex !== -1 && agentArgs[modelArgIndex + 1]) {
90
- model = agentArgs[modelArgIndex + 1];
91
- }
82
+ else if (arg === "--api-key" && argv[i + 1]) {
83
+ apiKey = argv[++i];
84
+ }
85
+ else if (arg === "--base-url" && argv[i + 1]) {
86
+ baseURL = argv[++i];
87
+ }
88
+ else if (arg === "--provider" && argv[i + 1]) {
89
+ provider = argv[++i];
92
90
  }
93
91
  else if (arg === "--shell" && argv[i + 1]) {
94
- return { agentCommand, agentArgs, shell: argv[++i], model, extensions };
92
+ return { shell: argv[++i], model, extensions, apiKey, baseURL, provider };
95
93
  }
96
94
  else if ((arg === "--extensions" || arg === "-e") && argv[i + 1]) {
97
95
  const exts = argv[++i].split(",").map(s => s.trim());
98
96
  extensions = extensions ? [...extensions, ...exts] : exts;
99
97
  }
100
98
  else if (arg === "--help" || arg === "-h") {
101
- console.log(`agent-sh — a shell-first terminal with ACP agent access
99
+ console.log(`agent-sh — a shell-first terminal where AI is one keystroke away
102
100
 
103
101
  Usage: agent-sh [options]
104
102
 
105
- Quick Start:
106
- npm start Start with default agent (pi-acp)
107
- npm run pi Start with pi-acp agent
108
- npm run claude Start with Claude agent
103
+ Provider Profiles:
104
+ --provider <name> Use a provider from ~/.agent-sh/settings.json
105
+ --model <name> Override default model
109
106
 
110
- Options:
111
- --agent <cmd> Agent command to launch (default: $AGENT_SH_AGENT or "pi-acp")
112
- --agent-args <args> Arguments for the agent (space-separated, quoted)
107
+ Direct LLM API:
108
+ --api-key <key> API key for OpenAI-compatible provider (or set OPENAI_API_KEY)
109
+ --base-url <url> Base URL for API (or set OPENAI_BASE_URL)
110
+
111
+ General Options:
113
112
  --shell <path> Shell to use (default: $SHELL or /bin/bash)
114
113
  -e, --extensions Extensions to load (comma-separated, repeatable)
115
114
  -h, --help Show this help
116
115
 
117
- Extensions:
118
- Extensions are loaded from (in order):
119
- 1. -e flags: npm packages or file paths
120
- 2. settings: ~/.agent-sh/settings.json → "extensions": [...]
121
- 3. directory: ~/.agent-sh/extensions/ (files or dirs with index.ts)
122
-
123
116
  Environment Variables:
124
- AGENT_SH_AGENT Default agent to use (e.g., "pi-acp", "claude")
117
+ OPENAI_API_KEY API key for LLM provider
118
+ OPENAI_BASE_URL Base URL override (e.g., http://localhost:11434/v1 for Ollama)
125
119
 
126
120
  Examples:
127
- npm start --agent pi-acp
128
- npm start -- -e my-extension-package
129
- npm start -- -e ./local-ext.ts -e another-package
121
+ # Use a configured provider
122
+ agent-sh --provider openai
123
+
124
+ # Direct API access
125
+ agent-sh --api-key "$KEY" --model gpt-4o
126
+
127
+ # Local model via Ollama
128
+ agent-sh --base-url http://localhost:11434/v1 --model llama3
130
129
 
131
130
  Inside the shell:
132
131
  Type normally Commands run in your real shell
133
- > <query> Send query to the AI agent
132
+ > <query> Ask the AI agent a question (execute mode)
133
+ ? <command> Have the agent run a command in your shell (help mode)
134
134
  > /help Show available slash commands
135
135
  Ctrl-C Cancel agent response (or signal shell as usual)
136
136
  `);
137
137
  process.exit(0);
138
138
  }
139
139
  }
140
- return { agentCommand, agentArgs, shell, model, extensions };
141
- }
142
- function formatAgentInfo(agentInfo, model, thoughtLevel) {
143
- const name = agentInfo.name.replace(/-acp$/, "").replace(/-/g, " ");
144
- let infoStr = `${p.dim}${name}${p.reset}`;
145
- if (model) {
146
- const cleanModel = model
147
- .replace(/^openai\//i, "")
148
- .replace(/^anthropic\//i, "")
149
- .replace(/^google\//i, "");
150
- infoStr += ` ${p.dim}(${cleanModel})${p.reset}`;
151
- }
152
- if (thoughtLevel) {
153
- // Clean up verbose mode names like "Thinking: medium" → "medium"
154
- const label = thoughtLevel.replace(/^Thinking:\s*/i, "");
155
- infoStr += ` ${p.dim}[${label}]${p.reset}`;
156
- }
157
- return `${infoStr} ${p.success}●${p.reset}`;
140
+ return { shell, model, extensions, apiKey, baseURL, provider };
158
141
  }
159
142
  async function main() {
160
- // Set up signal handlers before any terminal operations.
161
- // Ignore SIGTTOU to prevent suspension when modifying terminal settings.
143
+ if (process.env.AGENT_SH) {
144
+ console.error("agent-sh: already running inside an agent-sh session (nested sessions are not supported).");
145
+ process.exit(1);
146
+ }
162
147
  process.on("SIGTTOU", () => { });
163
- // Also ignore SIGTTIN which can occur when reading from terminal while backgrounded.
164
148
  process.on("SIGTTIN", () => { });
165
149
  const config = parseArgs(process.argv.slice(2));
166
- // Start with current process environment (fast, non-blocking)
167
- // We'll enrich it with shell env asynchronously in the background
150
+ // Capture user's full shell environment
168
151
  const baseEnv = {};
169
152
  for (const [k, v] of Object.entries(process.env)) {
170
153
  if (v !== undefined)
171
154
  baseEnv[k] = v;
172
155
  }
173
- config.shellEnv = baseEnv;
174
- // Asynchronously capture full shell environment without blocking startup
175
156
  const shellPath = config.shell || process.env.SHELL || "/bin/bash";
176
- captureShellEnvAsync(shellPath).then((shellEnv) => {
157
+ try {
158
+ const shellEnv = await captureShellEnvAsync(shellPath);
177
159
  if (Object.keys(shellEnv).length > 0) {
178
- const merged = mergeShellEnv(config.shellEnv, shellEnv);
179
- config.shellEnv = merged;
160
+ Object.assign(baseEnv, mergeShellEnv(baseEnv, shellEnv));
180
161
  if (process.env.DEBUG) {
181
- console.error('[agent-sh] Shell environment enriched asynchronously');
162
+ console.error('[agent-sh] Shell environment captured');
182
163
  }
183
164
  }
184
- }).catch(() => {
165
+ }
166
+ catch {
185
167
  // Ignore errors, we already have process.env as fallback
186
- });
187
- if (process.env.DEBUG) {
188
- console.error('[agent-sh] Using current process environment (async enrichment pending)');
189
168
  }
190
169
  // ── Core (frontend-agnostic) ──────────────────────────────────
191
170
  const core = createCore(config);
192
- const { bus, client } = core;
171
+ const { bus } = core;
172
+ // Track agent info from bus events (populated by extension backends)
173
+ let agentInfo = null;
174
+ bus.on("agent:info", (info) => { agentInfo = info; });
193
175
  // ── Interactive frontend ──────────────────────────────────────
194
176
  if (process.env.DEBUG) {
195
177
  console.error('[agent-sh] Setting up interactive frontend...');
@@ -208,8 +190,6 @@ async function main() {
208
190
  if (process.env.DEBUG) {
209
191
  console.error('[agent-sh] Creating Shell...');
210
192
  }
211
- // Small delay on macOS to ensure we're fully in the foreground process group
212
- // before spawning the PTY. This prevents SIGTTOU suspension.
213
193
  await new Promise(resolve => setTimeout(resolve, 100));
214
194
  const shell = new Shell({
215
195
  bus,
@@ -218,13 +198,11 @@ async function main() {
218
198
  shell: config.shell || process.env.SHELL || "/bin/bash",
219
199
  cwd: process.cwd(),
220
200
  onShowAgentInfo: () => {
221
- if (client.isConnected()) {
222
- const agentInfo = client.getAgentInfo();
223
- const model = client.getModel();
224
- if (agentInfo) {
225
- const mode = client.getCurrentMode();
226
- return { info: formatAgentInfo(agentInfo, model, mode?.name ?? null) };
227
- }
201
+ if (agentInfo) {
202
+ return { info: `${p.dim}${agentInfo.name}${agentInfo.model ? ` (${agentInfo.model})` : ""}${p.reset}` };
203
+ }
204
+ if (core.llmClient) {
205
+ return { info: `${p.dim}agent-sh (${core.llmClient.model})${p.reset}` };
228
206
  }
229
207
  return { info: "" };
230
208
  },
@@ -232,6 +210,40 @@ async function main() {
232
210
  if (process.env.DEBUG) {
233
211
  console.error('[agent-sh] Shell created');
234
212
  }
213
+ // ── Input modes ──────────────────────────────────────────────
214
+ bus.emit("input-mode:register", {
215
+ id: "execute",
216
+ trigger: ">",
217
+ label: "execute",
218
+ promptIcon: "❯",
219
+ indicator: "●",
220
+ onSubmit(query, b) {
221
+ b.emit("agent:submit", { query, modeLabel: "Execute", modeInstruction: "[mode: execute]" });
222
+ },
223
+ returnToSelf: true,
224
+ });
225
+ bus.emit("input-mode:register", {
226
+ id: "help",
227
+ trigger: "?",
228
+ label: "help",
229
+ promptIcon: "❯",
230
+ indicator: "❓",
231
+ onSubmit(query, b) {
232
+ const onToolDone = (e) => {
233
+ if (e.kind === "execute") {
234
+ b.emit("agent:cancel-request", { silent: true });
235
+ }
236
+ };
237
+ const cleanup = () => {
238
+ b.off("agent:tool-completed", onToolDone);
239
+ b.off("agent:processing-done", cleanup);
240
+ };
241
+ b.on("agent:tool-completed", onToolDone);
242
+ b.on("agent:processing-done", cleanup);
243
+ b.emit("agent:submit", { query, modeLabel: "Help", modeInstruction: "[mode: help]" });
244
+ },
245
+ returnToSelf: false,
246
+ });
235
247
  // ── Extensions ────────────────────────────────────────────────
236
248
  if (process.env.DEBUG) {
237
249
  console.error('[agent-sh] Setting up extensions...');
@@ -241,20 +253,12 @@ async function main() {
241
253
  slashCommands(extCtx);
242
254
  fileAutocomplete(extCtx);
243
255
  shellRecall(extCtx);
244
- // Shell-exec: start the Unix domain socket bridge so agent extensions
245
- // and MCP servers can route tool calls to the PTY via the EventBus.
246
- const tmpDir = shell.getTmpDir();
247
- if (tmpDir) {
248
- if (process.env.DEBUG) {
249
- console.error('[agent-sh] Starting shell-exec socket server...');
250
- }
251
- shellExec(extCtx, { socketPath: `${tmpDir}/shell.sock` });
252
- }
253
- // Load extensions with timeout to prevent blocking startup
256
+ commandSuggest(extCtx);
257
+ // Load user extensions (may register alternative agent backends)
254
258
  if (process.env.DEBUG) {
255
259
  console.error('[agent-sh] Loading extensions...');
256
260
  }
257
- const loadExtensionsTimeoutMs = 10000; // 10 seconds
261
+ const loadExtensionsTimeoutMs = 10000;
258
262
  await Promise.race([
259
263
  loadExtensions(extCtx, config.extensions),
260
264
  new Promise((_, reject) => setTimeout(() => reject(new Error(`Extension loading timeout after ${loadExtensionsTimeoutMs}ms`)), loadExtensionsTimeoutMs)),
@@ -264,21 +268,28 @@ async function main() {
264
268
  if (process.env.DEBUG) {
265
269
  console.error('[agent-sh] Extensions loaded');
266
270
  }
267
- // ── Agent connection (async — don't block shell startup) ──────
268
- const agentStartTimeoutMs = 35000; // 35 seconds (slightly longer than internal timeouts)
269
- Promise.race([
270
- core.start(),
271
- new Promise((_, reject) => setTimeout(() => reject(new Error(`Agent connection timeout`)), agentStartTimeoutMs)),
272
- ]).catch((err) => {
273
- console.error(`Failed to connect to ${config.agentCommand}:`, err);
274
- });
271
+ // ── Activate agent backend ────────────────────────────────────
272
+ // Extensions had their chance to register via agent:register-backend.
273
+ // If none did, the built-in AgentLoop gets wired to bus events.
274
+ core.activateBackend();
275
+ // ── Startup banner ───────────────────────────────────────────
276
+ const settings = getSettings();
277
+ if (settings.startupBanner !== false) {
278
+ const title = core.llmClient
279
+ ? `${p.accent}${p.bold}agent-sh${p.reset}${p.dim} · ${core.llmClient.model}${p.reset}`
280
+ : `${p.accent}${p.bold}agent-sh${p.reset}`;
281
+ const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}?${p.muted} to run in shell · ${p.warning}/help${p.muted} for commands${p.reset}`;
282
+ const termW = process.stdout.columns || 80;
283
+ const borderLine = `${p.muted}${"─".repeat(Math.min(termW, 60))}${p.reset}`;
284
+ process.stdout.write("\n" + borderLine + "\n" +
285
+ " " + title + "\n" +
286
+ " " + hint + "\n" +
287
+ borderLine + "\n\n");
288
+ }
275
289
  // ── Terminal lifecycle ────────────────────────────────────────
276
290
  process.on("SIGTERM", cleanup);
277
291
  process.on("SIGHUP", cleanup);
278
- // Handle terminal stop/resume signals properly
279
292
  process.on("SIGTSTP", () => {
280
- // Handle Ctrl+Z - suspend the entire process group
281
- // Restore terminal state before suspending
282
293
  if (process.stdin.isTTY) {
283
294
  try {
284
295
  process.stdin.setRawMode(false);
@@ -287,14 +298,11 @@ async function main() {
287
298
  // Ignore
288
299
  }
289
300
  }
290
- // Re-send SIGSTOP to actually suspend
291
301
  process.kill(process.pid, "SIGSTOP");
292
302
  });
293
303
  process.on("SIGCONT", () => {
294
- // Re-acquire terminal when brought back to foreground
295
304
  if (process.stdin.isTTY) {
296
305
  try {
297
- // Ensure we reacquire controlling terminal
298
306
  process.stdin.setRawMode(true);
299
307
  }
300
308
  catch {
@@ -312,17 +320,14 @@ async function main() {
312
320
  }
313
321
  process.exit(e.exitCode);
314
322
  });
315
- // Set up stdin - resume after all event listeners are in place
316
323
  if (process.env.DEBUG) {
317
324
  console.error('[agent-sh] Resuming stdin...');
318
325
  }
319
326
  process.stdin.resume();
320
- // Set raw mode after resume to avoid SIGTTOU issues
321
327
  if (process.stdin.isTTY) {
322
328
  if (process.env.DEBUG) {
323
329
  console.error('[agent-sh] Setting raw mode...');
324
330
  }
325
- // Use setImmediate to ensure we're in the next tick
326
331
  setImmediate(() => {
327
332
  try {
328
333
  process.stdin.setRawMode(true);
@@ -334,7 +339,6 @@ async function main() {
334
339
  if (process.env.DEBUG) {
335
340
  console.error(`[agent-sh] Failed to set raw mode: ${err}`);
336
341
  }
337
- // May fail if process is in background; SIGTTOU handler prevents suspension
338
342
  }
339
343
  });
340
344
  }
@@ -16,7 +16,10 @@ export interface InputContext {
16
16
  export declare class InputHandler {
17
17
  private ctx;
18
18
  private lineBuffer;
19
- private agentInputMode;
19
+ private activeMode;
20
+ private pendingReturnMode;
21
+ private modes;
22
+ private modesById;
20
23
  private editor;
21
24
  private autocompleteActive;
22
25
  private autocompleteIndex;
@@ -37,22 +40,28 @@ export declare class InputHandler {
37
40
  model?: string;
38
41
  };
39
42
  });
43
+ private registerMode;
40
44
  private loadHistory;
41
45
  private saveHistory;
42
- /** Write the agent prompt line with cursor at the correct position. */
43
- private writeAgentPromptLine;
46
+ /** Write the mode prompt line with cursor at the correct position. */
47
+ private writeModePromptLine;
44
48
  handleInput(data: string): void;
45
- private enterAgentInputMode;
46
- private exitAgentInputMode;
49
+ private enterMode;
50
+ private exitMode;
47
51
  /** Move to the start of the prompt area and clear everything below. */
48
52
  private clearPromptArea;
49
53
  printPrompt(): void;
50
- private renderAgentInput;
54
+ /**
55
+ * Called when agent processing completes. Returns true if the input
56
+ * handler re-entered a mode (so caller should skip shell prompt).
57
+ */
58
+ handleProcessingDone(): boolean;
59
+ private renderModeInput;
51
60
  private updateAutocomplete;
52
61
  private renderAutocomplete;
53
62
  private applyAutocomplete;
54
63
  private dismissAutocomplete;
55
64
  private clearAutocompleteLines;
56
- private handleAgentInput;
57
- private processAgentActions;
65
+ private handleModeInput;
66
+ private processModeActions;
58
67
  }