agent-sh 0.13.1 → 0.13.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.js +6 -5
- package/dist/cli/install.js +0 -5
- package/dist/core/settings.d.ts +2 -0
- package/dist/core/settings.js +1 -0
- package/dist/utils/diff-renderer.js +5 -5
- package/dist/utils/llm-client.d.ts +3 -0
- package/dist/utils/llm-client.js +8 -2
- package/examples/extensions/ash-acp-bridge/src/index.ts +2 -1
- package/examples/extensions/ashi/README.md +104 -127
- package/examples/extensions/ashi/package.json +2 -2
- package/examples/extensions/ashi/src/capture.ts +17 -1
- package/examples/extensions/ashi/src/cli.ts +3 -11
- package/examples/extensions/ashi/src/components.ts +21 -8
- package/examples/extensions/ashi/src/default-renderers.ts +23 -5
- package/examples/extensions/ashi/src/frontend.ts +47 -19
- package/examples/extensions/ashi/src/hooks.ts +5 -3
- package/examples/extensions/ashi/src/session-store.ts +1 -0
- package/package.json +1 -1
package/dist/agent/agent-loop.js
CHANGED
|
@@ -91,7 +91,7 @@ export class AgentLoop {
|
|
|
91
91
|
bus;
|
|
92
92
|
llmClient;
|
|
93
93
|
handlers;
|
|
94
|
-
thinkingLevel = "off";
|
|
94
|
+
thinkingLevel = getSettings().thinkingLevel ?? "off";
|
|
95
95
|
compositor = null;
|
|
96
96
|
toolProtocol;
|
|
97
97
|
instanceId;
|
|
@@ -289,6 +289,7 @@ export class AgentLoop {
|
|
|
289
289
|
return;
|
|
290
290
|
}
|
|
291
291
|
this.thinkingLevel = level;
|
|
292
|
+
updateSettings({ thinkingLevel: level });
|
|
292
293
|
this.bus.emit("config:changed", {});
|
|
293
294
|
});
|
|
294
295
|
onPipe("config:get-thinking", () => {
|
|
@@ -998,10 +999,7 @@ export class AgentLoop {
|
|
|
998
999
|
responseText = await this.executeLoop(signal);
|
|
999
1000
|
}
|
|
1000
1001
|
catch (e) {
|
|
1001
|
-
if (signal.aborted
|
|
1002
|
-
this.bus.emit("agent:cancelled", {});
|
|
1003
|
-
}
|
|
1004
|
-
else if (!signal.aborted) {
|
|
1002
|
+
if (!signal.aborted) {
|
|
1005
1003
|
if (e instanceof Error)
|
|
1006
1004
|
console.error("[agent-sh] query failed:\n" + e.stack);
|
|
1007
1005
|
const msg = this.formatError(e);
|
|
@@ -1009,6 +1007,9 @@ export class AgentLoop {
|
|
|
1009
1007
|
}
|
|
1010
1008
|
}
|
|
1011
1009
|
finally {
|
|
1010
|
+
if (signal.aborted && signal.reason !== "silent") {
|
|
1011
|
+
this.bus.emit("agent:cancelled", {});
|
|
1012
|
+
}
|
|
1012
1013
|
// Ensure any buffered text in the stream transform pipeline gets
|
|
1013
1014
|
// flushed as a complete line before response-done closes the box.
|
|
1014
1015
|
if (responseText && !responseText.endsWith("\n")) {
|
package/dist/cli/install.js
CHANGED
|
@@ -130,11 +130,6 @@ function normalizeBin(pkg) {
|
|
|
130
130
|
function maybeNpmBuild(target, pkg) {
|
|
131
131
|
if (!pkg.scripts?.build)
|
|
132
132
|
return;
|
|
133
|
-
const binPaths = Object.values(normalizeBin(pkg)).map((p) => path.join(target, p));
|
|
134
|
-
if (binPaths.length === 0)
|
|
135
|
-
return;
|
|
136
|
-
if (binPaths.every((p) => fs.existsSync(p)))
|
|
137
|
-
return;
|
|
138
133
|
console.log(`Running npm run build in ${target}...`);
|
|
139
134
|
const result = spawnSync("npm", ["run", "build"], { cwd: target, stdio: "inherit" });
|
|
140
135
|
if (result.status !== 0) {
|
package/dist/core/settings.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface Settings {
|
|
|
44
44
|
defaultProvider?: string;
|
|
45
45
|
/** Preferred agent backend (extension name, e.g. "pi", "claude-code"). */
|
|
46
46
|
defaultBackend?: string;
|
|
47
|
+
/** Default thinking/reasoning effort level for new sessions ("off"|"low"|"medium"|"high"). */
|
|
48
|
+
thinkingLevel?: string;
|
|
47
49
|
/** Shell output lines before spill-to-tempfile kicks in. */
|
|
48
50
|
shellTruncateThreshold?: number;
|
|
49
51
|
/** Lines kept from start of spilled shell output. */
|
package/dist/core/settings.js
CHANGED
|
@@ -275,14 +275,14 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
275
275
|
removedText = lang ? highlightLine(raw, lang) : raw;
|
|
276
276
|
}
|
|
277
277
|
if (useTrueColor) {
|
|
278
|
-
out.push(padToWidth(`${p.errorBg}${p.error}- ${no} │ ${preserveBg(removedText, p.errorBg)}
|
|
278
|
+
out.push(padToWidth(`${p.errorBg}${p.error}- ${no} │ ${preserveBg(removedText, p.errorBg)}`, textWidth) + p.reset);
|
|
279
279
|
}
|
|
280
280
|
else {
|
|
281
281
|
out.push(`${p.error}- ${no} │ ${removedText}${p.reset}`);
|
|
282
282
|
}
|
|
283
283
|
if (addedText !== null && addedNo !== null) {
|
|
284
284
|
if (useTrueColor) {
|
|
285
|
-
out.push(padToWidth(`${p.successBg}${p.success}+ ${addedNo} │ ${preserveBg(addedText, p.successBg)}
|
|
285
|
+
out.push(padToWidth(`${p.successBg}${p.success}+ ${addedNo} │ ${preserveBg(addedText, p.successBg)}`, textWidth) + p.reset);
|
|
286
286
|
}
|
|
287
287
|
else {
|
|
288
288
|
out.push(`${p.success}+ ${addedNo} │ ${addedText}${p.reset}`);
|
|
@@ -296,7 +296,7 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
296
296
|
const raw = truncateText(line.text, lineTextW);
|
|
297
297
|
const text = lang ? highlightLine(raw, lang) : raw;
|
|
298
298
|
if (useTrueColor) {
|
|
299
|
-
out.push(padToWidth(`${p.successBg}${p.success}+ ${no} │ ${preserveBg(text, p.successBg)}
|
|
299
|
+
out.push(padToWidth(`${p.successBg}${p.success}+ ${no} │ ${preserveBg(text, p.successBg)}`, textWidth) + p.reset);
|
|
300
300
|
}
|
|
301
301
|
else {
|
|
302
302
|
out.push(`${p.success}+ ${no} │ ${text}${p.reset}`);
|
|
@@ -368,7 +368,7 @@ function renderSplitHunk(hunk, layout) {
|
|
|
368
368
|
}
|
|
369
369
|
else if (row.left.type === "removed") {
|
|
370
370
|
if (useTrueColor) {
|
|
371
|
-
leftCol = padToWidth(`${p.errorBg}${p.error}${leftNo} │ ${preserveBg(leftText, p.errorBg)}
|
|
371
|
+
leftCol = padToWidth(`${p.errorBg}${p.error}${leftNo} │ ${preserveBg(leftText, p.errorBg)}`, colWidth) + p.reset;
|
|
372
372
|
}
|
|
373
373
|
else {
|
|
374
374
|
leftCol = padToWidth(`${p.error}${leftNo} │ ${leftText}${p.reset}`, colWidth);
|
|
@@ -382,7 +382,7 @@ function renderSplitHunk(hunk, layout) {
|
|
|
382
382
|
}
|
|
383
383
|
else if (row.right.type === "added") {
|
|
384
384
|
if (useTrueColor) {
|
|
385
|
-
rightCol = padToWidth(`${p.successBg}${p.success}${rightNo} │ ${preserveBg(rightText, p.successBg)}
|
|
385
|
+
rightCol = padToWidth(`${p.successBg}${p.success}${rightNo} │ ${preserveBg(rightText, p.successBg)}`, colWidth) + p.reset;
|
|
386
386
|
}
|
|
387
387
|
else {
|
|
388
388
|
rightCol = padToWidth(`${p.success}${rightNo} │ ${rightText}${p.reset}`, colWidth);
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
import OpenAI from "openai";
|
|
9
9
|
import type { ChatCompletionMessageParam, ChatCompletionTool } from "openai/resources/chat/completions.js";
|
|
10
10
|
export type { ChatCompletionMessageParam, ChatCompletionTool };
|
|
11
|
+
export type AgentShMessage = ChatCompletionMessageParam & {
|
|
12
|
+
meta?: Record<string, unknown>;
|
|
13
|
+
};
|
|
11
14
|
export interface LlmClientConfig {
|
|
12
15
|
apiKey: string;
|
|
13
16
|
baseURL?: string;
|
package/dist/utils/llm-client.js
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
* (command suggestions, completions).
|
|
7
7
|
*/
|
|
8
8
|
import OpenAI from "openai";
|
|
9
|
+
function stripMeta(m) {
|
|
10
|
+
if (!("meta" in m))
|
|
11
|
+
return m;
|
|
12
|
+
const { meta: _meta, ...rest } = m;
|
|
13
|
+
return rest;
|
|
14
|
+
}
|
|
9
15
|
function attributionHeaders(config) {
|
|
10
16
|
return {
|
|
11
17
|
"HTTP-Referer": config.appUrl ?? "https://agent-sh.dev",
|
|
@@ -40,7 +46,7 @@ export class LlmClient {
|
|
|
40
46
|
const body = {
|
|
41
47
|
...rest,
|
|
42
48
|
model: model ?? this.model,
|
|
43
|
-
messages,
|
|
49
|
+
messages: messages.map(stripMeta),
|
|
44
50
|
tools: tools?.length ? tools : undefined,
|
|
45
51
|
max_tokens: max_tokens ?? 65536,
|
|
46
52
|
stream: true,
|
|
@@ -53,7 +59,7 @@ export class LlmClient {
|
|
|
53
59
|
const body = {
|
|
54
60
|
...rest,
|
|
55
61
|
model: model ?? this.model,
|
|
56
|
-
messages,
|
|
62
|
+
messages: messages.map(stripMeta),
|
|
57
63
|
max_tokens: max_tokens ?? 1024,
|
|
58
64
|
};
|
|
59
65
|
const response = await this.client.chat.completions.create(body);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* In agent-shell (Emacs):
|
|
12
12
|
* (setq agent-shell-agentsh-acp-command '("agent-sh-acp"))
|
|
13
13
|
*/
|
|
14
|
-
import { createCore, type AgentShellCore } from "agent-sh";
|
|
14
|
+
import { createCore, NoopHistory, type AgentShellCore } from "agent-sh";
|
|
15
15
|
import { loadExtensions } from "agent-sh/extension-loader";
|
|
16
16
|
import { loadBuiltinExtensions } from "agent-sh/extensions";
|
|
17
17
|
import { activateAgent } from "agent-sh/agent";
|
|
@@ -483,6 +483,7 @@ async function handleSessionNew(id: number | string, params: Record<string, unkn
|
|
|
483
483
|
core = createCore({
|
|
484
484
|
model: cliArgs.model,
|
|
485
485
|
provider: cliArgs.provider,
|
|
486
|
+
history: new NoopHistory(),
|
|
486
487
|
});
|
|
487
488
|
wireEvents(core);
|
|
488
489
|
|
|
@@ -1,71 +1,90 @@
|
|
|
1
1
|
# ashi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@guanyilun/ashi)
|
|
4
|
+
[](https://github.com/guanyilun/agent-sh/blob/main/LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
the only frontend. Backend, tools, slash commands, providers, and skills come along unchanged.
|
|
6
|
+
`ash` (the built-in agent of [agent-sh](https://github.com/guanyilun/agent-sh)) running as a standalone interactive TUI — no shell underneath, just the agent.
|
|
7
|
+
|
|
8
|
+
Same backend, tools, slash commands, providers, and skills as `agent-sh`, mounted in a chat-style interface with session history, branching, and LLM-driven compaction.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
|
|
14
|
-
export PATH="$HOME/.agent-sh/bin:$PATH"
|
|
13
|
+
npm install -g @guanyilun/ashi
|
|
15
14
|
ashi
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
`npm install` and `npm run build` there, and symlinks the built bin into `~/.agent-sh/bin/`.
|
|
17
|
+
Reads `~/.agent-sh/settings.json` for provider profiles and defaults, same file as `agent-sh`. The quickest path is exporting `OPENROUTER_API_KEY` or `OPENAI_API_KEY` and running `ashi`.
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
To scaffold the config directory from scratch:
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
```bash
|
|
22
|
+
ashi init # creates ~/.agent-sh/settings.json and AGENTS.md
|
|
23
|
+
ashi auth login # store an API key interactively
|
|
24
|
+
```
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
for the full `settings.json` schema, provider profiles, and model selection details.
|
|
26
|
+
## Usage
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
```bash
|
|
29
|
+
ashi # launch with defaults
|
|
30
|
+
ashi --provider openrouter # pick a provider profile
|
|
31
|
+
ashi --model anthropic/claude-sonnet-4 # override model
|
|
32
|
+
ashi -e claude-code-bridge --backend claude-code # swap the agent backend
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### CLI flags
|
|
30
36
|
|
|
31
37
|
```
|
|
32
38
|
--provider <name> Provider profile from ~/.agent-sh/settings.json
|
|
33
39
|
--model <id> Override model
|
|
34
40
|
--api-key <key> Direct API key
|
|
35
41
|
--base-url <url> OpenAI-compatible base URL
|
|
36
|
-
--backend <name> Agent backend (default: ash)
|
|
42
|
+
--backend <name> Agent backend (default: ash). Requires the matching
|
|
43
|
+
backend extension to be loaded, e.g. via -e.
|
|
37
44
|
-e, --extensions Extra extensions to load (comma-separated)
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
[Extensions](https://github.com/guanyilun/agent-sh/blob/main/docs/extensions.md) for the
|
|
42
|
-
`ExtensionContext` API, event bus, content transforms, and custom backends.
|
|
47
|
+
The built-in backend is `ash`. To use a different one (claude-code, opencode, pi), load the corresponding bridge extension with `-e` and pass `--backend <name>`.
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
### Management subcommands
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
```
|
|
52
|
+
ashi install <name> [--force] Install an extension into ~/.agent-sh/extensions/
|
|
53
|
+
ashi uninstall <name> Remove an installed extension
|
|
54
|
+
ashi list List installed extensions
|
|
55
|
+
ashi auth login [provider] Store an API key (interactive)
|
|
56
|
+
ashi auth logout <provider> Remove a stored key
|
|
57
|
+
ashi auth list Show configured providers
|
|
58
|
+
ashi init [--force] Scaffold ~/.agent-sh/ (settings, AGENTS.md)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
These mirror `agent-sh`'s management commands so `ashi` works as a standalone CLI without needing the full `agent-sh` install.
|
|
62
|
+
|
|
63
|
+
### Keybindings
|
|
47
64
|
|
|
48
65
|
```
|
|
49
|
-
Esc
|
|
50
|
-
Ctrl+C
|
|
51
|
-
Ctrl+D
|
|
52
|
-
Ctrl+T
|
|
53
|
-
|
|
66
|
+
Esc Cancel active turn
|
|
67
|
+
Ctrl+C Clear editor
|
|
68
|
+
Ctrl+D Quit (when editor is empty)
|
|
69
|
+
Ctrl+T Toggle thinking-block visibility (hidden by default)
|
|
70
|
+
Shift+Tab Cycle thinking level (off → low → medium → high → …)
|
|
71
|
+
Ctrl+O Expand/collapse the most recent tool result
|
|
54
72
|
```
|
|
55
73
|
|
|
74
|
+
The current thinking level is shown in the footer as `[level]` next to the model name.
|
|
75
|
+
|
|
56
76
|
## Sessions
|
|
57
77
|
|
|
58
|
-
|
|
78
|
+
Many sessions per cwd, fresh by default:
|
|
59
79
|
|
|
60
80
|
```
|
|
61
|
-
/resume
|
|
62
|
-
/new
|
|
63
|
-
/name <text>
|
|
64
|
-
/sessions
|
|
81
|
+
/resume Browse past sessions in this cwd (interactive picker)
|
|
82
|
+
/new Start a fresh session (discards in-memory context)
|
|
83
|
+
/name <text> Set a display name for the current session
|
|
84
|
+
/sessions Text dump of all sessions in this cwd
|
|
65
85
|
```
|
|
66
86
|
|
|
67
|
-
Each session is its own tree (one JSONL file per session). Every entry has an `id` and
|
|
68
|
-
`parentId`; sibling branches stay on disk; you can rewind and branch within a session.
|
|
87
|
+
Each session is its own tree (one JSONL file per session). Every entry has an `id` and `parentId`; sibling branches stay on disk; you can rewind and branch within a session.
|
|
69
88
|
|
|
70
89
|
```
|
|
71
90
|
/fork Interactive in-session tree picker
|
|
@@ -73,8 +92,7 @@ Each session is its own tree (one JSONL file per session). Every entry has an `i
|
|
|
73
92
|
/branch Text dump of the active branch (root → leaf)
|
|
74
93
|
```
|
|
75
94
|
|
|
76
|
-
Storage: `~/.agent-sh/extensions/ashi/history/<cwd-slug>/sessions/<id>.jsonl`. Each line
|
|
77
|
-
is a `SessionEntry`:
|
|
95
|
+
Storage: `~/.agent-sh/extensions/ashi/history/<cwd-slug>/sessions/<id>.jsonl`. Each line is a `SessionEntry`:
|
|
78
96
|
|
|
79
97
|
```typescript
|
|
80
98
|
type SessionEntry =
|
|
@@ -83,65 +101,56 @@ type SessionEntry =
|
|
|
83
101
|
| { type: "compaction"; id; parentId; timestamp; summary; firstKeptId; tokensBefore };
|
|
84
102
|
```
|
|
85
103
|
|
|
86
|
-
Raw `AgentMessage` objects are stored verbatim (full tool call arguments, tool results,
|
|
87
|
-
etc.) so `/resume` and `/fork` faithfully reconstruct the original conversation — same
|
|
88
|
-
shape as pi's session format.
|
|
89
|
-
|
|
90
|
-
The kernel side adds three small handlers:
|
|
91
|
-
optional `parentSeq`/`getBranch`/`getTree`/`setLeaf` on `NuclearEntry`/`HistoryAdapter`
|
|
92
|
-
(useful for tree-aware HistoryAdapters in general — not used by this extension);
|
|
93
|
-
`conversation:allocate-seq` and `conversation:reset-for-session` so multi-session adapters
|
|
94
|
-
can swap context without nuclear-state bleed-through.
|
|
95
|
-
|
|
96
|
-
ashi itself bypasses agent-sh's `NuclearEntry` pipeline entirely by installing a
|
|
97
|
-
`NoopHistory` adapter — raw messages are captured directly via `agent:processing-done`
|
|
98
|
-
and the `conversation:get-messages` handler.
|
|
104
|
+
Raw `AgentMessage` objects are stored verbatim (full tool call arguments, tool results, etc.) so `/resume` and `/fork` faithfully reconstruct the original conversation.
|
|
99
105
|
|
|
100
106
|
## Compaction
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
LLM-driven path:
|
|
108
|
+
LLM-driven structured compaction, triggered automatically when prompt tokens cross the threshold or manually with `/compact`:
|
|
104
109
|
|
|
105
|
-
1.
|
|
106
|
-
|
|
107
|
-
2. LLM summarizes the older span into the pi structured format (Goal / Constraints /
|
|
108
|
-
Progress / Decisions / Next Steps / Critical Context).
|
|
110
|
+
1. Walk back from the newest message until ~20K tokens are kept; never cut at tool results or mid–assistant-tool-call group.
|
|
111
|
+
2. LLM summarizes the older span into a structured format (Goal / Constraints / Progress / Decisions / Next Steps / Critical Context).
|
|
109
112
|
3. The live message array becomes `[summary, ...kept messages]`.
|
|
110
|
-
4. The summary
|
|
111
|
-
`firstKeptId`, and `tokensBefore` — same shape as pi's compaction. Subsequent
|
|
112
|
-
compactions reference the previous one's summary so chains stay coherent.
|
|
113
|
+
4. The summary is persisted as a `CompactionEntry` carrying `summary`, `firstKeptId`, and `tokensBefore`. Subsequent compactions reference the previous one's summary so chains stay coherent.
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
`/compact`. If the LLM call fails or the conversation is too short, falls through to the
|
|
116
|
-
default eviction.
|
|
115
|
+
If the LLM call fails or the conversation is too short, falls through to the default eviction.
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
extension. The kernel side is just the advisable `conversation:compact` seam.
|
|
117
|
+
## Display configuration
|
|
120
118
|
|
|
121
|
-
|
|
119
|
+
Per-tool compactness lives under `ashi.display` in `~/.agent-sh/settings.json`:
|
|
122
120
|
|
|
123
|
-
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"ashi": {
|
|
124
|
+
"display": {
|
|
125
|
+
"default": { "result": "preview", "previewLines": 8 },
|
|
126
|
+
"read": { "result": "hidden" },
|
|
127
|
+
"ls": { "result": "hidden" },
|
|
128
|
+
"grep": { "result": "summary" },
|
|
129
|
+
"bash": { "result": "preview", "previewLines": 12 },
|
|
130
|
+
"edit": { "result": "preview" },
|
|
131
|
+
"write": { "result": "preview" }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
124
136
|
|
|
125
|
-
|
|
126
|
-
- Tool invocations with start/complete state
|
|
127
|
-
- Slash commands with autocomplete (`/help`, `/model`, `/backend`, `/resume`, `/new`, `/fork`, …)
|
|
128
|
-
- Multi-session tree history with `/resume` and `/fork` pickers
|
|
129
|
-
- LLM compaction with summaries that survive across `/resume`
|
|
130
|
-
- Loader, errors, info messages
|
|
131
|
-
- Inline images via the `image` ContentBlock and the `render:image` handler — the
|
|
132
|
-
bundled `latex-images` extension works against ashi without modification
|
|
133
|
-
(terminal must support iTerm2 or Kitty graphics)
|
|
137
|
+
`result` modes:
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
- `"hidden"` — call line only while streaming; line count (`↳ 42 lines`) after completion.
|
|
140
|
+
- `"summary"` — 2-line tail while streaming; line count after completion.
|
|
141
|
+
- `"preview"` — last `previewLines` lines of output (default 8), with a `... (N more lines)` hint when content overflows.
|
|
142
|
+
|
|
143
|
+
For `edit_file` / `write_file`, the diff frame is treated as the output and follows the same gating: shown for `preview`, hidden for `hidden`/`summary` (the call line already carries `+12 -3` stats). The line-count hint is suppressed for diff-producing tools so edits stay quiet.
|
|
144
|
+
|
|
145
|
+
Hit `Ctrl+O` to expand the most recent tool result inline regardless of mode. Press again to collapse.
|
|
146
|
+
|
|
147
|
+
Each tool inherits from `default` and is overridden by its own block. Unknown tool names fall through to `default`.
|
|
140
148
|
|
|
141
149
|
## Extension surface
|
|
142
150
|
|
|
143
|
-
Other extensions can override how chat entries render without forking ashi.
|
|
144
|
-
|
|
151
|
+
Other extensions can override how chat entries and tool results render without forking ashi. Hooks are exposed via `ctx.define` (defaults) + `ctx.advise` (override).
|
|
152
|
+
|
|
153
|
+
Components returned from these hooks are widgets from [`@earendil-works/pi-tui`](https://www.npmjs.com/package/@earendil-works/pi-tui) — the TUI framework ashi is built on.
|
|
145
154
|
|
|
146
155
|
### Chat hooks
|
|
147
156
|
|
|
@@ -153,9 +162,7 @@ Hooks are exposed via `ctx.define` (defaults) + `ctx.advise` (override).
|
|
|
153
162
|
|
|
154
163
|
### Tool hooks (per-tool)
|
|
155
164
|
|
|
156
|
-
Tool rendering is split into a call line (the input header) and a result body
|
|
157
|
-
(streaming output + final state). Each side is dispatched by tool name with a
|
|
158
|
-
`:default` fallback:
|
|
165
|
+
Tool rendering is split into a call line (the input header) and a result body (streaming output + final state). Each side is dispatched by tool name with a `:default` fallback:
|
|
159
166
|
|
|
160
167
|
| Hook | Args | Returns |
|
|
161
168
|
|---|---|---|
|
|
@@ -169,9 +176,7 @@ Tool rendering is split into a call line (the input header) and a result body
|
|
|
169
176
|
- `ToolCallView` extends `Component` with `setStatus({ exitCode, elapsedMs, summary })` — called once on completion.
|
|
170
177
|
- `ToolResultView` extends `Component` with `appendChunk(chunk)`, `setDiff(lines)`, `finalize({ exitCode, summary })`, and `toggleExpanded()` — ashi mutates the result view as output streams in and when the user hits `Ctrl+O`.
|
|
171
178
|
|
|
172
|
-
`mode` and `previewLines` on result args come from `ashi.display.{name}` config
|
|
173
|
-
(see below) so renderers can honor the user's compactness preference without
|
|
174
|
-
re-implementing the resolution logic.
|
|
179
|
+
`mode` and `previewLines` on result args come from `ashi.display.{name}` config so renderers can honor the user's compactness preference without re-implementing the resolution logic.
|
|
175
180
|
|
|
176
181
|
Example: override how `bash` calls render.
|
|
177
182
|
|
|
@@ -183,52 +188,22 @@ export default function activate(ctx) {
|
|
|
183
188
|
}
|
|
184
189
|
```
|
|
185
190
|
|
|
186
|
-
For non-render concerns (commands, settings, tools, providers) use the standard
|
|
187
|
-
agent-sh extension API.
|
|
191
|
+
For non-render concerns (commands, settings, tools, providers) use the standard `agent-sh` extension API. See the [agent-sh extension docs](https://github.com/guanyilun/agent-sh/blob/main/docs/extensions.md).
|
|
188
192
|
|
|
189
|
-
##
|
|
193
|
+
## Install from source
|
|
190
194
|
|
|
191
|
-
|
|
195
|
+
Alternative to the npm install, useful for hacking on ashi itself:
|
|
192
196
|
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"display": {
|
|
197
|
-
"default": { "result": "preview", "previewLines": 8 },
|
|
198
|
-
"read": { "result": "hidden" },
|
|
199
|
-
"ls": { "result": "hidden" },
|
|
200
|
-
"grep": { "result": "summary" },
|
|
201
|
-
"bash": { "result": "preview", "previewLines": 12 },
|
|
202
|
-
"edit": { "result": "preview" },
|
|
203
|
-
"write": { "result": "preview" }
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
197
|
+
```bash
|
|
198
|
+
agent-sh install ashi # copies examples/extensions/ashi → ~/.agent-sh/extensions/ashi
|
|
199
|
+
export PATH="$HOME/.agent-sh/bin:$PATH"
|
|
207
200
|
```
|
|
208
201
|
|
|
209
|
-
`
|
|
210
|
-
|
|
211
|
-
- `"hidden"` — call line only while streaming; line count (`↳ 42 lines`) after completion.
|
|
212
|
-
- `"summary"` — 2-line tail while streaming; line count after completion.
|
|
213
|
-
- `"preview"` — last `previewLines` lines of output (default 8), with a `... (N more lines)` hint when content overflows.
|
|
214
|
-
|
|
215
|
-
For `edit_file` / `write_file`, the diff frame is treated as the output and
|
|
216
|
-
follows the same gating: shown for `preview`, hidden for `hidden`/`summary`
|
|
217
|
-
(the call line already carries `+12 -3` stats). The line-count hint is
|
|
218
|
-
suppressed for diff-producing tools so edits stay quiet.
|
|
219
|
-
|
|
220
|
-
Hit `Ctrl+O` to expand the most recent tool result inline — shows the full
|
|
221
|
-
output buffer and the full diff regardless of mode. Press again to collapse.
|
|
222
|
-
|
|
223
|
-
Each tool inherits from `default` and is overridden by its own block. Unknown
|
|
224
|
-
tool names fall through to `default`. Built-in defaults aim for compactness
|
|
225
|
-
(`read`/`ls` hidden, `grep` summarized) — override under `default` to widen
|
|
226
|
-
everything, or under a specific tool to tune one.
|
|
202
|
+
`agent-sh install` runs `npm install` and `npm run build` in the copied directory and symlinks the built bin into `~/.agent-sh/bin/`.
|
|
227
203
|
|
|
228
204
|
## Development
|
|
229
205
|
|
|
230
|
-
`@guanyilun/ashi` depends on the published `agent-sh` package. To iterate against
|
|
231
|
-
the parent checkout instead, use `npm link`:
|
|
206
|
+
`@guanyilun/ashi` depends on the published `agent-sh` package. To iterate against a local checkout, use `npm link`:
|
|
232
207
|
|
|
233
208
|
```bash
|
|
234
209
|
# one-time: register the local agent-sh checkout
|
|
@@ -245,6 +220,8 @@ npm run dev # tsx-driven, no compile step
|
|
|
245
220
|
# or: npm run build && node dist/cli.js
|
|
246
221
|
```
|
|
247
222
|
|
|
248
|
-
Rebuild agent-sh (`npm run build` at the repo root) whenever you change the
|
|
249
|
-
|
|
250
|
-
|
|
223
|
+
Rebuild agent-sh (`npm run build` at the repo root) whenever you change the kernel — the link picks up `dist/` directly. To go back to the published version, run `npm unlink agent-sh && npm install` inside `examples/extensions/ashi`.
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guanyilun/ashi",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "Ash
|
|
4
|
+
"description": "Ash in an interactive TUI — agent-sh's built-in agent without the shell underneath",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
7
7
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@earendil-works/pi-tui": "^0.74.0",
|
|
51
|
-
"agent-sh": "^0.13.
|
|
51
|
+
"agent-sh": "^0.13.2",
|
|
52
52
|
"chalk": "^5.5.0",
|
|
53
53
|
"cli-highlight": "^2.1.11"
|
|
54
54
|
},
|
|
@@ -15,11 +15,27 @@ export function registerCapture(
|
|
|
15
15
|
getStore: () => MultiSessionStore,
|
|
16
16
|
): Capture {
|
|
17
17
|
let liveEntryIds: (string | null)[] = [];
|
|
18
|
+
const diffMeta = new Map<string, { diff: unknown; filePath: string }>();
|
|
19
|
+
|
|
20
|
+
ctx.bus.on("agent:tool-completed", (e) => {
|
|
21
|
+
const id = e.toolCallId;
|
|
22
|
+
const body = e.resultDisplay?.body;
|
|
23
|
+
if (id && body?.kind === "diff") {
|
|
24
|
+
diffMeta.set(id, { diff: body.diff, filePath: body.filePath });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const enrich = (m: AgentMessage): AgentMessage => {
|
|
29
|
+
if (m.role !== "tool" || !m.tool_call_id) return m;
|
|
30
|
+
const meta = diffMeta.get(m.tool_call_id);
|
|
31
|
+
if (!meta) return m;
|
|
32
|
+
return { ...m, meta: { ...m.meta, diff: meta.diff, filePath: meta.filePath } };
|
|
33
|
+
};
|
|
18
34
|
|
|
19
35
|
const flush = async (): Promise<void> => {
|
|
20
36
|
const messages = ctx.call("conversation:get-messages") as AgentMessage[] | undefined;
|
|
21
37
|
if (!messages || messages.length <= liveEntryIds.length) return;
|
|
22
|
-
const newMessages = messages.slice(liveEntryIds.length);
|
|
38
|
+
const newMessages = messages.slice(liveEntryIds.length).map(enrich);
|
|
23
39
|
const newIds = await getStore().current().appendMessages(newMessages);
|
|
24
40
|
liveEntryIds = [...liveEntryIds, ...newIds];
|
|
25
41
|
};
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* ashi — agent-sh's
|
|
4
|
-
*
|
|
5
|
-
* Boots the agent-sh kernel directly, skips the PTY shell and the
|
|
6
|
-
* default streaming tui-renderer, and mounts pi-tui as the sole
|
|
7
|
-
* frontend. Demonstrates that the kernel is frontend-agnostic — same
|
|
8
|
-
* backend, tools, slash commands, providers; different presentation.
|
|
3
|
+
* ashi — ash (agent-sh's built-in agent) in an interactive TUI.
|
|
9
4
|
*/
|
|
10
5
|
import { createCore, NoopHistory } from "agent-sh/core";
|
|
11
6
|
import { loadBuiltinExtensions } from "agent-sh/extensions";
|
|
@@ -43,10 +38,7 @@ function parseArgs(argv: string[]): AppConfig & { extensions?: string[] } {
|
|
|
43
38
|
else if ((a === "-e" || a === "--extensions") && argv[i + 1]) {
|
|
44
39
|
extensions.push(...argv[++i]!.split(",").map(s => s.trim()).filter(Boolean));
|
|
45
40
|
} else if (a === "-h" || a === "--help") {
|
|
46
|
-
process.stdout.write(
|
|
47
|
-
`Usage: ashi [--provider <name>] [--model <id>] [--api-key <key>] [--base-url <url>]\n` +
|
|
48
|
-
` [--backend <name>] [-e <ext>[,<ext>...]]\n\n` +
|
|
49
|
-
`Reads ~/.agent-sh/settings.json for providers and defaults.\n`);
|
|
41
|
+
process.stdout.write(MANAGEMENT_HELP + "\n");
|
|
50
42
|
process.exit(0);
|
|
51
43
|
}
|
|
52
44
|
}
|
|
@@ -54,7 +46,7 @@ function parseArgs(argv: string[]): AppConfig & { extensions?: string[] } {
|
|
|
54
46
|
return { shell: "/bin/sh", model, apiKey, baseURL, provider, backend, extensions };
|
|
55
47
|
}
|
|
56
48
|
|
|
57
|
-
const MANAGEMENT_HELP = `ashi — ash
|
|
49
|
+
const MANAGEMENT_HELP = `ashi — ash (agent-sh's built-in agent) in an interactive TUI
|
|
58
50
|
|
|
59
51
|
Management:
|
|
60
52
|
ashi install <name> [--force] Install an extension
|
|
@@ -98,7 +98,8 @@ export class ToolResultBody extends Container {
|
|
|
98
98
|
private outputText: Text;
|
|
99
99
|
private bodyText: Text;
|
|
100
100
|
private outputBuffer = "";
|
|
101
|
-
private
|
|
101
|
+
private diffRenderer: ((width: number) => string[]) | null = null;
|
|
102
|
+
private lastDiffWidth = -1;
|
|
102
103
|
private mode: ToolResultMode;
|
|
103
104
|
private previewLines: number;
|
|
104
105
|
private finalized = false;
|
|
@@ -120,8 +121,9 @@ export class ToolResultBody extends Container {
|
|
|
120
121
|
this.repaint();
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
this.
|
|
124
|
+
setDiffRenderer(fn: (width: number) => string[]): void {
|
|
125
|
+
this.diffRenderer = fn;
|
|
126
|
+
this.lastDiffWidth = -1;
|
|
125
127
|
this.repaint();
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -136,12 +138,23 @@ export class ToolResultBody extends Container {
|
|
|
136
138
|
this.repaint();
|
|
137
139
|
}
|
|
138
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
|
+
|
|
139
150
|
private repaint(): void {
|
|
140
|
-
|
|
141
|
-
// on the call line so showing it again is noise.
|
|
142
|
-
const hasDiff = this.diffLines.length > 0;
|
|
151
|
+
const hasDiff = this.diffRenderer !== null;
|
|
143
152
|
const showDiff = hasDiff && (this.expanded || this.mode === "preview");
|
|
144
|
-
|
|
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
|
+
}
|
|
145
158
|
|
|
146
159
|
// When a diff exists, the textual output ("Edited /path (+12 -3)") just
|
|
147
160
|
// restates the call line — suppress its line-count hint to keep edits quiet.
|
|
@@ -190,7 +203,7 @@ function lineCountHint(buffer: string, exitCode: number | null | undefined): str
|
|
|
190
203
|
return `${arrow}${theme.fg("muted", label)}`;
|
|
191
204
|
}
|
|
192
205
|
|
|
193
|
-
const GROUP_ICONS: Record<string, string> = { read: "◆", search: "⌕" };
|
|
206
|
+
export const GROUP_ICONS: Record<string, string> = { read: "◆", search: "⌕" };
|
|
194
207
|
|
|
195
208
|
interface GroupChild {
|
|
196
209
|
name: string;
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { ExtensionContext } from "agent-sh/types";
|
|
3
3
|
import { theme } from "./theme.js";
|
|
4
|
+
import { GROUP_ICONS } from "./components.js";
|
|
4
5
|
import type { ToolCallArgs, ToolCallView } from "./hooks.js";
|
|
5
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 icon ? `${theme.fg("warning", icon)} ` : "";
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
interface StatusOpts { exitCode: number | null; elapsedMs: number; summary?: string }
|
|
7
25
|
|
|
8
26
|
function fmtElapsed(ms: number): string {
|
|
@@ -92,7 +110,7 @@ function readLabel(args: ToolCallArgs): string {
|
|
|
92
110
|
const to = limit !== undefined ? from + limit - 1 : undefined;
|
|
93
111
|
range = theme.fg("warning", to ? `:${from}-${to}` : `:${from}`);
|
|
94
112
|
}
|
|
95
|
-
return `${bold("read")} ${accent(path ? relativize(path) : "…")}${range}`;
|
|
113
|
+
return `${iconPrefix("read")}${bold("read")} ${accent(path ? relativize(path) : "…")}${range}`;
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
function grepLabel(args: ToolCallArgs): string {
|
|
@@ -103,26 +121,26 @@ function grepLabel(args: ToolCallArgs): string {
|
|
|
103
121
|
const limit = num(r.limit);
|
|
104
122
|
const extras = [glob ? `(${glob})` : "", limit !== undefined ? `limit ${limit}` : ""].filter(Boolean).join(" ");
|
|
105
123
|
const tail = extras ? muted(` ${extras}`) : "";
|
|
106
|
-
return `${bold("grep")} ${accent(`/${pattern}/`)} ${muted(`in ${scope}`)}${tail}`;
|
|
124
|
+
return `${iconPrefix("grep")}${bold("grep")} ${accent(`/${pattern}/`)} ${muted(`in ${scope}`)}${tail}`;
|
|
107
125
|
}
|
|
108
126
|
|
|
109
127
|
function globLabel(args: ToolCallArgs): string {
|
|
110
128
|
const r = parseRaw(args.rawInput);
|
|
111
129
|
const pattern = str(r.pattern) ?? "…";
|
|
112
130
|
const scope = relativize(str(r.path) ?? ".");
|
|
113
|
-
return `${bold("glob")} ${accent(pattern)} ${muted(`in ${scope}`)}`;
|
|
131
|
+
return `${iconPrefix("glob")}${bold("glob")} ${accent(pattern)} ${muted(`in ${scope}`)}`;
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
function lsLabel(args: ToolCallArgs): string {
|
|
117
135
|
const r = parseRaw(args.rawInput);
|
|
118
136
|
const p = str(r.path) ?? ".";
|
|
119
|
-
return `${bold("ls")} ${accent(relativize(p))}`;
|
|
137
|
+
return `${iconPrefix("ls")}${bold("ls")} ${accent(relativize(p))}`;
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
function pathOnlyLabel(name: string, args: ToolCallArgs): string {
|
|
123
141
|
const r = parseRaw(args.rawInput);
|
|
124
142
|
const path = str(r.file_path) ?? str(r.path);
|
|
125
|
-
return `${bold(name)} ${accent(path ? relativize(path) : "…")}`;
|
|
143
|
+
return `${iconPrefix(name)}${bold(name)} ${accent(path ? relativize(path) : "…")}`;
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
function genericLabel(args: ToolCallArgs): string {
|
|
@@ -45,6 +45,45 @@ import { renderBoxFrame } from "agent-sh/utils/box-frame.js";
|
|
|
45
45
|
|
|
46
46
|
interface DiffStats { added: number; removed: number; isNewFile: boolean; isIdentical: boolean }
|
|
47
47
|
|
|
48
|
+
function buildDiffRenderer(
|
|
49
|
+
diff: DiffStats & Parameters<typeof renderDiff>[0],
|
|
50
|
+
filePath: string,
|
|
51
|
+
): (width: number) => string[] {
|
|
52
|
+
return (width) => {
|
|
53
|
+
const boxW = Math.max(40, width);
|
|
54
|
+
const contentW = Math.max(20, boxW - 4);
|
|
55
|
+
const inner = diff.isNewFile
|
|
56
|
+
? renderNewFilePreview(diff, 30)
|
|
57
|
+
: ((): string[] => {
|
|
58
|
+
const lines = renderDiff(diff, {
|
|
59
|
+
width: contentW, filePath, trueColor: true, maxLines: 30, mode: "unified",
|
|
60
|
+
});
|
|
61
|
+
return lines.length > 1 ? ["", ...lines.slice(1), ""] : lines;
|
|
62
|
+
})();
|
|
63
|
+
return renderBoxFrame(inner, {
|
|
64
|
+
width: boxW,
|
|
65
|
+
style: "rounded",
|
|
66
|
+
title: diffFrameTitle(filePath, diff),
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderNewFilePreview(
|
|
72
|
+
diff: { hunks?: { lines: { type: string; text: string }[] }[] },
|
|
73
|
+
maxLines: number,
|
|
74
|
+
): string[] {
|
|
75
|
+
const lines = diff.hunks?.[0]?.lines.filter((l) => l.type === "added") ?? [];
|
|
76
|
+
const shown = lines.slice(0, maxLines);
|
|
77
|
+
const overflow = lines.length - shown.length;
|
|
78
|
+
const noW = String(shown.length).length || 1;
|
|
79
|
+
const body = shown.map((l, i) => {
|
|
80
|
+
const no = String(i + 1).padStart(noW);
|
|
81
|
+
return `${theme.fg("muted", `${no} │`)} ${l.text}`;
|
|
82
|
+
});
|
|
83
|
+
if (overflow > 0) body.push(theme.fg("muted", `… ${overflow} more lines`));
|
|
84
|
+
return ["", ...body, ""];
|
|
85
|
+
}
|
|
86
|
+
|
|
48
87
|
function diffFrameTitle(filePath: string, diff: DiffStats): string {
|
|
49
88
|
const stats = diff.isNewFile
|
|
50
89
|
? theme.fg("success", `+${diff.added}`)
|
|
@@ -334,7 +373,13 @@ export function mountAshi(
|
|
|
334
373
|
if (found.kind === "group") {
|
|
335
374
|
found.group.recordCompletion(id, 0, summary);
|
|
336
375
|
} else {
|
|
337
|
-
|
|
376
|
+
const meta = m.meta as { diff?: unknown; filePath?: string } | undefined;
|
|
377
|
+
if (meta?.diff && typeof meta.filePath === "string") {
|
|
378
|
+
const diff = meta.diff as DiffStats & Parameters<typeof renderDiff>[0];
|
|
379
|
+
if (!diff.isIdentical) {
|
|
380
|
+
found.pair.result.setDiffRenderer(buildDiffRenderer(diff, meta.filePath));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
338
383
|
found.pair.result.finalize({ exitCode: 0, summary });
|
|
339
384
|
found.pair.call.setStatus({ exitCode: 0, elapsedMs: 0, summary });
|
|
340
385
|
}
|
|
@@ -485,27 +530,10 @@ export function mountAshi(
|
|
|
485
530
|
}
|
|
486
531
|
const pair = entry.pair;
|
|
487
532
|
const body = e.resultDisplay?.body;
|
|
488
|
-
const ok = e.exitCode === null || e.exitCode === 0;
|
|
489
533
|
if (body?.kind === "diff") {
|
|
490
534
|
const diff = body.diff as DiffStats & Parameters<typeof renderDiff>[0];
|
|
491
535
|
if (!diff.isIdentical) {
|
|
492
|
-
|
|
493
|
-
const boxW = Math.max(40, termW);
|
|
494
|
-
const contentW = Math.max(20, boxW - 4);
|
|
495
|
-
const diffLines = renderDiff(diff, {
|
|
496
|
-
width: contentW,
|
|
497
|
-
filePath: body.filePath,
|
|
498
|
-
trueColor: true,
|
|
499
|
-
maxLines: 30,
|
|
500
|
-
});
|
|
501
|
-
const inner = diffLines.length > 1 ? ["", ...diffLines.slice(1), ""] : diffLines;
|
|
502
|
-
const framed = renderBoxFrame(inner, {
|
|
503
|
-
width: boxW,
|
|
504
|
-
style: "rounded",
|
|
505
|
-
title: diffFrameTitle(body.filePath, diff),
|
|
506
|
-
bgColor: theme.bgCode(ok ? "toolSuccessBg" : "toolErrorBg"),
|
|
507
|
-
});
|
|
508
|
-
pair.result.setDiff(framed);
|
|
536
|
+
pair.result.setDiffRenderer(buildDiffRenderer(diff, body.filePath));
|
|
509
537
|
}
|
|
510
538
|
}
|
|
511
539
|
pair.call.setStatus({ exitCode: e.exitCode, elapsedMs: Date.now() - pair.startedAt, summary });
|
|
@@ -45,11 +45,13 @@ export interface ToolCallView extends Component {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** Mutated by ashi as output streams in and when the tool completes.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* setDiffRenderer is optional behavior — renderers may no-op if they don't
|
|
49
|
+
* show diffs. The renderer is called on each terminal-width change so diffs
|
|
50
|
+
* reflow on resize. toggleExpanded flips the view's internal expansion state
|
|
51
|
+
* (Ctrl+O). */
|
|
50
52
|
export interface ToolResultView extends Component {
|
|
51
53
|
appendChunk(chunk: string): void;
|
|
52
|
-
|
|
54
|
+
setDiffRenderer(fn: (width: number) => string[]): void;
|
|
53
55
|
finalize(opts: { exitCode: number | null; summary?: string }): void;
|
|
54
56
|
toggleExpanded(): void;
|
|
55
57
|
}
|