agent-sh 0.1.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 (50) hide show
  1. package/README.md +659 -0
  2. package/dist/acp-client.d.ts +76 -0
  3. package/dist/acp-client.js +507 -0
  4. package/dist/context-manager.d.ts +45 -0
  5. package/dist/context-manager.js +405 -0
  6. package/dist/core.d.ts +41 -0
  7. package/dist/core.js +76 -0
  8. package/dist/event-bus.d.ts +140 -0
  9. package/dist/event-bus.js +79 -0
  10. package/dist/executor.d.ts +31 -0
  11. package/dist/executor.js +116 -0
  12. package/dist/extension-loader.d.ts +16 -0
  13. package/dist/extension-loader.js +164 -0
  14. package/dist/extensions/file-autocomplete.d.ts +2 -0
  15. package/dist/extensions/file-autocomplete.js +63 -0
  16. package/dist/extensions/shell-recall.d.ts +9 -0
  17. package/dist/extensions/shell-recall.js +8 -0
  18. package/dist/extensions/slash-commands.d.ts +2 -0
  19. package/dist/extensions/slash-commands.js +105 -0
  20. package/dist/extensions/tui-renderer.d.ts +2 -0
  21. package/dist/extensions/tui-renderer.js +354 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +159 -0
  24. package/dist/input-handler.d.ts +48 -0
  25. package/dist/input-handler.js +302 -0
  26. package/dist/output-parser.d.ts +55 -0
  27. package/dist/output-parser.js +166 -0
  28. package/dist/shell.d.ts +54 -0
  29. package/dist/shell.js +219 -0
  30. package/dist/types.d.ts +71 -0
  31. package/dist/types.js +1 -0
  32. package/dist/utils/ansi.d.ts +12 -0
  33. package/dist/utils/ansi.js +23 -0
  34. package/dist/utils/box-frame.d.ts +21 -0
  35. package/dist/utils/box-frame.js +60 -0
  36. package/dist/utils/diff-renderer.d.ts +20 -0
  37. package/dist/utils/diff-renderer.js +506 -0
  38. package/dist/utils/diff.d.ts +24 -0
  39. package/dist/utils/diff.js +122 -0
  40. package/dist/utils/file-watcher.d.ts +31 -0
  41. package/dist/utils/file-watcher.js +101 -0
  42. package/dist/utils/markdown.d.ts +39 -0
  43. package/dist/utils/markdown.js +248 -0
  44. package/dist/utils/palette.d.ts +32 -0
  45. package/dist/utils/palette.js +36 -0
  46. package/dist/utils/tool-display.d.ts +33 -0
  47. package/dist/utils/tool-display.js +141 -0
  48. package/examples/extensions/interactive-prompts.ts +161 -0
  49. package/examples/extensions/solarized-theme.ts +27 -0
  50. package/package.json +72 -0
