agent-sh 0.14.1 → 0.14.3
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.
- package/dist/agent/agent-loop.d.ts +1 -1
- package/dist/agent/agent-loop.js +42 -31
- package/dist/agent/conversation-state.d.ts +3 -2
- package/dist/agent/conversation-state.js +20 -3
- package/dist/agent/events.d.ts +2 -0
- package/dist/agent/host-types.d.ts +3 -0
- package/dist/agent/index.js +2 -1
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/subagent.js +5 -1
- package/dist/agent/tool-protocol.d.ts +2 -2
- package/dist/agent/tool-protocol.js +5 -4
- package/dist/agent/tools/glob.d.ts +1 -1
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.d.ts +1 -1
- package/dist/agent/tools/grep.js +4 -2
- package/dist/agent/tools/ls.d.ts +1 -1
- package/dist/agent/tools/ls.js +4 -2
- package/dist/agent/tools/read-file.d.ts +1 -1
- package/dist/agent/tools/read-file.js +30 -2
- package/dist/agent/types.d.ts +11 -1
- package/dist/agent/types.js +6 -1
- package/dist/cli/index.js +0 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/settings.d.ts +3 -0
- package/dist/core/settings.js +2 -2
- package/dist/shell/events.d.ts +2 -0
- package/dist/shell/index.d.ts +6 -0
- package/dist/shell/index.js +10 -10
- package/dist/shell/output-parser.d.ts +11 -22
- package/dist/shell/output-parser.js +16 -34
- package/dist/shell/shell-context.d.ts +3 -6
- package/dist/shell/shell-context.js +15 -7
- package/dist/shell/shell.d.ts +4 -0
- package/dist/shell/shell.js +18 -30
- package/dist/shell/strategies/types.d.ts +6 -0
- package/dist/shell/strategies/zsh.js +7 -0
- package/dist/shell/terminal.d.ts +33 -0
- package/dist/shell/terminal.js +62 -0
- package/examples/extensions/ash-scheme/index.ts +2170 -0
- package/examples/extensions/ash-scheme/package.json +11 -0
- package/examples/extensions/ash-scheme-render.ts +58 -0
- package/examples/extensions/ashi/README.md +36 -26
- package/examples/extensions/ashi/package.json +9 -1
- package/examples/extensions/ashi/src/capture.ts +1 -0
- package/examples/extensions/ashi/src/cli.ts +53 -11
- package/examples/extensions/ashi/src/commands.ts +2 -20
- package/examples/extensions/ashi/src/compaction.ts +25 -96
- package/examples/extensions/ashi/src/components.ts +64 -166
- package/examples/extensions/ashi/src/default-schema-renderers.ts +232 -0
- package/examples/extensions/ashi/src/display-config.ts +21 -22
- package/examples/extensions/ashi/src/frontend.ts +355 -118
- package/examples/extensions/ashi/src/hooks.ts +47 -63
- package/examples/extensions/ashi/src/multi-session-store.ts +44 -3
- package/examples/extensions/ashi/src/schema.ts +386 -0
- package/examples/extensions/ashi/src/session-store.ts +115 -17
- package/examples/extensions/ashi/src/shell-mode.ts +52 -0
- package/examples/extensions/ashi/src/status-footer.ts +41 -6
- package/examples/extensions/ashi/src/theme.ts +2 -1
- package/examples/extensions/ashi-compact-llm.ts +93 -0
- package/examples/extensions/claude-code-bridge/index.ts +2 -0
- package/examples/extensions/opencode-bridge/index.ts +3 -0
- package/examples/extensions/opencode-provider.ts +252 -0
- package/examples/extensions/pi-bridge/index.ts +1 -0
- package/package.json +16 -1
- package/examples/extensions/ashi/src/default-renderers.ts +0 -171
package/dist/shell/index.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import "./events.js"; // augments BusEvents with shell-owned events
|
|
7
7
|
import { Shell } from "./shell.js";
|
|
8
|
-
import { DefaultCompositor
|
|
8
|
+
import { DefaultCompositor } from "../utils/compositor.js";
|
|
9
9
|
import { TerminalBuffer } from "../utils/terminal-buffer.js";
|
|
10
10
|
import { setPalette } from "../utils/palette.js";
|
|
11
11
|
import * as streamTransform from "../utils/stream-transform.js";
|
|
12
12
|
import activateShellContext from "./shell-context.js";
|
|
13
13
|
import activateTuiRenderer from "./tui-renderer.js";
|
|
14
|
+
import { processTerminal, surfaceFromTerminal } from "./terminal.js";
|
|
14
15
|
/**
|
|
15
16
|
* Register shell-owned handlers extensions can `ctx.call`, and attach
|
|
16
17
|
* the shell surface to ctx. Must run before `loadExtensions` so user
|
|
@@ -77,10 +78,11 @@ export function registerShellHandlers(ctx) {
|
|
|
77
78
|
* `src/cli/index.ts`) uses to drive lifecycle from process-level events.
|
|
78
79
|
*/
|
|
79
80
|
export function activateShell(ctx, opts) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
ctx.shell.compositor.setDefault("
|
|
83
|
-
ctx.shell.compositor.setDefault("
|
|
81
|
+
const terminal = opts.terminal ?? processTerminal();
|
|
82
|
+
const surface = surfaceFromTerminal(terminal);
|
|
83
|
+
ctx.shell.compositor.setDefault("agent", surface);
|
|
84
|
+
ctx.shell.compositor.setDefault("query", surface);
|
|
85
|
+
ctx.shell.compositor.setDefault("status", surface);
|
|
84
86
|
const shell = new Shell({
|
|
85
87
|
bus: ctx.bus,
|
|
86
88
|
handlers: { define: ctx.define, call: ctx.call },
|
|
@@ -90,13 +92,11 @@ export function activateShell(ctx, opts) {
|
|
|
90
92
|
cwd: opts.cwd,
|
|
91
93
|
instanceId: ctx.instanceId,
|
|
92
94
|
onShowAgentInfo: opts.onShowAgentInfo,
|
|
95
|
+
terminal,
|
|
93
96
|
});
|
|
94
|
-
const
|
|
95
|
-
shell.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
96
|
-
};
|
|
97
|
-
process.stdout.on("resize", onResize);
|
|
97
|
+
const offResize = terminal.onResize((cols, rows) => shell.resize(cols, rows));
|
|
98
98
|
ctx.onDispose(() => {
|
|
99
|
-
|
|
99
|
+
offResize();
|
|
100
100
|
shell.kill();
|
|
101
101
|
});
|
|
102
102
|
return {
|
|
@@ -1,42 +1,31 @@
|
|
|
1
1
|
import type { EventBus } from "../core/event-bus.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export interface OutputParserOpts {
|
|
3
|
+
/** Optional shell-specific cleanup applied to raw output before stripAnsi. */
|
|
4
|
+
cleanOutput?(raw: string): string;
|
|
5
|
+
}
|
|
6
6
|
export declare class OutputParser {
|
|
7
7
|
private bus;
|
|
8
8
|
private cwd;
|
|
9
9
|
private ownTag;
|
|
10
|
+
private cleanOutput;
|
|
10
11
|
private currentOutputCapture;
|
|
11
12
|
private lastCommand;
|
|
12
13
|
private foregroundBusy;
|
|
13
14
|
private promptReady;
|
|
14
|
-
constructor(bus: EventBus, initialCwd: string, ownTag: string);
|
|
15
|
-
/** Process a chunk of PTY output data. */
|
|
15
|
+
constructor(bus: EventBus, initialCwd: string, ownTag: string, opts?: OutputParserOpts);
|
|
16
16
|
processData(data: string): void;
|
|
17
|
-
/** Called when user presses Enter on a non-empty line. */
|
|
18
17
|
onCommandEntered(command: string, cwd: string): void;
|
|
19
|
-
/** Whether the shell's prompt is fully rendered and ready for input. */
|
|
20
18
|
isPromptReady(): boolean;
|
|
21
19
|
isForegroundBusy(): boolean;
|
|
22
20
|
getCwd(): string;
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* the InputHandler's lineBuffer which can't track history recall or tab
|
|
27
|
-
* completion. Returns data with the OSC stripped out.
|
|
28
|
-
*/
|
|
21
|
+
/** Pulls the actual command from the shell's OSC 9997 preexec marker —
|
|
22
|
+
* more reliable than the InputHandler's lineBuffer, which can't track
|
|
23
|
+
* history recall or tab completion. Returns data with the OSC stripped. */
|
|
29
24
|
private handlePreexec;
|
|
30
25
|
private parseOSC7;
|
|
31
|
-
/**
|
|
32
|
-
* Detect our custom prompt marker (OSC 9999) in the PTY stream.
|
|
33
|
-
* Each time a prompt appears, we finalize the previous command's output.
|
|
34
|
-
*/
|
|
26
|
+
/** OSC 9999 marker — each occurrence finalizes the previous command's output. */
|
|
35
27
|
private parsePromptMarker;
|
|
36
|
-
/**
|
|
37
|
-
* Detect end-of-prompt marker (OSC 9998). The prompt is fully rendered
|
|
38
|
-
* and the shell is ready for input.
|
|
39
|
-
*/
|
|
28
|
+
/** OSC 9998 — prompt is fully rendered and the shell is ready for input. */
|
|
40
29
|
private parsePromptEnd;
|
|
41
30
|
private removeEchoedCommand;
|
|
42
31
|
}
|
|
@@ -4,32 +4,27 @@ import { stripAnsi } from "../utils/ansi.js";
|
|
|
4
4
|
const PROMPT_RE = /\x1b\]9999;(?:id=([a-f0-9]+);)?PROMPT\x07/;
|
|
5
5
|
const PREEXEC_RE = /\x1b\]9997;(?:id=([a-f0-9]+);)?([^\x07]*)\x07/;
|
|
6
6
|
const READY_RE = /\x1b\]9998;(?:id=([a-f0-9]+);)?READY\x07/;
|
|
7
|
-
/**
|
|
8
|
-
* Parses PTY output to detect command boundaries, track cwd,
|
|
9
|
-
* and emit shell events. Owns the command lifecycle state.
|
|
10
|
-
*/
|
|
11
7
|
export class OutputParser {
|
|
12
8
|
bus;
|
|
13
9
|
cwd;
|
|
14
10
|
ownTag;
|
|
11
|
+
cleanOutput;
|
|
15
12
|
currentOutputCapture = "";
|
|
16
13
|
lastCommand = "";
|
|
17
14
|
foregroundBusy = false;
|
|
18
15
|
promptReady = false;
|
|
19
|
-
constructor(bus, initialCwd, ownTag) {
|
|
16
|
+
constructor(bus, initialCwd, ownTag, opts = {}) {
|
|
20
17
|
this.bus = bus;
|
|
21
18
|
this.cwd = initialCwd;
|
|
22
|
-
// Strip the "id=" prefix; we compare the value alone.
|
|
23
19
|
this.ownTag = ownTag.startsWith("id=") ? ownTag.slice(3) : ownTag;
|
|
20
|
+
this.cleanOutput = opts.cleanOutput ?? ((raw) => raw);
|
|
24
21
|
}
|
|
25
|
-
/** Process a chunk of PTY output data. */
|
|
26
22
|
processData(data) {
|
|
27
23
|
this.parseOSC7(data);
|
|
28
24
|
data = this.handlePreexec(data);
|
|
29
25
|
this.parsePromptMarker(data);
|
|
30
26
|
this.parsePromptEnd(data);
|
|
31
27
|
}
|
|
32
|
-
/** Called when user presses Enter on a non-empty line. */
|
|
33
28
|
onCommandEntered(command, cwd) {
|
|
34
29
|
this.lastCommand = command;
|
|
35
30
|
this.currentOutputCapture = "";
|
|
@@ -39,7 +34,6 @@ export class OutputParser {
|
|
|
39
34
|
this.bus.emit("shell:foreground-busy", { busy: true });
|
|
40
35
|
}
|
|
41
36
|
}
|
|
42
|
-
/** Whether the shell's prompt is fully rendered and ready for input. */
|
|
43
37
|
isPromptReady() {
|
|
44
38
|
return this.promptReady;
|
|
45
39
|
}
|
|
@@ -49,13 +43,9 @@ export class OutputParser {
|
|
|
49
43
|
getCwd() {
|
|
50
44
|
return this.cwd;
|
|
51
45
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* This carries the actual command text from the shell — more reliable than
|
|
56
|
-
* the InputHandler's lineBuffer which can't track history recall or tab
|
|
57
|
-
* completion. Returns data with the OSC stripped out.
|
|
58
|
-
*/
|
|
46
|
+
/** Pulls the actual command from the shell's OSC 9997 preexec marker —
|
|
47
|
+
* more reliable than the InputHandler's lineBuffer, which can't track
|
|
48
|
+
* history recall or tab completion. Returns data with the OSC stripped. */
|
|
59
49
|
handlePreexec(data) {
|
|
60
50
|
const match = PREEXEC_RE.exec(data);
|
|
61
51
|
if (!match)
|
|
@@ -66,7 +56,8 @@ export class OutputParser {
|
|
|
66
56
|
}
|
|
67
57
|
const command = match[2];
|
|
68
58
|
this.lastCommand = command;
|
|
69
|
-
|
|
59
|
+
// Discard echo accumulated before preexec.
|
|
60
|
+
this.currentOutputCapture = "";
|
|
70
61
|
if (!this.foregroundBusy) {
|
|
71
62
|
this.foregroundBusy = true;
|
|
72
63
|
this.bus.emit("shell:foreground-busy", { busy: true });
|
|
@@ -84,21 +75,16 @@ export class OutputParser {
|
|
|
84
75
|
}
|
|
85
76
|
}
|
|
86
77
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Detect our custom prompt marker (OSC 9999) in the PTY stream.
|
|
89
|
-
* Each time a prompt appears, we finalize the previous command's output.
|
|
90
|
-
*/
|
|
78
|
+
/** OSC 9999 marker — each occurrence finalizes the previous command's output. */
|
|
91
79
|
parsePromptMarker(data) {
|
|
92
80
|
const match = PROMPT_RE.exec(data);
|
|
93
81
|
if (match) {
|
|
94
82
|
if (match[1] !== this.ownTag) {
|
|
95
|
-
// Nested
|
|
96
|
-
// foreground output, do not finalize our own command.
|
|
83
|
+
// Nested or untagged emission: keep as opaque foreground output.
|
|
97
84
|
this.currentOutputCapture += data;
|
|
98
85
|
return;
|
|
99
86
|
}
|
|
100
87
|
const markerIdx = match.index;
|
|
101
|
-
// Capture any output that arrived in the same chunk before the marker
|
|
102
88
|
if (markerIdx > 0) {
|
|
103
89
|
this.currentOutputCapture += data.slice(0, markerIdx);
|
|
104
90
|
}
|
|
@@ -108,7 +94,8 @@ export class OutputParser {
|
|
|
108
94
|
this.bus.emit("shell:foreground-busy", { busy: false });
|
|
109
95
|
}
|
|
110
96
|
if (this.lastCommand) {
|
|
111
|
-
const
|
|
97
|
+
const raw = this.cleanOutput(this.currentOutputCapture);
|
|
98
|
+
const output = stripAnsi(raw).trim();
|
|
112
99
|
const cleaned = this.removeEchoedCommand(output, this.lastCommand);
|
|
113
100
|
this.bus.emit("shell:command-done", {
|
|
114
101
|
command: this.lastCommand,
|
|
@@ -121,21 +108,16 @@ export class OutputParser {
|
|
|
121
108
|
this.currentOutputCapture = "";
|
|
122
109
|
}
|
|
123
110
|
else {
|
|
124
|
-
// Cap
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// command-done context.
|
|
128
|
-
const MAX_CAPTURE = 128 * 1024; // 128 KB
|
|
111
|
+
// Cap to the tail so a long-running foreground program (tmux, vim)
|
|
112
|
+
// emitting output without prompt markers can't grow this unboundedly.
|
|
113
|
+
const MAX_CAPTURE = 128 * 1024;
|
|
129
114
|
this.currentOutputCapture += data;
|
|
130
115
|
if (this.currentOutputCapture.length > MAX_CAPTURE) {
|
|
131
116
|
this.currentOutputCapture = this.currentOutputCapture.slice(-MAX_CAPTURE);
|
|
132
117
|
}
|
|
133
118
|
}
|
|
134
119
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Detect end-of-prompt marker (OSC 9998). The prompt is fully rendered
|
|
137
|
-
* and the shell is ready for input.
|
|
138
|
-
*/
|
|
120
|
+
/** OSC 9998 — prompt is fully rendered and the shell is ready for input. */
|
|
139
121
|
parsePromptEnd(data) {
|
|
140
122
|
const match = READY_RE.exec(data);
|
|
141
123
|
if (!match)
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* user-shell exchanges) signals. Frontends without a PTY skip this
|
|
5
|
-
* built-in and the agent runs cwd-aware via core's process.cwd() default.
|
|
6
|
-
*/
|
|
1
|
+
/** Tracks PTY commands and cwd, spills long outputs, contributes per-query
|
|
2
|
+
* `<cwd>` (always) and `<shell_events>` (fresh user exchanges). Frontends
|
|
3
|
+
* without a PTY skip this and fall back to core's process.cwd() default. */
|
|
7
4
|
import type { ExtensionContext } from "./host-types.js";
|
|
8
5
|
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -6,12 +6,13 @@ export default function activate(ctx) {
|
|
|
6
6
|
let nextId = 1;
|
|
7
7
|
let currentCwd = process.cwd();
|
|
8
8
|
let agentShellActive = false;
|
|
9
|
+
let nextUserExcluded = false;
|
|
9
10
|
let lastSeq = 0;
|
|
10
11
|
bus.on("shell:command-done", (e) => {
|
|
11
12
|
const lines = e.output.split("\n");
|
|
12
13
|
const s = getSettings();
|
|
13
|
-
// Long outputs spill to a tempfile
|
|
14
|
-
//
|
|
14
|
+
// Long outputs spill to a tempfile the agent can `read_file` instead of
|
|
15
|
+
// carrying the full text in LLM context.
|
|
15
16
|
let output = e.output;
|
|
16
17
|
let spillPath;
|
|
17
18
|
if (lines.length > s.shellTruncateThreshold) {
|
|
@@ -25,6 +26,13 @@ export default function activate(ctx) {
|
|
|
25
26
|
spillPath = undefined;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
const source = agentShellActive
|
|
30
|
+
? "agent"
|
|
31
|
+
: nextUserExcluded
|
|
32
|
+
? "user-excluded"
|
|
33
|
+
: "user";
|
|
34
|
+
if (nextUserExcluded)
|
|
35
|
+
nextUserExcluded = false;
|
|
28
36
|
exchanges.push({
|
|
29
37
|
id: nextId++,
|
|
30
38
|
timestamp: Date.now(),
|
|
@@ -34,22 +42,22 @@ export default function activate(ctx) {
|
|
|
34
42
|
exitCode: e.exitCode,
|
|
35
43
|
outputLines: lines.length,
|
|
36
44
|
outputBytes: e.output.length,
|
|
37
|
-
source
|
|
45
|
+
source,
|
|
38
46
|
spillPath,
|
|
39
47
|
});
|
|
40
48
|
});
|
|
41
49
|
bus.on("shell:cwd-change", (e) => { currentCwd = e.cwd; });
|
|
42
50
|
bus.on("shell:agent-exec-start", () => { agentShellActive = true; });
|
|
43
51
|
bus.on("shell:agent-exec-done", () => { agentShellActive = false; });
|
|
44
|
-
|
|
52
|
+
bus.on("shell:user-exec-exclude-next", () => { nextUserExcluded = true; });
|
|
45
53
|
ctx.advise("cwd", () => currentCwd);
|
|
46
|
-
//
|
|
47
|
-
//
|
|
54
|
+
// Advise the core handler directly: this loads before the agent host
|
|
55
|
+
// attaches `ctx.agent`, so the sugar isn't available yet.
|
|
48
56
|
ctx.advise("query-context:build", (next) => {
|
|
49
57
|
const base = next();
|
|
50
58
|
const part = (() => {
|
|
51
59
|
const cwdTag = `<cwd>${currentCwd}</cwd>`;
|
|
52
|
-
const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source
|
|
60
|
+
const fresh = exchanges.filter((ex) => ex.id > lastSeq && ex.source === "user");
|
|
53
61
|
if (fresh.length === 0)
|
|
54
62
|
return cwdTag;
|
|
55
63
|
lastSeq = exchanges[exchanges.length - 1].id;
|
package/dist/shell/shell.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EventBus } from "../core/event-bus.js";
|
|
2
2
|
import { type InputContext } from "./input-handler.js";
|
|
3
|
+
import { type Terminal } from "./terminal.js";
|
|
3
4
|
export interface ShellHandlers {
|
|
4
5
|
define: (name: string, fn: (...args: any[]) => any) => void;
|
|
5
6
|
call: (name: string, ...args: any[]) => any;
|
|
@@ -19,6 +20,8 @@ export declare class Shell implements InputContext {
|
|
|
19
20
|
private handlers;
|
|
20
21
|
private inputHandler;
|
|
21
22
|
private outputParser;
|
|
23
|
+
private terminal;
|
|
24
|
+
private inputDispose;
|
|
22
25
|
private hardMuteScopes;
|
|
23
26
|
private softMuteScopes;
|
|
24
27
|
private unmuteScopes;
|
|
@@ -38,6 +41,7 @@ export declare class Shell implements InputContext {
|
|
|
38
41
|
shell: string;
|
|
39
42
|
cwd: string;
|
|
40
43
|
instanceId: string;
|
|
44
|
+
terminal?: Terminal;
|
|
41
45
|
});
|
|
42
46
|
/** Compositing-layer claim — overrides any unmute. */
|
|
43
47
|
acquireHardMute(reason: string): ShellScope;
|
package/dist/shell/shell.js
CHANGED
|
@@ -5,6 +5,7 @@ import { InputHandler } from "./input-handler.js";
|
|
|
5
5
|
import { OutputParser } from "./output-parser.js";
|
|
6
6
|
import { getSettings } from "../core/settings.js";
|
|
7
7
|
import { clearOpost } from "../utils/tty.js";
|
|
8
|
+
import { processTerminal } from "./terminal.js";
|
|
8
9
|
import { pickStrategy, FALLBACK_STRATEGY, SUPPORTED_SHELL_NAMES, } from "./strategies/index.js";
|
|
9
10
|
export class Shell {
|
|
10
11
|
ptyProcess;
|
|
@@ -12,6 +13,8 @@ export class Shell {
|
|
|
12
13
|
handlers;
|
|
13
14
|
inputHandler;
|
|
14
15
|
outputParser;
|
|
16
|
+
terminal;
|
|
17
|
+
inputDispose = null;
|
|
15
18
|
// hardMute is unconditional (overlay compositing); softMute is overridable
|
|
16
19
|
// by unmute (terminal_keys, permission UI). Gate: hard wins; otherwise
|
|
17
20
|
// muted iff softMute held without an unmute.
|
|
@@ -23,6 +26,7 @@ export class Shell {
|
|
|
23
26
|
strategy;
|
|
24
27
|
tmpDir;
|
|
25
28
|
constructor(opts) {
|
|
29
|
+
this.terminal = opts.terminal ?? processTerminal();
|
|
26
30
|
// Build environment — filter out undefined values (node-pty's native
|
|
27
31
|
// posix_spawnp fails if any env value is undefined)
|
|
28
32
|
const env = {};
|
|
@@ -58,18 +62,10 @@ export class Shell {
|
|
|
58
62
|
this.tmpDir = spawnConfig.tmpDir;
|
|
59
63
|
Object.assign(env, spawnConfig.envOverrides);
|
|
60
64
|
const shellArgs = spawnConfig.args;
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
process.stdin.setRawMode(false);
|
|
67
|
-
process.stdin.pause();
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// Ignore
|
|
71
|
-
}
|
|
72
|
-
}
|
|
65
|
+
// The PTY will become the controlling terminal for the child shell;
|
|
66
|
+
// suspend the host terminal's input around spawn to avoid TTY contention
|
|
67
|
+
// on macOS. Headless terminals make this a no-op.
|
|
68
|
+
const suspended = this.terminal.suspendInput?.();
|
|
73
69
|
this.ptyProcess = pty.spawn(shellBin, shellArgs, {
|
|
74
70
|
name: "xterm-256color",
|
|
75
71
|
cols: opts.cols,
|
|
@@ -77,22 +73,13 @@ export class Shell {
|
|
|
77
73
|
cwd: opts.cwd,
|
|
78
74
|
env,
|
|
79
75
|
});
|
|
80
|
-
|
|
81
|
-
if (process.stdin.isTTY) {
|
|
82
|
-
try {
|
|
83
|
-
process.stdin.resume();
|
|
84
|
-
if (wasRaw) {
|
|
85
|
-
process.stdin.setRawMode(true);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
// Ignore - will be set up later in index.ts
|
|
90
|
-
}
|
|
91
|
-
}
|
|
76
|
+
suspended?.resume();
|
|
92
77
|
clearOpost();
|
|
93
78
|
this.bus = opts.bus;
|
|
94
79
|
this.handlers = opts.handlers;
|
|
95
|
-
this.outputParser = new OutputParser(opts.bus, opts.cwd, instanceTag
|
|
80
|
+
this.outputParser = new OutputParser(opts.bus, opts.cwd, instanceTag, {
|
|
81
|
+
cleanOutput: this.strategy.cleanOutput?.bind(this.strategy),
|
|
82
|
+
});
|
|
96
83
|
// Ensure temp dir cleanup on abnormal exit (SIGKILL won't fire this,
|
|
97
84
|
// but it covers uncaught exceptions and normal process.exit paths)
|
|
98
85
|
if (this.tmpDir) {
|
|
@@ -259,15 +246,14 @@ export class Shell {
|
|
|
259
246
|
this.pendingEchoSkips--;
|
|
260
247
|
const rest = data.slice(nlIdx + 1);
|
|
261
248
|
if (rest)
|
|
262
|
-
|
|
249
|
+
this.terminal.write(rest);
|
|
263
250
|
return;
|
|
264
251
|
}
|
|
265
|
-
|
|
252
|
+
this.terminal.write(data);
|
|
266
253
|
});
|
|
267
254
|
}
|
|
268
255
|
setupInput() {
|
|
269
|
-
|
|
270
|
-
const str = data.toString("utf-8");
|
|
256
|
+
this.inputDispose = this.terminal.onInput((str) => {
|
|
271
257
|
this.inputHandler.handleInput(str);
|
|
272
258
|
});
|
|
273
259
|
}
|
|
@@ -304,7 +290,7 @@ export class Shell {
|
|
|
304
290
|
this.bus.onPipeAsync("shell:exec-request", async (payload) => {
|
|
305
291
|
const visible = this.acquireUnmute("exec-request");
|
|
306
292
|
this.skipNextLine();
|
|
307
|
-
|
|
293
|
+
this.terminal.write("\r\n");
|
|
308
294
|
this.bus.emit("shell:agent-exec-start", {});
|
|
309
295
|
try {
|
|
310
296
|
const output = await new Promise((resolve, reject) => {
|
|
@@ -347,6 +333,8 @@ export class Shell {
|
|
|
347
333
|
this.ptyProcess.onExit(callback);
|
|
348
334
|
}
|
|
349
335
|
kill() {
|
|
336
|
+
this.inputDispose?.();
|
|
337
|
+
this.inputDispose = null;
|
|
350
338
|
this.ptyProcess.kill();
|
|
351
339
|
if (this.tmpDir) {
|
|
352
340
|
fs.rmSync(this.tmpDir, { recursive: true, force: true });
|
|
@@ -47,4 +47,10 @@ export interface ShellStrategy {
|
|
|
47
47
|
* Returns null if the shell can't redraw — caller falls back to freshPrompt.
|
|
48
48
|
*/
|
|
49
49
|
redrawEscape(): string | null;
|
|
50
|
+
/**
|
|
51
|
+
* Strip shell-specific artifacts from raw PTY output before stripAnsi
|
|
52
|
+
* collapses SGR codes (e.g. zsh's PROMPT_SP inverse-video `%`). Default
|
|
53
|
+
* is identity — most shells need no cleanup.
|
|
54
|
+
*/
|
|
55
|
+
cleanOutput?(raw: string): string;
|
|
50
56
|
}
|
|
@@ -69,4 +69,11 @@ export const zshStrategy = {
|
|
|
69
69
|
redrawEscape() {
|
|
70
70
|
return "\x1b[9999~";
|
|
71
71
|
},
|
|
72
|
+
cleanOutput(raw) {
|
|
73
|
+
return raw.replace(PROMPT_SP_RE, "");
|
|
74
|
+
},
|
|
72
75
|
};
|
|
76
|
+
/** PROMPT_SP marker (inverse-video PROMPT_EOL_MARK, default `%`) zsh prints
|
|
77
|
+
* before a prompt when prior output didn't end at column 0. Matching the
|
|
78
|
+
* inverse-video wrapper preserves legitimate trailing `%`. */
|
|
79
|
+
const PROMPT_SP_RE = /\x1b\[7m.\x1b\[(?:0|27)m/g;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal — the user-facing I/O endpoint that a Shell talks to.
|
|
3
|
+
*
|
|
4
|
+
* Shell wraps a *pseudo*-terminal (the PTY the child shell sees). This
|
|
5
|
+
* interface is the *real* terminal (or its substitute) on the other end:
|
|
6
|
+
* bytes in, bytes out, dimensions, resize notifications. The default
|
|
7
|
+
* factory wires it to process.stdin/stdout for the CLI; headless hosts
|
|
8
|
+
* (multi-session web hubs, tests) supply their own.
|
|
9
|
+
*/
|
|
10
|
+
import type { RenderSurface } from "../utils/compositor.js";
|
|
11
|
+
export interface Terminal {
|
|
12
|
+
write(data: string): void;
|
|
13
|
+
onInput(cb: (data: string) => void): () => void;
|
|
14
|
+
onResize(cb: (cols: number, rows: number) => void): () => void;
|
|
15
|
+
cols(): number;
|
|
16
|
+
rows(): number;
|
|
17
|
+
/**
|
|
18
|
+
* Called around PTY spawn to avoid TTY contention: the child PTY becomes
|
|
19
|
+
* the controlling tty for the spawned shell. No-op when the terminal
|
|
20
|
+
* isn't a real tty.
|
|
21
|
+
*/
|
|
22
|
+
suspendInput?(): {
|
|
23
|
+
resume(): void;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Default Terminal: wraps process.stdin/stdout. */
|
|
27
|
+
export declare function processTerminal(): Terminal;
|
|
28
|
+
/**
|
|
29
|
+
* Adapt a Terminal to a RenderSurface (the compositor's sink type). Adds
|
|
30
|
+
* the OPOST-cleared `\n` → `\r\n` translation that StdoutSurface applies,
|
|
31
|
+
* since the PTY has OPOST disabled.
|
|
32
|
+
*/
|
|
33
|
+
export declare function surfaceFromTerminal(terminal: Terminal): RenderSurface;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Default Terminal: wraps process.stdin/stdout. */
|
|
2
|
+
export function processTerminal() {
|
|
3
|
+
return {
|
|
4
|
+
write(data) {
|
|
5
|
+
if (process.stdout.writable) {
|
|
6
|
+
try {
|
|
7
|
+
process.stdout.write(data);
|
|
8
|
+
}
|
|
9
|
+
catch { /* ignore */ }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
onInput(cb) {
|
|
13
|
+
const handler = (b) => cb(b.toString("utf-8"));
|
|
14
|
+
process.stdin.on("data", handler);
|
|
15
|
+
return () => { process.stdin.off("data", handler); };
|
|
16
|
+
},
|
|
17
|
+
onResize(cb) {
|
|
18
|
+
const handler = () => cb(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
19
|
+
process.stdout.on("resize", handler);
|
|
20
|
+
return () => { process.stdout.off("resize", handler); };
|
|
21
|
+
},
|
|
22
|
+
cols() { return process.stdout.columns || 80; },
|
|
23
|
+
rows() { return process.stdout.rows || 24; },
|
|
24
|
+
suspendInput() {
|
|
25
|
+
const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
|
|
26
|
+
if (process.stdin.isTTY) {
|
|
27
|
+
try {
|
|
28
|
+
process.stdin.setRawMode(false);
|
|
29
|
+
process.stdin.pause();
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore */ }
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
resume() {
|
|
35
|
+
if (process.stdin.isTTY) {
|
|
36
|
+
try {
|
|
37
|
+
process.stdin.resume();
|
|
38
|
+
if (wasRaw)
|
|
39
|
+
process.stdin.setRawMode(true);
|
|
40
|
+
}
|
|
41
|
+
catch { /* ignore */ }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Adapt a Terminal to a RenderSurface (the compositor's sink type). Adds
|
|
50
|
+
* the OPOST-cleared `\n` → `\r\n` translation that StdoutSurface applies,
|
|
51
|
+
* since the PTY has OPOST disabled.
|
|
52
|
+
*/
|
|
53
|
+
export function surfaceFromTerminal(terminal) {
|
|
54
|
+
const write = (text) => terminal.write(text.replace(/(?<!\r)\n/g, "\r\n"));
|
|
55
|
+
return {
|
|
56
|
+
write,
|
|
57
|
+
writeLine: (line) => write(line + "\n"),
|
|
58
|
+
get columns() { return terminal.cols(); },
|
|
59
|
+
get rows() { return terminal.rows(); },
|
|
60
|
+
onResize: (cb) => terminal.onResize(cb),
|
|
61
|
+
};
|
|
62
|
+
}
|