agent-sh 0.14.1 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-loop.d.ts +1 -1
- package/dist/agent/agent-loop.js +42 -31
- package/dist/agent/conversation-state.d.ts +3 -2
- package/dist/agent/conversation-state.js +20 -3
- package/dist/agent/events.d.ts +2 -0
- package/dist/agent/host-types.d.ts +3 -0
- package/dist/agent/index.js +2 -1
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/subagent.js +5 -1
- package/dist/agent/tool-protocol.d.ts +2 -2
- package/dist/agent/tool-protocol.js +5 -4
- package/dist/agent/tools/glob.d.ts +1 -1
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.d.ts +1 -1
- package/dist/agent/tools/grep.js +4 -2
- package/dist/agent/tools/ls.d.ts +1 -1
- package/dist/agent/tools/ls.js +4 -2
- package/dist/agent/tools/read-file.d.ts +1 -1
- package/dist/agent/tools/read-file.js +30 -2
- package/dist/agent/types.d.ts +11 -1
- package/dist/agent/types.js +6 -1
- package/dist/cli/index.js +0 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/settings.d.ts +3 -0
- package/dist/core/settings.js +2 -2
- package/dist/shell/index.d.ts +6 -0
- package/dist/shell/index.js +10 -10
- package/dist/shell/shell.d.ts +4 -0
- package/dist/shell/shell.js +15 -29
- package/dist/shell/terminal.d.ts +33 -0
- package/dist/shell/terminal.js +62 -0
- package/examples/extensions/ash-scheme/index.ts +2170 -0
- package/examples/extensions/ash-scheme/package.json +11 -0
- package/examples/extensions/ash-scheme-render.ts +58 -0
- package/examples/extensions/ashi/README.md +36 -26
- package/examples/extensions/ashi/package.json +9 -1
- package/examples/extensions/ashi/src/capture.ts +1 -0
- package/examples/extensions/ashi/src/cli.ts +21 -7
- package/examples/extensions/ashi/src/compaction.ts +25 -96
- package/examples/extensions/ashi/src/components.ts +64 -166
- package/examples/extensions/ashi/src/default-schema-renderers.ts +229 -0
- package/examples/extensions/ashi/src/display-config.ts +21 -22
- package/examples/extensions/ashi/src/frontend.ts +64 -65
- package/examples/extensions/ashi/src/hooks.ts +47 -63
- package/examples/extensions/ashi/src/multi-session-store.ts +44 -3
- package/examples/extensions/ashi/src/schema.ts +407 -0
- package/examples/extensions/ashi/src/session-store.ts +55 -4
- package/examples/extensions/ashi/src/status-footer.ts +27 -6
- package/examples/extensions/ashi-compact-llm.ts +93 -0
- package/examples/extensions/claude-code-bridge/index.ts +2 -0
- package/examples/extensions/opencode-bridge/index.ts +3 -0
- package/examples/extensions/opencode-provider.ts +252 -0
- package/examples/extensions/pi-bridge/index.ts +1 -0
- package/package.json +12 -1
- package/examples/extensions/ashi/src/default-renderers.ts +0 -171
|
@@ -147,6 +147,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
147
147
|
announcedTools.add(callID);
|
|
148
148
|
bus.emit("agent:tool-started", {
|
|
149
149
|
title: toolName,
|
|
150
|
+
name: toolName,
|
|
150
151
|
toolCallId: callID,
|
|
151
152
|
kind,
|
|
152
153
|
locations: toolLocations(state.input ?? {}),
|
|
@@ -290,6 +291,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
290
291
|
: req.questions.map((q, i) => `${q.header || `Q${i + 1}`}: ${q.question}`).join("; ");
|
|
291
292
|
bus.emit("agent:tool-started", {
|
|
292
293
|
title: "question",
|
|
294
|
+
name: "question",
|
|
293
295
|
toolCallId: callID,
|
|
294
296
|
kind: "execute",
|
|
295
297
|
displayDetail: detail,
|
|
@@ -355,6 +357,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
355
357
|
const summary = opts?.note ? `denied (${opts.note})` : `denied: ${detail}`;
|
|
356
358
|
bus.emit("agent:tool-started", {
|
|
357
359
|
title: "permission",
|
|
360
|
+
name: "permission",
|
|
358
361
|
toolCallId: callID,
|
|
359
362
|
kind: "execute",
|
|
360
363
|
displayDetail: detail,
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode-provider — OpenCode Zen & Go LLM providers for agent-sh
|
|
3
|
+
*
|
|
4
|
+
* Registers two providers with runtime model discovery:
|
|
5
|
+
* - opencode — Zen tier (https://opencode.ai/zen/v1)
|
|
6
|
+
* - opencode-go — Go tier (https://opencode.ai/zen/go/v1)
|
|
7
|
+
*
|
|
8
|
+
* Both are OpenAI-compatible endpoints. OpenCode's backend proxies
|
|
9
|
+
* all models (OpenAI, Anthropic, Google, etc.) through them — no
|
|
10
|
+
* per-model transport routing needed at the agent-sh level.
|
|
11
|
+
*
|
|
12
|
+
* ## Setup
|
|
13
|
+
* export OPENCODE_***REDACTED***
|
|
14
|
+
* agent-sh -e ./examples/extensions/opencode-provider.ts
|
|
15
|
+
*
|
|
16
|
+
* # Or add to settings.json:
|
|
17
|
+
* { "extensions": ["./examples/extensions/opencode-provider.ts"] }
|
|
18
|
+
*
|
|
19
|
+
* # Or store via auth:
|
|
20
|
+
* agent-sh auth login opencode
|
|
21
|
+
*
|
|
22
|
+
* ## Model discovery
|
|
23
|
+
* On startup the extension:
|
|
24
|
+
* 1. Fetches official /models endpoints (authoritative model list)
|
|
25
|
+
* 2. Merges metadata from models.dev (context windows, reasoning flags)
|
|
26
|
+
* 3. Falls back to models.dev membership → conservative defaults
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* /model — tab-completes all discovered models
|
|
30
|
+
* /provider — switch between opencode / opencode-go
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { AgentContext } from "agent-sh/types";
|
|
34
|
+
import { resolveApiKey } from "agent-sh/auth";
|
|
35
|
+
|
|
36
|
+
// ── Constants ──────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const ZEN_BASE_URL = "https://opencode.ai/zen/v1";
|
|
39
|
+
const GO_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
40
|
+
const ZEN_MODELS_ENDPOINT = `${ZEN_BASE_URL}/models`;
|
|
41
|
+
const GO_MODELS_ENDPOINT = `${GO_BASE_URL}/models`;
|
|
42
|
+
const MODELS_DEV_ENDPOINT = "https://models.dev/api.json";
|
|
43
|
+
|
|
44
|
+
/** Conservative defaults when models.dev metadata is unavailable. */
|
|
45
|
+
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
46
|
+
const DEFAULT_MAX_TOKENS = 16_384;
|
|
47
|
+
|
|
48
|
+
// ── Fallback model lists (curated; kept in sync with OpenCode docs) ──
|
|
49
|
+
|
|
50
|
+
// Single fallback model per tier — the live /models catalog replaces these on startup.
|
|
51
|
+
const ZEN_FALLBACK_MODELS = ["claude-sonnet-4-6"];
|
|
52
|
+
|
|
53
|
+
const GO_FALLBACK_MODELS = ["gpt-5.2"];
|
|
54
|
+
|
|
55
|
+
// ── Types ───────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
interface ModelsDevLimit {
|
|
58
|
+
context?: number;
|
|
59
|
+
output?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ModelsDevModelEntry {
|
|
63
|
+
id?: string;
|
|
64
|
+
name?: string;
|
|
65
|
+
reasoning?: boolean;
|
|
66
|
+
limit?: ModelsDevLimit;
|
|
67
|
+
modalities?: { input?: readonly string[] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ModelsDevProviderEntry {
|
|
71
|
+
models?: Record<string, ModelsDevModelEntry>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type ModelsDevResponse = Record<string, ModelsDevProviderEntry>;
|
|
75
|
+
|
|
76
|
+
interface OpenCodeModelListEntry {
|
|
77
|
+
id?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface OpenCodeModelListResponse {
|
|
81
|
+
data?: OpenCodeModelListEntry[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ModelDef {
|
|
85
|
+
id: string;
|
|
86
|
+
reasoning: boolean;
|
|
87
|
+
contextWindow: number;
|
|
88
|
+
maxTokens: number;
|
|
89
|
+
modalities: ("text" | "image")[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
async function fetchJson<T>(url: string): Promise<T> {
|
|
95
|
+
const res = await fetch(url, {
|
|
96
|
+
headers: { Accept: "application/json" },
|
|
97
|
+
signal: AbortSignal.timeout(15_000),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
throw new Error(`HTTP ${res.status} from ${url}`);
|
|
101
|
+
}
|
|
102
|
+
return res.json() as T;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizePosNum(v: unknown): number | undefined {
|
|
106
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── models.dev ──────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
async function fetchModelsDev(): Promise<ModelsDevResponse | undefined> {
|
|
112
|
+
try {
|
|
113
|
+
const payload = await fetchJson<ModelsDevResponse>(MODELS_DEV_ENDPOINT);
|
|
114
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
115
|
+
return payload;
|
|
116
|
+
} catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getModelsDevModel(
|
|
122
|
+
provider: ModelsDevProviderEntry | undefined,
|
|
123
|
+
modelId: string,
|
|
124
|
+
): ModelsDevModelEntry | undefined {
|
|
125
|
+
const direct = provider?.models?.[modelId];
|
|
126
|
+
if (direct) return direct;
|
|
127
|
+
if (!provider?.models) return undefined;
|
|
128
|
+
for (const m of Object.values(provider.models)) {
|
|
129
|
+
if (m.id === modelId) return m;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Official /models ────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
async function fetchOfficialModelIds(url: string): Promise<string[]> {
|
|
137
|
+
try {
|
|
138
|
+
const payload = await fetchJson<OpenCodeModelListResponse>(url);
|
|
139
|
+
if (!Array.isArray(payload.data)) throw new Error("Unexpected format");
|
|
140
|
+
const ids = new Set<string>();
|
|
141
|
+
for (const entry of payload.data) {
|
|
142
|
+
if (typeof entry.id === "string" && entry.id.trim()) {
|
|
143
|
+
ids.add(entry.id.trim());
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return Array.from(ids);
|
|
147
|
+
} catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Merge ───────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
function resolveModel(
|
|
155
|
+
modelId: string,
|
|
156
|
+
metadata: ModelsDevModelEntry | undefined,
|
|
157
|
+
): ModelDef {
|
|
158
|
+
const rawModalities = metadata?.modalities?.input;
|
|
159
|
+
const modalities: ("text" | "image")[] = Array.isArray(rawModalities)
|
|
160
|
+
? rawModalities.filter((v): v is "text" | "image" => v === "text" || v === "image")
|
|
161
|
+
: ["text"];
|
|
162
|
+
return {
|
|
163
|
+
id: modelId,
|
|
164
|
+
reasoning: metadata?.reasoning ?? false,
|
|
165
|
+
contextWindow: normalizePosNum(metadata?.limit?.context) ?? DEFAULT_CONTEXT_WINDOW,
|
|
166
|
+
maxTokens: normalizePosNum(metadata?.limit?.output) ?? DEFAULT_MAX_TOKENS,
|
|
167
|
+
modalities,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Reasoning params (OpenAI-compatible) ────────────────────────────
|
|
172
|
+
|
|
173
|
+
function buildReasoningParams(level: string): Record<string, unknown> {
|
|
174
|
+
if (level === "off") return {};
|
|
175
|
+
return { reasoning_effort: level === "xhigh" ? "high" : level };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Activation ──────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
export default function activate(ctx: AgentContext): void {
|
|
181
|
+
const apiKey =
|
|
182
|
+
process.env.OPENCODE_API_KEY ??
|
|
183
|
+
resolveApiKey("opencode").key ?? undefined;
|
|
184
|
+
|
|
185
|
+
// ── Phase 1: register both providers synchronously with fallback models ──
|
|
186
|
+
|
|
187
|
+
ctx.agent.providers.configure("opencode", { reasoningParams: buildReasoningParams });
|
|
188
|
+
ctx.agent.providers.register({
|
|
189
|
+
id: "opencode",
|
|
190
|
+
apiKey,
|
|
191
|
+
baseURL: ZEN_BASE_URL,
|
|
192
|
+
defaultModel: ZEN_FALLBACK_MODELS[0],
|
|
193
|
+
models: ZEN_FALLBACK_MODELS,
|
|
194
|
+
supportsReasoningEffort: true,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
ctx.agent.providers.configure("opencode-go", { reasoningParams: buildReasoningParams });
|
|
198
|
+
ctx.agent.providers.register({
|
|
199
|
+
id: "opencode-go",
|
|
200
|
+
apiKey,
|
|
201
|
+
baseURL: GO_BASE_URL,
|
|
202
|
+
defaultModel: GO_FALLBACK_MODELS[0],
|
|
203
|
+
models: GO_FALLBACK_MODELS,
|
|
204
|
+
supportsReasoningEffort: true,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!apiKey) return;
|
|
208
|
+
|
|
209
|
+
// ── Phase 2: fetch live catalogs and re-register with enriched metadata ──
|
|
210
|
+
|
|
211
|
+
fetchModelsDev()
|
|
212
|
+
.then(async (modelsDev) => {
|
|
213
|
+
const zenProvider = modelsDev?.opencode;
|
|
214
|
+
const goProvider = modelsDev?.["opencode-go"];
|
|
215
|
+
|
|
216
|
+
const [zenIds, goIds] = await Promise.all([
|
|
217
|
+
fetchOfficialModelIds(ZEN_MODELS_ENDPOINT),
|
|
218
|
+
fetchOfficialModelIds(GO_MODELS_ENDPOINT),
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
const resolveModels = (
|
|
222
|
+
ids: string[],
|
|
223
|
+
provider: ModelsDevProviderEntry | undefined,
|
|
224
|
+
fallback: string[],
|
|
225
|
+
): ModelDef[] => {
|
|
226
|
+
const source = ids.length > 0 ? ids : fallback;
|
|
227
|
+
return source.map((id) => resolveModel(id, getModelsDevModel(provider, id)));
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const zenModels = resolveModels(zenIds, zenProvider, ZEN_FALLBACK_MODELS);
|
|
231
|
+
const goModels = resolveModels(goIds, goProvider, GO_FALLBACK_MODELS);
|
|
232
|
+
|
|
233
|
+
ctx.agent.providers.register({
|
|
234
|
+
id: "opencode",
|
|
235
|
+
apiKey,
|
|
236
|
+
baseURL: ZEN_BASE_URL,
|
|
237
|
+
defaultModel: zenModels[0]?.id ?? ZEN_FALLBACK_MODELS[0],
|
|
238
|
+
models: zenModels,
|
|
239
|
+
supportsReasoningEffort: true,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
ctx.agent.providers.register({
|
|
243
|
+
id: "opencode-go",
|
|
244
|
+
apiKey,
|
|
245
|
+
baseURL: GO_BASE_URL,
|
|
246
|
+
defaultModel: goModels[0]?.id ?? GO_FALLBACK_MODELS[0],
|
|
247
|
+
models: goModels,
|
|
248
|
+
supportsReasoningEffort: true,
|
|
249
|
+
});
|
|
250
|
+
})
|
|
251
|
+
.catch(() => {});
|
|
252
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sh",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "A shell-first terminal where AI is one keystroke away",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"workspaces": [
|
|
7
|
+
"examples/extensions/*"
|
|
8
|
+
],
|
|
6
9
|
"main": "dist/core/index.js",
|
|
7
10
|
"types": "dist/core/index.d.ts",
|
|
8
11
|
"bin": {
|
|
@@ -38,6 +41,14 @@
|
|
|
38
41
|
"types": "./dist/shell/shell.d.ts",
|
|
39
42
|
"default": "./dist/shell/shell.js"
|
|
40
43
|
},
|
|
44
|
+
"./shell/strategies": {
|
|
45
|
+
"types": "./dist/shell/strategies/index.d.ts",
|
|
46
|
+
"default": "./dist/shell/strategies/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./shell/terminal": {
|
|
49
|
+
"types": "./dist/shell/terminal.d.ts",
|
|
50
|
+
"default": "./dist/shell/terminal.js"
|
|
51
|
+
},
|
|
41
52
|
"./utils/stream-transform": {
|
|
42
53
|
"types": "./dist/utils/stream-transform.d.ts",
|
|
43
54
|
"default": "./dist/utils/stream-transform.js"
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
2
|
-
import type { ExtensionContext } from "agent-sh/types";
|
|
3
|
-
import { theme } from "./theme.js";
|
|
4
|
-
import { GROUP_ICONS } from "./components.js";
|
|
5
|
-
import type { ToolCallArgs, ToolCallView } from "./hooks.js";
|
|
6
|
-
|
|
7
|
-
const TOOL_ICON: Record<string, string> = {
|
|
8
|
-
read_file: GROUP_ICONS.read!,
|
|
9
|
-
read: GROUP_ICONS.read!,
|
|
10
|
-
ls: GROUP_ICONS.read!,
|
|
11
|
-
grep: GROUP_ICONS.search!,
|
|
12
|
-
glob: GROUP_ICONS.search!,
|
|
13
|
-
edit: "✎",
|
|
14
|
-
edit_file: "✎",
|
|
15
|
-
write: "✎",
|
|
16
|
-
write_file: "✎",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function iconPrefix(name: string): string {
|
|
20
|
-
const icon = TOOL_ICON[name] ?? "⚙";
|
|
21
|
-
return `${theme.fg("warning", icon)} `;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface StatusOpts { exitCode: number | null; elapsedMs: number; summary?: string }
|
|
25
|
-
|
|
26
|
-
function fmtElapsed(ms: number): string {
|
|
27
|
-
if (ms < 1000) return `${ms}ms`;
|
|
28
|
-
if (ms < 10_000) return `${(ms / 1000).toFixed(2)}s`;
|
|
29
|
-
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
30
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
31
|
-
const m = Math.floor(totalSeconds / 60);
|
|
32
|
-
const s = totalSeconds % 60;
|
|
33
|
-
return `${m}m ${s}s`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function parseRaw(raw: unknown): Record<string, unknown> {
|
|
37
|
-
if (typeof raw === "string") {
|
|
38
|
-
try { return JSON.parse(raw) as Record<string, unknown>; } catch { return {}; }
|
|
39
|
-
}
|
|
40
|
-
if (raw && typeof raw === "object") return raw as Record<string, unknown>;
|
|
41
|
-
return {};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function str(v: unknown): string | undefined {
|
|
45
|
-
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function num(v: unknown): number | undefined {
|
|
49
|
-
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function relativize(fp: string): string {
|
|
53
|
-
const home = process.env.HOME;
|
|
54
|
-
const cwd = process.cwd();
|
|
55
|
-
if (fp.startsWith(`${cwd}/`)) return fp.slice(cwd.length + 1);
|
|
56
|
-
if (home && fp.startsWith(`${home}/`)) return `~/${fp.slice(home.length + 1)}`;
|
|
57
|
-
return fp;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function statusSuffix(opts?: StatusOpts): string {
|
|
61
|
-
if (!opts) return ` ${theme.fg("muted", "…")}`;
|
|
62
|
-
const ok = opts.exitCode === null || opts.exitCode === 0;
|
|
63
|
-
const mark = ok ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
64
|
-
const elapsed = opts.elapsedMs > 0 ? ` ${theme.fg("muted", fmtElapsed(opts.elapsedMs))}` : "";
|
|
65
|
-
const sum = opts.summary ? ` ${theme.fg("muted", opts.summary)}` : "";
|
|
66
|
-
return ` ${mark}${elapsed}${sum}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Renders a one-line tool call header from a label producer. The label is
|
|
70
|
-
* recomputed on setStatus so the trailing status mark updates in place. */
|
|
71
|
-
class LabeledCallLine extends Container implements ToolCallView {
|
|
72
|
-
private line: Text;
|
|
73
|
-
private status?: StatusOpts;
|
|
74
|
-
constructor(private label: () => string) {
|
|
75
|
-
super();
|
|
76
|
-
this.line = new Text("", 1, 0);
|
|
77
|
-
this.addChild(new Spacer(1));
|
|
78
|
-
this.addChild(this.line);
|
|
79
|
-
this.repaint();
|
|
80
|
-
}
|
|
81
|
-
setStatus(opts: StatusOpts): void {
|
|
82
|
-
this.status = opts;
|
|
83
|
-
this.repaint();
|
|
84
|
-
}
|
|
85
|
-
private repaint(): void {
|
|
86
|
-
this.line.setText(`${this.label()}${statusSuffix(this.status)}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const bold = (t: string): string => theme.bold(theme.fg("toolTitle", t));
|
|
91
|
-
const accent = (t: string): string => theme.fg("accent", t);
|
|
92
|
-
const muted = (t: string): string => theme.fg("muted", t);
|
|
93
|
-
|
|
94
|
-
function bashLabel(args: ToolCallArgs): string {
|
|
95
|
-
const r = parseRaw(args.rawInput);
|
|
96
|
-
const command = str(r.command) ?? "…";
|
|
97
|
-
const timeout = num(r.timeout);
|
|
98
|
-
const to = timeout ? muted(` (timeout ${timeout}s)`) : "";
|
|
99
|
-
return `${bold("$")} ${accent(command)}${to}`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function readLabel(args: ToolCallArgs): string {
|
|
103
|
-
const r = parseRaw(args.rawInput);
|
|
104
|
-
const path = str(r.file_path) ?? str(r.path);
|
|
105
|
-
const offset = num(r.offset);
|
|
106
|
-
const limit = num(r.limit);
|
|
107
|
-
let range = "";
|
|
108
|
-
if (offset !== undefined || limit !== undefined) {
|
|
109
|
-
const from = offset ?? 1;
|
|
110
|
-
const to = limit !== undefined ? from + limit - 1 : undefined;
|
|
111
|
-
range = theme.fg("warning", to ? `:${from}-${to}` : `:${from}`);
|
|
112
|
-
}
|
|
113
|
-
return `${iconPrefix("read")}${bold("read")} ${accent(path ? relativize(path) : "…")}${range}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function grepLabel(args: ToolCallArgs): string {
|
|
117
|
-
const r = parseRaw(args.rawInput);
|
|
118
|
-
const pattern = str(r.pattern) ?? "…";
|
|
119
|
-
const scope = relativize(str(r.path) ?? ".");
|
|
120
|
-
const glob = str(r.glob);
|
|
121
|
-
const limit = num(r.limit);
|
|
122
|
-
const extras = [glob ? `(${glob})` : "", limit !== undefined ? `limit ${limit}` : ""].filter(Boolean).join(" ");
|
|
123
|
-
const tail = extras ? muted(` ${extras}`) : "";
|
|
124
|
-
return `${iconPrefix("grep")}${bold("grep")} ${accent(`/${pattern}/`)} ${muted(`in ${scope}`)}${tail}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function globLabel(args: ToolCallArgs): string {
|
|
128
|
-
const r = parseRaw(args.rawInput);
|
|
129
|
-
const pattern = str(r.pattern) ?? "…";
|
|
130
|
-
const scope = relativize(str(r.path) ?? ".");
|
|
131
|
-
return `${iconPrefix("glob")}${bold("glob")} ${accent(pattern)} ${muted(`in ${scope}`)}`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function lsLabel(args: ToolCallArgs): string {
|
|
135
|
-
const r = parseRaw(args.rawInput);
|
|
136
|
-
const p = str(r.path) ?? ".";
|
|
137
|
-
return `${iconPrefix("ls")}${bold("ls")} ${accent(relativize(p))}`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function pathOnlyLabel(name: string, args: ToolCallArgs): string {
|
|
141
|
-
const r = parseRaw(args.rawInput);
|
|
142
|
-
const path = str(r.file_path) ?? str(r.path);
|
|
143
|
-
return `${iconPrefix(name)}${bold(name)} ${accent(path ? relativize(path) : "…")}`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function genericLabel(args: ToolCallArgs): string {
|
|
147
|
-
const detail = args.displayDetail ? ` ${muted(args.displayDetail)}` : "";
|
|
148
|
-
return `${iconPrefix(args.name)}${bold(args.title)}${detail}`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function registerDefaultToolRenderers(ctx: ExtensionContext): void {
|
|
152
|
-
const define = (name: string, fn: (args: ToolCallArgs) => ToolCallView): void => {
|
|
153
|
-
ctx.define(`ashi:render-tool-call:${name}`, fn);
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
define("bash", (args) => new LabeledCallLine(() => bashLabel(args)));
|
|
157
|
-
|
|
158
|
-
define("read_file", (args) => new LabeledCallLine(() => readLabel(args)));
|
|
159
|
-
define("read", (args) => new LabeledCallLine(() => readLabel(args)));
|
|
160
|
-
|
|
161
|
-
define("grep", (args) => new LabeledCallLine(() => grepLabel(args)));
|
|
162
|
-
define("glob", (args) => new LabeledCallLine(() => globLabel(args)));
|
|
163
|
-
define("ls", (args) => new LabeledCallLine(() => lsLabel(args)));
|
|
164
|
-
|
|
165
|
-
define("edit_file", (args) => new LabeledCallLine(() => pathOnlyLabel("edit", args)));
|
|
166
|
-
define("edit", (args) => new LabeledCallLine(() => pathOnlyLabel("edit", args)));
|
|
167
|
-
define("write_file", (args) => new LabeledCallLine(() => pathOnlyLabel("write", args)));
|
|
168
|
-
define("write", (args) => new LabeledCallLine(() => pathOnlyLabel("write", args)));
|
|
169
|
-
|
|
170
|
-
define("default", (args) => new LabeledCallLine(() => genericLabel(args)));
|
|
171
|
-
}
|