agent-sh 0.7.0 → 0.9.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.
- package/README.md +28 -33
- package/dist/agent/agent-loop.d.ts +31 -8
- package/dist/agent/agent-loop.js +277 -66
- package/dist/agent/conversation-state.d.ts +41 -9
- package/dist/agent/conversation-state.js +340 -17
- package/dist/agent/history-file.d.ts +36 -0
- package/dist/agent/history-file.js +167 -0
- package/dist/agent/nuclear-form.d.ts +41 -0
- package/dist/agent/nuclear-form.js +176 -0
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +16 -11
- package/dist/agent/token-budget.d.ts +13 -0
- package/dist/agent/token-budget.js +50 -0
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/tools/user-shell.js +4 -1
- package/dist/agent/types.d.ts +21 -1
- package/dist/context-manager.d.ts +0 -1
- package/dist/context-manager.js +5 -110
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -180
- package/dist/event-bus.d.ts +40 -0
- package/dist/event-bus.js +20 -1
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +104 -17
- package/dist/extensions/agent-backend.d.ts +13 -0
- package/dist/extensions/agent-backend.js +167 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +25 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +44 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +22 -8
- package/dist/extensions/tui-renderer.js +177 -122
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +25 -2
- package/dist/settings.js +25 -4
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +1 -1
- package/dist/{input-handler.js → shell/input-handler.js} +60 -43
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +24 -6
- package/dist/types.d.ts +49 -32
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +27 -0
- package/dist/utils/compositor.d.ts +62 -0
- package/dist/utils/compositor.js +88 -0
- package/dist/utils/diff-renderer.js +92 -4
- package/dist/utils/floating-panel.d.ts +34 -3
- package/dist/utils/floating-panel.js +315 -82
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +32 -3
- package/dist/utils/line-editor.js +218 -36
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +9 -1
- package/dist/utils/terminal-buffer.js +31 -2
- package/dist/utils/tool-display.d.ts +1 -0
- package/dist/utils/tool-display.js +1 -1
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +571 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +154 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- package/examples/extensions/claude-code-bridge/index.ts +77 -1
- package/examples/extensions/interactive-prompts.ts +82 -110
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +450 -0
- package/examples/extensions/pi-bridge/index.ts +87 -2
- package/examples/extensions/questionnaire.ts +249 -0
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/web-access.ts +327 -0
- package/package.json +9 -1
- package/dist/extensions/overlay-agent.d.ts +0 -11
- package/dist/extensions/overlay-agent.js +0 -43
- package/examples/extensions/terminal-buffer.ts +0 -184
package/dist/index.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { Shell } from "./shell.js";
|
|
4
|
+
import { Shell } from "./shell/shell.js";
|
|
5
5
|
import { createCore } from "./core.js";
|
|
6
6
|
import { palette as p } from "./utils/palette.js";
|
|
7
|
-
import
|
|
8
|
-
import slashCommands from "./extensions/slash-commands.js";
|
|
9
|
-
import fileAutocomplete from "./extensions/file-autocomplete.js";
|
|
10
|
-
import shellRecall from "./extensions/shell-recall.js";
|
|
11
|
-
import commandSuggest from "./extensions/command-suggest.js";
|
|
12
|
-
import terminalBuffer from "./extensions/terminal-buffer.js";
|
|
13
|
-
import overlayAgent from "./extensions/overlay-agent.js";
|
|
7
|
+
import { loadBuiltinExtensions } from "./extensions/index.js";
|
|
14
8
|
import { loadExtensions } from "./extension-loader.js";
|
|
15
9
|
import { getSettings } from "./settings.js";
|
|
16
10
|
import { discoverSkills } from "./agent/skills.js";
|
|
@@ -160,6 +154,13 @@ async function main() {
|
|
|
160
154
|
const shellEnv = await captureShellEnvAsync(shellPath);
|
|
161
155
|
if (Object.keys(shellEnv).length > 0) {
|
|
162
156
|
Object.assign(baseEnv, mergeShellEnv(baseEnv, shellEnv));
|
|
157
|
+
// Expose captured env vars to process.env so extensions can read them.
|
|
158
|
+
// Only add vars not already present to avoid clobbering runtime state.
|
|
159
|
+
for (const [k, v] of Object.entries(baseEnv)) {
|
|
160
|
+
if (process.env[k] === undefined) {
|
|
161
|
+
process.env[k] = v;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
163
164
|
if (process.env.DEBUG) {
|
|
164
165
|
console.error('[agent-sh] Shell environment captured');
|
|
165
166
|
}
|
|
@@ -195,6 +196,7 @@ async function main() {
|
|
|
195
196
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
196
197
|
const shell = new Shell({
|
|
197
198
|
bus,
|
|
199
|
+
handlers: core.handlers,
|
|
198
200
|
cols,
|
|
199
201
|
rows,
|
|
200
202
|
shell: config.shell || process.env.SHELL || "/bin/bash",
|
|
@@ -203,9 +205,6 @@ async function main() {
|
|
|
203
205
|
if (agentInfo) {
|
|
204
206
|
return { info: `${p.dim}${agentInfo.name}${agentInfo.model ? ` (${agentInfo.model})` : ""}${p.reset}` };
|
|
205
207
|
}
|
|
206
|
-
if (core.llmClient) {
|
|
207
|
-
return { info: `${p.dim}agent-sh (${core.llmClient.model})${p.reset}` };
|
|
208
|
-
}
|
|
209
208
|
return { info: "" };
|
|
210
209
|
},
|
|
211
210
|
});
|
|
@@ -229,13 +228,8 @@ async function main() {
|
|
|
229
228
|
console.error('[agent-sh] Setting up extensions...');
|
|
230
229
|
}
|
|
231
230
|
const extCtx = core.extensionContext({ quit: cleanup });
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
fileAutocomplete(extCtx);
|
|
235
|
-
shellRecall(extCtx);
|
|
236
|
-
commandSuggest(extCtx);
|
|
237
|
-
terminalBuffer(extCtx);
|
|
238
|
-
overlayAgent(extCtx);
|
|
231
|
+
// Load built-in extensions (individually disableable via settings.disabledBuiltins)
|
|
232
|
+
await loadBuiltinExtensions(extCtx, getSettings().disabledBuiltins);
|
|
239
233
|
// Load user extensions (may register alternative agent backends)
|
|
240
234
|
if (process.env.DEBUG) {
|
|
241
235
|
console.error('[agent-sh] Loading extensions...');
|
|
@@ -264,8 +258,8 @@ async function main() {
|
|
|
264
258
|
const bannerW = Math.min(termW, 60);
|
|
265
259
|
const productName = `${p.accent}${p.bold}agent-sh${p.reset}`;
|
|
266
260
|
const info = agentInfo;
|
|
267
|
-
const backendName = info?.name ?? "
|
|
268
|
-
const model = info?.model
|
|
261
|
+
const backendName = info?.name ?? "ash";
|
|
262
|
+
const model = info?.model;
|
|
269
263
|
const provider = info?.provider;
|
|
270
264
|
const modelValue = model
|
|
271
265
|
? provider ? `${model} [${provider}]` : model
|
package/dist/settings.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export declare const CONFIG_DIR: string;
|
|
2
|
+
/** Per-model capability overrides. */
|
|
3
|
+
export interface ModelCapabilityConfig {
|
|
4
|
+
/** Model identifier. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Whether the model supports reasoning/thinking tokens. */
|
|
7
|
+
reasoning?: boolean;
|
|
8
|
+
/** Context window size in tokens for this specific model. */
|
|
9
|
+
contextWindow?: number;
|
|
10
|
+
}
|
|
2
11
|
/** Provider profile — a named LLM configuration. */
|
|
3
12
|
export interface ProviderConfig {
|
|
4
13
|
/** API key (supports $ENV_VAR syntax for runtime expansion). */
|
|
@@ -7,8 +16,8 @@ export interface ProviderConfig {
|
|
|
7
16
|
baseURL?: string;
|
|
8
17
|
/** Default model to use. Falls back to first entry in models list. */
|
|
9
18
|
defaultModel?: string;
|
|
10
|
-
/** Models available for cycling. */
|
|
11
|
-
models?: string[];
|
|
19
|
+
/** Models available for cycling. Plain strings or objects with capabilities. */
|
|
20
|
+
models?: (string | ModelCapabilityConfig)[];
|
|
12
21
|
/** Context window size in tokens (e.g. 128000). Used for usage display. */
|
|
13
22
|
contextWindow?: number;
|
|
14
23
|
}
|
|
@@ -35,18 +44,32 @@ export interface Settings {
|
|
|
35
44
|
shellTailLines?: number;
|
|
36
45
|
/** Max lines for recall expand before requiring line ranges. */
|
|
37
46
|
recallExpandMaxLines?: number;
|
|
47
|
+
/** Fraction of content budget allocated to shell context (0-1, default 0.35). */
|
|
48
|
+
shellContextRatio?: number;
|
|
49
|
+
/** Max history file size in bytes (default: 102400 = 100KB). */
|
|
50
|
+
historyMaxBytes?: number;
|
|
51
|
+
/** Number of prior history entries to load on startup (default: 50). */
|
|
52
|
+
historyStartupEntries?: number;
|
|
53
|
+
/** Max nuclear entries kept in-context before flushing to history file (default: 200). */
|
|
54
|
+
nuclearMaxEntries?: number;
|
|
55
|
+
/** Auto-compact threshold as fraction of conversation budget (0-1, default 0.5). */
|
|
56
|
+
autoCompactThreshold?: number;
|
|
38
57
|
/** Max command output lines shown inline in TUI. */
|
|
39
58
|
maxCommandOutputLines?: number;
|
|
40
59
|
/** Max read tool output lines shown inline in TUI (0 = hide). */
|
|
41
60
|
readOutputMaxLines?: number;
|
|
42
61
|
/** Max diff lines shown before "ctrl+o to expand". */
|
|
43
62
|
diffMaxLines?: number;
|
|
63
|
+
/** Tool protocol: "api" (all tools), "deferred" (extensions via meta-tool), "inline" (text). */
|
|
64
|
+
toolMode?: "api" | "deferred" | "inline";
|
|
44
65
|
/** Additional directories to scan for skills (supports ~ expansion). */
|
|
45
66
|
skillPaths?: string[];
|
|
46
67
|
/** Show a startup banner when agent-sh launches. */
|
|
47
68
|
startupBanner?: boolean;
|
|
48
69
|
/** Show a subtle agent-sh indicator in the shell prompt. */
|
|
49
70
|
promptIndicator?: boolean;
|
|
71
|
+
/** Names of built-in extensions to disable (e.g. ["command-suggest"]). */
|
|
72
|
+
disabledBuiltins?: string[];
|
|
50
73
|
}
|
|
51
74
|
declare const DEFAULTS: Required<Settings>;
|
|
52
75
|
/** Load settings from disk (cached after first call). */
|
package/dist/settings.js
CHANGED
|
@@ -14,19 +14,26 @@ const DEFAULTS = {
|
|
|
14
14
|
historySize: 500,
|
|
15
15
|
providers: {},
|
|
16
16
|
defaultProvider: undefined,
|
|
17
|
-
defaultBackend: "
|
|
17
|
+
defaultBackend: "ash",
|
|
18
|
+
toolMode: "api",
|
|
18
19
|
contextWindowSize: 20,
|
|
19
20
|
contextBudget: 16384,
|
|
20
21
|
shellTruncateThreshold: 10,
|
|
21
22
|
shellHeadLines: 5,
|
|
22
23
|
shellTailLines: 5,
|
|
23
24
|
recallExpandMaxLines: 100,
|
|
25
|
+
shellContextRatio: 0.35,
|
|
26
|
+
historyMaxBytes: 102400,
|
|
27
|
+
historyStartupEntries: 50,
|
|
28
|
+
nuclearMaxEntries: 200,
|
|
29
|
+
autoCompactThreshold: 0.5,
|
|
24
30
|
maxCommandOutputLines: 3,
|
|
25
31
|
readOutputMaxLines: 10,
|
|
26
32
|
diffMaxLines: 20,
|
|
27
33
|
skillPaths: [],
|
|
28
34
|
startupBanner: true,
|
|
29
35
|
promptIndicator: true,
|
|
36
|
+
disabledBuiltins: [],
|
|
30
37
|
};
|
|
31
38
|
let cached = null;
|
|
32
39
|
/** Load settings from disk (cached after first call). */
|
|
@@ -86,15 +93,29 @@ export function resolveProvider(name) {
|
|
|
86
93
|
const provider = settings.providers?.[name];
|
|
87
94
|
if (!provider)
|
|
88
95
|
return null;
|
|
89
|
-
const
|
|
90
|
-
const
|
|
96
|
+
const rawModels = provider.models ?? (provider.defaultModel ? [provider.defaultModel] : []);
|
|
97
|
+
const modelIds = [];
|
|
98
|
+
const caps = new Map();
|
|
99
|
+
for (const m of rawModels) {
|
|
100
|
+
if (typeof m === "string") {
|
|
101
|
+
modelIds.push(m);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
modelIds.push(m.id);
|
|
105
|
+
if (m.reasoning !== undefined || m.contextWindow !== undefined) {
|
|
106
|
+
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const defaultModel = provider.defaultModel ?? modelIds[0];
|
|
91
111
|
return {
|
|
92
112
|
id: name,
|
|
93
113
|
apiKey: provider.apiKey ? expandEnvVars(provider.apiKey) : undefined,
|
|
94
114
|
baseURL: provider.baseURL,
|
|
95
115
|
defaultModel,
|
|
96
|
-
models:
|
|
116
|
+
models: modelIds.length ? modelIds : (defaultModel ? [defaultModel] : []),
|
|
97
117
|
contextWindow: provider.contextWindow,
|
|
118
|
+
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
98
119
|
};
|
|
99
120
|
}
|
|
100
121
|
/** Get all configured provider names. */
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { visibleLen } from "
|
|
4
|
-
import { palette as p } from "
|
|
5
|
-
import { LineEditor } from "
|
|
6
|
-
import { CONFIG_DIR, getSettings } from "
|
|
3
|
+
import { visibleLen } from "../utils/ansi.js";
|
|
4
|
+
import { palette as p } from "../utils/palette.js";
|
|
5
|
+
import { LineEditor } from "../utils/line-editor.js";
|
|
6
|
+
import { CONFIG_DIR, getSettings } from "../settings.js";
|
|
7
7
|
const HISTORY_FILE = path.join(CONFIG_DIR, "history");
|
|
8
8
|
export class InputHandler {
|
|
9
9
|
ctx;
|
|
@@ -86,22 +86,24 @@ export class InputHandler {
|
|
|
86
86
|
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
87
87
|
const promptPrefix = infoPrefix + p.warning + p.bold + icon + " " + p.reset;
|
|
88
88
|
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1; // icon + space
|
|
89
|
-
|
|
89
|
+
const display = showBuffer ? this.editor.displayText : "";
|
|
90
|
+
const dCursor = showBuffer ? this.editor.displayCursor : 0;
|
|
91
|
+
if (!showBuffer || !display.includes("\n")) {
|
|
90
92
|
// Single-line: simple rendering
|
|
91
|
-
const bufferText = showBuffer ? p.accent +
|
|
93
|
+
const bufferText = showBuffer ? p.accent + display + p.reset : "";
|
|
92
94
|
process.stdout.write(promptPrefix + bufferText);
|
|
93
|
-
const bufferVisLen =
|
|
95
|
+
const bufferVisLen = display.length;
|
|
94
96
|
const totalVisLen = promptVisLen + bufferVisLen;
|
|
95
97
|
this.promptWrappedLines = totalVisLen > 0 ? Math.floor((totalVisLen - 1) / termW) : 0;
|
|
96
98
|
// Position cursor within the buffer
|
|
97
|
-
if (showBuffer &&
|
|
98
|
-
const charsAfterCursor =
|
|
99
|
+
if (showBuffer && dCursor < display.length) {
|
|
100
|
+
const charsAfterCursor = display.length - dCursor;
|
|
99
101
|
process.stdout.write(`\x1b[${charsAfterCursor}D`);
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
else {
|
|
103
105
|
// Multi-line: render each line with continuation indent
|
|
104
|
-
const lines =
|
|
106
|
+
const lines = display.split("\n");
|
|
105
107
|
const indent = " ".repeat(promptVisLen);
|
|
106
108
|
let totalTermLines = 0;
|
|
107
109
|
for (let li = 0; li < lines.length; li++) {
|
|
@@ -116,8 +118,8 @@ export class InputHandler {
|
|
|
116
118
|
totalTermLines += lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
117
119
|
}
|
|
118
120
|
this.promptWrappedLines = totalTermLines - 1;
|
|
119
|
-
// Position cursor: find which line and column the cursor is on
|
|
120
|
-
let charsRemaining =
|
|
121
|
+
// Position cursor: find which display line and column the cursor is on
|
|
122
|
+
let charsRemaining = dCursor;
|
|
121
123
|
let cursorLine = 0;
|
|
122
124
|
for (let li = 0; li < lines.length; li++) {
|
|
123
125
|
if (charsRemaining <= lines[li].length) {
|
|
@@ -127,13 +129,31 @@ export class InputHandler {
|
|
|
127
129
|
charsRemaining -= lines[li].length + 1; // +1 for \n
|
|
128
130
|
cursorLine = li + 1;
|
|
129
131
|
}
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
// Compute terminal rows for cursor positioning (not logical lines)
|
|
133
|
+
// Each logical line may wrap across multiple terminal rows.
|
|
134
|
+
const cursorColAbs = promptVisLen + charsRemaining;
|
|
135
|
+
const cursorTermRow = Math.floor(cursorColAbs / termW);
|
|
136
|
+
// Count terminal rows occupied by lines after cursor's logical line
|
|
137
|
+
let termRowsAfterCursor = 0;
|
|
138
|
+
for (let li = cursorLine + 1; li < lines.length; li++) {
|
|
139
|
+
const lineVisLen = promptVisLen + lines[li].length;
|
|
140
|
+
termRowsAfterCursor += lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
141
|
+
}
|
|
142
|
+
// Also count remaining terminal rows on cursor's own logical line
|
|
143
|
+
const cursorLineVisLen = promptVisLen + lines[cursorLine].length;
|
|
144
|
+
const cursorLineTotalRows = cursorLineVisLen > 0 ? Math.ceil(cursorLineVisLen / termW) : 1;
|
|
145
|
+
const rowsAfterCursorInLine = cursorLineTotalRows - 1 - cursorTermRow;
|
|
146
|
+
const totalRowsFromEnd = termRowsAfterCursor + rowsAfterCursorInLine;
|
|
147
|
+
if (totalRowsFromEnd > 0) {
|
|
148
|
+
process.stdout.write(`\x1b[${totalRowsFromEnd}A`);
|
|
149
|
+
}
|
|
150
|
+
const cursorCol = cursorColAbs % termW;
|
|
151
|
+
if (cursorCol > 0) {
|
|
152
|
+
process.stdout.write(`\r\x1b[${cursorCol}C`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
process.stdout.write(`\r`);
|
|
134
156
|
}
|
|
135
|
-
const cursorCol = (cursorLine === 0 ? promptVisLen : promptVisLen) + charsRemaining;
|
|
136
|
-
process.stdout.write(`\r\x1b[${cursorCol}C`);
|
|
137
157
|
}
|
|
138
158
|
}
|
|
139
159
|
handleInput(data) {
|
|
@@ -249,16 +269,17 @@ export class InputHandler {
|
|
|
249
269
|
this.activeMode = mode;
|
|
250
270
|
this.editor.clear();
|
|
251
271
|
// Enable kitty keyboard protocol (progressive enhancement flag 1)
|
|
252
|
-
// so Shift+Enter sends \x1b[13;2u instead of plain \r
|
|
253
|
-
|
|
272
|
+
// so Shift+Enter sends \x1b[13;2u instead of plain \r.
|
|
273
|
+
// Enable bracket paste mode so pasted text doesn't trigger submit.
|
|
274
|
+
process.stdout.write("\x1b[>1u\x1b[?2004h");
|
|
254
275
|
this.writeModePromptLine(false);
|
|
255
276
|
}
|
|
256
277
|
exitMode() {
|
|
257
278
|
this.dismissAutocomplete();
|
|
258
279
|
this.activeMode = null;
|
|
259
280
|
this.editor.clear();
|
|
260
|
-
// Disable kitty keyboard protocol
|
|
261
|
-
process.stdout.write("\x1b[<u");
|
|
281
|
+
// Disable kitty keyboard protocol and bracket paste mode
|
|
282
|
+
process.stdout.write("\x1b[<u\x1b[?2004l");
|
|
262
283
|
this.clearPromptArea();
|
|
263
284
|
this.printPrompt();
|
|
264
285
|
}
|
|
@@ -294,7 +315,7 @@ export class InputHandler {
|
|
|
294
315
|
this.updateAutocomplete();
|
|
295
316
|
}
|
|
296
317
|
updateAutocomplete() {
|
|
297
|
-
const buf = this.editor.
|
|
318
|
+
const buf = this.editor.text;
|
|
298
319
|
let command = null;
|
|
299
320
|
let commandArgs = null;
|
|
300
321
|
if (buf.startsWith("/")) {
|
|
@@ -350,7 +371,7 @@ export class InputHandler {
|
|
|
350
371
|
: `${indicator} `;
|
|
351
372
|
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
352
373
|
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1;
|
|
353
|
-
const col = promptVisLen + this.editor.
|
|
374
|
+
const col = promptVisLen + this.editor.displayCursor;
|
|
354
375
|
process.stdout.write(`\r\x1b[${col}C`);
|
|
355
376
|
}
|
|
356
377
|
applyAutocomplete() {
|
|
@@ -359,18 +380,16 @@ export class InputHandler {
|
|
|
359
380
|
const selected = this.autocompleteItems[this.autocompleteIndex];
|
|
360
381
|
if (!selected)
|
|
361
382
|
return;
|
|
362
|
-
const atPos = this.editor.
|
|
383
|
+
const atPos = this.editor.text.lastIndexOf("@");
|
|
363
384
|
const isFileAc = atPos >= 0 &&
|
|
364
|
-
(atPos === 0 || this.editor.
|
|
365
|
-
!this.editor.
|
|
385
|
+
(atPos === 0 || this.editor.text[atPos - 1] === " ") &&
|
|
386
|
+
!this.editor.text.slice(atPos + 1).includes(" ");
|
|
366
387
|
if (isFileAc) {
|
|
367
|
-
this.editor.
|
|
368
|
-
this.editor.buffer.slice(0, atPos) + "@" + selected.name;
|
|
388
|
+
this.editor.setText(this.editor.text.slice(0, atPos) + "@" + selected.name);
|
|
369
389
|
}
|
|
370
390
|
else {
|
|
371
|
-
this.editor.
|
|
391
|
+
this.editor.setText(selected.name);
|
|
372
392
|
}
|
|
373
|
-
this.editor.cursor = this.editor.buffer.length;
|
|
374
393
|
this.clearAutocompleteLines();
|
|
375
394
|
this.autocompleteActive = false;
|
|
376
395
|
this.autocompleteItems = [];
|
|
@@ -419,8 +438,8 @@ export class InputHandler {
|
|
|
419
438
|
switch (act.action) {
|
|
420
439
|
case "changed": {
|
|
421
440
|
// If the buffer is exactly a trigger char for a different mode, switch to it
|
|
422
|
-
const switchMode = this.modes.get(this.editor.
|
|
423
|
-
if (this.editor.
|
|
441
|
+
const switchMode = this.modes.get(this.editor.text);
|
|
442
|
+
if (this.editor.text.length === 1 && switchMode && switchMode !== this.activeMode) {
|
|
424
443
|
this.dismissAutocomplete();
|
|
425
444
|
this.clearPromptArea();
|
|
426
445
|
this.activeMode = switchMode;
|
|
@@ -437,10 +456,10 @@ export class InputHandler {
|
|
|
437
456
|
if (this.autocompleteActive) {
|
|
438
457
|
this.applyAutocomplete();
|
|
439
458
|
}
|
|
440
|
-
// Use editor.
|
|
459
|
+
// Use editor.text (not act.buffer) so autocomplete selections
|
|
441
460
|
// take effect — act.buffer is a stale snapshot from before
|
|
442
|
-
// applyAutocomplete() updated the
|
|
443
|
-
const query = this.editor.
|
|
461
|
+
// applyAutocomplete() updated the editor.
|
|
462
|
+
const query = this.editor.text.trim();
|
|
444
463
|
if (query) {
|
|
445
464
|
// Add to history (avoid consecutive duplicates)
|
|
446
465
|
if (this.history.length === 0 || this.history[this.history.length - 1] !== query) {
|
|
@@ -452,7 +471,7 @@ export class InputHandler {
|
|
|
452
471
|
this.savedBuffer = "";
|
|
453
472
|
this.clearAutocompleteLines();
|
|
454
473
|
this.clearPromptArea();
|
|
455
|
-
process.stdout.write("\x1b[<u"); // disable kitty
|
|
474
|
+
process.stdout.write("\x1b[<u\x1b[?2004l"); // disable kitty + bracket paste
|
|
456
475
|
const currentMode = this.activeMode;
|
|
457
476
|
this.activeMode = null;
|
|
458
477
|
this.editor.clear();
|
|
@@ -506,14 +525,13 @@ export class InputHandler {
|
|
|
506
525
|
}
|
|
507
526
|
else if (this.history.length > 0) {
|
|
508
527
|
if (this.historyIndex === -1) {
|
|
509
|
-
this.savedBuffer = this.editor.
|
|
528
|
+
this.savedBuffer = this.editor.text;
|
|
510
529
|
this.historyIndex = this.history.length - 1;
|
|
511
530
|
}
|
|
512
531
|
else if (this.historyIndex > 0) {
|
|
513
532
|
this.historyIndex--;
|
|
514
533
|
}
|
|
515
|
-
this.editor.
|
|
516
|
-
this.editor.cursor = this.editor.buffer.length;
|
|
534
|
+
this.editor.setText(this.history[this.historyIndex]);
|
|
517
535
|
this.clearAutocompleteLines();
|
|
518
536
|
this.writeModePromptLine();
|
|
519
537
|
}
|
|
@@ -531,13 +549,12 @@ export class InputHandler {
|
|
|
531
549
|
else if (this.historyIndex !== -1) {
|
|
532
550
|
if (this.historyIndex < this.history.length - 1) {
|
|
533
551
|
this.historyIndex++;
|
|
534
|
-
this.editor.
|
|
552
|
+
this.editor.setText(this.history[this.historyIndex]);
|
|
535
553
|
}
|
|
536
554
|
else {
|
|
537
555
|
this.historyIndex = -1;
|
|
538
|
-
this.editor.
|
|
556
|
+
this.editor.setText(this.savedBuffer);
|
|
539
557
|
}
|
|
540
|
-
this.editor.cursor = this.editor.buffer.length;
|
|
541
558
|
this.clearAutocompleteLines();
|
|
542
559
|
this.writeModePromptLine();
|
|
543
560
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { EventBus } from "
|
|
1
|
+
import type { EventBus } from "../event-bus.js";
|
|
2
2
|
import { type InputContext } from "./input-handler.js";
|
|
3
|
+
export interface ShellHandlers {
|
|
4
|
+
define: (name: string, fn: (...args: any[]) => any) => void;
|
|
5
|
+
call: (name: string, ...args: any[]) => any;
|
|
6
|
+
}
|
|
3
7
|
export declare class Shell implements InputContext {
|
|
4
8
|
private ptyProcess;
|
|
5
9
|
private bus;
|
|
10
|
+
private handlers;
|
|
6
11
|
private inputHandler;
|
|
7
12
|
private outputParser;
|
|
8
13
|
private paused;
|
|
@@ -14,6 +19,7 @@ export declare class Shell implements InputContext {
|
|
|
14
19
|
private tmpDir?;
|
|
15
20
|
constructor(opts: {
|
|
16
21
|
bus: EventBus;
|
|
22
|
+
handlers: ShellHandlers;
|
|
17
23
|
onShowAgentInfo?: () => {
|
|
18
24
|
info: string;
|
|
19
25
|
model?: string;
|
|
@@ -43,7 +49,7 @@ export declare class Shell implements InputContext {
|
|
|
43
49
|
* Routed through shell:redraw-prompt pipe so extensions (e.g. overlay)
|
|
44
50
|
* can suppress it by setting `handled: true`.
|
|
45
51
|
*/
|
|
46
|
-
freshPrompt():
|
|
52
|
+
freshPrompt(): boolean;
|
|
47
53
|
onCommandEntered(command: string, cwd: string): void;
|
|
48
54
|
private setupOutput;
|
|
49
55
|
private setupInput;
|
|
@@ -4,11 +4,12 @@ import * as path from "path";
|
|
|
4
4
|
import * as pty from "node-pty";
|
|
5
5
|
import { InputHandler } from "./input-handler.js";
|
|
6
6
|
import { OutputParser } from "./output-parser.js";
|
|
7
|
-
import { getSettings } from "
|
|
8
|
-
import { RefCounter } from "
|
|
7
|
+
import { getSettings } from "../settings.js";
|
|
8
|
+
import { RefCounter } from "../utils/output-writer.js";
|
|
9
9
|
export class Shell {
|
|
10
10
|
ptyProcess;
|
|
11
11
|
bus;
|
|
12
|
+
handlers;
|
|
12
13
|
inputHandler;
|
|
13
14
|
outputParser;
|
|
14
15
|
paused = false;
|
|
@@ -139,6 +140,7 @@ export class Shell {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
this.bus = opts.bus;
|
|
143
|
+
this.handlers = opts.handlers;
|
|
142
144
|
this.outputParser = new OutputParser(opts.bus, opts.cwd);
|
|
143
145
|
// Ensure temp dir cleanup on abnormal exit (SIGKILL won't fire this,
|
|
144
146
|
// but it covers uncaught exceptions and normal process.exit paths)
|
|
@@ -163,6 +165,10 @@ export class Shell {
|
|
|
163
165
|
this.bus.on("shell:pty-write", ({ data }) => {
|
|
164
166
|
this.ptyProcess.write(data);
|
|
165
167
|
});
|
|
168
|
+
// Allow extensions to resize the PTY (sends SIGWINCH to child)
|
|
169
|
+
this.bus.on("shell:pty-resize", ({ cols, rows }) => {
|
|
170
|
+
this.ptyProcess.resize(cols, rows);
|
|
171
|
+
});
|
|
166
172
|
// Ref-counted stdout hold — overlay extensions suppress PTY output
|
|
167
173
|
this.bus.on("shell:stdout-hold", () => { this.stdoutHold.increment(); });
|
|
168
174
|
this.bus.on("shell:stdout-release", () => { this.stdoutHold.decrement(); });
|
|
@@ -221,7 +227,9 @@ export class Shell {
|
|
|
221
227
|
});
|
|
222
228
|
if (!result.handled) {
|
|
223
229
|
this.ptyProcess.write("\n");
|
|
230
|
+
return true;
|
|
224
231
|
}
|
|
232
|
+
return false;
|
|
225
233
|
}
|
|
226
234
|
onCommandEntered(command, cwd) {
|
|
227
235
|
this.outputParser.onCommandEntered(command, cwd);
|
|
@@ -261,18 +269,28 @@ export class Shell {
|
|
|
261
269
|
* zero frontend knowledge; any frontend can subscribe to the same events.
|
|
262
270
|
*/
|
|
263
271
|
setupAgentLifecycle() {
|
|
264
|
-
|
|
272
|
+
// Default agent lifecycle: pause the shell while the agent works,
|
|
273
|
+
// then redraw the prompt when done. Extensions advise these handlers
|
|
274
|
+
// to change behavior (e.g. tmux split keeps the shell interactive).
|
|
275
|
+
this.handlers.define("shell:on-processing-start", () => {
|
|
265
276
|
this.agentActive = true;
|
|
266
277
|
this.paused = true;
|
|
267
278
|
});
|
|
268
|
-
this.
|
|
279
|
+
this.handlers.define("shell:on-processing-done", () => {
|
|
269
280
|
this.paused = false;
|
|
270
281
|
this.agentActive = false;
|
|
271
|
-
this.echoSkip = true;
|
|
272
282
|
if (!this.inputHandler.handleProcessingDone()) {
|
|
273
|
-
this.freshPrompt()
|
|
283
|
+
if (this.freshPrompt()) {
|
|
284
|
+
this.echoSkip = true;
|
|
285
|
+
}
|
|
274
286
|
}
|
|
275
287
|
});
|
|
288
|
+
this.bus.on("agent:processing-start", () => {
|
|
289
|
+
this.handlers.call("shell:on-processing-start");
|
|
290
|
+
});
|
|
291
|
+
this.bus.on("agent:processing-done", () => {
|
|
292
|
+
this.handlers.call("shell:on-processing-done");
|
|
293
|
+
});
|
|
276
294
|
// Permission prompts need stdout unpaused so the interactive UI renders,
|
|
277
295
|
// then re-paused after the decision.
|
|
278
296
|
this.bus.on("permission:request", () => {
|