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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/dist/agent/agent-loop.js +2 -5
  4. package/dist/agent/extensions/rolling-history/index.js +20 -8
  5. package/dist/agent/extensions/rolling-history/recall.d.ts +2 -2
  6. package/dist/agent/extensions/rolling-history/recall.js +17 -7
  7. package/dist/agent/providers/openai-compatible.d.ts +8 -0
  8. package/dist/agent/providers/openai-compatible.js +9 -2
  9. package/dist/agent/store.js +6 -1
  10. package/dist/agent/token-budget.d.ts +2 -1
  11. package/dist/agent/token-budget.js +6 -1
  12. package/dist/agent/types.d.ts +4 -1
  13. package/dist/cli/index.js +1 -1
  14. package/dist/core/event-bus.d.ts +16 -1
  15. package/dist/core/event-bus.js +73 -11
  16. package/dist/core/index.js +18 -0
  17. package/dist/shell/tui-renderer.js +116 -174
  18. package/dist/utils/diff-renderer.js +65 -30
  19. package/dist/utils/executor.js +19 -11
  20. package/dist/utils/floating-panel.d.ts +1 -0
  21. package/dist/utils/floating-panel.js +28 -26
  22. package/dist/utils/markdown.js +56 -44
  23. package/dist/utils/palette.d.ts +11 -0
  24. package/dist/utils/palette.js +11 -0
  25. package/docs/agent.md +13 -11
  26. package/docs/architecture.md +3 -5
  27. package/docs/extensions.md +21 -20
  28. package/docs/library.md +6 -3
  29. package/docs/troubleshooting.md +2 -2
  30. package/docs/tui-composition.md +11 -3
  31. package/docs/usage.md +70 -50
  32. package/examples/extensions/ashi/src/chat/assistant.ts +6 -4
  33. package/examples/extensions/ashi/src/compaction.ts +4 -7
  34. package/examples/extensions/ashi/src/frontend.ts +2 -0
  35. package/examples/extensions/ashi/src/schema.ts +8 -2
  36. package/examples/extensions/command-suggest.ts +90 -0
  37. package/examples/extensions/solarized-theme.ts +11 -0
  38. package/package.json +5 -5
  39. package/src/agent/agent-loop.ts +2 -5
  40. package/src/agent/extensions/rolling-history/index.ts +20 -8
  41. package/src/agent/extensions/rolling-history/recall.ts +28 -7
  42. package/src/agent/providers/openai-compatible.ts +19 -4
  43. package/src/agent/store.ts +5 -1
  44. package/src/agent/token-budget.ts +10 -1
  45. package/src/agent/types.ts +4 -1
  46. package/src/cli/index.ts +1 -1
  47. package/src/core/event-bus.ts +67 -12
  48. package/src/core/index.ts +18 -0
  49. package/src/shell/tui-renderer.ts +131 -207
  50. package/src/utils/diff-renderer.ts +62 -29
  51. package/src/utils/executor.ts +17 -14
  52. package/src/utils/floating-panel.ts +24 -22
  53. package/src/utils/markdown.ts +49 -40
  54. 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 and model:
5
+ The simplest way to run agent-sh — just provide an API key:
6
6
 
7
7
  ```bash
8
- # Using environment variables
9
- OPENAI_API_KEY="your-key" agent-sh --model gpt-4o
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
- # Using CLI flags
12
- agent-sh --api-key "your-key" --base-url http://localhost:11434/v1 --model llama3
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 --api-key "$KEY" --model gpt-4o
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 agent-sh --api-key "$KEY" --model gpt-4o
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, examples, AGENTS.md)
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": "llama-3.1-70b",
59
- "models": ["llama-3.1-70b"]
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
- ### OpenAI
87
+ ### DeepSeek
88
88
 
89
89
  ```bash
90
- export OPENAI_API_KEY="sk-..."
91
- agent-sh --model gpt-4o
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
- ### DeepSeek
94
+ ### OpenAI
96
95
 
97
96
  ```bash
98
- export DEEPSEEK_API_KEY="sk-..."
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 llama3
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 anthropic/claude-sonnet-4-20250514
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 meta-llama/Llama-3-70b-chat-hf
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-3.3-70b-versatile
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 local-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 your-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 --api-key "$OPENAI_API_KEY" --model gpt-4o
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": "openai",
171
+ "defaultProvider": "deepseek",
172
172
  "providers": {
173
- "openai": {
174
- "apiKey": "$OPENAI_API_KEY",
175
- "defaultModel": "gpt-4o",
176
- "models": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
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": "llama3",
183
- "models": ["llama3", "mistral", "codellama"]
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": "anthropic/claude-sonnet-4.5",
190
+ "defaultModel": "deepseek/deepseek-v4-flash",
189
191
  "models": [
190
- { "id": "anthropic/claude-sonnet-4.5", "contextWindow": 200000, "reasoning": true },
191
- { "id": "google/gemini-2.5-pro", "contextWindow": 1000000 }
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 openai --model gpt-4-turbo # override the default model
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 the context window
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
- If neither is set, agent-sh falls back to a conservative 60k-token default.
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
- "gpt-4o-mini",
222
- { "id": "gpt-4o", "contextWindow": 128000 },
223
- { "id": "o1-preview", "contextWindow": 128000, "reasoning": true }
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. `gpt-4o [openai]`)
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
- const keptIds = kept.map((_, i) => capture.getEntryIdAt(cutIdx + i));
51
- if (keptIds.some((id) => id === null)) {
52
- ctx.bus.emit("ui:error", { message: "compaction: a kept message has no on-disk entry — capture invariant broken" });
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).join("\n");
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).join("\n");
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.5",
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"
@@ -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 (!result) {
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: `[auto-compact] above threshold (${totalEstimate.toLocaleString()} > ${threshold.toLocaleString()}) but nothing to evict — conversation may be too short`,
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 evicted conversation turns. " +
115
- "Use when you need context from earlier in the conversation that was compacted away. " +
116
- "Search is regex-based and covers both summaries and full body text. " +
117
- "If search doesn't find what you expect, try broader/shorter terms or browse to scan the timeline.",
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 evicted turns, search: regex search, expand: show full turn",
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 query (for action=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(store: Store, query: string): Promise<string> {
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 summary = `Found ${total} match${total === 1 ? "" : "es"} for "${query}"`;
110
- return `${summary}\n\n${hits.slice(0, 30).join("\n\n")}`;
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(store: Store, limit = 25): Promise<string> {
128
- const lines = await readSummaryLines(store, limit);
129
- if (lines.length === 0) return "No conversation history.";
130
- return ["Recent summary entries:", ...lines.map((l) => ` ${l}`)].join("\n");
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
  }