agent-sh 0.1.0 → 0.2.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.
@@ -9,10 +9,7 @@ export class OutputParser {
9
9
  currentOutputCapture = "";
10
10
  lastCommand = "";
11
11
  foregroundBusy = false;
12
- capturingPrompt = false;
13
- promptCaptureComplete = false;
14
- promptBuffer = "";
15
- lastPrompt = "";
12
+ promptReady = false;
16
13
  constructor(bus, initialCwd) {
17
14
  this.bus = bus;
18
15
  this.cwd = initialCwd;
@@ -20,19 +17,7 @@ export class OutputParser {
20
17
  /** Process a chunk of PTY output data. */
21
18
  processData(data) {
22
19
  this.parseOSC7(data);
23
- // Bracketed prompt capture: accumulate bytes between OSC 9999 and 9998.
24
- // parsePromptMarker may start capture (setting promptBuffer to the tail
25
- // of the current chunk), so we only append subsequent chunks here.
26
- const wasCapturing = this.capturingPrompt;
27
20
  this.parsePromptMarker(data);
28
- // If we were already capturing before this chunk (and still are), append
29
- // the full chunk. If capture just started in parsePromptMarker above, the
30
- // tail after the start marker is already in promptBuffer — don't double-add.
31
- if (wasCapturing && this.capturingPrompt) {
32
- this.promptBuffer += data;
33
- }
34
- // Check for end marker. Must run after the append above so that
35
- // multi-chunk captures include this chunk's data before we finalize.
36
21
  this.parsePromptEnd(data);
37
22
  }
38
23
  /** Called when user presses Enter on a non-empty line. */
@@ -45,28 +30,9 @@ export class OutputParser {
45
30
  this.bus.emit("shell:foreground-busy", { busy: true });
46
31
  }
47
32
  }
48
- /** Returns the full captured prompt bytes, or empty if incomplete. */
49
- getLastPrompt() {
50
- if (!this.promptCaptureComplete)
51
- return "";
52
- return this.lastPrompt;
53
- }
54
- /**
55
- * Returns just the last line of the captured prompt (e.g. p10k's "❯ " line).
56
- * This is safe to replay with \r because it's linear text (colors + chars),
57
- * not relative cursor positioning. Returns empty if no complete capture.
58
- */
59
- getLastPromptLine() {
60
- if (!this.promptCaptureComplete)
61
- return "";
62
- // Find the last \r\n or \n — everything after it is the final prompt line
63
- const lastNewline = this.lastPrompt.lastIndexOf("\n");
64
- if (lastNewline < 0)
65
- return this.lastPrompt;
66
- return this.lastPrompt.slice(lastNewline + 1);
67
- }
68
- isPromptCaptureComplete() {
69
- return this.promptCaptureComplete;
33
+ /** Whether the shell's prompt is fully rendered and ready for input. */
34
+ isPromptReady() {
35
+ return this.promptReady;
70
36
  }
71
37
  isForegroundBusy() {
72
38
  return this.foregroundBusy;
@@ -90,7 +56,14 @@ export class OutputParser {
90
56
  * Each time a prompt appears, we finalize the previous command's output.
91
57
  */
92
58
  parsePromptMarker(data) {
93
- if (data.includes("\x1b]9999;PROMPT\x07")) {
59
+ const marker = "\x1b]9999;PROMPT\x07";
60
+ const markerIdx = data.indexOf(marker);
61
+ if (markerIdx !== -1) {
62
+ // Capture any output that arrived in the same chunk before the marker
63
+ if (markerIdx > 0) {
64
+ this.currentOutputCapture += data.slice(0, markerIdx);
65
+ }
66
+ this.promptReady = false;
94
67
  if (this.foregroundBusy) {
95
68
  this.foregroundBusy = false;
96
69
  this.bus.emit("shell:foreground-busy", { busy: false });
@@ -107,54 +80,19 @@ export class OutputParser {
107
80
  }
108
81
  this.lastCommand = "";
109
82
  this.currentOutputCapture = "";
110
- // Start bracketed prompt capture: accumulate bytes until OSC 9998
111
- this.capturingPrompt = true;
112
- this.promptCaptureComplete = false;
113
- this.promptBuffer = "";
114
- const markerEnd = data.indexOf("\x1b]9999;PROMPT\x07") + "\x1b]9999;PROMPT\x07".length;
115
- if (markerEnd < data.length) {
116
- this.promptBuffer = data.slice(markerEnd);
117
- }
118
83
  }
119
84
  else {
120
85
  this.currentOutputCapture += data;
121
86
  }
122
87
  }
123
88
  /**
124
- * Detect end-of-prompt marker (OSC 9998). Finalizes the bracketed capture.
125
- *
126
- * By the time this runs, the current chunk has already been appended to
127
- * promptBuffer (either by parsePromptMarker for the first chunk, or by
128
- * the wasCapturing guard in processData for subsequent chunks). So we
129
- * just need to trim everything from the end marker onward.
89
+ * Detect end-of-prompt marker (OSC 9998). The prompt is fully rendered
90
+ * and the shell is ready for input.
130
91
  */
131
92
  parsePromptEnd(data) {
132
- if (!this.capturingPrompt)
133
- return;
134
- if (!data.includes("\x1b]9998;READY\x07"))
135
- return;
136
- // promptBuffer already contains this chunk's data. Find the end marker
137
- // within the buffer and trim everything from it onward.
138
- const endMarker = "\x1b]9998;READY\x07";
139
- const bufEndIdx = this.promptBuffer.indexOf(endMarker);
140
- if (bufEndIdx >= 0) {
141
- this.promptBuffer = this.promptBuffer.slice(0, bufEndIdx);
93
+ if (data.includes("\x1b]9998;READY\x07")) {
94
+ this.promptReady = true;
142
95
  }
143
- this.capturingPrompt = false;
144
- this.promptCaptureComplete = true;
145
- this.lastPrompt = this.sanitizePromptForReplay(this.promptBuffer);
146
- }
147
- /**
148
- * Strip internal OSC markers from captured prompt so replay is clean.
149
- * We intentionally strip all OSC 7 sequences — they're used for cwd
150
- * reporting and have no visual effect, so replaying them would just
151
- * cause duplicate cwd-change events.
152
- */
153
- sanitizePromptForReplay(raw) {
154
- return raw
155
- .replace(/\x1b\]7;[^\x07]*\x07/g, "") // OSC 7 (cwd reporting)
156
- .replace(/\x1b\]9999;PROMPT\x07/g, "") // start marker
157
- .replace(/\x1b\]9998;READY\x07/g, ""); // end marker
158
96
  }
159
97
  removeEchoedCommand(output, command) {
160
98
  const lines = output.split("\n");
package/dist/shell.d.ts CHANGED
@@ -7,6 +7,7 @@ export declare class Shell implements InputContext {
7
7
  private outputParser;
8
8
  private paused;
9
9
  private agentActive;
10
+ private isZsh;
10
11
  private tmpDir?;
11
12
  constructor(opts: {
12
13
  bus: EventBus;
@@ -24,10 +25,11 @@ export declare class Shell implements InputContext {
24
25
  isAgentActive(): boolean;
25
26
  writeToPty(data: string): void;
26
27
  /**
27
- * Lightweight redraw: replay just the last line of the shell's prompt
28
- * (e.g. p10k's "❯ "). This works because agent input mode only overwrites
29
- * the final prompt linethe path bar above is still intact. The last
30
- * line is linear text (colors + chars + clear-to-end), no cursor positioning.
28
+ * Lightweight redraw: ask the shell to redraw its own prompt via a hidden
29
+ * ZLE widget (zsh) bound to \e[9999~. The shell knows how to draw its
30
+ * prompt correctlywe don't try to replay captured bytes.
31
+ *
32
+ * For bash, falls back to sending \n for a fresh prompt cycle.
31
33
  */
32
34
  redrawPrompt(): void;
33
35
  /**
@@ -45,6 +47,8 @@ export declare class Shell implements InputContext {
45
47
  * zero frontend knowledge; any frontend can subscribe to the same events.
46
48
  */
47
49
  private setupAgentLifecycle;
50
+ /** Temp directory used for shell config and sockets. */
51
+ getTmpDir(): string | undefined;
48
52
  resize(cols: number, rows: number): void;
49
53
  onExit(callback: (e: {
50
54
  exitCode: number;
package/dist/shell.js CHANGED
@@ -11,6 +11,7 @@ export class Shell {
11
11
  outputParser;
12
12
  paused = false;
13
13
  agentActive = false;
14
+ isZsh = false;
14
15
  tmpDir;
15
16
  constructor(opts) {
16
17
  // Build environment — filter out undefined values (node-pty's native
@@ -38,6 +39,7 @@ export class Shell {
38
39
  let shellArgs;
39
40
  const osc7Cmd = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
40
41
  const promptMarker = 'printf "\\e]9999;PROMPT\\a"';
42
+ this.isZsh = isZsh;
41
43
  if (isZsh) {
42
44
  // For zsh: use ZDOTDIR to source user's real config, then append
43
45
  // our hooks via precmd_functions (additive — doesn't clobber p10k/omz).
@@ -68,6 +70,14 @@ export class Shell {
68
70
  " }",
69
71
  "fi",
70
72
  "zle -N zle-line-init __agent_sh_line_init",
73
+ "",
74
+ "# Hidden widget to trigger prompt redraw from Node.js side",
75
+ "# Bound to an unused escape sequence that no real key produces",
76
+ "__agent_sh_redraw() {",
77
+ " zle reset-prompt",
78
+ "}",
79
+ "zle -N __agent_sh_redraw",
80
+ "bindkey '\\e[9999~' __agent_sh_redraw",
71
81
  ].join("\n") + "\n");
72
82
  env.ZDOTDIR = this.tmpDir;
73
83
  shellArgs = ["--no-globalrcs"];
@@ -131,10 +141,11 @@ export class Shell {
131
141
  this.ptyProcess.write(data);
132
142
  }
133
143
  /**
134
- * Lightweight redraw: replay just the last line of the shell's prompt
135
- * (e.g. p10k's "❯ "). This works because agent input mode only overwrites
136
- * the final prompt linethe path bar above is still intact. The last
137
- * line is linear text (colors + chars + clear-to-end), no cursor positioning.
144
+ * Lightweight redraw: ask the shell to redraw its own prompt via a hidden
145
+ * ZLE widget (zsh) bound to \e[9999~. The shell knows how to draw its
146
+ * prompt correctlywe don't try to replay captured bytes.
147
+ *
148
+ * For bash, falls back to sending \n for a fresh prompt cycle.
138
149
  */
139
150
  redrawPrompt() {
140
151
  const result = this.bus.emitPipe("shell:redraw-prompt", {
@@ -142,12 +153,12 @@ export class Shell {
142
153
  handled: false,
143
154
  });
144
155
  if (!result.handled) {
145
- const lastLine = this.outputParser.getLastPromptLine();
146
- if (lastLine) {
147
- process.stdout.write("\r" + lastLine);
156
+ if (this.isZsh) {
157
+ // Trigger the hidden ZLE widget — zle reset-prompt redraws cleanly
158
+ this.ptyProcess.write("\x1b[9999~");
148
159
  }
149
160
  else {
150
- // Fallback: send \n for a fresh prompt cycle
161
+ // Bash: no zle reset-prompt equivalent, use fresh prompt cycle
151
162
  this.ptyProcess.write("\n");
152
163
  }
153
164
  }
@@ -202,8 +213,35 @@ export class Shell {
202
213
  this.paused = true;
203
214
  return payload;
204
215
  });
216
+ // Shell exec: write a command to the live PTY and capture its output.
217
+ // stdout is paused during agent processing, so PTY output flows through
218
+ // OutputParser (for OSC detection) but never reaches the terminal.
219
+ this.bus.onPipeAsync("shell:exec-request", async (payload) => {
220
+ const output = await new Promise((resolve, reject) => {
221
+ const timeout = setTimeout(() => {
222
+ this.bus.off("shell:command-done", handler);
223
+ // Kill any hung command
224
+ this.ptyProcess.write("\x03");
225
+ reject(new Error("Shell exec timed out after 30s"));
226
+ }, 30_000);
227
+ const handler = (e) => {
228
+ clearTimeout(timeout);
229
+ this.bus.off("shell:command-done", handler);
230
+ resolve({ output: e.output, cwd: e.cwd });
231
+ };
232
+ this.bus.on("shell:command-done", handler);
233
+ // Start capture and write to PTY
234
+ this.outputParser.onCommandEntered(payload.command, this.outputParser.getCwd());
235
+ this.ptyProcess.write(payload.command + "\r");
236
+ });
237
+ return { ...payload, output: output.output, cwd: output.cwd, done: true };
238
+ });
205
239
  }
206
240
  // ── Public API (used by index.ts) ──
241
+ /** Temp directory used for shell config and sockets. */
242
+ getTmpDir() {
243
+ return this.tmpDir;
244
+ }
207
245
  resize(cols, rows) {
208
246
  this.ptyProcess.resize(cols, rows);
209
247
  }
package/dist/types.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface AgentShellConfig {
8
8
  shell?: string;
9
9
  model?: string;
10
10
  extensions?: string[];
11
+ /** Full shell environment (from user's rc files) for agent subprocess. */
12
+ shellEnv?: Record<string, string>;
11
13
  }
12
14
  /**
13
15
  * Context passed to user/third-party extensions.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Minimal line editor with readline-style keybindings.
3
+ *
4
+ * Pure logic — no I/O, no rendering, no event bus. Consumers feed raw
5
+ * terminal input bytes and receive high-level actions back. Buffer and
6
+ * cursor state are public for rendering.
7
+ */
8
+ export type LineEditAction = {
9
+ action: "changed";
10
+ } | {
11
+ action: "submit";
12
+ buffer: string;
13
+ } | {
14
+ action: "cancel";
15
+ } | {
16
+ action: "delete-empty";
17
+ } | {
18
+ action: "tab";
19
+ } | {
20
+ action: "arrow-up";
21
+ } | {
22
+ action: "arrow-down";
23
+ };
24
+ export declare class LineEditor {
25
+ buffer: string;
26
+ cursor: number;
27
+ /** Process raw terminal input, return actions for the consumer. */
28
+ feed(data: string): LineEditAction[];
29
+ clear(): void;
30
+ /**
31
+ * Parse and handle a CSI sequence (\x1b[...) starting at `start`.
32
+ * Returns the number of bytes consumed.
33
+ */
34
+ private handleCSI;
35
+ private wordBackward;
36
+ private wordForward;
37
+ private deleteWordBackward;
38
+ private deleteWordForward;
39
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Minimal line editor with readline-style keybindings.
3
+ *
4
+ * Pure logic — no I/O, no rendering, no event bus. Consumers feed raw
5
+ * terminal input bytes and receive high-level actions back. Buffer and
6
+ * cursor state are public for rendering.
7
+ */
8
+ // ── Line editor ─────────────────────────────────────────────────
9
+ export class LineEditor {
10
+ buffer = "";
11
+ cursor = 0;
12
+ /** Process raw terminal input, return actions for the consumer. */
13
+ feed(data) {
14
+ const actions = [];
15
+ let i = 0;
16
+ while (i < data.length) {
17
+ const ch = data[i];
18
+ // ── Escape sequences ────────────────────────────────
19
+ if (ch === "\x1b") {
20
+ const next = data[i + 1];
21
+ // Bare Escape (nothing follows in this chunk)
22
+ if (next == null) {
23
+ actions.push({ action: "cancel" });
24
+ i++;
25
+ continue;
26
+ }
27
+ // CSI sequence: \x1b[...
28
+ if (next === "[") {
29
+ const { consumed } = this.handleCSI(data, i, actions);
30
+ i += consumed;
31
+ continue;
32
+ }
33
+ // Alt/Option + key: \x1b followed by char
34
+ i += 2; // consume \x1b + next byte
35
+ if (next === "\x7f") {
36
+ // Option+Backspace: delete word backward
37
+ if (this.deleteWordBackward())
38
+ actions.push({ action: "changed" });
39
+ }
40
+ else if (next === "b") {
41
+ // Alt+B: word backward
42
+ if (this.wordBackward())
43
+ actions.push({ action: "changed" });
44
+ }
45
+ else if (next === "f") {
46
+ // Alt+F: word forward
47
+ if (this.wordForward())
48
+ actions.push({ action: "changed" });
49
+ }
50
+ else if (next === "d") {
51
+ // Alt+D: delete word forward
52
+ if (this.deleteWordForward())
53
+ actions.push({ action: "changed" });
54
+ }
55
+ // Other Alt+key — ignore
56
+ continue;
57
+ }
58
+ // ── Control characters ──────────────────────────────
59
+ if (ch === "\r") {
60
+ actions.push({ action: "submit", buffer: this.buffer });
61
+ i++;
62
+ continue;
63
+ }
64
+ if (ch === "\x03") {
65
+ actions.push({ action: "cancel" });
66
+ i++;
67
+ continue;
68
+ }
69
+ if (ch === "\t") {
70
+ actions.push({ action: "tab" });
71
+ i++;
72
+ continue;
73
+ }
74
+ if (ch === "\x7f" || ch === "\b") {
75
+ // Backspace
76
+ if (this.buffer.length === 0) {
77
+ actions.push({ action: "delete-empty" });
78
+ }
79
+ else if (this.cursor > 0) {
80
+ this.buffer = this.buffer.slice(0, this.cursor - 1) + this.buffer.slice(this.cursor);
81
+ this.cursor--;
82
+ actions.push({ action: "changed" });
83
+ }
84
+ i++;
85
+ continue;
86
+ }
87
+ // Ctrl-A: home
88
+ if (ch === "\x01") {
89
+ if (this.cursor > 0) {
90
+ this.cursor = 0;
91
+ actions.push({ action: "changed" });
92
+ }
93
+ i++;
94
+ continue;
95
+ }
96
+ // Ctrl-E: end
97
+ if (ch === "\x05") {
98
+ if (this.cursor < this.buffer.length) {
99
+ this.cursor = this.buffer.length;
100
+ actions.push({ action: "changed" });
101
+ }
102
+ i++;
103
+ continue;
104
+ }
105
+ // Ctrl-B: back one char
106
+ if (ch === "\x02") {
107
+ if (this.cursor > 0) {
108
+ this.cursor--;
109
+ actions.push({ action: "changed" });
110
+ }
111
+ i++;
112
+ continue;
113
+ }
114
+ // Ctrl-F: forward one char
115
+ if (ch === "\x06") {
116
+ if (this.cursor < this.buffer.length) {
117
+ this.cursor++;
118
+ actions.push({ action: "changed" });
119
+ }
120
+ i++;
121
+ continue;
122
+ }
123
+ // Ctrl-U: delete to start of line
124
+ if (ch === "\x15") {
125
+ if (this.cursor > 0) {
126
+ this.buffer = this.buffer.slice(this.cursor);
127
+ this.cursor = 0;
128
+ actions.push({ action: "changed" });
129
+ }
130
+ i++;
131
+ continue;
132
+ }
133
+ // Ctrl-K: delete to end of line
134
+ if (ch === "\x0b") {
135
+ if (this.cursor < this.buffer.length) {
136
+ this.buffer = this.buffer.slice(0, this.cursor);
137
+ actions.push({ action: "changed" });
138
+ }
139
+ i++;
140
+ continue;
141
+ }
142
+ // Ctrl-W: delete word backward
143
+ if (ch === "\x17") {
144
+ if (this.deleteWordBackward())
145
+ actions.push({ action: "changed" });
146
+ i++;
147
+ continue;
148
+ }
149
+ // Other control chars — ignore
150
+ if (ch.charCodeAt(0) < 0x20) {
151
+ i++;
152
+ continue;
153
+ }
154
+ // ── Printable character ─────────────────────────────
155
+ this.buffer = this.buffer.slice(0, this.cursor) + ch + this.buffer.slice(this.cursor);
156
+ this.cursor++;
157
+ actions.push({ action: "changed" });
158
+ i++;
159
+ }
160
+ return actions;
161
+ }
162
+ clear() {
163
+ this.buffer = "";
164
+ this.cursor = 0;
165
+ }
166
+ // ── CSI sequence handling ───────────────────────────────────
167
+ /**
168
+ * Parse and handle a CSI sequence (\x1b[...) starting at `start`.
169
+ * Returns the number of bytes consumed.
170
+ */
171
+ handleCSI(data, start, actions) {
172
+ // Skip \x1b[
173
+ let j = start + 2;
174
+ // Accumulate parameter bytes (0x20-0x3F: digits, semicolons, etc.)
175
+ let params = "";
176
+ while (j < data.length && data.charCodeAt(j) >= 0x20 && data.charCodeAt(j) < 0x40) {
177
+ params += data[j];
178
+ j++;
179
+ }
180
+ const final = j < data.length ? data[j] : "";
181
+ const consumed = j - start + (final ? 1 : 0);
182
+ // Dispatch on final byte
183
+ switch (final) {
184
+ case "A": // Up arrow
185
+ actions.push({ action: "arrow-up" });
186
+ break;
187
+ case "B": // Down arrow
188
+ actions.push({ action: "arrow-down" });
189
+ break;
190
+ case "C": // Right (or modified right: 1;3C, 1;5C = word right)
191
+ if (params.includes(";")) {
192
+ if (this.wordForward())
193
+ actions.push({ action: "changed" });
194
+ }
195
+ else {
196
+ if (this.cursor < this.buffer.length) {
197
+ this.cursor++;
198
+ actions.push({ action: "changed" });
199
+ }
200
+ }
201
+ break;
202
+ case "D": // Left (or modified left: 1;3D, 1;5D = word left)
203
+ if (params.includes(";")) {
204
+ if (this.wordBackward())
205
+ actions.push({ action: "changed" });
206
+ }
207
+ else {
208
+ if (this.cursor > 0) {
209
+ this.cursor--;
210
+ actions.push({ action: "changed" });
211
+ }
212
+ }
213
+ break;
214
+ case "H": // Home
215
+ if (this.cursor > 0) {
216
+ this.cursor = 0;
217
+ actions.push({ action: "changed" });
218
+ }
219
+ break;
220
+ case "F": // End
221
+ if (this.cursor < this.buffer.length) {
222
+ this.cursor = this.buffer.length;
223
+ actions.push({ action: "changed" });
224
+ }
225
+ break;
226
+ case "~": // Extended keys: Delete (3~), etc.
227
+ if (params === "3") {
228
+ // Delete key: delete char under cursor
229
+ if (this.cursor < this.buffer.length) {
230
+ this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
231
+ actions.push({ action: "changed" });
232
+ }
233
+ }
234
+ break;
235
+ // All other CSI sequences — silently ignored
236
+ }
237
+ return { consumed };
238
+ }
239
+ // ── Word movement / deletion helpers ────────────────────────
240
+ wordBackward() {
241
+ if (this.cursor === 0)
242
+ return false;
243
+ let pos = this.cursor;
244
+ // Skip spaces
245
+ while (pos > 0 && this.buffer[pos - 1] === " ")
246
+ pos--;
247
+ // Skip word chars
248
+ while (pos > 0 && this.buffer[pos - 1] !== " ")
249
+ pos--;
250
+ if (pos === this.cursor)
251
+ return false;
252
+ this.cursor = pos;
253
+ return true;
254
+ }
255
+ wordForward() {
256
+ if (this.cursor >= this.buffer.length)
257
+ return false;
258
+ let pos = this.cursor;
259
+ // Skip word chars
260
+ while (pos < this.buffer.length && this.buffer[pos] !== " ")
261
+ pos++;
262
+ // Skip spaces
263
+ while (pos < this.buffer.length && this.buffer[pos] === " ")
264
+ pos++;
265
+ if (pos === this.cursor)
266
+ return false;
267
+ this.cursor = pos;
268
+ return true;
269
+ }
270
+ deleteWordBackward() {
271
+ if (this.cursor === 0)
272
+ return false;
273
+ const start = this.cursor;
274
+ this.wordBackward();
275
+ this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(start);
276
+ return true;
277
+ }
278
+ deleteWordForward() {
279
+ if (this.cursor >= this.buffer.length)
280
+ return false;
281
+ const start = this.cursor;
282
+ this.wordForward();
283
+ this.buffer = this.buffer.slice(0, start) + this.buffer.slice(this.cursor);
284
+ this.cursor = start;
285
+ return true;
286
+ }
287
+ }
@@ -4,6 +4,15 @@ export interface ToolCallRender {
4
4
  title: string;
5
5
  /** Optional command string for bash-like tools. */
6
6
  command?: string;
7
+ /** Tool kind from ACP (read, edit, execute, search, etc.). */
8
+ kind?: string;
9
+ /** File locations affected by the tool call. */
10
+ locations?: {
11
+ path: string;
12
+ line?: number | null;
13
+ }[];
14
+ /** Raw input parameters sent to the tool. */
15
+ rawInput?: unknown;
7
16
  }
8
17
  export interface ToolResultRender {
9
18
  exitCode: number | null;