agent-sh 0.15.5 → 0.15.7
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/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/agent/agent-loop.js +2 -5
- package/dist/agent/extensions/rolling-history/index.js +20 -8
- package/dist/agent/extensions/rolling-history/recall.d.ts +2 -2
- package/dist/agent/extensions/rolling-history/recall.js +17 -7
- package/dist/agent/providers/openai-compatible.d.ts +8 -0
- package/dist/agent/providers/openai-compatible.js +9 -2
- package/dist/agent/store.js +6 -1
- package/dist/agent/token-budget.d.ts +2 -1
- package/dist/agent/token-budget.js +6 -1
- package/dist/agent/types.d.ts +4 -1
- package/dist/cli/index.js +1 -1
- package/dist/core/event-bus.d.ts +16 -1
- package/dist/core/event-bus.js +73 -11
- package/dist/core/index.js +18 -0
- package/dist/shell/tui-renderer.js +116 -174
- package/dist/utils/diff-renderer.js +65 -30
- package/dist/utils/executor.js +19 -11
- package/dist/utils/floating-panel.d.ts +1 -0
- package/dist/utils/floating-panel.js +28 -26
- package/dist/utils/markdown.js +56 -44
- package/dist/utils/palette.d.ts +11 -0
- package/dist/utils/palette.js +11 -0
- package/docs/agent.md +13 -11
- package/docs/architecture.md +3 -5
- package/docs/extensions.md +21 -20
- package/docs/library.md +6 -3
- package/docs/troubleshooting.md +2 -2
- package/docs/tui-composition.md +11 -3
- package/docs/usage.md +70 -50
- package/examples/extensions/ashi/src/chat/assistant.ts +6 -4
- package/examples/extensions/ashi/src/compaction.ts +4 -7
- package/examples/extensions/ashi/src/frontend.ts +2 -0
- package/examples/extensions/ashi/src/schema.ts +8 -2
- package/examples/extensions/command-suggest.ts +90 -0
- package/examples/extensions/solarized-theme.ts +11 -0
- package/package.json +5 -5
- package/src/agent/agent-loop.ts +2 -5
- package/src/agent/extensions/rolling-history/index.ts +20 -8
- package/src/agent/extensions/rolling-history/recall.ts +28 -7
- package/src/agent/providers/openai-compatible.ts +19 -4
- package/src/agent/store.ts +5 -1
- package/src/agent/token-budget.ts +10 -1
- package/src/agent/types.ts +4 -1
- package/src/cli/index.ts +1 -1
- package/src/core/event-bus.ts +67 -12
- package/src/core/index.ts +18 -0
- package/src/shell/tui-renderer.ts +131 -207
- package/src/utils/diff-renderer.ts +62 -29
- package/src/utils/executor.ts +17 -14
- package/src/utils/floating-panel.ts +24 -22
- package/src/utils/markdown.ts +49 -40
- package/src/utils/palette.ts +30 -5
package/docs/usage.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Running agent-sh
|
|
4
4
|
|
|
5
|
-
The simplest way to run agent-sh — just provide an API key
|
|
5
|
+
The simplest way to run agent-sh — just provide an API key:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
#
|
|
9
|
-
|
|
8
|
+
# DeepSeek is a built-in provider — set the key and go (defaults to deepseek-v4-flash)
|
|
9
|
+
DEEPSEEK_API_KEY="your-key" agent-sh
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
agent-sh --api-key "your-key" --base-url http://localhost:11434/v1 --model
|
|
11
|
+
# Any OpenAI-compatible endpoint via CLI flags (e.g. a local Ollama server)
|
|
12
|
+
agent-sh --api-key "your-key" --base-url http://localhost:11434/v1 --model gemma4
|
|
13
13
|
|
|
14
14
|
# Using npx
|
|
15
|
-
npx agent-sh --
|
|
15
|
+
DEEPSEEK_API_KEY="your-key" npx agent-sh --model deepseek-v4-flash
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Environment variables `OPENAI_API_KEY` and `OPENAI_BASE_URL` are supported as alternatives to CLI flags.
|
|
@@ -30,13 +30,13 @@ agent-sh --backend pi
|
|
|
30
30
|
npm run dev
|
|
31
31
|
|
|
32
32
|
# Debug mode
|
|
33
|
-
DEBUG=1
|
|
33
|
+
DEBUG=1 DEEPSEEK_API_KEY="$KEY" agent-sh
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
### Subcommands
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
agent-sh init # scaffold ~/.agent-sh/ (settings
|
|
39
|
+
agent-sh init # scaffold ~/.agent-sh/ (settings.json + settings.example.json, extensions/ dir)
|
|
40
40
|
agent-sh install <name> # install a bundled extension (e.g. agent-sh install pi-bridge)
|
|
41
41
|
agent-sh install ./path/to/ext # install from a local path
|
|
42
42
|
agent-sh uninstall <name> # remove an installed extension
|
|
@@ -55,8 +55,8 @@ Any provider you declare under `providers` in `settings.json` is also accepted b
|
|
|
55
55
|
"providers": {
|
|
56
56
|
"my-llama": {
|
|
57
57
|
"baseURL": "http://localhost:8000/v1",
|
|
58
|
-
"defaultModel": "
|
|
59
|
-
"models": ["
|
|
58
|
+
"defaultModel": "gemma4",
|
|
59
|
+
"models": ["gemma4"]
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -84,26 +84,26 @@ For unreleased changes on `main`, use the clone-and-link flow from the [Quick St
|
|
|
84
84
|
|
|
85
85
|
agent-sh works with any OpenAI-compatible API. Here are common configurations:
|
|
86
86
|
|
|
87
|
-
###
|
|
87
|
+
### DeepSeek
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
export
|
|
91
|
-
agent-sh
|
|
92
|
-
# or: agent-sh --model gpt-4o-mini
|
|
90
|
+
export DEEPSEEK_API_KEY="sk-..."
|
|
91
|
+
agent-sh # defaults to deepseek-v4-flash
|
|
93
92
|
```
|
|
94
93
|
|
|
95
|
-
###
|
|
94
|
+
### OpenAI
|
|
96
95
|
|
|
97
96
|
```bash
|
|
98
|
-
export
|
|
99
|
-
agent-sh
|
|
97
|
+
export OPENAI_API_KEY="sk-..."
|
|
98
|
+
agent-sh --model gpt-5.4
|
|
99
|
+
# or: agent-sh --model gpt-5.4-mini
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
### Ollama (Local)
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
105
|
# No API key needed — Ollama doesn't require authentication
|
|
106
|
-
agent-sh --api-key dummy --base-url http://localhost:11434/v1 --model
|
|
106
|
+
agent-sh --api-key dummy --base-url http://localhost:11434/v1 --model gemma4
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
### OpenRouter
|
|
@@ -111,7 +111,7 @@ agent-sh --api-key dummy --base-url http://localhost:11434/v1 --model llama3
|
|
|
111
111
|
```bash
|
|
112
112
|
agent-sh --api-key "$OPENROUTER_KEY" \
|
|
113
113
|
--base-url https://openrouter.ai/api/v1 \
|
|
114
|
-
--model
|
|
114
|
+
--model deepseek/deepseek-v4-flash
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
### Together AI
|
|
@@ -119,7 +119,7 @@ agent-sh --api-key "$OPENROUTER_KEY" \
|
|
|
119
119
|
```bash
|
|
120
120
|
agent-sh --api-key "$TOGETHER_KEY" \
|
|
121
121
|
--base-url https://api.together.xyz/v1 \
|
|
122
|
-
--model
|
|
122
|
+
--model deepseek-ai/DeepSeek-V3
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
### Groq
|
|
@@ -127,7 +127,7 @@ agent-sh --api-key "$TOGETHER_KEY" \
|
|
|
127
127
|
```bash
|
|
128
128
|
agent-sh --api-key "$GROQ_KEY" \
|
|
129
129
|
--base-url https://api.groq.com/openai/v1 \
|
|
130
|
-
--model llama-
|
|
130
|
+
--model deepseek-r1-distill-llama-70b
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
### LM Studio
|
|
@@ -135,7 +135,7 @@ agent-sh --api-key "$GROQ_KEY" \
|
|
|
135
135
|
```bash
|
|
136
136
|
agent-sh --api-key dummy \
|
|
137
137
|
--base-url http://localhost:1234/v1 \
|
|
138
|
-
--model
|
|
138
|
+
--model mimo
|
|
139
139
|
```
|
|
140
140
|
|
|
141
141
|
### vLLM
|
|
@@ -143,7 +143,7 @@ agent-sh --api-key dummy \
|
|
|
143
143
|
```bash
|
|
144
144
|
agent-sh --api-key dummy \
|
|
145
145
|
--base-url http://localhost:8000/v1 \
|
|
146
|
-
--model
|
|
146
|
+
--model deepseek-v4-flash
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
## Using agent-sh as Your Default Shell
|
|
@@ -152,7 +152,7 @@ Add to the end of your `~/.zshrc` or `~/.bashrc`:
|
|
|
152
152
|
|
|
153
153
|
```bash
|
|
154
154
|
if [[ -z "$AGENT_SH" && $- == *i* && -t 0 ]]; then
|
|
155
|
-
exec agent-sh
|
|
155
|
+
exec agent-sh # uses DEEPSEEK_API_KEY from your env (deepseek-v4-flash)
|
|
156
156
|
fi
|
|
157
157
|
```
|
|
158
158
|
|
|
@@ -168,27 +168,29 @@ Instead of passing `--api-key` and `--base-url` every time, define named provide
|
|
|
168
168
|
|
|
169
169
|
```json
|
|
170
170
|
{
|
|
171
|
-
"defaultProvider": "
|
|
171
|
+
"defaultProvider": "deepseek",
|
|
172
172
|
"providers": {
|
|
173
|
-
"
|
|
174
|
-
"apiKey": "$
|
|
175
|
-
"defaultModel": "
|
|
176
|
-
"models": ["
|
|
177
|
-
"contextWindow": 128000
|
|
173
|
+
"deepseek": {
|
|
174
|
+
"apiKey": "$DEEPSEEK_API_KEY",
|
|
175
|
+
"defaultModel": "deepseek-v4-flash",
|
|
176
|
+
"models": ["deepseek-v4-flash", "deepseek-v4-pro"]
|
|
178
177
|
},
|
|
179
178
|
"ollama": {
|
|
180
179
|
"apiKey": "not-needed",
|
|
181
180
|
"baseURL": "http://localhost:11434/v1",
|
|
182
|
-
"defaultModel": "
|
|
183
|
-
"models": [
|
|
181
|
+
"defaultModel": "gemma4",
|
|
182
|
+
"models": [
|
|
183
|
+
"mimo",
|
|
184
|
+
{ "id": "gemma4", "contextWindow": 128000, "modalities": ["text", "image"] }
|
|
185
|
+
]
|
|
184
186
|
},
|
|
185
187
|
"openrouter": {
|
|
186
188
|
"apiKey": "$OPENROUTER_KEY",
|
|
187
189
|
"baseURL": "https://openrouter.ai/api/v1",
|
|
188
|
-
"defaultModel": "
|
|
190
|
+
"defaultModel": "deepseek/deepseek-v4-flash",
|
|
189
191
|
"models": [
|
|
190
|
-
{ "id": "
|
|
191
|
-
{ "id": "
|
|
192
|
+
{ "id": "deepseek/deepseek-v4-flash", "contextWindow": 1000000, "reasoning": true },
|
|
193
|
+
{ "id": "deepseek/deepseek-v4-pro", "contextWindow": 1048576, "reasoning": true }
|
|
192
194
|
]
|
|
193
195
|
}
|
|
194
196
|
}
|
|
@@ -198,32 +200,50 @@ Instead of passing `--api-key` and `--base-url` every time, define named provide
|
|
|
198
200
|
Then just run:
|
|
199
201
|
|
|
200
202
|
```bash
|
|
201
|
-
agent-sh # uses defaultProvider
|
|
203
|
+
agent-sh # uses defaultProvider (deepseek)
|
|
202
204
|
agent-sh --provider ollama # use a specific provider
|
|
203
|
-
agent-sh --provider
|
|
205
|
+
agent-sh --provider ollama --model gemma4 # override the default model
|
|
204
206
|
```
|
|
205
207
|
|
|
206
208
|
The `apiKey` field supports `$ENV_VAR` and `${ENV_VAR}` syntax — variables are expanded at runtime, so you don't store secrets in the file.
|
|
207
209
|
|
|
208
|
-
### Declaring
|
|
209
|
-
|
|
210
|
-
agent-sh adapts its auto-compaction trigger to the model's context window. There are two places to declare it:
|
|
211
|
-
|
|
212
|
-
- **Provider-level `contextWindow`** — applies to every model in that provider unless a more specific value is set.
|
|
213
|
-
- **Per-model `contextWindow`** (inside an entry of `models`) — overrides the provider-level value for a specific model, and also lets you tag reasoning-capable models via `reasoning: true`.
|
|
210
|
+
### Declaring model capabilities
|
|
214
211
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
Entries in `models` can be plain strings (just the model id, uses the provider-level `contextWindow`) or objects:
|
|
212
|
+
Entries in a provider's `models` list can be plain strings (just the id) or objects that declare what the model can do. agent-sh uses these to size its context budget, cap output, route reasoning, and enable image input. Every field except `id` is optional.
|
|
218
213
|
|
|
219
214
|
```json
|
|
220
215
|
"models": [
|
|
221
|
-
"
|
|
222
|
-
{
|
|
223
|
-
|
|
216
|
+
"deepseek-v4-flash",
|
|
217
|
+
{
|
|
218
|
+
"id": "gemma4",
|
|
219
|
+
"contextWindow": 128000,
|
|
220
|
+
"maxTokens": 8192,
|
|
221
|
+
"modalities": ["text", "image"]
|
|
222
|
+
},
|
|
223
|
+
{ "id": "mimo", "reasoning": true },
|
|
224
|
+
{ "id": "deepseek-v4-pro", "contextWindow": 1000000, "reasoning": true, "echoReasoning": true }
|
|
224
225
|
]
|
|
225
226
|
```
|
|
226
227
|
|
|
228
|
+
| Field | Type | Default | Effect |
|
|
229
|
+
|---|---|---|---|
|
|
230
|
+
| `id` | `string` | — | Model identifier sent to the API (required). |
|
|
231
|
+
| `contextWindow` | `number` | provider-level `contextWindow`, else `60000` | Total token budget. Drives the `/context` display and the `autoCompactThreshold` auto-compaction trigger. |
|
|
232
|
+
| `maxTokens` | `number` | 40% of this model's `contextWindow` capped at `65536`, else `65536` | Max output (completion) tokens requested per turn. |
|
|
233
|
+
| `reasoning` | `boolean` | `false` | Marks the model as thinking-capable, so `/thinking` levels apply to it. |
|
|
234
|
+
| `modalities` | `("text" \| "image")[]` | `["text"]` | Input modalities. Include `"image"` to let the agent read image files (PNG/JPEG/GIF/WebP) with `read_file`; without it, attached images are dropped before the request. |
|
|
235
|
+
| `echoReasoning` | `boolean` | `false` | Echo `reasoning_content` back on assistant turns. Required by DeepSeek's reasoner; leave off otherwise (leaky proxies may forward it to the model as malformed input). |
|
|
236
|
+
|
|
237
|
+
A plain-string entry inherits the provider-level values and the defaults above. These provider-level fields apply to every model unless a per-model entry overrides them:
|
|
238
|
+
|
|
239
|
+
| Provider field | Effect |
|
|
240
|
+
|---|---|
|
|
241
|
+
| `contextWindow` | Fallback context window for models that don't declare their own. |
|
|
242
|
+
| `reasoningShape` | Borrow another registered provider's reasoning-request shape by id (e.g. `"openrouter"`). Defaults to the OpenAI-compatible shape. |
|
|
243
|
+
| `echoReasoningPatterns` | Case-insensitive regex sources matched against model ids; a match defaults that model to `echoReasoning: true` (a per-model `echoReasoning` still wins). |
|
|
244
|
+
|
|
245
|
+
If neither level declares a `contextWindow`, agent-sh falls back to a conservative 60k-token budget. Override that fallback globally with the `AGENT_SH_DEFAULT_CONTEXT_WINDOW` environment variable (a positive integer; ignored otherwise).
|
|
246
|
+
|
|
227
247
|
### Switching models at runtime
|
|
228
248
|
|
|
229
249
|
- **`/model`** — show the current model
|
|
@@ -274,7 +294,7 @@ Switching mid-conversation preserves your conversation state — only the LLM en
|
|
|
274
294
|
On launch, agent-sh displays a structured startup banner showing:
|
|
275
295
|
|
|
276
296
|
- **Backend** — which agent backend is active (`ash`, `claude-code`, `pi`, etc.)
|
|
277
|
-
- **Model** — current model with provider in brackets (e.g. `
|
|
297
|
+
- **Model** — current model with provider in brackets (e.g. `deepseek-v4-flash [deepseek]`)
|
|
278
298
|
- **Extensions** — loaded extensions (from CLI `-e`, settings, or `~/.agent-sh/extensions/`)
|
|
279
299
|
- **Skills** — discovered skills (global + project)
|
|
280
300
|
|
|
@@ -7,6 +7,8 @@ export type RenderBlock =
|
|
|
7
7
|
|
|
8
8
|
export type ContentTransform = (blocks: RenderBlock[]) => RenderBlock[];
|
|
9
9
|
|
|
10
|
+
const stripTrailing = (s: string): string => s.replace(/\s+$/, "");
|
|
11
|
+
|
|
10
12
|
export class AssistantMessage {
|
|
11
13
|
readonly node: RenderNode;
|
|
12
14
|
private container: ContainerView;
|
|
@@ -23,20 +25,20 @@ export class AssistantMessage {
|
|
|
23
25
|
|
|
24
26
|
appendText(t: string): void {
|
|
25
27
|
this.buffer += t;
|
|
26
|
-
this.md.setText(this.buffer);
|
|
28
|
+
this.md.setText(stripTrailing(this.buffer));
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
appendCodeBlock(language: string, code: string): void {
|
|
30
32
|
const prefix = this.buffer && !this.buffer.endsWith("\n") ? "\n" : "";
|
|
31
33
|
this.buffer += `${prefix}\`\`\`${language}\n${code}\n\`\`\`\n`;
|
|
32
|
-
this.md.setText(this.buffer);
|
|
34
|
+
this.md.setText(stripTrailing(this.buffer));
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
finalize(): void {
|
|
36
38
|
if (this.buffer === "") this.buffer = " ";
|
|
37
39
|
const blocks = this.transform([{ type: "text", text: this.buffer }]);
|
|
38
40
|
if (blocks.every((b) => b.type === "text")) {
|
|
39
|
-
this.md.setText(this.buffer);
|
|
41
|
+
this.md.setText(stripTrailing(this.buffer));
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
this.rebuild(blocks);
|
|
@@ -55,7 +57,7 @@ export class AssistantMessage {
|
|
|
55
57
|
this.container.addChild(m.node);
|
|
56
58
|
} else if (block.text.trim()) {
|
|
57
59
|
const m = this.nodes.markdown({ paddingX: 1 });
|
|
58
|
-
m.setText(block.text);
|
|
60
|
+
m.setText(stripTrailing(block.text));
|
|
59
61
|
this.container.addChild(m.node);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -39,19 +39,16 @@ export function registerCompaction(
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const older = messages.slice(0, cutIdx);
|
|
42
|
-
const kept = messages.slice(cutIdx);
|
|
43
42
|
const tokensBefore = (ctx.call("conversation:estimate-prompt-tokens") as number) ?? 0;
|
|
44
43
|
const customSummary = (await ctx.call("ashi:compact:build-summary", older)) as string | null | undefined;
|
|
45
44
|
|
|
46
45
|
const store = getStore().current();
|
|
47
46
|
await store.appendCompaction(firstKeptId, tokensBefore, customSummary ?? undefined);
|
|
48
|
-
ctx.call("conversation:replace-messages", store.buildMessages());
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
capture.resetTo([null, ...keptIds]);
|
|
48
|
+
// Take messages and ids from one rebuild so capture's index→id map can't drift.
|
|
49
|
+
const { messages: rebuilt, entryIds } = store.buildBranchWithIds();
|
|
50
|
+
ctx.call("conversation:replace-messages", rebuilt);
|
|
51
|
+
capture.resetTo(entryIds);
|
|
55
52
|
|
|
56
53
|
const tokensAfter = (ctx.call("conversation:estimate-prompt-tokens") as number) ?? 0;
|
|
57
54
|
return { before: tokensBefore, after: tokensAfter, evictedCount: older.length };
|
|
@@ -650,6 +650,8 @@ export function mountAshi(
|
|
|
650
650
|
activeThinking = null;
|
|
651
651
|
activeTools.clear();
|
|
652
652
|
openGroup = null;
|
|
653
|
+
compactions = 0;
|
|
654
|
+
statusFooter.update({ compactions });
|
|
653
655
|
clearChat();
|
|
654
656
|
const branch = getStore().current().getBranch();
|
|
655
657
|
const toolMap = new Map<string, ReplayEntry>();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { theme } from "./theme.js";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
3
4
|
import type { ThemeColor } from "./theme.js";
|
|
4
5
|
import type { ToolEntryConfig } from "./display-config.js";
|
|
@@ -188,6 +189,11 @@ export function renderBody(body: Body, env: Env, diff: DiffSlot): string {
|
|
|
188
189
|
// The tail is capped even when expanded so a huge result can't flood scrollback; the agent still sees it all.
|
|
189
190
|
const DEFAULT_EXPANDED_LINES = 200;
|
|
190
191
|
|
|
192
|
+
function clampLines(lines: string[], width: number): string {
|
|
193
|
+
if (width <= 0) return lines.join("\n");
|
|
194
|
+
return lines.map((l) => truncateToWidth(l, width, "…")).join("\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
191
197
|
function renderStream(buffer: string, env: Env): string {
|
|
192
198
|
const display = buffer.replace(/\n+$/, "");
|
|
193
199
|
if (env.expanded) {
|
|
@@ -205,14 +211,14 @@ function renderStream(buffer: string, env: Env): string {
|
|
|
205
211
|
}
|
|
206
212
|
if (env.mode === "summary") {
|
|
207
213
|
if (!env.finalized) {
|
|
208
|
-
const tail = display.split("\n").slice(-2).
|
|
214
|
+
const tail = clampLines(display.split("\n").slice(-2), env.width);
|
|
209
215
|
return theme.fg("muted", tail);
|
|
210
216
|
}
|
|
211
217
|
return lineCountHint(buffer);
|
|
212
218
|
}
|
|
213
219
|
if (!display) return "";
|
|
214
220
|
const lines = display.split("\n");
|
|
215
|
-
const trimmed = lines.slice(-env.previewLines).
|
|
221
|
+
const trimmed = clampLines(lines.slice(-env.previewLines), env.width);
|
|
216
222
|
const remaining = Math.max(0, lines.length - env.previewLines);
|
|
217
223
|
// The preview is the tail, so the hidden lines come before it — note goes above.
|
|
218
224
|
const overflow = remaining > 0
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* command-suggest extension
|
|
3
|
+
*
|
|
4
|
+
* Registers the suggest_command tool. When the agent calls it, the response
|
|
5
|
+
* finishes and the user drops to the shell prompt with the command pre-typed
|
|
6
|
+
* — no copy-paste, no mode toggle, just review and press Enter.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* agent-sh -e ./examples/extensions/command-suggest.ts
|
|
10
|
+
*
|
|
11
|
+
* # Or install permanently:
|
|
12
|
+
* cp examples/extensions/command-suggest.ts ~/.agent-sh/extensions/
|
|
13
|
+
*/
|
|
14
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
15
|
+
|
|
16
|
+
export default function activate(ctx: ExtensionContext): void {
|
|
17
|
+
const { bus } = ctx;
|
|
18
|
+
|
|
19
|
+
// No shell to deliver to (e.g. ashi) — the suggestion would go nowhere.
|
|
20
|
+
if (!ctx.shell) return;
|
|
21
|
+
|
|
22
|
+
let pendingCommand: string | null = null;
|
|
23
|
+
|
|
24
|
+
// ── Tool ────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
ctx.agent?.registerTool({
|
|
27
|
+
name: "suggest_command",
|
|
28
|
+
description:
|
|
29
|
+
"Stage a shell command at the user's prompt. After this response " +
|
|
30
|
+
"completes, the command appears in their shell prompt (not inside " +
|
|
31
|
+
"agent-input mode), ready to edit or run with Enter. " +
|
|
32
|
+
"Only call this when the user is asking for a command to run, or otherwise " +
|
|
33
|
+
"signals they want one staged — e.g. \"give me the command to …\", " +
|
|
34
|
+
"\"what do I run to …\". Do NOT call it unprompted after a general question, " +
|
|
35
|
+
"an explanation, or any turn where no command was requested. " +
|
|
36
|
+
"Prefer it over telling the user to copy-paste a command. " +
|
|
37
|
+
"Only the most recent call matters. Call with an empty string to clear.",
|
|
38
|
+
input_schema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
command: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description:
|
|
44
|
+
"The shell command to place in the user's prompt. " +
|
|
45
|
+
"Multi-line commands are collapsed to a single line.",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ["command"],
|
|
49
|
+
},
|
|
50
|
+
showOutput: true,
|
|
51
|
+
|
|
52
|
+
getDisplayInfo: () => ({ icon: "⏎" }),
|
|
53
|
+
|
|
54
|
+
formatCall: (args) => {
|
|
55
|
+
const cmd = (args.command as string).trim();
|
|
56
|
+
if (!cmd) return "(clear suggestion)";
|
|
57
|
+
return cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async execute(args) {
|
|
61
|
+
const cmd = (args.command as string).trim();
|
|
62
|
+
if (!cmd) {
|
|
63
|
+
pendingCommand = null;
|
|
64
|
+
return { content: "Cleared pending command suggestion.", exitCode: 0, isError: false };
|
|
65
|
+
}
|
|
66
|
+
// Collapse newlines to spaces so the command stays on one readline buffer.
|
|
67
|
+
pendingCommand = cmd.replace(/\n/g, " ");
|
|
68
|
+
return {
|
|
69
|
+
content: `Will suggest at shell prompt: ${pendingCommand}`,
|
|
70
|
+
exitCode: 0,
|
|
71
|
+
isError: false,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── Injection hook ──────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
// Replace the default handler — which re-enters agent-input mode when sticky —
|
|
79
|
+
// so a pending command lands at a fresh shell prompt instead. The "\n" leads
|
|
80
|
+
// the same PTY write so the new prompt appears before the command text fills it.
|
|
81
|
+
ctx.advise("shell:on-processing-redraw", (next) => {
|
|
82
|
+
if (pendingCommand) {
|
|
83
|
+
const cmd = pendingCommand;
|
|
84
|
+
pendingCommand = null;
|
|
85
|
+
bus.emit("shell:pty-write", { data: "\n" + cmd });
|
|
86
|
+
} else {
|
|
87
|
+
next();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -23,5 +23,16 @@ export default function activate(ctx: ShellContext) {
|
|
|
23
23
|
errorBg: "\x1b[48;2;42;30;30m", // base03 with red tint
|
|
24
24
|
successBgEmph: "\x1b[48;2;20;70;50m", // stronger green tint
|
|
25
25
|
errorBgEmph: "\x1b[48;2;70;30;30m", // stronger red tint
|
|
26
|
+
|
|
27
|
+
mdHeading: "\x1b[38;2;181;137;0m", // yellow (#b58900)
|
|
28
|
+
mdLink: "\x1b[38;2;38;139;210m", // blue (#268bd2)
|
|
29
|
+
mdLinkUrl: "\x1b[38;2;88;110;117m", // base01 (#586e75)
|
|
30
|
+
mdCode: "\x1b[38;2;42;161;152m", // cyan (#2aa198)
|
|
31
|
+
mdCodeBlock: "\x1b[38;2;133;153;0m", // green (#859900)
|
|
32
|
+
mdCodeBlockBorder: "\x1b[38;2;88;110;117m", // base01 (#586e75)
|
|
33
|
+
mdQuote: "\x1b[38;2;88;110;117m", // base01 (#586e75)
|
|
34
|
+
mdQuoteBorder: "\x1b[38;2;88;110;117m", // base01 (#586e75)
|
|
35
|
+
mdHr: "\x1b[38;2;88;110;117m", // base01 (#586e75)
|
|
36
|
+
mdListBullet: "\x1b[38;2;38;139;210m", // blue (#268bd2)
|
|
26
37
|
});
|
|
27
38
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sh",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.7",
|
|
4
4
|
"description": "A composable agent runtime — pair any frontend with any agent backend over one shared extension layer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -73,6 +73,10 @@
|
|
|
73
73
|
"types": "./dist/agent/types.d.ts",
|
|
74
74
|
"default": "./dist/agent/types.js"
|
|
75
75
|
},
|
|
76
|
+
"./skills": {
|
|
77
|
+
"types": "./dist/agent/skills.d.ts",
|
|
78
|
+
"default": "./dist/agent/skills.js"
|
|
79
|
+
},
|
|
76
80
|
"./store": {
|
|
77
81
|
"types": "./dist/agent/store.d.ts",
|
|
78
82
|
"default": "./dist/agent/store.js"
|
|
@@ -109,10 +113,6 @@
|
|
|
109
113
|
"types": "./dist/agent/token-budget.d.ts",
|
|
110
114
|
"default": "./dist/agent/token-budget.js"
|
|
111
115
|
},
|
|
112
|
-
"./agent/history-file": {
|
|
113
|
-
"types": "./dist/agent/history-file.d.ts",
|
|
114
|
-
"default": "./dist/agent/history-file.js"
|
|
115
|
-
},
|
|
116
116
|
"./agent/nuclear-form": {
|
|
117
117
|
"types": "./dist/agent/nuclear-form.d.ts",
|
|
118
118
|
"default": "./dist/agent/nuclear-form.js"
|
package/src/agent/agent-loop.ts
CHANGED
|
@@ -971,12 +971,9 @@ export class AgentLoop implements AgentBackend {
|
|
|
971
971
|
// tool-heavy workloads.
|
|
972
972
|
const target = Math.floor(threshold * 0.25);
|
|
973
973
|
const result = await this.compactWithHooks(target, 1);
|
|
974
|
-
if (
|
|
975
|
-
// Auto-compact fired but nothing was evictable. This can happen
|
|
976
|
-
// in short conversations with heavy tool output where the pin
|
|
977
|
-
// fraction consumes all turns. Log it so it's not silent.
|
|
974
|
+
if (result) {
|
|
978
975
|
this.bus.emit("ui:info", {
|
|
979
|
-
message: `
|
|
976
|
+
message: `(auto-compacted: ~${result.before.toLocaleString()} → ~${result.after.toLocaleString()} tokens, evicted ${result.evictedCount})`,
|
|
980
977
|
});
|
|
981
978
|
}
|
|
982
979
|
cachedSystemPrompt = undefined;
|
|
@@ -111,32 +111,44 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
111
111
|
name: TOOL_NAME,
|
|
112
112
|
displayName: "recall",
|
|
113
113
|
description:
|
|
114
|
-
"Browse, search, or expand
|
|
115
|
-
"Use when you need context from
|
|
116
|
-
"Search
|
|
117
|
-
"
|
|
114
|
+
"Browse, search, or expand the persistent conversation memory — all captured turns across this and recent sessions. " +
|
|
115
|
+
"Use when you need context from prior turns or past sessions that may no longer be in the active window. " +
|
|
116
|
+
"Search accepts a regex pattern (e.g. 'foo|bar') and falls back to literal matching if the pattern is invalid. " +
|
|
117
|
+
"Covers both summaries and full body text. " +
|
|
118
|
+
"If search doesn't find what you expect, try broader/shorter terms or browse to scan the timeline. " +
|
|
119
|
+
"Use offset for pagination on both browse and search.",
|
|
118
120
|
input_schema: {
|
|
119
121
|
type: "object",
|
|
120
122
|
properties: {
|
|
121
123
|
action: {
|
|
122
124
|
type: "string",
|
|
123
125
|
enum: ["browse", "search", "expand"],
|
|
124
|
-
description: "browse: list
|
|
126
|
+
description: "browse: list recent captured turns, search: regex search across memory, expand: show full turn body",
|
|
125
127
|
},
|
|
126
|
-
query: { type: "string", description: "Search
|
|
128
|
+
query: { type: "string", description: "Search pattern — a regex (e.g. 'foo|bar') or literal text (for action=search)" },
|
|
127
129
|
turn_id: { type: "string", description: "Turn ID to expand (for action=expand)" },
|
|
130
|
+
offset: {
|
|
131
|
+
type: "number",
|
|
132
|
+
description: "Skip first N results; for browse, start at this entry offset; for search, skip first N hits. Default 0.",
|
|
133
|
+
},
|
|
134
|
+
limit: {
|
|
135
|
+
type: "number",
|
|
136
|
+
description: "Max entries to return for browse (default 25) or search (default 30).",
|
|
137
|
+
},
|
|
128
138
|
},
|
|
129
139
|
required: ["action"],
|
|
130
140
|
},
|
|
131
141
|
execute: async (args) => {
|
|
132
142
|
const action = args.action as string;
|
|
143
|
+
const offset = (args.offset as number) ?? 0;
|
|
144
|
+
const limit = (args.limit as number) ?? (action === "search" ? 30 : 25);
|
|
133
145
|
let content: string;
|
|
134
146
|
if (action === "search") {
|
|
135
|
-
content = await recallSearch(summaryStore, (args.query as string) ?? "");
|
|
147
|
+
content = await recallSearch(summaryStore, (args.query as string) ?? "", offset, limit);
|
|
136
148
|
} else if (action === "expand") {
|
|
137
149
|
content = await recallExpand(summaryStore, args.turn_id as string);
|
|
138
150
|
} else {
|
|
139
|
-
content = await recallBrowse(summaryStore);
|
|
151
|
+
content = await recallBrowse(summaryStore, offset, limit);
|
|
140
152
|
}
|
|
141
153
|
return { content, exitCode: 0, isError: false };
|
|
142
154
|
},
|
|
@@ -76,7 +76,12 @@ async function findCacheChild(store: Store, parentId: string): Promise<RecallCac
|
|
|
76
76
|
return null;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export async function recallSearch(
|
|
79
|
+
export async function recallSearch(
|
|
80
|
+
store: Store,
|
|
81
|
+
query: string,
|
|
82
|
+
offset = 0,
|
|
83
|
+
maxResults = 30,
|
|
84
|
+
): Promise<string> {
|
|
80
85
|
if (!query.trim()) return "No query provided.";
|
|
81
86
|
const regex = buildSearchRegex(query);
|
|
82
87
|
const hits: string[] = [];
|
|
@@ -106,8 +111,13 @@ export async function recallSearch(store: Store, query: string): Promise<string>
|
|
|
106
111
|
|
|
107
112
|
if (hits.length === 0) return `No results found for "${query}".`;
|
|
108
113
|
const total = hits.length;
|
|
109
|
-
const
|
|
110
|
-
|
|
114
|
+
const paged = hits.slice(offset, offset + maxResults);
|
|
115
|
+
const range =
|
|
116
|
+
offset > 0 || paged.length < total
|
|
117
|
+
? ` (showing ${offset + 1}–${offset + paged.length} of ${total})`
|
|
118
|
+
: "";
|
|
119
|
+
const summary = `Found ${total} match${total === 1 ? "" : "es"} for "${query}"${range}`;
|
|
120
|
+
return `${summary}\n\n${paged.join("\n\n")}`;
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
export async function recallExpand(store: Store, id: string): Promise<string> {
|
|
@@ -124,8 +134,19 @@ export async function recallExpand(store: Store, id: string): Promise<string> {
|
|
|
124
134
|
return `${header}\n\n(no expanded content available — recall cache may have been cleared)`;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
export async function recallBrowse(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
export async function recallBrowse(
|
|
138
|
+
store: Store,
|
|
139
|
+
offset = 0,
|
|
140
|
+
limit = 25,
|
|
141
|
+
): Promise<string> {
|
|
142
|
+
const overRead = Math.max(limit * 3, offset + limit);
|
|
143
|
+
const allLines = await readSummaryLines(store, overRead);
|
|
144
|
+
if (allLines.length === 0) return "No conversation history.";
|
|
145
|
+
const end = Math.min(offset + limit, allLines.length);
|
|
146
|
+
const paged = allLines.slice(offset, end);
|
|
147
|
+
const range =
|
|
148
|
+
offset > 0 || end < allLines.length
|
|
149
|
+
? ` (entries ${offset + 1}–${end} of ${allLines.length} shown)`
|
|
150
|
+
: "";
|
|
151
|
+
return [`Recent summary entries${range}:`, ...paged.map((l) => ` ${l}`)].join("\n");
|
|
131
152
|
}
|