agent-sh 0.8.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 +25 -34
- package/dist/agent/agent-loop.d.ts +29 -6
- package/dist/agent/agent-loop.js +177 -59
- package/dist/agent/conversation-state.d.ts +3 -1
- package/dist/agent/conversation-state.js +6 -2
- package/dist/agent/nuclear-form.js +5 -4
- package/dist/agent/system-prompt.d.ts +4 -5
- package/dist/agent/system-prompt.js +12 -28
- package/dist/{token-budget.js → agent/token-budget.js} +1 -1
- package/dist/agent/tool-protocol.d.ts +83 -0
- package/dist/agent/tool-protocol.js +386 -0
- package/dist/agent/types.d.ts +21 -1
- package/dist/core.d.ts +7 -7
- package/dist/core.js +76 -194
- package/dist/event-bus.d.ts +26 -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 +16 -1
- package/dist/extensions/terminal-buffer.d.ts +1 -1
- package/dist/extensions/terminal-buffer.js +13 -4
- package/dist/extensions/tui-renderer.js +63 -43
- package/dist/index.js +14 -20
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +4 -1
- 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} +20 -6
- package/dist/types.d.ts +49 -10
- 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 +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +26 -10
- package/dist/utils/handler-registry.js +52 -16
- package/dist/utils/line-editor.d.ts +23 -3
- package/dist/utils/line-editor.js +180 -42
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +1 -1
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +5 -1
- package/dist/utils/terminal-buffer.js +18 -2
- 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/interactive-prompts.ts +82 -110
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +450 -0
- 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 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/examples/extensions/terminal-buffer.ts +0 -184
- /package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { AgentLoop } from "../agent/agent-loop.js";
|
|
2
|
+
import { LlmClient } from "../utils/llm-client.js";
|
|
3
|
+
import { resolveProvider, getProviderNames, getSettings } from "../settings.js";
|
|
4
|
+
export default function agentBackend(ctx) {
|
|
5
|
+
const { bus } = ctx;
|
|
6
|
+
// ── Resolve providers ──────────────────────────────────────
|
|
7
|
+
const config = ctx.call("config:get-shell-config") ?? {};
|
|
8
|
+
const settings = getSettings();
|
|
9
|
+
let activeProvider = null;
|
|
10
|
+
const providerRegistry = new Map();
|
|
11
|
+
for (const name of getProviderNames()) {
|
|
12
|
+
const p = resolveProvider(name);
|
|
13
|
+
if (p)
|
|
14
|
+
providerRegistry.set(name, p);
|
|
15
|
+
}
|
|
16
|
+
const providerName = config.provider ?? settings.defaultProvider;
|
|
17
|
+
if (providerName) {
|
|
18
|
+
activeProvider = providerRegistry.get(providerName) ?? null;
|
|
19
|
+
}
|
|
20
|
+
// ── Build modes ────────────────────────────────────────────
|
|
21
|
+
const buildModes = () => {
|
|
22
|
+
const allModes = [];
|
|
23
|
+
for (const [id, p] of providerRegistry) {
|
|
24
|
+
if (!p.apiKey)
|
|
25
|
+
continue;
|
|
26
|
+
for (const model of p.models) {
|
|
27
|
+
const mc = p.modelCapabilities?.get(model);
|
|
28
|
+
allModes.push({
|
|
29
|
+
model,
|
|
30
|
+
provider: id,
|
|
31
|
+
providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
|
|
32
|
+
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
33
|
+
reasoning: mc?.reasoning,
|
|
34
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return allModes;
|
|
39
|
+
};
|
|
40
|
+
const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
|
|
41
|
+
const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
|
|
42
|
+
const effectiveModel = config.model ?? activeProvider?.defaultModel;
|
|
43
|
+
let modes = buildModes();
|
|
44
|
+
if (modes.length === 0 && effectiveApiKey && effectiveModel) {
|
|
45
|
+
modes = [{ model: effectiveModel }];
|
|
46
|
+
}
|
|
47
|
+
const initialModeIndex = Math.max(0, modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id)));
|
|
48
|
+
// ── Create LLM client ─────────────────────────────────────
|
|
49
|
+
if (!effectiveApiKey)
|
|
50
|
+
return; // No LLM provider configured — skip
|
|
51
|
+
if (!effectiveModel) {
|
|
52
|
+
bus.emit("ui:error", { message: "No model specified. Use --model or configure a provider with defaultModel in ~/.agent-sh/settings.json" });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const llmClient = new LlmClient({
|
|
56
|
+
apiKey: effectiveApiKey,
|
|
57
|
+
baseURL: effectiveBaseURL,
|
|
58
|
+
model: effectiveModel,
|
|
59
|
+
});
|
|
60
|
+
// Expose LLM client for other extensions (e.g. command-suggest)
|
|
61
|
+
ctx.define("llm:get-client", () => llmClient);
|
|
62
|
+
// ── Initial modes (queryable via pipe) ─────────────────────
|
|
63
|
+
bus.onPipe("config:get-initial-modes", () => ({
|
|
64
|
+
modes,
|
|
65
|
+
initialModeIndex,
|
|
66
|
+
}));
|
|
67
|
+
// ── Create agent loop ──────────────────────────────────────
|
|
68
|
+
const agentLoop = new AgentLoop({
|
|
69
|
+
bus,
|
|
70
|
+
contextManager: ctx.contextManager,
|
|
71
|
+
llmClient,
|
|
72
|
+
handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call },
|
|
73
|
+
modes,
|
|
74
|
+
initialModeIndex,
|
|
75
|
+
compositor: ctx.compositor,
|
|
76
|
+
});
|
|
77
|
+
// Register as backend
|
|
78
|
+
bus.emit("agent:register-backend", {
|
|
79
|
+
name: "ash",
|
|
80
|
+
kill: () => agentLoop.kill(),
|
|
81
|
+
start: async () => {
|
|
82
|
+
agentLoop.wire();
|
|
83
|
+
bus.emit("agent:info", {
|
|
84
|
+
name: "ash",
|
|
85
|
+
version: "0.4",
|
|
86
|
+
model: llmClient.model,
|
|
87
|
+
provider: modes[initialModeIndex]?.provider,
|
|
88
|
+
contextWindow: modes[initialModeIndex]?.contextWindow,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
// ── Runtime provider registration ──────────────────────────
|
|
93
|
+
bus.on("provider:register", (p) => {
|
|
94
|
+
const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
|
|
95
|
+
const modelIds = [];
|
|
96
|
+
const caps = new Map();
|
|
97
|
+
for (const m of rawModels) {
|
|
98
|
+
if (typeof m === "string") {
|
|
99
|
+
modelIds.push(m);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
modelIds.push(m.id);
|
|
103
|
+
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
providerRegistry.set(p.id, {
|
|
107
|
+
id: p.id,
|
|
108
|
+
apiKey: p.apiKey,
|
|
109
|
+
baseURL: p.baseURL,
|
|
110
|
+
defaultModel: p.defaultModel,
|
|
111
|
+
models: modelIds,
|
|
112
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
113
|
+
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
114
|
+
});
|
|
115
|
+
const addModes = modelIds.map((m) => {
|
|
116
|
+
const mc = caps.get(m);
|
|
117
|
+
return {
|
|
118
|
+
model: m,
|
|
119
|
+
provider: p.id,
|
|
120
|
+
providerConfig: { apiKey: p.apiKey ?? "", baseURL: p.baseURL },
|
|
121
|
+
contextWindow: mc?.contextWindow,
|
|
122
|
+
reasoning: mc?.reasoning,
|
|
123
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
bus.emit("config:add-modes", { modes: addModes });
|
|
127
|
+
});
|
|
128
|
+
// ── Runtime provider switching ─────────────────────────────
|
|
129
|
+
bus.on("config:switch-provider", ({ provider: name }) => {
|
|
130
|
+
const p = providerRegistry.get(name);
|
|
131
|
+
if (!p) {
|
|
132
|
+
bus.emit("ui:error", { message: `Unknown provider: ${name}` });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const newApiKey = p.apiKey;
|
|
136
|
+
if (!newApiKey) {
|
|
137
|
+
bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const switchModel = p.defaultModel ?? p.models[0];
|
|
141
|
+
if (!switchModel) {
|
|
142
|
+
bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
llmClient.reconfigure({
|
|
146
|
+
apiKey: newApiKey,
|
|
147
|
+
baseURL: p.baseURL,
|
|
148
|
+
model: switchModel,
|
|
149
|
+
});
|
|
150
|
+
const newModes = p.models.map((m) => {
|
|
151
|
+
const mc = p.modelCapabilities?.get(m);
|
|
152
|
+
return {
|
|
153
|
+
model: m,
|
|
154
|
+
provider: name,
|
|
155
|
+
providerConfig: { apiKey: newApiKey, baseURL: p.baseURL },
|
|
156
|
+
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
157
|
+
reasoning: mc?.reasoning,
|
|
158
|
+
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
bus.emit("config:set-modes", { modes: newModes });
|
|
162
|
+
activeProvider = p;
|
|
163
|
+
bus.emit("agent:info", { name: "ash", version: "0.4", model: switchModel, provider: name, contextWindow: p.contextWindow });
|
|
164
|
+
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
165
|
+
bus.emit("config:changed", {});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Command suggestion extension (fast-path LLM feature).
|
|
3
3
|
*
|
|
4
|
-
* After a shell command fails (non-zero exit), uses
|
|
4
|
+
* After a shell command fails (non-zero exit), uses LlmClient.complete()
|
|
5
5
|
* to suggest a fix. Shows the suggestion below the prompt.
|
|
6
6
|
*
|
|
7
|
-
* Only active when
|
|
7
|
+
* Only active when an LLM client is available (registered by agent-backend).
|
|
8
8
|
*/
|
|
9
9
|
import type { ExtensionContext } from "../types.js";
|
|
10
|
-
export default function activate({ bus,
|
|
10
|
+
export default function activate({ bus, call }: ExtensionContext): void;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export default function activate({ bus,
|
|
2
|
-
if (!llmClient)
|
|
3
|
-
return;
|
|
1
|
+
export default function activate({ bus, call }) {
|
|
4
2
|
let suggesting = false;
|
|
5
3
|
bus.on("shell:command-done", ({ command, output, exitCode, cwd }) => {
|
|
6
4
|
if (exitCode === null || exitCode === 0)
|
|
@@ -9,6 +7,9 @@ export default function activate({ bus, llmClient }) {
|
|
|
9
7
|
return;
|
|
10
8
|
if (suggesting)
|
|
11
9
|
return; // don't stack suggestions
|
|
10
|
+
const llmClient = call("llm:get-client");
|
|
11
|
+
if (!llmClient)
|
|
12
|
+
return;
|
|
12
13
|
suggesting = true;
|
|
13
14
|
// Truncate output to avoid blowing up the prompt
|
|
14
15
|
const truncated = output.length > 1000
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in extension manifest.
|
|
3
|
+
*
|
|
4
|
+
* These extensions ship with agent-sh and load before user extensions.
|
|
5
|
+
* They receive unscoped contexts (not reloadable) and can be individually
|
|
6
|
+
* disabled via the `disabledBuiltins` setting in ~/.agent-sh/settings.json.
|
|
7
|
+
*/
|
|
8
|
+
import type { ExtensionContext } from "../types.js";
|
|
9
|
+
type ActivateFn = (ctx: ExtensionContext) => void;
|
|
10
|
+
export declare const BUILTIN_EXTENSIONS: Array<{
|
|
11
|
+
name: string;
|
|
12
|
+
load: () => Promise<ActivateFn>;
|
|
13
|
+
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Load built-in extensions sequentially, skipping any in the disabled list.
|
|
16
|
+
* Returns the names of extensions that were loaded.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadBuiltinExtensions(ctx: ExtensionContext, disabled?: string[]): Promise<string[]>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const BUILTIN_EXTENSIONS = [
|
|
2
|
+
{ name: "agent-backend", load: () => import("./agent-backend.js").then(m => m.default) },
|
|
3
|
+
{ name: "tui-renderer", load: () => import("./tui-renderer.js").then(m => m.default) },
|
|
4
|
+
{ name: "slash-commands", load: () => import("./slash-commands.js").then(m => m.default) },
|
|
5
|
+
{ name: "file-autocomplete", load: () => import("./file-autocomplete.js").then(m => m.default) },
|
|
6
|
+
{ name: "shell-recall", load: () => import("./shell-recall.js").then(m => m.default) },
|
|
7
|
+
{ name: "command-suggest", load: () => import("./command-suggest.js").then(m => m.default) },
|
|
8
|
+
{ name: "terminal-buffer", load: () => import("./terminal-buffer.js").then(m => m.default) },
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* Load built-in extensions sequentially, skipping any in the disabled list.
|
|
12
|
+
* Returns the names of extensions that were loaded.
|
|
13
|
+
*/
|
|
14
|
+
export async function loadBuiltinExtensions(ctx, disabled = []) {
|
|
15
|
+
const disabledSet = new Set(disabled);
|
|
16
|
+
const loaded = [];
|
|
17
|
+
for (const ext of BUILTIN_EXTENSIONS) {
|
|
18
|
+
if (disabledSet.has(ext.name))
|
|
19
|
+
continue;
|
|
20
|
+
const activate = await ext.load();
|
|
21
|
+
activate(ctx);
|
|
22
|
+
loaded.push(ext.name);
|
|
23
|
+
}
|
|
24
|
+
return loaded;
|
|
25
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ExtensionContext } from "../types.js";
|
|
2
|
-
export default function activate(
|
|
2
|
+
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { palette as p } from "../utils/palette.js";
|
|
14
14
|
import { discoverSkills, loadSkillContent } from "../agent/skills.js";
|
|
15
|
-
|
|
15
|
+
import { reloadExtensions } from "../extension-loader.js";
|
|
16
|
+
export default function activate(ctx) {
|
|
17
|
+
const { bus, contextManager } = ctx;
|
|
16
18
|
const commands = new Map();
|
|
17
19
|
const register = (cmd) => {
|
|
18
20
|
const name = cmd.name.startsWith("/") ? cmd.name : `/${cmd.name}`;
|
|
@@ -103,6 +105,19 @@ export default function activate({ bus, contextManager }) {
|
|
|
103
105
|
bus.emit("ui:info", { message: lines.join("\n") });
|
|
104
106
|
},
|
|
105
107
|
});
|
|
108
|
+
register({
|
|
109
|
+
name: "/reload",
|
|
110
|
+
description: "Reload user extensions from ~/.agent-sh/extensions/",
|
|
111
|
+
handler: async () => {
|
|
112
|
+
const names = await reloadExtensions(ctx);
|
|
113
|
+
if (names.length > 0) {
|
|
114
|
+
bus.emit("ui:info", { message: `Reloaded: ${names.join(", ")}` });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
bus.emit("ui:info", { message: "No extensions to reload." });
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
});
|
|
106
121
|
// ── Extension registration ────────────────────────────────────
|
|
107
122
|
bus.on("command:register", (cmd) => {
|
|
108
123
|
register(cmd);
|
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
* Requires: npm install @xterm/headless@5.5.0 @xterm/addon-serialize@0.13.0
|
|
12
12
|
*/
|
|
13
13
|
import type { ExtensionContext } from "../types.js";
|
|
14
|
-
export default function activate(
|
|
14
|
+
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -19,7 +19,8 @@ function interpretEscapes(str) {
|
|
|
19
19
|
function settle(ms = 100) {
|
|
20
20
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
21
|
}
|
|
22
|
-
export default function activate(
|
|
22
|
+
export default function activate(ctx) {
|
|
23
|
+
const { bus, terminalBuffer: tb, registerTool, registerInstruction } = ctx;
|
|
23
24
|
if (!tb)
|
|
24
25
|
return; // @xterm/headless not installed
|
|
25
26
|
registerTool({
|
|
@@ -30,7 +31,14 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }) {
|
|
|
30
31
|
"diagnosing errors on screen, or checking state before/after sending keystrokes with terminal_keys.",
|
|
31
32
|
input_schema: {
|
|
32
33
|
type: "object",
|
|
33
|
-
properties: {
|
|
34
|
+
properties: {
|
|
35
|
+
include_scrollback: {
|
|
36
|
+
type: "boolean",
|
|
37
|
+
description: "If true, include scrollback buffer (content that scrolled off screen) " +
|
|
38
|
+
"in addition to the visible viewport. Useful for capturing output from " +
|
|
39
|
+
"long-running or streaming commands. Default: false.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
34
42
|
},
|
|
35
43
|
showOutput: true,
|
|
36
44
|
getDisplayInfo: () => ({
|
|
@@ -38,8 +46,9 @@ export default function activate({ bus, terminalBuffer: tb, registerTool }) {
|
|
|
38
46
|
icon: "⊞",
|
|
39
47
|
locations: [],
|
|
40
48
|
}),
|
|
41
|
-
async execute() {
|
|
42
|
-
const
|
|
49
|
+
async execute(args) {
|
|
50
|
+
const includeScrollback = args.include_scrollback ?? false;
|
|
51
|
+
const { text, altScreen, cursorX, cursorY } = tb.readScreen({ includeScrollback });
|
|
43
52
|
const info = [
|
|
44
53
|
altScreen ? "mode: alternate screen" : "mode: normal",
|
|
45
54
|
`cursor: row=${cursorY} col=${cursorX}`,
|
|
@@ -11,14 +11,13 @@
|
|
|
11
11
|
* can subscribe to the same events.
|
|
12
12
|
*/
|
|
13
13
|
import { highlight } from "cli-highlight";
|
|
14
|
-
import { MarkdownRenderer, wrapLine } from "../utils/markdown.js";
|
|
14
|
+
import { MarkdownRenderer, wrapLine, MAX_CONTENT_WIDTH } from "../utils/markdown.js";
|
|
15
15
|
import { createFencedBlockTransform } from "../utils/stream-transform.js";
|
|
16
16
|
import { palette as p } from "../utils/palette.js";
|
|
17
17
|
import { renderToolCall, createSpinner, formatElapsed, SPINNER_FRAMES, } from "../utils/tool-display.js";
|
|
18
18
|
import { renderDiff } from "../utils/diff-renderer.js";
|
|
19
19
|
import { renderBoxFrame } from "../utils/box-frame.js";
|
|
20
20
|
import { getSettings } from "../settings.js";
|
|
21
|
-
import { StdoutWriter } from "../utils/output-writer.js";
|
|
22
21
|
/** Encode a PNG buffer as a terminal inline image escape sequence. */
|
|
23
22
|
function encodeImageForTerminal(data) {
|
|
24
23
|
const b64 = data.toString("base64");
|
|
@@ -68,12 +67,12 @@ function createRenderState() {
|
|
|
68
67
|
};
|
|
69
68
|
}
|
|
70
69
|
export default function activate(ctx) {
|
|
71
|
-
const { bus,
|
|
72
|
-
const writer = new StdoutWriter();
|
|
70
|
+
const { bus, define, compositor } = ctx;
|
|
73
71
|
const s = createRenderState();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
/** Shorthand — get the current agent surface. */
|
|
73
|
+
function out() { return compositor.surface("agent"); }
|
|
74
|
+
/** Capped width for borders, tool lines, and content — keeps everything aligned. */
|
|
75
|
+
function cappedW() { return Math.min(MAX_CONTENT_WIDTH + 2, out().columns); }
|
|
77
76
|
// Gate: other extensions (e.g. overlay) can advise this to suppress
|
|
78
77
|
// TUI rendering of agent output while they own the display.
|
|
79
78
|
define("tui:should-render-agent", () => true);
|
|
@@ -81,6 +80,9 @@ export default function activate(ctx) {
|
|
|
81
80
|
// ── Advisable rendering handlers ───────────────────────────────
|
|
82
81
|
// Extensions advise these to customize how the TUI renders content.
|
|
83
82
|
// Each handler receives data and returns rendered strings.
|
|
83
|
+
define("tui:response-border", (position, width) => {
|
|
84
|
+
return `${p.dim}${p.accent}${"─".repeat(width)}${p.reset}`;
|
|
85
|
+
});
|
|
84
86
|
define("tui:response-start", () => { });
|
|
85
87
|
define("tui:response-end", (_hadToolCalls) => { });
|
|
86
88
|
define("tui:render-info", (message) => `${p.muted}${message}${p.reset}`);
|
|
@@ -210,7 +212,7 @@ export default function activate(ctx) {
|
|
|
210
212
|
break;
|
|
211
213
|
case "raw":
|
|
212
214
|
flushForRaw();
|
|
213
|
-
|
|
215
|
+
out().write(block.escape);
|
|
214
216
|
break;
|
|
215
217
|
}
|
|
216
218
|
}
|
|
@@ -391,6 +393,8 @@ export default function activate(ctx) {
|
|
|
391
393
|
if (e.key === "\x14")
|
|
392
394
|
toggleThinkingDisplay(); // Ctrl+T
|
|
393
395
|
});
|
|
396
|
+
// Interactive tool UI — stop spinner while tool has control
|
|
397
|
+
bus.on("tool:interactive-start", () => { stopCurrentSpinner(); });
|
|
394
398
|
bus.on("ui:info", (e) => {
|
|
395
399
|
stopCurrentSpinner();
|
|
396
400
|
showInfo(e.message);
|
|
@@ -400,23 +404,25 @@ export default function activate(ctx) {
|
|
|
400
404
|
});
|
|
401
405
|
bus.on("ui:error", (e) => showError(e.message));
|
|
402
406
|
bus.on("ui:suggestion", (e) => {
|
|
403
|
-
|
|
407
|
+
compositor.surface("status").writeLine(`${p.dim}💡 ${e.text}${p.reset}`);
|
|
404
408
|
});
|
|
405
409
|
// ── Rendering functions ─────────────────────────────────────
|
|
406
410
|
function drain() {
|
|
407
411
|
if (!s.renderer)
|
|
408
412
|
return;
|
|
409
413
|
for (const line of s.renderer.drainLines()) {
|
|
410
|
-
|
|
414
|
+
out().write(line + "\n");
|
|
411
415
|
// Track whether we just emitted a blank line (for contentGap dedup).
|
|
412
416
|
// Lines from the renderer are indented (" "), so a blank line is " " or empty.
|
|
413
417
|
lastEmittedLineBlank = line.trimEnd() === "" || line.trimEnd().replace(/\x1b\[[^m]*m/g, "").trim() === "";
|
|
414
418
|
}
|
|
415
419
|
}
|
|
416
420
|
function startAgentResponse() {
|
|
417
|
-
s.renderer = new MarkdownRenderer(
|
|
421
|
+
s.renderer = new MarkdownRenderer(cappedW());
|
|
418
422
|
s.hadToolCalls = false;
|
|
419
|
-
|
|
423
|
+
const border = ctx.call("tui:response-border", "top", cappedW());
|
|
424
|
+
if (border)
|
|
425
|
+
s.renderer.writeLine(border);
|
|
420
426
|
drain();
|
|
421
427
|
ctx.call("tui:response-start");
|
|
422
428
|
}
|
|
@@ -434,7 +440,7 @@ export default function activate(ctx) {
|
|
|
434
440
|
s.renderer.flush();
|
|
435
441
|
drain();
|
|
436
442
|
}
|
|
437
|
-
|
|
443
|
+
out().write(gap);
|
|
438
444
|
}
|
|
439
445
|
}
|
|
440
446
|
s.lastContentKind = kind;
|
|
@@ -453,14 +459,16 @@ export default function activate(ctx) {
|
|
|
453
459
|
if (s.renderer) {
|
|
454
460
|
ctx.call("tui:response-end", s.hadToolCalls);
|
|
455
461
|
s.renderer.flush();
|
|
456
|
-
|
|
462
|
+
const border = ctx.call("tui:response-border", "bottom", cappedW());
|
|
463
|
+
if (border)
|
|
464
|
+
s.renderer.writeLine(border);
|
|
457
465
|
drain();
|
|
458
|
-
|
|
466
|
+
out().write("\n");
|
|
459
467
|
s.renderer = null;
|
|
460
468
|
}
|
|
461
469
|
}
|
|
462
470
|
function showUserQuery(query) {
|
|
463
|
-
const model = backendInfo?.model
|
|
471
|
+
const model = backendInfo?.model;
|
|
464
472
|
const backend = backendInfo?.name;
|
|
465
473
|
let modelLabel;
|
|
466
474
|
if (backend && model) {
|
|
@@ -472,10 +480,13 @@ export default function activate(ctx) {
|
|
|
472
480
|
else if (backend) {
|
|
473
481
|
modelLabel = `${p.bold}${backend}${p.reset}`;
|
|
474
482
|
}
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
483
|
+
const querySurface = compositor.surface("query");
|
|
484
|
+
const framed = ctx.call("tui:render-user-query", query, querySurface.columns, modelLabel);
|
|
485
|
+
if (framed.length > 0) {
|
|
486
|
+
querySurface.write("\n");
|
|
487
|
+
for (const line of framed) {
|
|
488
|
+
querySurface.writeLine(line);
|
|
489
|
+
}
|
|
479
490
|
}
|
|
480
491
|
}
|
|
481
492
|
function writeAgentText(text) {
|
|
@@ -486,7 +497,7 @@ export default function activate(ctx) {
|
|
|
486
497
|
s.isThinking = false;
|
|
487
498
|
if (s.showThinkingText && s.renderer) {
|
|
488
499
|
s.renderer.flush();
|
|
489
|
-
const w = Math.min(80,
|
|
500
|
+
const w = Math.min(80, out().columns);
|
|
490
501
|
s.renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
|
|
491
502
|
drain();
|
|
492
503
|
}
|
|
@@ -525,7 +536,7 @@ export default function activate(ctx) {
|
|
|
525
536
|
drain();
|
|
526
537
|
});
|
|
527
538
|
function writeCodeBlock(language, code) {
|
|
528
|
-
ctx.call("render:code-block", language, code,
|
|
539
|
+
ctx.call("render:code-block", language, code, cappedW());
|
|
529
540
|
}
|
|
530
541
|
function flushForRaw() {
|
|
531
542
|
closeToolLine();
|
|
@@ -539,7 +550,7 @@ export default function activate(ctx) {
|
|
|
539
550
|
flushForRaw();
|
|
540
551
|
const escape = encodeImageForTerminal(data);
|
|
541
552
|
if (escape) {
|
|
542
|
-
|
|
553
|
+
out().write(" " + escape + "\n");
|
|
543
554
|
}
|
|
544
555
|
});
|
|
545
556
|
function writeInlineImage(data) {
|
|
@@ -567,7 +578,7 @@ export default function activate(ctx) {
|
|
|
567
578
|
function renderDiffBody(diff, filePath, width) {
|
|
568
579
|
if (diff.isIdentical)
|
|
569
580
|
return [];
|
|
570
|
-
const boxW = Math.min(120, width);
|
|
581
|
+
const boxW = Math.min(120, width - 2); // -2 for writeLine indent
|
|
571
582
|
const contentW = boxW - 4;
|
|
572
583
|
const diffLines = renderDiff(diff, {
|
|
573
584
|
width: contentW,
|
|
@@ -663,12 +674,12 @@ export default function activate(ctx) {
|
|
|
663
674
|
locations: extra?.locations,
|
|
664
675
|
rawInput: extra?.rawInput,
|
|
665
676
|
displayDetail: extra?.displayDetail,
|
|
666
|
-
},
|
|
677
|
+
}, cappedW());
|
|
667
678
|
if (extra?.groupContinuation && lines.length > 0) {
|
|
668
679
|
// Swap the colored kind icon for a muted tree connector,
|
|
669
680
|
// and strip the tool name prefix — show detail only.
|
|
670
681
|
const detail = extra.displayDetail || extractDetail(extra);
|
|
671
|
-
const maxW = Math.max(1,
|
|
682
|
+
const maxW = Math.max(1, cappedW() - 6);
|
|
672
683
|
const text = detail.length > maxW ? detail.slice(0, maxW - 1) + "…" : detail;
|
|
673
684
|
lines[0] = detail
|
|
674
685
|
? `${p.muted}├${p.reset} ${p.dim}${text}${p.reset}`
|
|
@@ -687,7 +698,7 @@ export default function activate(ctx) {
|
|
|
687
698
|
s.toolLineOpen = false;
|
|
688
699
|
}
|
|
689
700
|
else {
|
|
690
|
-
|
|
701
|
+
out().write(` ${batchPrefix}${lines[lines.length - 1]}`);
|
|
691
702
|
s.toolLineOpen = true;
|
|
692
703
|
}
|
|
693
704
|
}
|
|
@@ -702,7 +713,7 @@ export default function activate(ctx) {
|
|
|
702
713
|
const elapsed = s.toolStartTime ? formatElapsed(Date.now() - s.toolStartTime) : "";
|
|
703
714
|
const mark = ctx.call("tui:render-tool-complete", exitCode, elapsed, resultDisplay?.summary);
|
|
704
715
|
if (s.toolLineOpen && s.commandOutputLineCount === 0) {
|
|
705
|
-
|
|
716
|
+
out().write(` ${mark}\n`);
|
|
706
717
|
s.toolLineOpen = false;
|
|
707
718
|
}
|
|
708
719
|
else {
|
|
@@ -719,7 +730,7 @@ export default function activate(ctx) {
|
|
|
719
730
|
function renderResultBody(body) {
|
|
720
731
|
if (!s.renderer)
|
|
721
732
|
return;
|
|
722
|
-
const lines = ctx.call("render:result-body", body,
|
|
733
|
+
const lines = ctx.call("render:result-body", body, cappedW()) ?? [];
|
|
723
734
|
for (const line of lines) {
|
|
724
735
|
s.renderer.writeLine(line);
|
|
725
736
|
}
|
|
@@ -748,7 +759,7 @@ export default function activate(ctx) {
|
|
|
748
759
|
s.spinner.frame++;
|
|
749
760
|
const elapsed = formatElapsed(Date.now() - s.spinner.startTime);
|
|
750
761
|
const line = ctx.call("tui:render-spinner", s.spinnerLabel, frame, elapsed, s.spinnerOpts.hint);
|
|
751
|
-
|
|
762
|
+
out().write(`\r ${line}\x1b[K`);
|
|
752
763
|
}
|
|
753
764
|
}, 80);
|
|
754
765
|
}
|
|
@@ -758,13 +769,13 @@ export default function activate(ctx) {
|
|
|
758
769
|
s.spinnerInterval = null;
|
|
759
770
|
}
|
|
760
771
|
if (s.spinner) {
|
|
761
|
-
|
|
772
|
+
out().write("\r\x1b[2K");
|
|
762
773
|
s.spinner = null;
|
|
763
774
|
}
|
|
764
775
|
}
|
|
765
776
|
function closeToolLine() {
|
|
766
777
|
if (s.toolLineOpen) {
|
|
767
|
-
|
|
778
|
+
out().write("\n");
|
|
768
779
|
s.toolLineOpen = false;
|
|
769
780
|
}
|
|
770
781
|
}
|
|
@@ -847,7 +858,15 @@ export default function activate(ctx) {
|
|
|
847
858
|
}
|
|
848
859
|
}
|
|
849
860
|
else if (s.commandOutputOverflow > 0 && maxLines > 0) {
|
|
850
|
-
|
|
861
|
+
// Show last line of output so the user sees the tail (often the most useful part)
|
|
862
|
+
const tail = s.commandOverflowLines[s.commandOverflowLines.length - 1];
|
|
863
|
+
const hidden = tail ? s.commandOutputOverflow - 1 : s.commandOutputOverflow;
|
|
864
|
+
if (hidden > 0) {
|
|
865
|
+
s.renderer.writeLine(renderCommandLine(`… ${hidden} more lines`));
|
|
866
|
+
}
|
|
867
|
+
if (tail) {
|
|
868
|
+
s.renderer.writeLine(renderCommandLine(tail));
|
|
869
|
+
}
|
|
851
870
|
}
|
|
852
871
|
s.commandOutputOverflow = 0;
|
|
853
872
|
s.commandOverflowLines = [];
|
|
@@ -864,7 +883,7 @@ export default function activate(ctx) {
|
|
|
864
883
|
if (diff.isIdentical)
|
|
865
884
|
return;
|
|
866
885
|
contentGap("diff");
|
|
867
|
-
const lines = ctx.call("render:result-body", { kind: "diff", diff, filePath },
|
|
886
|
+
const lines = ctx.call("render:result-body", { kind: "diff", diff, filePath }, cappedW()) ?? [];
|
|
868
887
|
if (!s.renderer)
|
|
869
888
|
startAgentResponse();
|
|
870
889
|
for (const line of lines) {
|
|
@@ -883,7 +902,7 @@ export default function activate(ctx) {
|
|
|
883
902
|
}
|
|
884
903
|
if (!entry.expandedLines) {
|
|
885
904
|
const { filePath, diff } = entry;
|
|
886
|
-
const boxW = Math.min(
|
|
905
|
+
const boxW = Math.min(cappedW() - 2, out().columns - 2); // -2 for writeLine indent
|
|
887
906
|
const contentW = boxW - 4;
|
|
888
907
|
const diffLines = renderDiff(diff, {
|
|
889
908
|
width: contentW,
|
|
@@ -900,16 +919,16 @@ export default function activate(ctx) {
|
|
|
900
919
|
footer: [` ${p.dim}ctrl+o to collapse${p.reset}`],
|
|
901
920
|
});
|
|
902
921
|
}
|
|
903
|
-
|
|
922
|
+
out().write("\n");
|
|
904
923
|
for (const line of entry.expandedLines) {
|
|
905
|
-
|
|
924
|
+
out().write(line + "\n");
|
|
906
925
|
}
|
|
907
926
|
}
|
|
908
927
|
function showFileDiffCached(entry) {
|
|
909
|
-
const lines = ctx.call("render:result-body", { kind: "diff", diff: entry.diff, filePath: entry.filePath },
|
|
910
|
-
|
|
928
|
+
const lines = ctx.call("render:result-body", { kind: "diff", diff: entry.diff, filePath: entry.filePath }, cappedW()) ?? [];
|
|
929
|
+
out().write("\n");
|
|
911
930
|
for (const line of lines) {
|
|
912
|
-
|
|
931
|
+
out().write(line + "\n");
|
|
913
932
|
}
|
|
914
933
|
}
|
|
915
934
|
function toggleThinkingDisplay() {
|
|
@@ -941,7 +960,7 @@ export default function activate(ctx) {
|
|
|
941
960
|
else {
|
|
942
961
|
if (s.renderer) {
|
|
943
962
|
s.renderer.flush();
|
|
944
|
-
const w = Math.min(80,
|
|
963
|
+
const w = Math.min(80, out().columns);
|
|
945
964
|
s.renderer.writeLine(`${p.dim}${"─".repeat(w)}${p.reset}`);
|
|
946
965
|
drain();
|
|
947
966
|
}
|
|
@@ -949,9 +968,10 @@ export default function activate(ctx) {
|
|
|
949
968
|
}
|
|
950
969
|
}
|
|
951
970
|
function showError(message) {
|
|
952
|
-
|
|
971
|
+
const s = compositor.surface("status");
|
|
972
|
+
s.write("\n" + ctx.call("tui:render-error", message) + "\n");
|
|
953
973
|
}
|
|
954
974
|
function showInfo(message) {
|
|
955
|
-
|
|
975
|
+
compositor.surface("status").writeLine(ctx.call("tui:render-info", message));
|
|
956
976
|
}
|
|
957
977
|
}
|