@@ -0,0 +1,302 @@
1
+ import { visibleLen } from "./utils/ansi.js";
2
+ import { palette as p } from "./utils/palette.js";
3
+ export class InputHandler {
4
+ ctx;
5
+ lineBuffer = "";
6
+ agentInputMode = false;
7
+ agentInputBuffer = "";
8
+ autocompleteActive = false;
9
+ autocompleteIndex = 0;
10
+ autocompleteItems = [];
11
+ autocompleteLines = 0;
12
+ bus;
13
+ onShowAgentInfo;
14
+ constructor(opts) {
15
+ this.ctx = opts.ctx;
16
+ this.bus = opts.bus;
17
+ this.onShowAgentInfo = opts.onShowAgentInfo;
18
+ }
19
+ /** Write the agent prompt line (clear + info prefix + ❯ + buffer text). */
20
+ writeAgentPromptLine(showBuffer = true) {
21
+ const agentInfo = this.onShowAgentInfo();
22
+ const infoPrefix = agentInfo.info ? `${agentInfo.info} ` : "";
23
+ process.stdout.write("\r\x1b[2K" +
24
+ infoPrefix +
25
+ p.warning + p.bold + "❯ " + p.reset +
26
+ (showBuffer ? p.accent + this.agentInputBuffer + p.reset : ""));
27
+ }
28
+ handleInput(data) {
29
+ // If agent is running (processing a query), only Ctrl-C and control keys
30
+ if (this.ctx.isAgentActive()) {
31
+ if (data === "\x03") {
32
+ this.bus.emit("agent:cancel-request", {});
33
+ }
34
+ else if (data.length === 1 && data.charCodeAt(0) < 32) {
35
+ this.bus.emit("input:keypress", { key: data });
36
+ }
37
+ return;
38
+ }
39
+ // Forward control chars that normal shell mode doesn't handle
40
+ if (data.length === 1 && data.charCodeAt(0) < 32 && !this.agentInputMode) {
41
+ const code = data.charCodeAt(0);
42
+ // Don't intercept keys that shell mode handles: CR, Ctrl-C, Ctrl-D, Tab
43
+ if (code !== 0x0d && code !== 0x03 && code !== 0x04 && code !== 0x09) {
44
+ this.bus.emit("input:keypress", { key: data });
45
+ }
46
+ }
47
+ // If in agent input mode (typing a query after ">")
48
+ if (this.agentInputMode) {
49
+ this.handleAgentInput(data);
50
+ return;
51
+ }
52
+ for (let i = 0; i < data.length; i++) {
53
+ const ch = data[i];
54
+ if (ch === "\r") {
55
+ // Record the command — output will be captured until next prompt marker
56
+ if (this.lineBuffer.trim()) {
57
+ this.ctx.onCommandEntered(this.lineBuffer.trim(), this.ctx.getCwd());
58
+ }
59
+ this.lineBuffer = "";
60
+ this.ctx.writeToPty(ch);
61
+ }
62
+ else if (ch === "\x7f" || ch === "\b") {
63
+ this.lineBuffer = this.lineBuffer.slice(0, -1);
64
+ this.ctx.writeToPty(ch);
65
+ }
66
+ else if (ch === "\x03") {
67
+ this.lineBuffer = "";
68
+ this.ctx.writeToPty(ch);
69
+ }
70
+ else if (ch === "\x04") {
71
+ this.lineBuffer = "";
72
+ this.ctx.writeToPty(ch);
73
+ }
74
+ else if (ch.charCodeAt(0) < 32 && ch !== "\t") {
75
+ this.lineBuffer = "";
76
+ this.ctx.writeToPty(ch);
77
+ }
78
+ else {
79
+ // Check if ">" at start of empty line → enter agent input mode
80
+ // But not if a foreground process (ssh, vim, etc.) is running
81
+ if (this.lineBuffer === "" && ch === ">" && !this.ctx.isForegroundBusy()) {
82
+ this.enterAgentInputMode();
83
+ return; // don't process remaining chars
84
+ }
85
+ this.lineBuffer += ch;
86
+ this.ctx.writeToPty(ch);
87
+ }
88
+ }
89
+ }
90
+ enterAgentInputMode() {
91
+ this.agentInputMode = true;
92
+ this.agentInputBuffer = "";
93
+ this.writeAgentPromptLine(false);
94
+ }
95
+ exitAgentInputMode() {
96
+ this.dismissAutocomplete();
97
+ this.agentInputMode = false;
98
+ this.agentInputBuffer = "";
99
+ process.stdout.write("\r\x1b[2K");
100
+ this.printPrompt();
101
+ }
102
+ printPrompt() {
103
+ this.ctx.redrawPrompt();
104
+ }
105
+ renderAgentInput() {
106
+ this.clearAutocompleteLines();
107
+ this.writeAgentPromptLine();
108
+ this.updateAutocomplete();
109
+ }
110
+ updateAutocomplete() {
111
+ const { items } = this.bus.emitPipe("autocomplete:request", {
112
+ buffer: this.agentInputBuffer,
113
+ items: [],
114
+ });
115
+ if (items.length > 0) {
116
+ this.autocompleteItems = items;
117
+ this.autocompleteActive = true;
118
+ if (this.autocompleteIndex >= items.length)
119
+ this.autocompleteIndex = 0;
120
+ this.renderAutocomplete();
121
+ }
122
+ else {
123
+ this.autocompleteActive = false;
124
+ this.autocompleteItems = [];
125
+ this.autocompleteLines = 0;
126
+ }
127
+ }
128
+ renderAutocomplete() {
129
+ if (!this.autocompleteActive || this.autocompleteItems.length === 0)
130
+ return;
131
+ const lines = [];
132
+ for (let i = 0; i < this.autocompleteItems.length; i++) {
133
+ const item = this.autocompleteItems[i];
134
+ const selected = i === this.autocompleteIndex;
135
+ if (selected) {
136
+ lines.push(` \x1b[7m ${p.accent}${item.name.padEnd(12)}${p.reset}\x1b[7m ${item.description} ${p.reset}`);
137
+ }
138
+ else {
139
+ lines.push(` ${p.muted}${item.name.padEnd(12)} ${item.description}${p.reset}`);
140
+ }
141
+ }
142
+ process.stdout.write("\n" + lines.join("\n"));
143
+ this.autocompleteLines = lines.length;
144
+ if (this.autocompleteLines > 0) {
145
+ process.stdout.write(`\x1b[${this.autocompleteLines}A`);
146
+ }
147
+ const agentInfo = this.onShowAgentInfo();
148
+ const infoLength = visibleLen(agentInfo.info);
149
+ const col = infoLength + 2 + this.agentInputBuffer.length;
150
+ process.stdout.write(`\r\x1b[${col}C`);
151
+ }
152
+ clearAutocompleteLines() {
153
+ if (this.autocompleteLines <= 0)
154
+ return;
155
+ process.stdout.write("\x1b7"); // save cursor
156
+ for (let i = 0; i < this.autocompleteLines; i++) {
157
+ process.stdout.write("\n\x1b[2K"); // move down, clear line
158
+ }
159
+ process.stdout.write("\x1b8"); // restore cursor
160
+ this.autocompleteLines = 0;
161
+ }
162
+ applyAutocomplete() {
163
+ if (!this.autocompleteActive || this.autocompleteItems.length === 0)
164
+ return;
165
+ const selected = this.autocompleteItems[this.autocompleteIndex];
166
+ if (!selected)
167
+ return;
168
+ const atPos = this.agentInputBuffer.lastIndexOf("@");
169
+ const isFileAc = atPos >= 0 &&
170
+ (atPos === 0 || this.agentInputBuffer[atPos - 1] === " ") &&
171
+ !this.agentInputBuffer.slice(atPos + 1).includes(" ");
172
+ if (isFileAc) {
173
+ this.agentInputBuffer =
174
+ this.agentInputBuffer.slice(0, atPos) + "@" + selected.name;
175
+ }
176
+ else {
177
+ this.agentInputBuffer = selected.name;
178
+ }
179
+ this.clearAutocompleteLines();
180
+ this.autocompleteActive = false;
181
+ this.autocompleteItems = [];
182
+ this.autocompleteIndex = 0;
183
+ this.writeAgentPromptLine();
184
+ if (isFileAc)
185
+ this.updateAutocomplete();
186
+ }
187
+ dismissAutocomplete() {
188
+ this.clearAutocompleteLines();
189
+ this.autocompleteActive = false;
190
+ this.autocompleteItems = [];
191
+ this.autocompleteIndex = 0;
192
+ }
193
+ handleAgentInput(data) {
194
+ for (let i = 0; i < data.length; i++) {
195
+ const ch = data[i];
196
+ // Detect arrow key sequences: \x1b[A (up), \x1b[B (down)
197
+ if (ch === "\x1b" && data[i + 1] === "[") {
198
+ const arrow = data[i + 2];
199
+ if (arrow === "A" && this.autocompleteActive) {
200
+ // Arrow up
201
+ this.autocompleteIndex =
202
+ this.autocompleteIndex === 0
203
+ ? this.autocompleteItems.length - 1
204
+ : this.autocompleteIndex - 1;
205
+ this.clearAutocompleteLines();
206
+ this.writeAgentPromptLine();
207
+ this.renderAutocomplete();
208
+ i += 2;
209
+ continue;
210
+ }
211
+ else if (arrow === "B" && this.autocompleteActive) {
212
+ this.autocompleteIndex =
213
+ this.autocompleteIndex === this.autocompleteItems.length - 1
214
+ ? 0
215
+ : this.autocompleteIndex + 1;
216
+ this.clearAutocompleteLines();
217
+ this.writeAgentPromptLine();
218
+ this.renderAutocomplete();
219
+ i += 2;
220
+ continue;
221
+ }
222
+ else if (!this.autocompleteActive) {
223
+ // Escape without arrow: cancel agent input mode
224
+ this.dismissAutocomplete();
225
+ this.exitAgentInputMode();
226
+ return;
227
+ }
228
+ // Other escape sequences (e.g. left/right arrow) — ignore for now
229
+ i += 2;
230
+ continue;
231
+ }
232
+ if (ch === "\x1b") {
233
+ // Bare escape (no bracket follows)
234
+ if (this.autocompleteActive) {
235
+ this.dismissAutocomplete();
236
+ this.writeAgentPromptLine();
237
+ }
238
+ else {
239
+ this.dismissAutocomplete();
240
+ this.exitAgentInputMode();
241
+ }
242
+ return;
243
+ }
244
+ if (ch === "\t") {
245
+ if (this.autocompleteActive) {
246
+ this.applyAutocomplete();
247
+ }
248
+ continue;
249
+ }
250
+ if (ch === "\r") {
251
+ if (this.autocompleteActive) {
252
+ this.applyAutocomplete();
253
+ }
254
+ const query = this.agentInputBuffer.trim();
255
+ this.clearAutocompleteLines();
256
+ process.stdout.write("\r\x1b[2K");
257
+ this.agentInputMode = false;
258
+ this.agentInputBuffer = "";
259
+ this.dismissAutocomplete();
260
+ if (query && query.startsWith("/")) {
261
+ const spaceIdx = query.indexOf(" ");
262
+ const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
263
+ const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
264
+ this.bus.emit("command:execute", { name, args });
265
+ this.ctx.redrawPrompt();
266
+ }
267
+ else if (query) {
268
+ this.bus.emit("agent:submit", { query });
269
+ }
270
+ else {
271
+ this.exitAgentInputMode();
272
+ }
273
+ return;
274
+ }
275
+ else if (ch === "\x03") {
276
+ // Ctrl-C: cancel
277
+ this.dismissAutocomplete();
278
+ this.exitAgentInputMode();
279
+ return;
280
+ }
281
+ else if (ch === "\x7f" || ch === "\b") {
282
+ // Backspace
283
+ if (this.agentInputBuffer.length > 0) {
284
+ this.agentInputBuffer = this.agentInputBuffer.slice(0, -1);
285
+ this.autocompleteIndex = 0;
286
+ this.renderAgentInput();
287
+ }
288
+ else {
289
+ this.dismissAutocomplete();
290
+ this.exitAgentInputMode();
291
+ return;
292
+ }
293
+ }
294
+ else if (ch.charCodeAt(0) >= 32) {
295
+ // Printable character
296
+ this.agentInputBuffer += ch;
297
+ this.autocompleteIndex = 0;
298
+ this.renderAgentInput();
299
+ }
300
+ }
301
+ }
302
+ }
@@ -0,0 +1,55 @@
1
+ import type { EventBus } from "./event-bus.js";
2
+ /**
3
+ * Parses PTY output to detect command boundaries, track cwd,
4
+ * and emit shell events. Owns the command lifecycle state.
5
+ */
6
+ export declare class OutputParser {
7
+ private bus;
8
+ private cwd;
9
+ private currentOutputCapture;
10
+ private lastCommand;
11
+ private foregroundBusy;
12
+ private capturingPrompt;
13
+ private promptCaptureComplete;
14
+ private promptBuffer;
15
+ private lastPrompt;
16
+ constructor(bus: EventBus, initialCwd: string);
17
+ /** Process a chunk of PTY output data. */
18
+ processData(data: string): void;
19
+ /** Called when user presses Enter on a non-empty line. */
20
+ onCommandEntered(command: string, cwd: string): void;
21
+ /** Returns the full captured prompt bytes, or empty if incomplete. */
22
+ getLastPrompt(): string;
23
+ /**
24
+ * Returns just the last line of the captured prompt (e.g. p10k's "❯ " line).
25
+ * This is safe to replay with \r because it's linear text (colors + chars),
26
+ * not relative cursor positioning. Returns empty if no complete capture.
27
+ */
28
+ getLastPromptLine(): string;
29
+ isPromptCaptureComplete(): boolean;
30
+ isForegroundBusy(): boolean;
31
+ getCwd(): string;
32
+ private parseOSC7;
33
+ /**
34
+ * Detect our custom prompt marker (OSC 9999) in the PTY stream.
35
+ * Each time a prompt appears, we finalize the previous command's output.
36
+ */
37
+ private parsePromptMarker;
38
+ /**
39
+ * Detect end-of-prompt marker (OSC 9998). Finalizes the bracketed capture.
40
+ *
41
+ * By the time this runs, the current chunk has already been appended to
42
+ * promptBuffer (either by parsePromptMarker for the first chunk, or by
43
+ * the wasCapturing guard in processData for subsequent chunks). So we
44
+ * just need to trim everything from the end marker onward.
45
+ */
46
+ private parsePromptEnd;
47
+ /**
48
+ * Strip internal OSC markers from captured prompt so replay is clean.
49
+ * We intentionally strip all OSC 7 sequences — they're used for cwd
50
+ * reporting and have no visual effect, so replaying them would just
51
+ * cause duplicate cwd-change events.
52
+ */
53
+ private sanitizePromptForReplay;
54
+ private removeEchoedCommand;
55
+ }
@@ -0,0 +1,166 @@
1
+ import { stripAnsi } from "./utils/ansi.js";
2
+ /**
3
+ * Parses PTY output to detect command boundaries, track cwd,
4
+ * and emit shell events. Owns the command lifecycle state.
5
+ */
6
+ export class OutputParser {
7
+ bus;
8
+ cwd;
9
+ currentOutputCapture = "";
10
+ lastCommand = "";
11
+ foregroundBusy = false;
12
+ capturingPrompt = false;
13
+ promptCaptureComplete = false;
14
+ promptBuffer = "";
15
+ lastPrompt = "";
16
+ constructor(bus, initialCwd) {
17
+ this.bus = bus;
18
+ this.cwd = initialCwd;
19
+ }
20
+ /** Process a chunk of PTY output data. */
21
+ processData(data) {
22
+ 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
+ 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
+ this.parsePromptEnd(data);
37
+ }
38
+ /** Called when user presses Enter on a non-empty line. */
39
+ onCommandEntered(command, cwd) {
40
+ this.lastCommand = command;
41
+ this.currentOutputCapture = "";
42
+ this.bus.emit("shell:command-start", { command, cwd });
43
+ if (!this.foregroundBusy) {
44
+ this.foregroundBusy = true;
45
+ this.bus.emit("shell:foreground-busy", { busy: true });
46
+ }
47
+ }
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;
70
+ }
71
+ isForegroundBusy() {
72
+ return this.foregroundBusy;
73
+ }
74
+ getCwd() {
75
+ return this.cwd;
76
+ }
77
+ // ── Parsing ─────────────────────────────────────────────────
78
+ parseOSC7(data) {
79
+ const match = data.match(/\x1b\]7;file:\/\/[^/]*(\/[^\x07\x1b]*)/);
80
+ if (match?.[1]) {
81
+ const newCwd = decodeURIComponent(match[1]);
82
+ if (newCwd !== this.cwd) {
83
+ this.cwd = newCwd;
84
+ this.bus.emit("shell:cwd-change", { cwd: this.cwd });
85
+ }
86
+ }
87
+ }
88
+ /**
89
+ * Detect our custom prompt marker (OSC 9999) in the PTY stream.
90
+ * Each time a prompt appears, we finalize the previous command's output.
91
+ */
92
+ parsePromptMarker(data) {
93
+ if (data.includes("\x1b]9999;PROMPT\x07")) {
94
+ if (this.foregroundBusy) {
95
+ this.foregroundBusy = false;
96
+ this.bus.emit("shell:foreground-busy", { busy: false });
97
+ }
98
+ if (this.lastCommand) {
99
+ const output = stripAnsi(this.currentOutputCapture).trim();
100
+ const cleaned = this.removeEchoedCommand(output, this.lastCommand);
101
+ this.bus.emit("shell:command-done", {
102
+ command: this.lastCommand,
103
+ output: cleaned,
104
+ cwd: this.cwd,
105
+ exitCode: null,
106
+ });
107
+ }
108
+ this.lastCommand = "";
109
+ 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
+ }
119
+ else {
120
+ this.currentOutputCapture += data;
121
+ }
122
+ }
123
+ /**
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.
130
+ */
131
+ 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);
142
+ }
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
+ }
159
+ removeEchoedCommand(output, command) {
160
+ const lines = output.split("\n");
161
+ if (lines.length > 0 && lines[0].includes(command.slice(0, 20))) {
162
+ return lines.slice(1).join("\n").trim();
163
+ }
164
+ return output;
165
+ }
166
+ }
@@ -0,0 +1,54 @@
1
+ import type { EventBus } from "./event-bus.js";
2
+ import { type InputContext } from "./input-handler.js";
3
+ export declare class Shell implements InputContext {
4
+ private ptyProcess;
5
+ private bus;
6
+ private inputHandler;
7
+ private outputParser;
8
+ private paused;
9
+ private agentActive;
10
+ private tmpDir?;
11
+ constructor(opts: {
12
+ bus: EventBus;
13
+ onShowAgentInfo?: () => {
14
+ info: string;
15
+ model?: string;
16
+ };
17
+ cols: number;
18
+ rows: number;
19
+ shell: string;
20
+ cwd: string;
21
+ });
22
+ isForegroundBusy(): boolean;
23
+ getCwd(): string;
24
+ isAgentActive(): boolean;
25
+ writeToPty(data: string): void;
26
+ /**
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 line — the path bar above is still intact. The last
30
+ * line is linear text (colors + chars + clear-to-end), no cursor positioning.
31
+ */
32
+ redrawPrompt(): void;
33
+ /**
34
+ * Heavy redraw: send \n to PTY to trigger a full precmd → prompt cycle.
35
+ * Use this after agent responses where stdout has moved far from where
36
+ * zle expects the cursor. The blank line is acceptable as a separator.
37
+ */
38
+ freshPrompt(): void;
39
+ onCommandEntered(command: string, cwd: string): void;
40
+ private setupOutput;
41
+ private setupInput;
42
+ /**
43
+ * React to agent lifecycle events — Shell manages its own state
44
+ * rather than being driven by AcpClient. This means AcpClient has
45
+ * zero frontend knowledge; any frontend can subscribe to the same events.
46
+ */
47
+ private setupAgentLifecycle;
48
+ resize(cols: number, rows: number): void;
49
+ onExit(callback: (e: {
50
+ exitCode: number;
51
+ signal?: number;
52
+ }) => void): void;
53
+ kill(): void;
54
+ }