agent-sh 0.14.0 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -18
- 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/llm-client.js +1 -0
- 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 +13 -3
- package/dist/agent/types.js +6 -1
- package/dist/cli/args.js +3 -1
- package/dist/cli/index.js +0 -0
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.js +86 -2
- package/dist/cli/subcommands.js +4 -1
- 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/index.d.ts +6 -0
- package/dist/shell/index.js +10 -10
- package/dist/shell/shell.d.ts +4 -0
- package/dist/shell/shell.js +15 -29
- package/dist/shell/terminal.d.ts +33 -0
- package/dist/shell/terminal.js +62 -0
- package/dist/utils/tool-interactive.js +4 -2
- 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 +25 -8
- 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 +229 -0
- package/examples/extensions/ashi/src/display-config.ts +21 -22
- package/examples/extensions/ashi/src/frontend.ts +64 -65
- 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 +407 -0
- package/examples/extensions/ashi/src/session-store.ts +55 -4
- package/examples/extensions/ashi/src/status-footer.ts +27 -6
- package/examples/extensions/ashi-compact-llm.ts +93 -0
- package/examples/extensions/claude-code-bridge/index.ts +9 -2
- package/examples/extensions/claude-code-bridge/package.json +1 -1
- package/examples/extensions/opencode-bridge/index.ts +208 -53
- package/examples/extensions/opencode-bridge/package.json +1 -1
- package/examples/extensions/opencode-provider.ts +252 -0
- package/examples/extensions/pi-bridge/index.ts +1 -0
- package/package.json +12 -1
- package/examples/extensions/ashi/src/default-renderers.ts +0 -171
package/dist/cli/install.js
CHANGED
|
@@ -75,6 +75,84 @@ function readPackageJson(target) {
|
|
|
75
75
|
return null;
|
|
76
76
|
return JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
|
|
77
77
|
}
|
|
78
|
+
function hostAgentShVersion() {
|
|
79
|
+
try {
|
|
80
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
81
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function satisfies(version, spec) {
|
|
88
|
+
if (spec === version || spec === "*" || spec === "latest")
|
|
89
|
+
return true;
|
|
90
|
+
const [vMaj, vMin, vPatch] = version.split(/[.-]/, 3).map(Number);
|
|
91
|
+
if ([vMaj, vMin, vPatch].some(Number.isNaN))
|
|
92
|
+
return true;
|
|
93
|
+
const m = spec.match(/^([\^~]?)(\d+)\.(\d+)\.(\d+)/);
|
|
94
|
+
if (!m)
|
|
95
|
+
return true;
|
|
96
|
+
const op = m[1];
|
|
97
|
+
const sMaj = Number(m[2]);
|
|
98
|
+
const sMin = Number(m[3]);
|
|
99
|
+
const sPatch = Number(m[4]);
|
|
100
|
+
if (op === "")
|
|
101
|
+
return vMaj === sMaj && vMin === sMin && vPatch === sPatch;
|
|
102
|
+
if (op === "~")
|
|
103
|
+
return vMaj === sMaj && vMin === sMin && vPatch >= sPatch;
|
|
104
|
+
// ^x.y.z: zero-major treats minor as the breaking boundary (npm rule).
|
|
105
|
+
if (sMaj > 0)
|
|
106
|
+
return vMaj === sMaj && (vMin > sMin || (vMin === sMin && vPatch >= sPatch));
|
|
107
|
+
if (sMin > 0)
|
|
108
|
+
return vMaj === 0 && vMin === sMin && vPatch >= sPatch;
|
|
109
|
+
return vMaj === 0 && vMin === 0 && vPatch === sPatch;
|
|
110
|
+
}
|
|
111
|
+
/** Warn when the extension's `agent-sh` pin can't admit the host version;
|
|
112
|
+
* only rewrite when --sync-deps is set. */
|
|
113
|
+
function syncAgentShVersion(target, syncDeps) {
|
|
114
|
+
const hostVersion = hostAgentShVersion();
|
|
115
|
+
if (!hostVersion)
|
|
116
|
+
return;
|
|
117
|
+
// Prerelease hosts aren't on npm; rewriting would leave npm install unable to resolve.
|
|
118
|
+
if (hostVersion.includes("-"))
|
|
119
|
+
return;
|
|
120
|
+
const pkgJson = path.join(target, "package.json");
|
|
121
|
+
if (!fs.existsSync(pkgJson))
|
|
122
|
+
return;
|
|
123
|
+
const raw = fs.readFileSync(pkgJson, "utf-8");
|
|
124
|
+
const pkg = JSON.parse(raw);
|
|
125
|
+
const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
126
|
+
const name = path.basename(target);
|
|
127
|
+
let changed = false;
|
|
128
|
+
let warned = false;
|
|
129
|
+
for (const section of sections) {
|
|
130
|
+
const deps = pkg[section];
|
|
131
|
+
if (!deps || typeof deps !== "object")
|
|
132
|
+
continue;
|
|
133
|
+
const d = deps;
|
|
134
|
+
const current = d["agent-sh"];
|
|
135
|
+
if (typeof current !== "string")
|
|
136
|
+
continue;
|
|
137
|
+
if (current.startsWith("file:"))
|
|
138
|
+
continue;
|
|
139
|
+
if (satisfies(hostVersion, current))
|
|
140
|
+
continue;
|
|
141
|
+
if (syncDeps) {
|
|
142
|
+
console.log(`agent-sh: rewriting ${name} agent-sh ${current} -> ${hostVersion}.`);
|
|
143
|
+
d["agent-sh"] = hostVersion;
|
|
144
|
+
changed = true;
|
|
145
|
+
}
|
|
146
|
+
else if (!warned) {
|
|
147
|
+
console.warn(`agent-sh: ${name} pins agent-sh ${current}, which doesn't admit host ${hostVersion}. ` +
|
|
148
|
+
`npm install will land an older agent-sh inside the extension and drift from the running host. ` +
|
|
149
|
+
`Re-run with --sync-deps to rewrite the pin to ${hostVersion}, or update the bridge's source pin.`);
|
|
150
|
+
warned = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (changed)
|
|
154
|
+
fs.writeFileSync(pkgJson, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
155
|
+
}
|
|
78
156
|
/** Relative `file:` deps in bundled extensions (e.g. `"agent-sh": "file:../../.."`)
|
|
79
157
|
* point at the wrong location after the source is copied into ~/.agent-sh/extensions/.
|
|
80
158
|
* Resolve them against the original source dir so npm install in the target succeeds. */
|
|
@@ -165,7 +243,7 @@ function linkBins(target, pkg) {
|
|
|
165
243
|
}
|
|
166
244
|
export async function runInstall(spec, opts = {}) {
|
|
167
245
|
if (!spec) {
|
|
168
|
-
console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force]\n\n" +
|
|
246
|
+
console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force] [--sync-deps]\n\n" +
|
|
169
247
|
"Bundled extensions:\n" +
|
|
170
248
|
listBundled()
|
|
171
249
|
.map((n) => ` ${n}`)
|
|
@@ -191,9 +269,15 @@ export async function runInstall(spec, opts = {}) {
|
|
|
191
269
|
}
|
|
192
270
|
let linkedBins = [];
|
|
193
271
|
if (resolved.isDirectory) {
|
|
194
|
-
fs.cpSync(resolved.sourcePath, target, {
|
|
272
|
+
fs.cpSync(resolved.sourcePath, target, {
|
|
273
|
+
recursive: true,
|
|
274
|
+
// Skip source node_modules: maybeNpmInstall short-circuits on
|
|
275
|
+
// existing node_modules, silently leaving the bridge's deps stale.
|
|
276
|
+
filter: (src) => path.basename(src) !== "node_modules",
|
|
277
|
+
});
|
|
195
278
|
try {
|
|
196
279
|
rewriteFileDeps(target, resolved.sourcePath);
|
|
280
|
+
syncAgentShVersion(target, opts.syncDeps ?? false);
|
|
197
281
|
const pkg = readPackageJson(target);
|
|
198
282
|
if (pkg) {
|
|
199
283
|
maybeNpmInstall(target, pkg);
|
package/dist/cli/subcommands.js
CHANGED
|
@@ -3,7 +3,10 @@ import { runInstall, runUninstall, runList } from "./install.js";
|
|
|
3
3
|
import { runAuth } from "./auth/cli.js";
|
|
4
4
|
const SUBCOMMANDS = {
|
|
5
5
|
init: (args) => runInit({ force: args.includes("--force") }),
|
|
6
|
-
install: (args) => runInstall(args[0] ?? "", {
|
|
6
|
+
install: (args) => runInstall(args[0] ?? "", {
|
|
7
|
+
force: args.includes("--force"),
|
|
8
|
+
syncDeps: args.includes("--sync-deps"),
|
|
9
|
+
}),
|
|
7
10
|
uninstall: (args) => runUninstall(args[0] ?? ""),
|
|
8
11
|
list: () => runList(),
|
|
9
12
|
auth: (args) => runAuth(args),
|
package/dist/core/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type { AgentContext, AgentConfig, AgentSurface, AgentConfigSurface, Agent
|
|
|
17
17
|
export type { ShellContext, ShellConfig, ShellSurface, ShellConfigSurface, ExtensionContext, RemoteSession, RemoteSessionOptions, RenderSurface, InputModeConfig, TerminalSession, BlockTransformOptions, FencedBlockTransformOptions, AppConfig } from "../shell/host-types.js";
|
|
18
18
|
export { palette, setPalette, resetPalette } from "../utils/palette.js";
|
|
19
19
|
export type { ColorPalette } from "../utils/palette.js";
|
|
20
|
-
export type { AgentBackend, ToolDefinition } from "../agent/types.js";
|
|
20
|
+
export type { AgentBackend, ToolDefinition, ImageContent } from "../agent/types.js";
|
|
21
21
|
export { runSubagent, type SubagentOptions } from "../agent/subagent.js";
|
|
22
22
|
export { LlmClient } from "../agent/llm-client.js";
|
|
23
23
|
export { HistoryFile, InMemoryHistory, NoopHistory, type HistoryAdapter } from "../agent/history-file.js";
|
package/dist/core/settings.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface ModelCapabilityConfig {
|
|
|
13
13
|
maxTokens?: number;
|
|
14
14
|
/** Echo reasoning_content back on assistant turns. Required by DeepSeek. */
|
|
15
15
|
echoReasoning?: boolean;
|
|
16
|
+
/** Content modalities the model supports (e.g. ["text", "image"]). */
|
|
17
|
+
modalities?: ("text" | "image")[];
|
|
16
18
|
}
|
|
17
19
|
/** Provider profile — a named LLM configuration. */
|
|
18
20
|
export interface ProviderConfig {
|
|
@@ -163,6 +165,7 @@ export interface ResolvedProvider {
|
|
|
163
165
|
contextWindow?: number;
|
|
164
166
|
maxTokens?: number;
|
|
165
167
|
echoReasoning?: boolean;
|
|
168
|
+
modalities?: ("text" | "image")[];
|
|
166
169
|
}>;
|
|
167
170
|
/** Borrow another registered provider's reasoning request shape by id. */
|
|
168
171
|
reasoningShape?: string;
|
package/dist/core/settings.js
CHANGED
|
@@ -150,8 +150,8 @@ export function resolveProvider(name) {
|
|
|
150
150
|
}
|
|
151
151
|
else {
|
|
152
152
|
modelIds.push(m.id);
|
|
153
|
-
if (m.reasoning !== undefined || m.contextWindow !== undefined || m.maxTokens !== undefined || m.echoReasoning !== undefined) {
|
|
154
|
-
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, maxTokens: m.maxTokens, echoReasoning: m.echoReasoning });
|
|
153
|
+
if (m.reasoning !== undefined || m.contextWindow !== undefined || m.maxTokens !== undefined || m.echoReasoning !== undefined || m.modalities !== undefined) {
|
|
154
|
+
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, maxTokens: m.maxTokens, echoReasoning: m.echoReasoning, modalities: m.modalities });
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
}
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import "./events.js";
|
|
7
7
|
import type { ExtensionContext } from "./host-types.js";
|
|
8
|
+
import { type Terminal } from "./terminal.js";
|
|
8
9
|
export interface ShellActivateOptions {
|
|
9
10
|
cols: number;
|
|
10
11
|
rows: number;
|
|
@@ -16,6 +17,11 @@ export interface ShellActivateOptions {
|
|
|
16
17
|
info: string;
|
|
17
18
|
model?: string;
|
|
18
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Host-side I/O endpoint. Defaults to processTerminal() so the CLI
|
|
22
|
+
* works unchanged; headless callers (web hubs, tests) supply their own.
|
|
23
|
+
*/
|
|
24
|
+
terminal?: Terminal;
|
|
19
25
|
}
|
|
20
26
|
export interface ShellHandle {
|
|
21
27
|
/** Terminate the PTY. */
|
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 {
|
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,18 +73,7 @@ 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;
|
|
@@ -259,15 +244,14 @@ export class Shell {
|
|
|
259
244
|
this.pendingEchoSkips--;
|
|
260
245
|
const rest = data.slice(nlIdx + 1);
|
|
261
246
|
if (rest)
|
|
262
|
-
|
|
247
|
+
this.terminal.write(rest);
|
|
263
248
|
return;
|
|
264
249
|
}
|
|
265
|
-
|
|
250
|
+
this.terminal.write(data);
|
|
266
251
|
});
|
|
267
252
|
}
|
|
268
253
|
setupInput() {
|
|
269
|
-
|
|
270
|
-
const str = data.toString("utf-8");
|
|
254
|
+
this.inputDispose = this.terminal.onInput((str) => {
|
|
271
255
|
this.inputHandler.handleInput(str);
|
|
272
256
|
});
|
|
273
257
|
}
|
|
@@ -304,7 +288,7 @@ export class Shell {
|
|
|
304
288
|
this.bus.onPipeAsync("shell:exec-request", async (payload) => {
|
|
305
289
|
const visible = this.acquireUnmute("exec-request");
|
|
306
290
|
this.skipNextLine();
|
|
307
|
-
|
|
291
|
+
this.terminal.write("\r\n");
|
|
308
292
|
this.bus.emit("shell:agent-exec-start", {});
|
|
309
293
|
try {
|
|
310
294
|
const output = await new Promise((resolve, reject) => {
|
|
@@ -347,6 +331,8 @@ export class Shell {
|
|
|
347
331
|
this.ptyProcess.onExit(callback);
|
|
348
332
|
}
|
|
349
333
|
kill() {
|
|
334
|
+
this.inputDispose?.();
|
|
335
|
+
this.inputDispose = null;
|
|
350
336
|
this.ptyProcess.kill();
|
|
351
337
|
if (this.tmpDir) {
|
|
352
338
|
fs.rmSync(this.tmpDir, { recursive: true, force: true });
|
|
@@ -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
|
+
}
|
|
@@ -14,7 +14,6 @@ export function createToolUI(bus, surface) {
|
|
|
14
14
|
if (finished)
|
|
15
15
|
return;
|
|
16
16
|
finished = true;
|
|
17
|
-
clearLines(surface, prevLineCount);
|
|
18
17
|
bus.offPipe("input:intercept", interceptor);
|
|
19
18
|
bus.emit("shell:stdout-hide", {});
|
|
20
19
|
bus.emit("tool:interactive-end", {});
|
|
@@ -45,7 +44,10 @@ export function createToolUI(bus, surface) {
|
|
|
45
44
|
bus.emit("tool:interactive-start", {});
|
|
46
45
|
bus.emit("shell:stdout-show", {});
|
|
47
46
|
bus.onPipe("input:intercept", interceptor);
|
|
48
|
-
|
|
47
|
+
// Drop to a fresh row in case the cursor was mid-line; uncounted
|
|
48
|
+
// so clearLines on dismiss stops at the gap, not above it.
|
|
49
|
+
surface.write("\n");
|
|
50
|
+
session.onMount?.(() => render(), done);
|
|
49
51
|
render();
|
|
50
52
|
});
|
|
51
53
|
},
|