agent-sh 0.14.0 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -18
- package/dist/agent/agent-loop.d.ts +1 -1
- package/dist/agent/agent-loop.js +42 -31
- package/dist/agent/conversation-state.d.ts +3 -2
- package/dist/agent/conversation-state.js +20 -3
- package/dist/agent/events.d.ts +2 -0
- package/dist/agent/host-types.d.ts +3 -0
- package/dist/agent/index.js +2 -1
- package/dist/agent/llm-client.js +1 -0
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/subagent.js +5 -1
- package/dist/agent/tool-protocol.d.ts +2 -2
- package/dist/agent/tool-protocol.js +5 -4
- package/dist/agent/tools/glob.d.ts +1 -1
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.d.ts +1 -1
- package/dist/agent/tools/grep.js +4 -2
- package/dist/agent/tools/ls.d.ts +1 -1
- package/dist/agent/tools/ls.js +4 -2
- package/dist/agent/tools/read-file.d.ts +1 -1
- package/dist/agent/tools/read-file.js +30 -2
- package/dist/agent/types.d.ts +13 -3
- package/dist/agent/types.js +6 -1
- package/dist/cli/args.js +3 -1
- package/dist/cli/index.js +0 -0
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.js +86 -2
- package/dist/cli/subcommands.js +4 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/settings.d.ts +3 -0
- package/dist/core/settings.js +2 -2
- package/dist/shell/index.d.ts +6 -0
- package/dist/shell/index.js +10 -10
- package/dist/shell/shell.d.ts +4 -0
- package/dist/shell/shell.js +15 -29
- package/dist/shell/terminal.d.ts +33 -0
- package/dist/shell/terminal.js +62 -0
- package/dist/utils/tool-interactive.js +4 -2
- package/examples/extensions/ash-scheme/index.ts +2170 -0
- package/examples/extensions/ash-scheme/package.json +11 -0
- package/examples/extensions/ash-scheme-render.ts +58 -0
- package/examples/extensions/ashi/README.md +36 -26
- package/examples/extensions/ashi/package.json +9 -1
- package/examples/extensions/ashi/src/capture.ts +1 -0
- package/examples/extensions/ashi/src/cli.ts +25 -8
- package/examples/extensions/ashi/src/compaction.ts +25 -96
- package/examples/extensions/ashi/src/components.ts +64 -166
- package/examples/extensions/ashi/src/default-schema-renderers.ts +229 -0
- package/examples/extensions/ashi/src/display-config.ts +21 -22
- package/examples/extensions/ashi/src/frontend.ts +64 -65
- package/examples/extensions/ashi/src/hooks.ts +47 -63
- package/examples/extensions/ashi/src/multi-session-store.ts +44 -3
- package/examples/extensions/ashi/src/schema.ts +407 -0
- package/examples/extensions/ashi/src/session-store.ts +55 -4
- package/examples/extensions/ashi/src/status-footer.ts +27 -6
- package/examples/extensions/ashi-compact-llm.ts +93 -0
- package/examples/extensions/claude-code-bridge/index.ts +9 -2
- package/examples/extensions/claude-code-bridge/package.json +1 -1
- package/examples/extensions/opencode-bridge/index.ts +208 -53
- package/examples/extensions/opencode-bridge/package.json +1 -1
- package/examples/extensions/opencode-provider.ts +252 -0
- package/examples/extensions/pi-bridge/index.ts +1 -0
- package/package.json +12 -1
- package/examples/extensions/ashi/src/default-renderers.ts +0 -171
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
Text,
|
|
7
7
|
} from "@earendil-works/pi-tui";
|
|
8
8
|
import { markdownTheme, theme } from "./theme.js";
|
|
9
|
-
import type { ToolResultMode } from "./display-config.js";
|
|
10
9
|
|
|
11
10
|
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
12
11
|
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
@@ -94,115 +93,6 @@ export class ThinkingBlock extends Container {
|
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
export class ToolResultBody extends Container {
|
|
98
|
-
private outputText: Text;
|
|
99
|
-
private bodyText: Text;
|
|
100
|
-
private outputBuffer = "";
|
|
101
|
-
private diffRenderer: ((width: number) => string[]) | null = null;
|
|
102
|
-
private lastDiffWidth = -1;
|
|
103
|
-
private mode: ToolResultMode;
|
|
104
|
-
private previewLines: number;
|
|
105
|
-
private finalized = false;
|
|
106
|
-
private expanded = false;
|
|
107
|
-
private exitCode: number | null | undefined;
|
|
108
|
-
|
|
109
|
-
constructor(mode: ToolResultMode, previewLines: number) {
|
|
110
|
-
super();
|
|
111
|
-
this.mode = mode;
|
|
112
|
-
this.previewLines = previewLines;
|
|
113
|
-
this.outputText = new Text("", 1, 0);
|
|
114
|
-
this.bodyText = new Text("", 0, 0);
|
|
115
|
-
this.addChild(this.outputText);
|
|
116
|
-
this.addChild(this.bodyText);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
appendChunk(chunk: string): void {
|
|
120
|
-
this.outputBuffer += chunk;
|
|
121
|
-
this.repaint();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
setDiffRenderer(fn: (width: number) => string[]): void {
|
|
125
|
-
this.diffRenderer = fn;
|
|
126
|
-
this.lastDiffWidth = -1;
|
|
127
|
-
this.repaint();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
finalize(opts: { exitCode: number | null; summary?: string }): void {
|
|
131
|
-
this.finalized = true;
|
|
132
|
-
this.exitCode = opts.exitCode;
|
|
133
|
-
this.repaint();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
toggleExpanded(): void {
|
|
137
|
-
this.expanded = !this.expanded;
|
|
138
|
-
this.repaint();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
override render(width: number): string[] {
|
|
142
|
-
if (this.diffRenderer && width !== this.lastDiffWidth) {
|
|
143
|
-
this.lastDiffWidth = width;
|
|
144
|
-
const showDiff = this.expanded || this.mode === "preview";
|
|
145
|
-
this.bodyText.setText(showDiff ? this.diffRenderer(width).join("\n") : "");
|
|
146
|
-
}
|
|
147
|
-
return super.render(width);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private repaint(): void {
|
|
151
|
-
const hasDiff = this.diffRenderer !== null;
|
|
152
|
-
const showDiff = hasDiff && (this.expanded || this.mode === "preview");
|
|
153
|
-
if (showDiff && this.lastDiffWidth >= 0 && this.diffRenderer) {
|
|
154
|
-
this.bodyText.setText(this.diffRenderer(this.lastDiffWidth).join("\n"));
|
|
155
|
-
} else if (!showDiff) {
|
|
156
|
-
this.bodyText.setText("");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// When a diff exists, the textual output ("Edited /path (+12 -3)") just
|
|
160
|
-
// restates the call line — suppress its line-count hint to keep edits quiet.
|
|
161
|
-
if (hasDiff && !this.expanded) {
|
|
162
|
-
this.outputText.setText("");
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (!this.outputBuffer) {
|
|
166
|
-
this.outputText.setText("");
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (this.expanded) {
|
|
170
|
-
this.outputText.setText(theme.fg("toolOutput", this.outputBuffer));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (this.mode === "hidden") {
|
|
174
|
-
if (!this.finalized) { this.outputText.setText(""); return; }
|
|
175
|
-
this.outputText.setText(lineCountHint(this.outputBuffer, this.exitCode));
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (this.mode === "summary") {
|
|
179
|
-
if (!this.finalized) {
|
|
180
|
-
// Brief tail while streaming; collapses to a line count on finalize.
|
|
181
|
-
const tail = this.outputBuffer.split("\n").slice(-2).join("\n");
|
|
182
|
-
this.outputText.setText(theme.fg("muted", tail));
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
this.outputText.setText(lineCountHint(this.outputBuffer, this.exitCode));
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const lines = this.outputBuffer.split("\n");
|
|
189
|
-
const trimmed = lines.slice(-this.previewLines).join("\n");
|
|
190
|
-
const remaining = Math.max(0, lines.length - this.previewLines);
|
|
191
|
-
const overflow = remaining > 0
|
|
192
|
-
? `\n${theme.fg("muted", `... (${remaining} more ${remaining === 1 ? "line" : "lines"})`)}`
|
|
193
|
-
: "";
|
|
194
|
-
this.outputText.setText(`${theme.fg("toolOutput", trimmed)}${overflow}`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function lineCountHint(buffer: string, exitCode: number | null | undefined): string {
|
|
199
|
-
const lines = buffer.split("\n").filter((l) => l.length > 0);
|
|
200
|
-
const label = lines.length === 1 ? "1 line" : `${lines.length} lines`;
|
|
201
|
-
const ok = exitCode === null || exitCode === 0;
|
|
202
|
-
const arrow = ok ? theme.fg("muted", "↳ ") : theme.fg("error", "↳ ");
|
|
203
|
-
return `${arrow}${theme.fg("muted", label)}`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
96
|
export const GROUP_ICONS: Record<string, string> = { read: "◆", search: "⌕" };
|
|
207
97
|
|
|
208
98
|
interface GroupChild {
|
|
@@ -222,85 +112,95 @@ function shortToolName(name: string): string {
|
|
|
222
112
|
return SHORT_TOOL_NAMES[name] ?? name;
|
|
223
113
|
}
|
|
224
114
|
|
|
225
|
-
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
115
|
+
/** An open-ended run of same-kind tool calls. Grows by tail-merge: the caller
|
|
116
|
+
* extends an existing group when its `kind` matches the next call and the
|
|
117
|
+
* group is still the chat's tail. Trailing child renders with └, others ├.
|
|
118
|
+
*
|
|
119
|
+
* When `maxVisible` is finite and exceeded, the oldest children collapse to
|
|
120
|
+
* a summary line ("⋯ N earlier ✓") and the last (maxVisible − 1) stay
|
|
121
|
+
* visible. `toggleExpanded()` reveals all; `maxVisible = Infinity` disables
|
|
122
|
+
* eviction. */
|
|
229
123
|
export class ToolGroup extends Container {
|
|
230
124
|
private headerText: Text;
|
|
125
|
+
private summaryText: Text;
|
|
231
126
|
private childContainer: Container;
|
|
232
|
-
|
|
233
|
-
private kind: string;
|
|
234
|
-
private total: number;
|
|
127
|
+
readonly kind: string;
|
|
235
128
|
private maxVisible: number;
|
|
236
|
-
private
|
|
237
|
-
private
|
|
238
|
-
private
|
|
239
|
-
private renderedCount = 0;
|
|
240
|
-
private completedCount = 0;
|
|
241
|
-
private allOk = true;
|
|
129
|
+
private allChildren: GroupChild[] = [];
|
|
130
|
+
private callsById = new Map<string, GroupChild>();
|
|
131
|
+
private expanded = false;
|
|
242
132
|
|
|
243
|
-
constructor(kind: string,
|
|
133
|
+
constructor(kind: string, maxVisible: number = Infinity) {
|
|
244
134
|
super();
|
|
245
135
|
this.kind = kind;
|
|
246
|
-
this.total = total;
|
|
247
136
|
this.maxVisible = maxVisible;
|
|
248
137
|
this.headerText = new Text("", 1, 0);
|
|
138
|
+
this.summaryText = new Text("", 1, 0);
|
|
249
139
|
this.childContainer = new Container();
|
|
250
|
-
this.aggregateText = new Text("", 1, 0);
|
|
251
140
|
this.addChild(new Spacer(1));
|
|
252
141
|
this.addChild(this.headerText);
|
|
142
|
+
this.addChild(this.summaryText);
|
|
253
143
|
this.addChild(this.childContainer);
|
|
254
|
-
this.addChild(this.aggregateText);
|
|
255
144
|
this.repaintHeader();
|
|
256
145
|
}
|
|
257
146
|
|
|
258
147
|
addCall(toolCallId: string, name: string, detail: string): void {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
this.childContainer.addChild(text);
|
|
265
|
-
this.renderedCount++;
|
|
266
|
-
this.repaintChild(child);
|
|
267
|
-
}
|
|
148
|
+
const text = new Text("", 1, 0);
|
|
149
|
+
const child: GroupChild = { name: shortToolName(name), detail: detail || "…", text };
|
|
150
|
+
if (toolCallId) this.callsById.set(toolCallId, child);
|
|
151
|
+
this.allChildren.push(child);
|
|
152
|
+
this.repaint();
|
|
268
153
|
}
|
|
269
154
|
|
|
270
155
|
recordCompletion(toolCallId: string, exitCode: number | null, summary?: string): void {
|
|
271
|
-
this.
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
child.status = { exitCode, summary };
|
|
276
|
-
this.repaintChild(child);
|
|
277
|
-
} else if (summary) {
|
|
278
|
-
this.hiddenSummaries.push(summary);
|
|
279
|
-
}
|
|
280
|
-
if (this.completedCount >= this.total) this.finalize();
|
|
156
|
+
const child = this.callsById.get(toolCallId);
|
|
157
|
+
if (!child) return;
|
|
158
|
+
child.status = { exitCode, summary };
|
|
159
|
+
this.repaint();
|
|
281
160
|
}
|
|
282
161
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const more = theme.fg("muted", `+${collapsed} more`);
|
|
295
|
-
const sumText = this.hiddenSummaries.length > 0
|
|
296
|
-
? ` ${theme.fg("muted", this.hiddenSummaries.join(", "))}`
|
|
297
|
-
: "";
|
|
298
|
-
this.aggregateText.setText(`${theme.fg("muted", "└")} ${more} ${mark}${sumText}`);
|
|
162
|
+
toggleExpanded(): void {
|
|
163
|
+
this.expanded = !this.expanded;
|
|
164
|
+
this.repaint();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** How many children at the tail are visible right now. When collapsed and
|
|
168
|
+
* over the cap, this is maxVisible − 1 (one line goes to the summary). */
|
|
169
|
+
private visibleSliceStart(): number {
|
|
170
|
+
if (this.expanded || !Number.isFinite(this.maxVisible)) return 0;
|
|
171
|
+
if (this.allChildren.length <= this.maxVisible) return 0;
|
|
172
|
+
return this.allChildren.length - (this.maxVisible - 1);
|
|
299
173
|
}
|
|
300
174
|
|
|
301
|
-
|
|
175
|
+
private repaint(): void {
|
|
176
|
+
const start = this.visibleSliceStart();
|
|
177
|
+
const evicted = this.allChildren.slice(0, start);
|
|
178
|
+
const visible = this.allChildren.slice(start);
|
|
179
|
+
|
|
180
|
+
if (evicted.length === 0) {
|
|
181
|
+
this.summaryText.setText("");
|
|
182
|
+
} else {
|
|
183
|
+
const allOk = evicted.every(
|
|
184
|
+
(c) => !c.status || c.status.exitCode === null || c.status.exitCode === 0,
|
|
185
|
+
);
|
|
186
|
+
const mark = allOk ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
187
|
+
const noun = evicted.length === 1 ? "earlier call" : "earlier calls";
|
|
188
|
+
this.summaryText.setText(
|
|
189
|
+
`${theme.fg("muted", "├")} ${theme.fg("muted", "⋯")} ${theme.fg("muted", `${evicted.length} ${noun}`)} ${mark}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Reconcile childContainer to exactly the `visible` slice, in order. We
|
|
194
|
+
// rebuild rather than diff because group sizes are small.
|
|
195
|
+
this.childContainer.clear();
|
|
196
|
+
visible.forEach((child, idx) => {
|
|
197
|
+
const isLast = idx === visible.length - 1;
|
|
198
|
+
this.repaintChild(child, isLast);
|
|
199
|
+
this.childContainer.addChild(child.text);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
302
202
|
|
|
303
|
-
private repaintChild(child: GroupChild, isLast
|
|
203
|
+
private repaintChild(child: GroupChild, isLast: boolean): void {
|
|
304
204
|
let tail: string;
|
|
305
205
|
if (!child.status) {
|
|
306
206
|
tail = ` ${theme.fg("muted", "…")}`;
|
|
@@ -311,8 +211,6 @@ export class ToolGroup extends Container {
|
|
|
311
211
|
tail = ` ${mark}${sum}`;
|
|
312
212
|
}
|
|
313
213
|
const connector = isLast ? "└" : "├";
|
|
314
|
-
// Tool name omitted when it duplicates the kind header (e.g. read_file
|
|
315
|
-
// children under "◆ read").
|
|
316
214
|
const namePart = child.name !== this.kind
|
|
317
215
|
? `${theme.bold(theme.fg("toolTitle", child.name))} `
|
|
318
216
|
: "";
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Default schema-style renderers shipped with ashi. Each model below could
|
|
2
|
+
// equally well live in an external extension — they use only the public
|
|
3
|
+
// "@guanyilun/ashi/render" surface, proving the schema covers ashi's own
|
|
4
|
+
// variety.
|
|
5
|
+
|
|
6
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
7
|
+
import type { RenderModel, Segment, ToolDisplay, TitleIcon } from "./schema.js";
|
|
8
|
+
|
|
9
|
+
function parseRaw(raw: unknown): Record<string, unknown> {
|
|
10
|
+
if (typeof raw === "string") {
|
|
11
|
+
try { return JSON.parse(raw) as Record<string, unknown>; } catch { return {}; }
|
|
12
|
+
}
|
|
13
|
+
if (raw && typeof raw === "object") return raw as Record<string, unknown>;
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const str = (v: unknown): string | undefined =>
|
|
18
|
+
typeof v === "string" && v.length > 0 ? v : undefined;
|
|
19
|
+
const num = (v: unknown): number | undefined =>
|
|
20
|
+
typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
21
|
+
|
|
22
|
+
function compact(s: string, max = 80): string {
|
|
23
|
+
const c = s.replace(/\s+/g, " ").trim();
|
|
24
|
+
return c.length > max ? c.slice(0, max - 1) + "…" : c;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function relativize(fp: string): string {
|
|
28
|
+
const home = process.env.HOME;
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
if (fp.startsWith(`${cwd}/`)) return fp.slice(cwd.length + 1);
|
|
31
|
+
if (home && fp.startsWith(`${home}/`)) return `~/${fp.slice(home.length + 1)}`;
|
|
32
|
+
return fp;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const nameSeg = (text: string): Segment => ({ text, style: { bold: true, color: "toolTitle" } });
|
|
36
|
+
const accentSeg = (text: string): Segment => ({ text, style: { color: "accent" } });
|
|
37
|
+
const mutedSeg = (text: string): Segment => ({ text, style: { color: "muted" } });
|
|
38
|
+
const warnSeg = (text: string): Segment => ({ text, style: { color: "warning" } });
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// bash — full command toggle on Ctrl+O, syntax-highlighted, streaming output.
|
|
42
|
+
|
|
43
|
+
interface BashInit { command: string; timeout?: number }
|
|
44
|
+
|
|
45
|
+
const bashModel: RenderModel<BashInit> = {
|
|
46
|
+
initial: ({ rawInput }) => {
|
|
47
|
+
const r = parseRaw(rawInput);
|
|
48
|
+
return { command: str(r.command) ?? "…", timeout: num(r.timeout) };
|
|
49
|
+
},
|
|
50
|
+
view: (s, env): ToolDisplay => {
|
|
51
|
+
const title: Segment[] = [
|
|
52
|
+
nameSeg("$ "),
|
|
53
|
+
{ text: env.expanded ? s.command : compact(s.command), highlight: "bash" },
|
|
54
|
+
];
|
|
55
|
+
if (s.timeout !== undefined) title.push(mutedSeg(` (timeout ${s.timeout}s)`));
|
|
56
|
+
return {
|
|
57
|
+
title,
|
|
58
|
+
status: s.status,
|
|
59
|
+
body: { kind: "stream", text: s.output },
|
|
60
|
+
expandable: true,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// read — file path + optional offset:limit range.
|
|
67
|
+
|
|
68
|
+
interface ReadInit { path: string; range?: string }
|
|
69
|
+
|
|
70
|
+
const readModel: RenderModel<ReadInit> = {
|
|
71
|
+
initial: ({ rawInput }) => {
|
|
72
|
+
const r = parseRaw(rawInput);
|
|
73
|
+
const path = str(r.file_path) ?? str(r.path);
|
|
74
|
+
const offset = num(r.offset);
|
|
75
|
+
const limit = num(r.limit);
|
|
76
|
+
let range: string | undefined;
|
|
77
|
+
if (offset !== undefined || limit !== undefined) {
|
|
78
|
+
const from = offset ?? 1;
|
|
79
|
+
const to = limit !== undefined ? from + limit - 1 : undefined;
|
|
80
|
+
range = to ? `:${from}-${to}` : `:${from}`;
|
|
81
|
+
}
|
|
82
|
+
return { path: path ? relativize(path) : "…", range };
|
|
83
|
+
},
|
|
84
|
+
view: (s) => ({
|
|
85
|
+
titleIcon: "read",
|
|
86
|
+
title: [
|
|
87
|
+
nameSeg("read "),
|
|
88
|
+
accentSeg(s.path),
|
|
89
|
+
...(s.range ? [warnSeg(s.range)] : []),
|
|
90
|
+
],
|
|
91
|
+
status: s.status,
|
|
92
|
+
body: { kind: "stream", text: s.output },
|
|
93
|
+
expandable: true,
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// grep / glob / ls — pattern + scope.
|
|
99
|
+
|
|
100
|
+
interface GrepInit { pattern: string; scope: string; extras: string }
|
|
101
|
+
|
|
102
|
+
const grepModel: RenderModel<GrepInit> = {
|
|
103
|
+
initial: ({ rawInput }) => {
|
|
104
|
+
const r = parseRaw(rawInput);
|
|
105
|
+
const glob = str(r.glob);
|
|
106
|
+
const limit = num(r.limit);
|
|
107
|
+
const extras = [glob ? `(${glob})` : "", limit !== undefined ? `limit ${limit}` : ""]
|
|
108
|
+
.filter(Boolean).join(" ");
|
|
109
|
+
return {
|
|
110
|
+
pattern: str(r.pattern) ?? "…",
|
|
111
|
+
scope: relativize(str(r.path) ?? "."),
|
|
112
|
+
extras,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
view: (s) => ({
|
|
116
|
+
titleIcon: "search",
|
|
117
|
+
title: [
|
|
118
|
+
nameSeg("grep "),
|
|
119
|
+
accentSeg(`/${s.pattern}/`),
|
|
120
|
+
{ text: " " },
|
|
121
|
+
mutedSeg(`in ${s.scope}`),
|
|
122
|
+
...(s.extras ? [mutedSeg(` ${s.extras}`)] : []),
|
|
123
|
+
],
|
|
124
|
+
status: s.status,
|
|
125
|
+
body: { kind: "stream", text: s.output },
|
|
126
|
+
expandable: true,
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
interface PathPatternInit { pattern: string; scope: string }
|
|
131
|
+
|
|
132
|
+
const globModel: RenderModel<PathPatternInit> = {
|
|
133
|
+
initial: ({ rawInput }) => {
|
|
134
|
+
const r = parseRaw(rawInput);
|
|
135
|
+
return { pattern: str(r.pattern) ?? "…", scope: relativize(str(r.path) ?? ".") };
|
|
136
|
+
},
|
|
137
|
+
view: (s) => ({
|
|
138
|
+
titleIcon: "search",
|
|
139
|
+
title: [nameSeg("glob "), accentSeg(s.pattern), { text: " " }, mutedSeg(`in ${s.scope}`)],
|
|
140
|
+
status: s.status,
|
|
141
|
+
body: { kind: "stream", text: s.output },
|
|
142
|
+
expandable: true,
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
interface LsInit { path: string }
|
|
147
|
+
|
|
148
|
+
const lsModel: RenderModel<LsInit> = {
|
|
149
|
+
initial: ({ rawInput }) => {
|
|
150
|
+
const r = parseRaw(rawInput);
|
|
151
|
+
return { path: relativize(str(r.path) ?? ".") };
|
|
152
|
+
},
|
|
153
|
+
view: (s) => ({
|
|
154
|
+
titleIcon: "read",
|
|
155
|
+
title: [nameSeg("ls "), accentSeg(s.path)],
|
|
156
|
+
status: s.status,
|
|
157
|
+
body: { kind: "stream", text: s.output },
|
|
158
|
+
expandable: true,
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// edit_file / write_file — path + framework-supplied diff body. The "Edited
|
|
164
|
+
// /path (+N -M)" streaming text is suppressed because the diff body already
|
|
165
|
+
// shows that information; per-line output reappears via expand on Ctrl+O.
|
|
166
|
+
|
|
167
|
+
interface EditInit { path: string; verb: string }
|
|
168
|
+
|
|
169
|
+
function editLikeModel(verb: string): RenderModel<EditInit> {
|
|
170
|
+
return {
|
|
171
|
+
initial: ({ rawInput }) => {
|
|
172
|
+
const r = parseRaw(rawInput);
|
|
173
|
+
const path = str(r.file_path) ?? str(r.path);
|
|
174
|
+
return { path: path ? relativize(path) : "…", verb };
|
|
175
|
+
},
|
|
176
|
+
view: (s, env) => ({
|
|
177
|
+
titleIcon: "edit",
|
|
178
|
+
title: [nameSeg(`${s.verb} `), accentSeg(s.path)],
|
|
179
|
+
status: s.status,
|
|
180
|
+
// Collapsed-with-diff: diff only (the "Edited /path (+N -M)" stream line
|
|
181
|
+
// restates the call line). Expanded-with-diff: diff + stream output.
|
|
182
|
+
body: s.hasDiff
|
|
183
|
+
? (env.expanded
|
|
184
|
+
? { kind: "compound", parts: [{ kind: "diff" }, { kind: "stream", text: s.output }] }
|
|
185
|
+
: { kind: "diff" })
|
|
186
|
+
: { kind: "stream", text: s.output },
|
|
187
|
+
expandable: true,
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// default — fallback for any tool without a specific renderer.
|
|
194
|
+
|
|
195
|
+
interface DefaultInit { title: string; detail?: string; icon: TitleIcon }
|
|
196
|
+
|
|
197
|
+
const defaultModel: RenderModel<DefaultInit> = {
|
|
198
|
+
initial: ({ title, displayDetail }) => ({
|
|
199
|
+
title,
|
|
200
|
+
detail: displayDetail,
|
|
201
|
+
icon: "generic",
|
|
202
|
+
}),
|
|
203
|
+
view: (s) => ({
|
|
204
|
+
titleIcon: s.icon,
|
|
205
|
+
title: [
|
|
206
|
+
nameSeg(s.title),
|
|
207
|
+
...(s.detail ? [{ text: " " }, mutedSeg(s.detail)] : []),
|
|
208
|
+
],
|
|
209
|
+
status: s.status,
|
|
210
|
+
body: { kind: "stream", text: s.output },
|
|
211
|
+
expandable: true,
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
export function registerDefaultSchemaRenderers(ctx: ExtensionContext): void {
|
|
218
|
+
ctx.define("ashi:render-tool:bash", () => bashModel);
|
|
219
|
+
ctx.define("ashi:render-tool:read_file", () => readModel);
|
|
220
|
+
ctx.define("ashi:render-tool:read", () => readModel);
|
|
221
|
+
ctx.define("ashi:render-tool:grep", () => grepModel);
|
|
222
|
+
ctx.define("ashi:render-tool:glob", () => globModel);
|
|
223
|
+
ctx.define("ashi:render-tool:ls", () => lsModel);
|
|
224
|
+
ctx.define("ashi:render-tool:edit_file", () => editLikeModel("edit"));
|
|
225
|
+
ctx.define("ashi:render-tool:edit", () => editLikeModel("edit"));
|
|
226
|
+
ctx.define("ashi:render-tool:write_file", () => editLikeModel("write"));
|
|
227
|
+
ctx.define("ashi:render-tool:write", () => editLikeModel("write"));
|
|
228
|
+
ctx.define("ashi:render-tool:default", () => defaultModel);
|
|
229
|
+
}
|
|
@@ -7,12 +7,11 @@ export interface ToolEntryConfig {
|
|
|
7
7
|
previewLines: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export interface
|
|
11
|
-
|
|
12
|
-
[toolName: string]: ToolEntryConfig;
|
|
10
|
+
export interface DisplayResolver {
|
|
11
|
+
resolve(name: string, modelDisplay?: Partial<ToolEntryConfig>): ToolEntryConfig;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
const DEFAULT_ENTRY: ToolEntryConfig = { result: "preview", previewLines:
|
|
14
|
+
const DEFAULT_ENTRY: ToolEntryConfig = { result: "preview", previewLines: 5 };
|
|
16
15
|
|
|
17
16
|
const BUILTIN_OVERRIDES: Record<string, Partial<ToolEntryConfig>> = {
|
|
18
17
|
read: { result: "hidden" },
|
|
@@ -20,7 +19,7 @@ const BUILTIN_OVERRIDES: Record<string, Partial<ToolEntryConfig>> = {
|
|
|
20
19
|
grep: { result: "summary" },
|
|
21
20
|
find: { result: "summary" },
|
|
22
21
|
glob: { result: "summary" },
|
|
23
|
-
bash: { result: "preview"
|
|
22
|
+
bash: { result: "preview" },
|
|
24
23
|
edit: { result: "preview" },
|
|
25
24
|
edit_file: { result: "preview" },
|
|
26
25
|
write: { result: "preview" },
|
|
@@ -29,6 +28,14 @@ const BUILTIN_OVERRIDES: Record<string, Partial<ToolEntryConfig>> = {
|
|
|
29
28
|
|
|
30
29
|
interface AshiSettings extends Record<string, unknown> {
|
|
31
30
|
display?: Record<string, Partial<ToolEntryConfig>>;
|
|
31
|
+
groupMaxVisible?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadGroupMaxVisible(): number {
|
|
35
|
+
const ashi = getExtensionSettings<AshiSettings>("ashi", {});
|
|
36
|
+
const v = ashi.groupMaxVisible;
|
|
37
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 2) return Infinity;
|
|
38
|
+
return Math.floor(v);
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
function mergeEntry(base: ToolEntryConfig, patch?: Partial<ToolEntryConfig>): ToolEntryConfig {
|
|
@@ -39,24 +46,16 @@ function mergeEntry(base: ToolEntryConfig, patch?: Partial<ToolEntryConfig>): To
|
|
|
39
46
|
};
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
export function
|
|
49
|
+
export function loadDisplayResolver(): DisplayResolver {
|
|
43
50
|
const ashi = getExtensionSettings<AshiSettings>("ashi", {});
|
|
44
51
|
const userDisplay = ashi.display ?? {};
|
|
45
52
|
const userDefault = mergeEntry(DEFAULT_ENTRY, userDisplay.default);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
userDisplay[name],
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
return config;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function entryFor(config: ToolDisplayConfig, name: string): ToolEntryConfig {
|
|
61
|
-
return config[name] ?? config.default;
|
|
53
|
+
return {
|
|
54
|
+
resolve(name, modelDisplay) {
|
|
55
|
+
let entry = mergeEntry(userDefault, BUILTIN_OVERRIDES[name]);
|
|
56
|
+
if (modelDisplay) entry = mergeEntry(entry, modelDisplay);
|
|
57
|
+
if (userDisplay[name]) entry = mergeEntry(entry, userDisplay[name]);
|
|
58
|
+
return entry;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
62
61
|
}
|