agent-sh 0.9.0 → 0.10.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 +14 -21
- package/dist/agent/agent-loop.d.ts +43 -3
- package/dist/agent/agent-loop.js +811 -128
- package/dist/agent/conversation-state.d.ts +72 -21
- package/dist/agent/conversation-state.js +357 -150
- package/dist/agent/history-file.d.ts +13 -4
- package/dist/agent/history-file.js +110 -36
- package/dist/agent/nuclear-form.d.ts +28 -3
- package/dist/agent/nuclear-form.js +84 -3
- package/dist/agent/skills.d.ts +2 -4
- package/dist/agent/skills.js +10 -4
- package/dist/agent/subagent.d.ts +23 -0
- package/dist/agent/subagent.js +53 -11
- package/dist/agent/system-prompt.d.ts +34 -1
- package/dist/agent/system-prompt.js +96 -47
- package/dist/agent/token-budget.d.ts +5 -4
- package/dist/agent/token-budget.js +14 -19
- package/dist/agent/tool-protocol.d.ts +23 -1
- package/dist/agent/tool-protocol.js +169 -4
- package/dist/agent/tools/bash.js +3 -3
- package/dist/agent/tools/edit-file.js +9 -6
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.js +27 -3
- package/dist/agent/tools/ls.js +5 -6
- package/dist/agent/types.d.ts +1 -1
- package/dist/context-manager.d.ts +17 -0
- package/dist/context-manager.js +37 -4
- package/dist/core.js +27 -6
- package/dist/event-bus.d.ts +59 -2
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.js +50 -13
- package/dist/extensions/agent-backend.d.ts +8 -7
- package/dist/extensions/agent-backend.js +69 -48
- package/dist/extensions/index.js +0 -1
- package/dist/extensions/slash-commands.js +14 -9
- package/dist/extensions/tui-renderer.js +62 -78
- package/dist/index.js +25 -6
- package/dist/settings.d.ts +36 -5
- package/dist/settings.js +53 -9
- package/dist/shell/input-handler.d.ts +2 -1
- package/dist/shell/input-handler.js +82 -73
- package/dist/shell/shell.js +19 -2
- package/dist/types.d.ts +12 -0
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +1 -1
- package/dist/utils/compositor.d.ts +5 -0
- package/dist/utils/compositor.js +31 -3
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +221 -143
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/handler-registry.d.ts +5 -0
- package/dist/utils/handler-registry.js +6 -0
- package/dist/utils/line-editor.d.ts +11 -1
- package/dist/utils/line-editor.js +44 -5
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
- package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
- package/examples/extensions/claude-code-bridge/index.ts +198 -51
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +39 -25
- package/examples/extensions/overlay-agent.ts +3 -3
- package/examples/extensions/peer-mesh.ts +115 -0
- package/examples/extensions/pi-bridge/index.ts +2 -2
- package/examples/extensions/questionnaire.ts +16 -5
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +163 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +8 -0
- package/package.json +36 -2
- package/dist/agent/tools/display.d.ts +0 -13
- package/dist/agent/tools/display.js +0 -70
- package/dist/agent/tools/user-shell.d.ts +0 -13
- package/dist/agent/tools/user-shell.js +0 -87
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -134
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import { AgentLoop } from "../agent/agent-loop.js";
|
|
2
2
|
import { LlmClient } from "../utils/llm-client.js";
|
|
3
3
|
import { resolveProvider, getProviderNames, getSettings } from "../settings.js";
|
|
4
|
+
/** Read the user's persisted defaultModel for a provider, if any. */
|
|
5
|
+
function persistedModelFor(providerName) {
|
|
6
|
+
if (!providerName)
|
|
7
|
+
return undefined;
|
|
8
|
+
return getSettings().providers?.[providerName]?.defaultModel;
|
|
9
|
+
}
|
|
4
10
|
export default function agentBackend(ctx) {
|
|
5
11
|
const { bus } = ctx;
|
|
6
|
-
// ── Resolve providers ──────────────────────────────────────
|
|
7
12
|
const config = ctx.call("config:get-shell-config") ?? {};
|
|
8
|
-
|
|
9
|
-
let activeProvider = null;
|
|
13
|
+
// Seed from settings.json; runtime provider:register events add more.
|
|
10
14
|
const providerRegistry = new Map();
|
|
11
15
|
for (const name of getProviderNames()) {
|
|
12
16
|
const p = resolveProvider(name);
|
|
13
17
|
if (p)
|
|
14
18
|
providerRegistry.set(name, p);
|
|
15
19
|
}
|
|
16
|
-
const providerName = config.provider ?? settings.defaultProvider;
|
|
17
|
-
if (providerName) {
|
|
18
|
-
activeProvider = providerRegistry.get(providerName) ?? null;
|
|
19
|
-
}
|
|
20
|
-
// ── Build modes ────────────────────────────────────────────
|
|
21
20
|
const buildModes = () => {
|
|
22
21
|
const allModes = [];
|
|
23
22
|
for (const [id, p] of providerRegistry) {
|
|
@@ -37,48 +36,38 @@ export default function agentBackend(ctx) {
|
|
|
37
36
|
}
|
|
38
37
|
return allModes;
|
|
39
38
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
39
|
+
// Placeholder client — reconfigured at core:extensions-loaded. Any
|
|
40
|
+
// stream() call before then fails from the OpenAI SDK; start() won't
|
|
41
|
+
// wire the loop until we've resolved, so users never hit that path.
|
|
42
|
+
const llmClient = new LlmClient({ apiKey: "not-configured", model: "not-configured" });
|
|
61
43
|
ctx.define("llm:get-client", () => llmClient);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
44
|
+
let modes = [];
|
|
45
|
+
let initialModeIndex = 0;
|
|
46
|
+
let resolved = false;
|
|
47
|
+
bus.onPipe("config:get-initial-modes", () => ({ modes, initialModeIndex }));
|
|
48
|
+
// AgentLoop must be constructed *before* user extensions activate,
|
|
49
|
+
// because its ctor defines handlers (history:append, etc.) that
|
|
50
|
+
// extensions like superash call synchronously during their own
|
|
51
|
+
// activate. Advise-before-define works for advisers, but plain calls
|
|
52
|
+
// would hit a no-op stub.
|
|
68
53
|
const agentLoop = new AgentLoop({
|
|
69
54
|
bus,
|
|
70
55
|
contextManager: ctx.contextManager,
|
|
71
56
|
llmClient,
|
|
72
|
-
handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call },
|
|
57
|
+
handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
|
|
73
58
|
modes,
|
|
74
59
|
initialModeIndex,
|
|
75
60
|
compositor: ctx.compositor,
|
|
61
|
+
instanceId: ctx.instanceId,
|
|
76
62
|
});
|
|
77
|
-
// Register as backend
|
|
78
63
|
bus.emit("agent:register-backend", {
|
|
79
64
|
name: "ash",
|
|
80
65
|
kill: () => agentLoop.kill(),
|
|
81
66
|
start: async () => {
|
|
67
|
+
if (!resolved) {
|
|
68
|
+
bus.emit("ui:error", { message: "Agent backend not started — no LLM provider available. See earlier messages." });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
82
71
|
agentLoop.wire();
|
|
83
72
|
bus.emit("agent:info", {
|
|
84
73
|
name: "ash",
|
|
@@ -89,7 +78,34 @@ export default function agentBackend(ctx) {
|
|
|
89
78
|
});
|
|
90
79
|
},
|
|
91
80
|
});
|
|
92
|
-
|
|
81
|
+
bus.on("core:extensions-loaded", () => {
|
|
82
|
+
const settings = getSettings();
|
|
83
|
+
const providerName = config.provider ?? settings.defaultProvider;
|
|
84
|
+
const activeProvider = providerName ? providerRegistry.get(providerName) ?? null : null;
|
|
85
|
+
// User's persisted defaultModel wins over the provider's declared
|
|
86
|
+
// default. Dynamic providers (openrouter) re-register with their
|
|
87
|
+
// hardcoded DEFAULT_MODELS[0] each startup, which would otherwise
|
|
88
|
+
// clobber the user's /model selection.
|
|
89
|
+
const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
|
|
90
|
+
const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
|
|
91
|
+
const effectiveModel = config.model ?? persistedModelFor(providerName) ?? activeProvider?.defaultModel;
|
|
92
|
+
if (!effectiveApiKey) {
|
|
93
|
+
bus.emit("ui:error", { message: "No LLM provider configured. Set --api-key, configure a provider in ~/.agent-sh/settings.json, or load a provider extension (e.g. openrouter) that sets OPENROUTER_API_KEY." });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!effectiveModel) {
|
|
97
|
+
bus.emit("ui:error", { message: "No model specified. Use --model or configure a provider with defaultModel in ~/.agent-sh/settings.json" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
modes = buildModes();
|
|
101
|
+
if (modes.length === 0)
|
|
102
|
+
modes = [{ model: effectiveModel }];
|
|
103
|
+
initialModeIndex = Math.max(0, modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id)));
|
|
104
|
+
llmClient.reconfigure({ apiKey: effectiveApiKey, baseURL: effectiveBaseURL, model: effectiveModel });
|
|
105
|
+
bus.emit("config:set-modes", { modes, activeIndex: initialModeIndex });
|
|
106
|
+
resolved = true;
|
|
107
|
+
// start() emits agent:info after wiring.
|
|
108
|
+
});
|
|
93
109
|
bus.on("provider:register", (p) => {
|
|
94
110
|
const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
|
|
95
111
|
const modelIds = [];
|
|
@@ -124,16 +140,26 @@ export default function agentBackend(ctx) {
|
|
|
124
140
|
};
|
|
125
141
|
});
|
|
126
142
|
bus.emit("config:add-modes", { modes: addModes });
|
|
143
|
+
// Late-registration reconcile: if this completes the user's persisted
|
|
144
|
+
// default (openrouter's async fetch delivers the full catalog after
|
|
145
|
+
// we've already fallen back to mode 0), quietly switch to it.
|
|
146
|
+
if (!resolved)
|
|
147
|
+
return;
|
|
148
|
+
const pendingProvider = getSettings().defaultProvider;
|
|
149
|
+
if (pendingProvider !== p.id)
|
|
150
|
+
return;
|
|
151
|
+
const pendingModel = persistedModelFor(pendingProvider);
|
|
152
|
+
if (pendingModel && modelIds.includes(pendingModel) && llmClient.model !== pendingModel) {
|
|
153
|
+
bus.emit("config:switch-model", { model: pendingModel });
|
|
154
|
+
}
|
|
127
155
|
});
|
|
128
|
-
// ── Runtime provider switching ─────────────────────────────
|
|
129
156
|
bus.on("config:switch-provider", ({ provider: name }) => {
|
|
130
157
|
const p = providerRegistry.get(name);
|
|
131
158
|
if (!p) {
|
|
132
159
|
bus.emit("ui:error", { message: `Unknown provider: ${name}` });
|
|
133
160
|
return;
|
|
134
161
|
}
|
|
135
|
-
|
|
136
|
-
if (!newApiKey) {
|
|
162
|
+
if (!p.apiKey) {
|
|
137
163
|
bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
|
|
138
164
|
return;
|
|
139
165
|
}
|
|
@@ -142,24 +168,19 @@ export default function agentBackend(ctx) {
|
|
|
142
168
|
bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
|
|
143
169
|
return;
|
|
144
170
|
}
|
|
145
|
-
llmClient.reconfigure({
|
|
146
|
-
apiKey: newApiKey,
|
|
147
|
-
baseURL: p.baseURL,
|
|
148
|
-
model: switchModel,
|
|
149
|
-
});
|
|
171
|
+
llmClient.reconfigure({ apiKey: p.apiKey, baseURL: p.baseURL, model: switchModel });
|
|
150
172
|
const newModes = p.models.map((m) => {
|
|
151
173
|
const mc = p.modelCapabilities?.get(m);
|
|
152
174
|
return {
|
|
153
175
|
model: m,
|
|
154
176
|
provider: name,
|
|
155
|
-
providerConfig: { apiKey:
|
|
177
|
+
providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
|
|
156
178
|
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
157
179
|
reasoning: mc?.reasoning,
|
|
158
180
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
159
181
|
};
|
|
160
182
|
});
|
|
161
183
|
bus.emit("config:set-modes", { modes: newModes });
|
|
162
|
-
activeProvider = p;
|
|
163
184
|
bus.emit("agent:info", { name: "ash", version: "0.4", model: switchModel, provider: name, contextWindow: p.contextWindow });
|
|
164
185
|
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
165
186
|
bus.emit("config:changed", {});
|
package/dist/extensions/index.js
CHANGED
|
@@ -5,7 +5,6 @@ export const BUILTIN_EXTENSIONS = [
|
|
|
5
5
|
{ name: "file-autocomplete", load: () => import("./file-autocomplete.js").then(m => m.default) },
|
|
6
6
|
{ name: "shell-recall", load: () => import("./shell-recall.js").then(m => m.default) },
|
|
7
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
8
|
];
|
|
10
9
|
/**
|
|
11
10
|
* Load built-in extensions sequentially, skipping any in the disabled list.
|
|
@@ -79,7 +79,7 @@ export default function activate(ctx) {
|
|
|
79
79
|
});
|
|
80
80
|
register({
|
|
81
81
|
name: "/compact",
|
|
82
|
-
description: "Compact conversation
|
|
82
|
+
description: "Compact conversation via the active compaction strategy",
|
|
83
83
|
handler: () => {
|
|
84
84
|
bus.emit("agent:compact-request", {});
|
|
85
85
|
},
|
|
@@ -90,19 +90,15 @@ export default function activate(ctx) {
|
|
|
90
90
|
handler: () => {
|
|
91
91
|
const stats = bus.emitPipe("context:get-stats", {
|
|
92
92
|
activeTokens: 0,
|
|
93
|
-
|
|
94
|
-
recallArchiveSize: 0,
|
|
93
|
+
totalTokens: 0,
|
|
95
94
|
budgetTokens: 0,
|
|
96
95
|
});
|
|
97
96
|
const pct = stats.budgetTokens > 0
|
|
98
97
|
? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
|
|
99
98
|
: 0;
|
|
100
|
-
|
|
101
|
-
`Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
|
|
102
|
-
|
|
103
|
-
`Recall archive: ${stats.recallArchiveSize} entries`,
|
|
104
|
-
];
|
|
105
|
-
bus.emit("ui:info", { message: lines.join("\n") });
|
|
99
|
+
bus.emit("ui:info", {
|
|
100
|
+
message: `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
|
|
101
|
+
});
|
|
106
102
|
},
|
|
107
103
|
});
|
|
108
104
|
register({
|
|
@@ -118,10 +114,19 @@ export default function activate(ctx) {
|
|
|
118
114
|
}
|
|
119
115
|
},
|
|
120
116
|
});
|
|
117
|
+
// Handler form so extensions can trigger reload programmatically
|
|
118
|
+
// (e.g. an ash-callable reload_extensions tool in superash).
|
|
119
|
+
ctx.define("extensions:reload", async () => {
|
|
120
|
+
return await reloadExtensions(ctx);
|
|
121
|
+
});
|
|
121
122
|
// ── Extension registration ────────────────────────────────────
|
|
122
123
|
bus.on("command:register", (cmd) => {
|
|
123
124
|
register(cmd);
|
|
124
125
|
});
|
|
126
|
+
bus.on("command:unregister", ({ name }) => {
|
|
127
|
+
const key = name.startsWith("/") ? name : `/${name}`;
|
|
128
|
+
commands.delete(key);
|
|
129
|
+
});
|
|
125
130
|
// ── Skill commands (/skill:<name>) ────────────────────────────
|
|
126
131
|
const getSkills = () => {
|
|
127
132
|
const cwd = contextManager?.getCwd() ?? process.cwd();
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { highlight } from "cli-highlight";
|
|
14
14
|
import { MarkdownRenderer, wrapLine, MAX_CONTENT_WIDTH } from "../utils/markdown.js";
|
|
15
|
+
import { DEFAULT_CONTEXT_WINDOW } from "../agent/token-budget.js";
|
|
15
16
|
import { createFencedBlockTransform } from "../utils/stream-transform.js";
|
|
16
17
|
import { palette as p } from "../utils/palette.js";
|
|
17
18
|
import { renderToolCall, createSpinner, formatElapsed, SPINNER_FRAMES, } from "../utils/tool-display.js";
|
|
@@ -63,12 +64,14 @@ function createRenderState() {
|
|
|
63
64
|
isThinking: false,
|
|
64
65
|
showThinkingText: false,
|
|
65
66
|
thinkingPending: false,
|
|
66
|
-
lastTruncatedDiff: null,
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
export default function activate(ctx) {
|
|
70
70
|
const { bus, define, compositor } = ctx;
|
|
71
71
|
const s = createRenderState();
|
|
72
|
+
/** Track the shell's cwd so path shortening is relative to where the user actually is. */
|
|
73
|
+
let shellCwd = process.cwd();
|
|
74
|
+
bus.on("shell:cwd-change", (e) => { shellCwd = e.cwd; });
|
|
72
75
|
/** Shorthand — get the current agent surface. */
|
|
73
76
|
function out() { return compositor.surface("agent"); }
|
|
74
77
|
/** Capped width for borders, tool lines, and content — keeps everything aligned. */
|
|
@@ -226,7 +229,7 @@ export default function activate(ctx) {
|
|
|
226
229
|
s.isThinking = false;
|
|
227
230
|
if (pendingUsage && s.renderer) {
|
|
228
231
|
const { prompt_tokens, completion_tokens } = pendingUsage;
|
|
229
|
-
const maxTokens = backendInfo?.contextWindow ??
|
|
232
|
+
const maxTokens = backendInfo?.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
230
233
|
s.renderer.writeLine("");
|
|
231
234
|
s.renderer.writeLine(ctx.call("tui:render-usage", prompt_tokens, completion_tokens, maxTokens));
|
|
232
235
|
drain();
|
|
@@ -379,17 +382,37 @@ export default function activate(ctx) {
|
|
|
379
382
|
s.renderer.flush();
|
|
380
383
|
drain();
|
|
381
384
|
}
|
|
385
|
+
// Diff rendering is handled in the async pipe below so it can yield
|
|
386
|
+
// to the event loop between hunks (keeping the spinner responsive).
|
|
387
|
+
});
|
|
388
|
+
// Async pipe: render diffs via the tui:render-diff handler (extensions can
|
|
389
|
+
// advise to customize). Runs after the sync `on` handler above (which
|
|
390
|
+
// flushes state) and before shell.ts's pipe (which pauses stdout).
|
|
391
|
+
bus.onPipeAsync("permission:request", async (e) => {
|
|
392
|
+
if (!shouldRender())
|
|
393
|
+
return e;
|
|
382
394
|
if (e.kind === "file-write" && e.metadata?.diff) {
|
|
383
395
|
showCollapsedThinking();
|
|
384
|
-
|
|
396
|
+
const lines = ctx.call("tui:render-diff", e.title, e.metadata.diff, cappedW());
|
|
397
|
+
if (lines.length > 0) {
|
|
398
|
+
if (!s.renderer)
|
|
399
|
+
startAgentResponse();
|
|
400
|
+
contentGap("diff");
|
|
401
|
+
for (const line of lines)
|
|
402
|
+
s.renderer.writeLine(line);
|
|
403
|
+
drain();
|
|
404
|
+
}
|
|
405
|
+
// The diff box IS the visual representation of the upcoming tool call.
|
|
406
|
+
// Mark lastContentKind as "tool" so the tool call line that follows
|
|
407
|
+
// doesn't inject an extra gap between the diff box and the checkmark.
|
|
408
|
+
s.lastContentKind = "tool";
|
|
385
409
|
}
|
|
386
410
|
// Don't endAgentResponse() here — permission requests that aren't
|
|
387
411
|
// file-write diffs are handled inline (auto-approved or by extensions).
|
|
388
412
|
// Closing the response prematurely causes double separator borders.
|
|
413
|
+
return e;
|
|
389
414
|
});
|
|
390
415
|
bus.on("input:keypress", (e) => {
|
|
391
|
-
if (e.key === "\x0f")
|
|
392
|
-
expandLastDiff(); // Ctrl+O
|
|
393
416
|
if (e.key === "\x14")
|
|
394
417
|
toggleThinkingDisplay(); // Ctrl+T
|
|
395
418
|
});
|
|
@@ -518,9 +541,23 @@ export default function activate(ctx) {
|
|
|
518
541
|
}
|
|
519
542
|
let highlighted;
|
|
520
543
|
try {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
544
|
+
// highlight.js warns to console.error for unsupported languages (elisp, org, etc).
|
|
545
|
+
// Suppress so it doesn't leak into the terminal.
|
|
546
|
+
const origError = console.error;
|
|
547
|
+
console.error = (...args) => {
|
|
548
|
+
const msg = args.join(" ");
|
|
549
|
+
if (msg.includes("Could not find the language"))
|
|
550
|
+
return;
|
|
551
|
+
origError.apply(console, args);
|
|
552
|
+
};
|
|
553
|
+
try {
|
|
554
|
+
highlighted = language
|
|
555
|
+
? highlight(code, { language })
|
|
556
|
+
: highlight(code); // auto-detect
|
|
557
|
+
}
|
|
558
|
+
finally {
|
|
559
|
+
console.error = origError;
|
|
560
|
+
}
|
|
524
561
|
}
|
|
525
562
|
catch {
|
|
526
563
|
highlighted = code;
|
|
@@ -574,6 +611,17 @@ export default function activate(ctx) {
|
|
|
574
611
|
}
|
|
575
612
|
return [];
|
|
576
613
|
});
|
|
614
|
+
/**
|
|
615
|
+
* Default renderer for standalone diffs (e.g. permission prompts).
|
|
616
|
+
* Extensions can advise this to customize diff rendering:
|
|
617
|
+
*
|
|
618
|
+
* ctx.advise("tui:render-diff", (next, filePath, diff, width) => {
|
|
619
|
+
* return myCustomDiffBox(filePath, diff, width);
|
|
620
|
+
* });
|
|
621
|
+
*/
|
|
622
|
+
define("tui:render-diff", (filePath, diff, width) => {
|
|
623
|
+
return renderDiffBody(diff, filePath, width);
|
|
624
|
+
});
|
|
577
625
|
/** Render a diff as framed box lines (pure — no TUI state side effects). */
|
|
578
626
|
function renderDiffBody(diff, filePath, width) {
|
|
579
627
|
if (diff.isIdentical)
|
|
@@ -586,18 +634,8 @@ export default function activate(ctx) {
|
|
|
586
634
|
maxLines: getSettings().diffMaxLines,
|
|
587
635
|
trueColor: true,
|
|
588
636
|
});
|
|
589
|
-
const lastLine = diffLines[diffLines.length - 1] ?? "";
|
|
590
|
-
const isTruncated = lastLine.includes("… ");
|
|
591
|
-
if (isTruncated) {
|
|
592
|
-
s.lastTruncatedDiff = { filePath, diff, expanded: false };
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
s.lastTruncatedDiff = null;
|
|
596
|
-
}
|
|
597
637
|
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
598
|
-
const footer =
|
|
599
|
-
? [` ${p.dim}ctrl+o to expand${p.reset}`]
|
|
600
|
-
: undefined;
|
|
638
|
+
const footer = undefined;
|
|
601
639
|
return renderBoxFrame(body, {
|
|
602
640
|
width: boxW,
|
|
603
641
|
style: "rounded",
|
|
@@ -625,11 +663,10 @@ export default function activate(ctx) {
|
|
|
625
663
|
function extractDetail(extra) {
|
|
626
664
|
if (extra.locations && extra.locations.length > 0) {
|
|
627
665
|
const loc = extra.locations[0];
|
|
628
|
-
const cwd = process.cwd();
|
|
629
666
|
const home = process.env.HOME;
|
|
630
667
|
let fp = loc.path;
|
|
631
|
-
if (fp.startsWith(
|
|
632
|
-
fp = fp.slice(
|
|
668
|
+
if (fp.startsWith(shellCwd + "/"))
|
|
669
|
+
fp = fp.slice(shellCwd.length + 1);
|
|
633
670
|
else if (home && fp.startsWith(home + "/"))
|
|
634
671
|
fp = "~/" + fp.slice(home.length + 1);
|
|
635
672
|
return loc.line ? `${fp}:${loc.line}` : fp;
|
|
@@ -642,11 +679,10 @@ export default function activate(ctx) {
|
|
|
642
679
|
if (typeof raw.pattern === "string")
|
|
643
680
|
return raw.pattern;
|
|
644
681
|
if (typeof raw.path === "string") {
|
|
645
|
-
const cwd = process.cwd();
|
|
646
682
|
const home = process.env.HOME;
|
|
647
683
|
let fp = raw.path;
|
|
648
|
-
if (fp.startsWith(
|
|
649
|
-
fp = fp.slice(
|
|
684
|
+
if (fp.startsWith(shellCwd + "/"))
|
|
685
|
+
fp = fp.slice(shellCwd.length + 1);
|
|
650
686
|
else if (home && fp.startsWith(home + "/"))
|
|
651
687
|
fp = "~/" + fp.slice(home.length + 1);
|
|
652
688
|
return fp;
|
|
@@ -674,7 +710,7 @@ export default function activate(ctx) {
|
|
|
674
710
|
locations: extra?.locations,
|
|
675
711
|
rawInput: extra?.rawInput,
|
|
676
712
|
displayDetail: extra?.displayDetail,
|
|
677
|
-
}, cappedW());
|
|
713
|
+
}, cappedW(), shellCwd);
|
|
678
714
|
if (extra?.groupContinuation && lines.length > 0) {
|
|
679
715
|
// Swap the colored kind icon for a muted tree connector,
|
|
680
716
|
// and strip the tool name prefix — show detail only.
|
|
@@ -879,58 +915,6 @@ export default function activate(ctx) {
|
|
|
879
915
|
: `${p.success}+${diff.added}${p.reset} ${p.error}-${diff.removed}${p.reset}`;
|
|
880
916
|
return `${p.dim}${filePath}${p.reset} ${stats}`;
|
|
881
917
|
}
|
|
882
|
-
function showFileDiff(filePath, diff) {
|
|
883
|
-
if (diff.isIdentical)
|
|
884
|
-
return;
|
|
885
|
-
contentGap("diff");
|
|
886
|
-
const lines = ctx.call("render:result-body", { kind: "diff", diff, filePath }, cappedW()) ?? [];
|
|
887
|
-
if (!s.renderer)
|
|
888
|
-
startAgentResponse();
|
|
889
|
-
for (const line of lines) {
|
|
890
|
-
s.renderer.writeLine(line);
|
|
891
|
-
}
|
|
892
|
-
drain();
|
|
893
|
-
}
|
|
894
|
-
function expandLastDiff() {
|
|
895
|
-
if (!s.lastTruncatedDiff)
|
|
896
|
-
return;
|
|
897
|
-
const entry = s.lastTruncatedDiff;
|
|
898
|
-
entry.expanded = !entry.expanded;
|
|
899
|
-
if (!entry.expanded) {
|
|
900
|
-
showFileDiffCached(entry);
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
if (!entry.expandedLines) {
|
|
904
|
-
const { filePath, diff } = entry;
|
|
905
|
-
const boxW = Math.min(cappedW() - 2, out().columns - 2); // -2 for writeLine indent
|
|
906
|
-
const contentW = boxW - 4;
|
|
907
|
-
const diffLines = renderDiff(diff, {
|
|
908
|
-
width: contentW,
|
|
909
|
-
filePath,
|
|
910
|
-
maxLines: 500,
|
|
911
|
-
trueColor: true,
|
|
912
|
-
});
|
|
913
|
-
const body = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
914
|
-
entry.expandedLines = renderBoxFrame(body, {
|
|
915
|
-
width: boxW,
|
|
916
|
-
style: "rounded",
|
|
917
|
-
borderColor: p.dim,
|
|
918
|
-
title: diffTitle(filePath, diff),
|
|
919
|
-
footer: [` ${p.dim}ctrl+o to collapse${p.reset}`],
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
out().write("\n");
|
|
923
|
-
for (const line of entry.expandedLines) {
|
|
924
|
-
out().write(line + "\n");
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
function showFileDiffCached(entry) {
|
|
928
|
-
const lines = ctx.call("render:result-body", { kind: "diff", diff: entry.diff, filePath: entry.filePath }, cappedW()) ?? [];
|
|
929
|
-
out().write("\n");
|
|
930
|
-
for (const line of lines) {
|
|
931
|
-
out().write(line + "\n");
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
918
|
function toggleThinkingDisplay() {
|
|
935
919
|
s.showThinkingText = !s.showThinkingText;
|
|
936
920
|
if (s.spinner) {
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,13 @@ import { discoverSkills } from "./agent/skills.js";
|
|
|
15
15
|
*/
|
|
16
16
|
async function captureShellEnvAsync(shell) {
|
|
17
17
|
return new Promise((resolve) => {
|
|
18
|
+
let settled = false;
|
|
19
|
+
const done = (result) => {
|
|
20
|
+
if (settled)
|
|
21
|
+
return;
|
|
22
|
+
settled = true;
|
|
23
|
+
resolve(result);
|
|
24
|
+
};
|
|
18
25
|
try {
|
|
19
26
|
const shellName = path.basename(shell);
|
|
20
27
|
const isZsh = shellName.includes("zsh");
|
|
@@ -30,8 +37,9 @@ async function captureShellEnvAsync(shell) {
|
|
|
30
37
|
output += data.toString("utf-8");
|
|
31
38
|
});
|
|
32
39
|
child.on("close", (code) => {
|
|
40
|
+
clearTimeout(timer);
|
|
33
41
|
if (code !== 0 || !output) {
|
|
34
|
-
|
|
42
|
+
done({});
|
|
35
43
|
return;
|
|
36
44
|
}
|
|
37
45
|
const env = {};
|
|
@@ -40,18 +48,19 @@ async function captureShellEnvAsync(shell) {
|
|
|
40
48
|
if (eq > 0)
|
|
41
49
|
env[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
42
50
|
}
|
|
43
|
-
|
|
51
|
+
done(env);
|
|
44
52
|
});
|
|
45
53
|
child.on("error", () => {
|
|
46
|
-
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
done({});
|
|
47
56
|
});
|
|
48
|
-
setTimeout(() => {
|
|
57
|
+
const timer = setTimeout(() => {
|
|
49
58
|
child.kill("SIGTERM");
|
|
50
|
-
|
|
59
|
+
done({});
|
|
51
60
|
}, 5000);
|
|
52
61
|
}
|
|
53
62
|
catch {
|
|
54
|
-
|
|
63
|
+
done({});
|
|
55
64
|
}
|
|
56
65
|
});
|
|
57
66
|
}
|
|
@@ -245,6 +254,9 @@ async function main() {
|
|
|
245
254
|
if (process.env.DEBUG) {
|
|
246
255
|
console.error('[agent-sh] Extensions loaded');
|
|
247
256
|
}
|
|
257
|
+
// Tell deferred-init listeners (agent-backend) that the provider
|
|
258
|
+
// registry is now complete.
|
|
259
|
+
core.bus.emit("core:extensions-loaded", {});
|
|
248
260
|
// ── Discover skills ───────────────────────────────────────────
|
|
249
261
|
const skills = discoverSkills(process.cwd());
|
|
250
262
|
// ── Activate agent backend ────────────────────────────────────
|
|
@@ -281,6 +293,13 @@ async function main() {
|
|
|
281
293
|
sections += `\n ${p.dim}${s.name}${p.reset}`;
|
|
282
294
|
}
|
|
283
295
|
}
|
|
296
|
+
const extSections = bus.emitPipe("banner:collect", { sections: [] }).sections;
|
|
297
|
+
for (const sec of extSections) {
|
|
298
|
+
sections += `\n\n ${p.muted}${sec.label}:${p.reset}`;
|
|
299
|
+
for (const item of sec.items) {
|
|
300
|
+
sections += `\n ${p.dim}${item}${p.reset}`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
284
303
|
const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}>/help${p.muted} for commands${p.reset}`;
|
|
285
304
|
const borderLine = `${p.muted}${"─".repeat(bannerW)}${p.reset}`;
|
|
286
305
|
process.stdout.write("\n" + borderLine + "\n" +
|
package/dist/settings.d.ts
CHANGED
|
@@ -50,26 +50,46 @@ export interface Settings {
|
|
|
50
50
|
historyMaxBytes?: number;
|
|
51
51
|
/** Number of prior history entries to load on startup (default: 50). */
|
|
52
52
|
historyStartupEntries?: number;
|
|
53
|
-
/** Max nuclear entries kept in-context before flushing to history file (default: 200). */
|
|
54
|
-
nuclearMaxEntries?: number;
|
|
55
53
|
/** Auto-compact threshold as fraction of conversation budget (0-1, default 0.5). */
|
|
56
54
|
autoCompactThreshold?: number;
|
|
57
55
|
/** Max command output lines shown inline in TUI. */
|
|
58
56
|
maxCommandOutputLines?: number;
|
|
59
57
|
/** Max read tool output lines shown inline in TUI (0 = hide). */
|
|
60
58
|
readOutputMaxLines?: number;
|
|
61
|
-
/** Max diff lines
|
|
59
|
+
/** Max diff lines rendered in the TUI (Infinity = no limit). */
|
|
62
60
|
diffMaxLines?: number;
|
|
63
|
-
/** Tool protocol:
|
|
64
|
-
|
|
61
|
+
/** Tool protocol:
|
|
62
|
+
* "api" — all tools sent with full schema.
|
|
63
|
+
* "deferred" — extensions dispatched through `use_extension(name, args)` meta-tool.
|
|
64
|
+
* "deferred-lookup" — extensions loaded on demand via `load_tool(names[])`; once loaded, callable as first-class tools.
|
|
65
|
+
* "inline" — tools described as text.
|
|
66
|
+
*/
|
|
67
|
+
toolMode?: "api" | "deferred" | "deferred-lookup" | "inline";
|
|
65
68
|
/** Additional directories to scan for skills (supports ~ expansion). */
|
|
66
69
|
skillPaths?: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Enable the "diagnose" tool — lets the agent evaluate JavaScript
|
|
72
|
+
* expressions against its own runtime state. Powerful for introspection
|
|
73
|
+
* (e.g. this.conversation.turns.length) but grants arbitrary code
|
|
74
|
+
* execution within the agent process. Off by default because the
|
|
75
|
+
* agent already has unrestricted bash access — this is a convenience,
|
|
76
|
+
* not a new capability.
|
|
77
|
+
*/
|
|
78
|
+
diagnose?: boolean;
|
|
67
79
|
/** Show a startup banner when agent-sh launches. */
|
|
68
80
|
startupBanner?: boolean;
|
|
69
81
|
/** Show a subtle agent-sh indicator in the shell prompt. */
|
|
70
82
|
promptIndicator?: boolean;
|
|
71
83
|
/** Names of built-in extensions to disable (e.g. ["command-suggest"]). */
|
|
72
84
|
disabledBuiltins?: string[];
|
|
85
|
+
/**
|
|
86
|
+
* Names of user extensions in ~/.agent-sh/extensions/ to skip when
|
|
87
|
+
* auto-discovering. Match by basename without extension for files
|
|
88
|
+
* (e.g. "peer-mesh" matches peer-mesh.ts), or by directory name for
|
|
89
|
+
* directory-style extensions (e.g. "superash" matches superash/index.ts).
|
|
90
|
+
* Beats having to rename files to .disabled every time.
|
|
91
|
+
*/
|
|
92
|
+
disabledExtensions?: string[];
|
|
73
93
|
}
|
|
74
94
|
declare const DEFAULTS: Required<Settings>;
|
|
75
95
|
/** Load settings from disk (cached after first call). */
|
|
@@ -87,6 +107,17 @@ export declare function getSettings(): Settings & typeof DEFAULTS;
|
|
|
87
107
|
export declare function getExtensionSettings<T extends Record<string, unknown>>(namespace: string, defaults: T): T;
|
|
88
108
|
/** Reset cached settings (for testing or after external edit). */
|
|
89
109
|
export declare function reloadSettings(): void;
|
|
110
|
+
/**
|
|
111
|
+
* Deep-merge a patch into ~/.agent-sh/settings.json on disk.
|
|
112
|
+
*
|
|
113
|
+
* Reads the raw file (preserving unknown keys), merges the patch, writes back
|
|
114
|
+
* with 2-space indentation, and clears the cache so subsequent getSettings()
|
|
115
|
+
* calls see the new values.
|
|
116
|
+
*
|
|
117
|
+
* Used by runtime controls (`/model`, `/backend`) that want their selection
|
|
118
|
+
* to persist as the default across restarts.
|
|
119
|
+
*/
|
|
120
|
+
export declare function updateSettings(patch: Record<string, unknown>): void;
|
|
90
121
|
/**
|
|
91
122
|
* Expand $ENV_VAR references in a string.
|
|
92
123
|
* Supports $VAR and ${VAR} syntax.
|