agent-sh 0.15.6 → 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 (50) 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/cli/index.js +1 -1
  13. package/dist/core/event-bus.d.ts +16 -1
  14. package/dist/core/event-bus.js +73 -11
  15. package/dist/core/index.js +18 -0
  16. package/dist/shell/tui-renderer.js +115 -174
  17. package/dist/utils/executor.js +19 -11
  18. package/dist/utils/floating-panel.d.ts +1 -0
  19. package/dist/utils/floating-panel.js +28 -26
  20. package/dist/utils/markdown.js +19 -21
  21. package/dist/utils/palette.d.ts +11 -0
  22. package/dist/utils/palette.js +11 -0
  23. package/docs/agent.md +13 -11
  24. package/docs/architecture.md +3 -5
  25. package/docs/extensions.md +21 -20
  26. package/docs/library.md +6 -3
  27. package/docs/troubleshooting.md +2 -2
  28. package/docs/tui-composition.md +11 -3
  29. package/docs/usage.md +70 -50
  30. package/examples/extensions/ashi/src/chat/assistant.ts +6 -4
  31. package/examples/extensions/ashi/src/compaction.ts +4 -7
  32. package/examples/extensions/ashi/src/frontend.ts +2 -0
  33. package/examples/extensions/ashi/src/schema.ts +8 -2
  34. package/examples/extensions/command-suggest.ts +4 -0
  35. package/examples/extensions/solarized-theme.ts +11 -0
  36. package/package.json +1 -1
  37. package/src/agent/agent-loop.ts +2 -5
  38. package/src/agent/extensions/rolling-history/index.ts +20 -8
  39. package/src/agent/extensions/rolling-history/recall.ts +28 -7
  40. package/src/agent/providers/openai-compatible.ts +19 -4
  41. package/src/agent/store.ts +5 -1
  42. package/src/agent/token-budget.ts +10 -1
  43. package/src/cli/index.ts +1 -1
  44. package/src/core/event-bus.ts +67 -12
  45. package/src/core/index.ts +18 -0
  46. package/src/shell/tui-renderer.ts +130 -207
  47. package/src/utils/executor.ts +17 -14
  48. package/src/utils/floating-panel.ts +24 -22
  49. package/src/utils/markdown.ts +17 -20
  50. package/src/utils/palette.ts +30 -5
@@ -23,7 +23,18 @@ export interface ColorPalette {
23
23
  dim: string;
24
24
  italic: string;
25
25
  underline: string;
26
+ strikethrough: string;
26
27
  reset: string;
28
+ mdHeading: string;
29
+ mdLink: string;
30
+ mdLinkUrl: string;
31
+ mdCode: string;
32
+ mdCodeBlock: string;
33
+ mdCodeBlockBorder: string;
34
+ mdQuote: string;
35
+ mdQuoteBorder: string;
36
+ mdHr: string;
37
+ mdListBullet: string;
27
38
  }
28
39
  /** Active palette — import and use directly in components. */
29
40
  export declare const palette: ColorPalette;
@@ -23,7 +23,18 @@ const defaultPalette = {
23
23
  dim: "\x1b[2m",
24
24
  italic: "\x1b[3m",
25
25
  underline: "\x1b[4m",
26
+ strikethrough: "\x1b[9m",
26
27
  reset: "\x1b[0m",
28
+ mdHeading: "\x1b[38;2;240;198;116m", // #f0c674 gold
29
+ mdLink: "\x1b[38;2;129;162;190m", // #81a2be blue
30
+ mdLinkUrl: "\x1b[38;2;102;102;102m", // #666666 dim gray
31
+ mdCode: "\x1b[38;2;138;190;183m", // #8abeb7 teal
32
+ mdCodeBlock: "\x1b[38;2;181;189;104m", // #b5bd68 green
33
+ mdCodeBlockBorder: "\x1b[38;2;128;128;128m", // #808080 gray
34
+ mdQuote: "\x1b[38;2;128;128;128m", // #808080 gray
35
+ mdQuoteBorder: "\x1b[38;2;128;128;128m", // #808080 gray
36
+ mdHr: "\x1b[38;2;128;128;128m", // #808080 gray
37
+ mdListBullet: "\x1b[38;2;138;190;183m", // #8abeb7 teal
27
38
  };
28
39
  /** Active palette — import and use directly in components. */
29
40
  export const palette = { ...defaultPalette };
package/docs/agent.md CHANGED
@@ -43,14 +43,16 @@ Compaction is pluggable: the `conversation:compact` handler is advisable, so ext
43
43
 
44
44
  The system prompt is assembled once per `cwd` and cached (invalidated when the working directory changes), so the prefix is stable for provider-side prompt caching. It includes:
45
45
 
46
- 1. **Identity** — "You are an AI coding assistant running inside agent-sh..."
47
- 2. **Tool decision guide** — when to use which built-in tool
48
- 3. **Tool usage guidelines** — read before editing, prefer edit over write, use grep/glob to find files, etc.
49
- 4. **Project conventions** — `CLAUDE.md`/`AGENT.md` walked from cwd to root (cwd-stable; see next section)
50
- 5. **Skills** — discovered project/global skills (cwd-stable)
51
- 6. **Extension instructions** — blocks registered by extensions via `registerInstruction()` (e.g. proactive recall guidance)
52
- 7. **Available tools** — name + description of every registered tool
53
- 8. **Extension-appended content** — extensions can advise `system-prompt:build` to append additional context (instance IDs, memory files, etc.)
46
+ 1. **Identity** — "You are ash, an AI coding assistant running inside agent-sh..." (advisable via `system-prompt:identity`)
47
+ 2. **Frontend surface** — the active frontend's self-description, placed right after the identity (advisable via `system-prompt:frontend`; omitted when none)
48
+ 3. **Static guide** — agent-sh's own code map (paths to `docs/`, `src/`, `examples/extensions/`), generic tool guidance, and the `<query_context>`/`<dynamic_context>` envelope contract
49
+ 4. **Global memory** — `~/.agent-sh/AGENTS.md`, if present
50
+ 5. **Global skills** — discovered global skills (cwd-stable)
51
+ 6. **Project conventions + skills** — `CLAUDE.md`/`AGENT.md` walked from cwd to root, plus discovered project skills (cwd-stable; see next section)
52
+ 7. **Extension instructions** — blocks registered by extensions via `registerInstruction()` (e.g. proactive recall guidance)
53
+ 8. **Image support** — appended when the active model accepts image input
54
+
55
+ Built-in tools are not inlined here — they're passed to the provider via the API `tools` parameter. Extensions can advise `system-prompt:build` directly to append further context (instance IDs, memory files, etc.).
54
56
 
55
57
  Per-turn signals live in two symmetric handlers, both empty by default:
56
58
 
@@ -218,7 +220,7 @@ When the LLM requests multiple tool calls in a single response, the agent groups
218
220
 
219
221
  2. **Parallel execution** — side-effect-free tools (`modifiesFiles` unset) run in parallel via `Promise.all`. Side-effecting tools run sequentially.
220
222
 
221
- 3. **Output truncation** — tool results over 16KB (~4K tokens) are head+tail truncated before being added to the conversation, preventing a single tool call from blowing through the context window.
223
+ 3. **Output truncation** — tool results over the tool's `maxResultBytes` (default 100KB, ~25K tokens) are head+tail truncated (60/40 split) before being added to the conversation, preventing a single tool call from blowing through the context window.
222
224
 
223
225
  ### Structured result display
224
226
 
@@ -260,7 +262,7 @@ For OpenRouter, the flag is set automatically: model ids matching the built-in p
260
262
  "echoReasoningPatterns": ["my-custom-deepseek-fork"],
261
263
  "models": [
262
264
  { "id": "deepseek/deepseek-v3.2", "echoReasoning": false },
263
- { "id": "openai/gpt-5.5", "reasoning": true }
265
+ { "id": "z-ai/glm-5.1", "reasoning": true }
264
266
  ]
265
267
  }
266
268
  }
@@ -367,7 +369,7 @@ Each entry is a `(provider, model)` target — a serializable identity plus capa
367
369
 
368
370
  ```typescript
369
371
  interface Model {
370
- id: string; // model id, e.g. "openai/gpt-5"
372
+ id: string; // model id, e.g. "deepseek/deepseek-v4-flash"
371
373
  provider: string; // identity is the (provider, id) pair
372
374
  contextWindow?: number; // per-model override for the auto-compact threshold
373
375
  maxTokens?: number;
@@ -20,7 +20,7 @@ index.ts — interactive terminal frontend:
20
20
  ├── Agent host (always activated via activateAgent(ctx) before built-ins load):
21
21
  │ ash backend — provider resolution, LlmClient, lazy AgentLoop
22
22
  │ core tools — bash/read/write/edit/grep/glob/ls/list_skills registered at activate time
23
- │ built-in providers — openrouter, openai, openai-compatible, deepseek (unconditional)
23
+ │ built-in providers — openrouter, openai, deepseek, ollama, zai-coding-plan, opencode (unconditional); openai-compatible when OPENAI_BASE_URL is set
24
24
 
25
25
  ├── Backend registry (owned by core; backends register via `agent:register-backend`):
26
26
  │ core.activateBackend() — picks the named/persisted/first backend and calls its start()
@@ -28,7 +28,7 @@ index.ts — interactive terminal frontend:
28
28
  ├── Built-in extensions (loaded via declarative manifest, individually disableable):
29
29
  │ shell-context — PTY exchange tracking, cwd advisor, <cwd>/<shell_events> producer
30
30
  │ tui-renderer — markdown rendering, inline diffs, thinking display, spinner
31
- │ slash-commands — /help, /model, /backend, /thinking, /compact, /context, /reload
31
+ │ slash-commands — /help, /model, /thinking, /backend, /reload (the ash backend adds /compact, /context)
32
32
  │ file-autocomplete — @ file path completion
33
33
 
34
34
  ├── Shared utilities:
@@ -36,7 +36,6 @@ index.ts — interactive terminal frontend:
36
36
  │ diff-renderer — syntax-highlighted diffs (split/unified/summary)
37
37
  │ box-frame — bordered TUI panels
38
38
  │ tool-display — width-adaptive tool call rendering + pure spinner
39
- │ output-writer — OutputWriter interface (StdoutWriter, BufferWriter for tests)
40
39
  │ stream-transform — content block transforms for response pipeline
41
40
 
42
41
  └── User extensions (opt-in, loaded from -e flag / settings.json / extensions dir):
@@ -147,7 +146,7 @@ agent-sh/
147
146
  │ │ ├── types.ts # AgentBackend, ToolDefinition, ToolResult
148
147
  │ │ ├── agent-loop.ts # ash AgentLoop (constructed lazily in start())
149
148
  │ │ ├── llm-client.ts, llm-facade.ts # ash LLM transport + ctx.agent.llm facade
150
- │ │ ├── providers/ # openai, openrouter, deepseek, openai-compatible
149
+ │ │ ├── providers/ # openai, openrouter, deepseek, openai-compatible, ollama, zai-coding-plan, opencode
151
150
  │ │ ├── token-budget.ts # Shared constants (RESPONSE_RESERVE, DEFAULT_CONTEXT_WINDOW)
152
151
  │ │ ├── tool-registry.ts, tool-protocol.ts
153
152
  │ │ ├── live-view.ts # In-memory messages array + compaction + recall archive
@@ -185,7 +184,6 @@ agent-sh/
185
184
  │ ├── solarized-theme.ts # Theme example
186
185
  │ ├── secret-guard.ts # Secret redaction
187
186
  │ ├── latex-images.ts # LaTeX equation rendering
188
- │ ├── ollama.ts # Ollama provider (local + cloud)
189
187
  │ ├── claude-code-bridge/ # Claude Code SDK backend
190
188
  │ ├── pi-bridge/ # Pi agent backend
191
189
  │ ├── ash-mcp-bridge/ # MCP server bridge
@@ -468,17 +468,17 @@ Per-request producers (`mode: "per-request"`) only fire under backends that expo
468
468
 
469
469
  ## Custom Providers
470
470
 
471
- Providers describe the OpenAI-compatible endpoints the `ash` backend can talk to. The built-ins (openrouter, openai, openai-compatible, deepseek) register from `src/agent/providers/`; extensions can register their own — local daemons, hosted gateways, fine-tuned model catalogs — and they show up under `agent-sh auth list` and `/model`.
471
+ Providers describe the OpenAI-compatible endpoints the `ash` backend can talk to. The built-ins (openrouter, openai, openai-compatible, deepseek, ollama, zai-coding-plan, opencode) register from `src/agent/providers/`; extensions can register their own — local daemons, hosted gateways, fine-tuned model catalogs — and they show up under `agent-sh auth list` and `/model`.
472
472
 
473
473
  ```typescript
474
474
  import type { AgentContext } from "agent-sh/types";
475
475
 
476
476
  export default function activate(ctx: AgentContext): void {
477
477
  ctx.agent.providers.register({
478
- id: "ollama",
479
- baseURL: "http://localhost:11434/v1",
480
- defaultModel: "llama3.2",
481
- models: ["llama3.2", "qwen2.5-coder"],
478
+ id: "llama-cpp",
479
+ baseURL: "http://localhost:8080/v1",
480
+ defaultModel: "gemma4",
481
+ models: ["gemma4"],
482
482
  noAuth: true,
483
483
  });
484
484
  }
@@ -549,9 +549,6 @@ These are registered by AgentLoop (constructed when the ash backend's `start()`
549
549
  | `conversation:estimate-tokens` | `() → number` | Local chars/4 estimate of the conversation size. |
550
550
  | `conversation:estimate-prompt-tokens` | `() → number` | API-grounded estimate (last `prompt_tokens` + local delta since). Used by the auto-compact trigger. |
551
551
  | `conversation:inject-note` | `(text) → void` | Inject a `role:"user"` note mid-loop — how extensions deliver async results (subagent output, peer messages) into the next iteration. |
552
- | `conversation:nucleate-user` / `-agent` / `-tool` | `(msg) → NuclearEntry` | Turn a message into its one-line summary. Advise to extract extra metadata (e.g. `[why: ...]` annotations). |
553
- | `conversation:format-prior-history` | `(entries) → string` | Render prior-session history into a preamble. Advise for session-grouped output. |
554
- | `history:append` / `:search` / `:find-by-seq` / `:read-recent` | — | Shell-history-style persistent log at `~/.agent-sh/history`. Advise to add indexing, filtering, or external stores. |
555
552
  | `tool:execute` | `(ctx) → ToolResult` | Wrap the full tool lifecycle: permission → execute → emit events. |
556
553
 
557
554
  **`dynamic-context:build`** — Each advisor appends its own context. Multiple extensions compose independently:
@@ -717,7 +714,7 @@ agent-sh -e ./examples/extensions/latex-images.ts
717
714
 
718
715
  Input modes change what happens when the user types and presses Enter. Each mode binds a trigger character (typed at the start of an empty line) to a custom `onSubmit` handler. The built-in mode (`>` for agent) is registered this way — it's not special.
719
716
 
720
- The flow: user types trigger → prompt changes to show the mode → user types their input → presses Enter → `onSubmit` fires → your handler emits `agent:submit`. You can optionally include a `modeInstruction` that gets prepended to the user message.
717
+ The flow: user types trigger → prompt changes to show the mode → user types their input → presses Enter → `onSubmit` fires → your handler emits `agent:submit`. To steer the agent for this mode, build your instruction into the `query` string before emitting `agent:submit` carries only `query` (and optional `images`).
721
718
 
722
719
  ```typescript
723
720
  bus.emit("input-mode:register", {
@@ -728,8 +725,8 @@ bus.emit("input-mode:register", {
728
725
  indicator: "🌐", // status indicator before the icon
729
726
  onSubmit(query, bus) {
730
727
  bus.emit("agent:submit", {
731
- query, // what the user typed
732
- modeInstruction: "[mode: translate] Translate the following to Spanish.",
728
+ // prepend the mode instruction to what the user typed
729
+ query: `[mode: translate] Translate the following to Spanish.\n\n${query}`,
733
730
  });
734
731
  },
735
732
  returnToSelf: true, // re-enter this mode after agent finishes
@@ -743,7 +740,7 @@ bus.emit("input-mode:register", {
743
740
  | `label` | `string` | Shown in the prompt area |
744
741
  | `promptIcon` | `string` | Chevron/icon character in the prompt |
745
742
  | `indicator` | `string` | Status indicator before the icon |
746
- | `onSubmit` | `(query, bus) => void` | Called on Enter. Emits `agent:submit` with `query` + optional `modeInstruction` |
743
+ | `onSubmit` | `(query, bus) => void` | Called on Enter. Emits `agent:submit` with `query` (build any mode instruction into the `query` string yourself) |
747
744
  | `returnToSelf` | `boolean` | Re-enter this mode after the agent finishes |
748
745
 
749
746
  Each trigger character can only be claimed by one mode. Slash commands and readline keybindings work in every mode.
@@ -826,7 +823,7 @@ If your extension wants to signal "this session is interactive — read the scre
826
823
  Internally, a remote session:
827
824
 
828
825
  1. **Redirects render streams** — `"agent"`, `"query"`, `"status"` all route to the provided surface
829
- 2. **Keeps the shell interactive** — advises `shell:on-processing-start` and `shell:on-processing-done` to skip pause/unpause
826
+ 2. **Keeps the shell interactive** — advises `shell:on-processing-start` and `shell:on-processing-redraw` to skip pause/redraw (it deliberately leaves `shell:on-processing-done` alone so the agent-turn state cleanup always runs)
830
827
  3. **Suppresses chrome** — advises `tui:response-border`, `tui:render-user-query`, `tui:render-usage` based on options
831
828
 
832
829
  Calling `session.close()` removes all advisors and restores all compositor routing in one call.
@@ -860,11 +857,11 @@ session.close();
860
857
 
861
858
  ## Shell Lifecycle Handlers
862
859
 
863
- The shell's behavior during agent processing is controlled by two advisable handlers. Extensions advise these to change how the shell responds when the agent starts and stops working.
860
+ The shell's behavior during agent processing is controlled by three handlers. Two are advisable; the third runs unconditional cleanup and should not be suppressed.
864
861
 
865
862
  ### `shell:on-processing-start`
866
863
 
867
- Default: pauses the shell (blocks PTY output and input) while the agent works. This is correct when agent output shares stdout with the terminal.
864
+ Default: pauses the shell (blocks PTY output and input) and acquires the agent-turn mute scope while the agent works. This is correct when agent output shares stdout with the terminal.
868
865
 
869
866
  ```typescript
870
867
  // Skip pause — agent output goes to a separate surface
@@ -874,19 +871,23 @@ ctx.advise("shell:on-processing-start", (next) => {
874
871
  });
875
872
  ```
876
873
 
877
- ### `shell:on-processing-done`
874
+ ### `shell:on-processing-redraw`
878
875
 
879
- Default: unpauses the shell, re-enters agent input mode or redraws the shell prompt.
876
+ Default: re-enters agent input mode or redraws the shell prompt. This is the advisable half of "agent finished" — advise it to skip the redraw when your extension already owns the screen.
880
877
 
881
878
  ```typescript
882
879
  // Skip prompt redraw — already handled by the extension
883
- ctx.advise("shell:on-processing-done", (next) => {
880
+ ctx.advise("shell:on-processing-redraw", (next) => {
884
881
  if (mySessionActive) return; // skip
885
- return next(); // default: unpause + redraw
882
+ return next(); // default: redraw / re-enter input mode
886
883
  });
887
884
  ```
888
885
 
889
- > **Note:** `createRemoteSession()` advises both of these automatically. You only need to advise them directly if you're building custom lifecycle behavior without using remote sessions.
886
+ ### `shell:on-processing-done`
887
+
888
+ Runs when the agent turn ends: it releases the agent-turn mute scope (unconditional state cleanup) and then calls `shell:on-processing-redraw`. **Don't advise this to return early** — skipping it would strand the mute scope past the end of the turn. Suppress the redraw via `shell:on-processing-redraw` instead.
889
+
890
+ > **Note:** `createRemoteSession()` advises `shell:on-processing-start` and `shell:on-processing-redraw` automatically. You only need to advise them directly if you're building custom lifecycle behavior without using remote sessions.
890
891
 
891
892
  ## Rendering Architecture
892
893
 
package/docs/library.md CHANGED
@@ -23,8 +23,9 @@ import { activateAgent } from "agent-sh/agent";
23
23
  import { loadBuiltinExtensions } from "agent-sh/extensions";
24
24
 
25
25
  const core = createCore({
26
- apiKey: process.env.OPENAI_API_KEY,
27
- model: "gpt-4o",
26
+ // These are ash-backend config, not kernel config — see note below.
27
+ provider: "deepseek", // built-in provider → DeepSeek endpoint + deepseek-v4-flash default
28
+ apiKey: process.env.DEEPSEEK_API_KEY,
28
29
  });
29
30
 
30
31
  const ctx = core.extensionContext({ quit: () => process.exit(0) });
@@ -44,6 +45,8 @@ core.bus.emit("agent:submit", { query: "explain this codebase" });
44
45
 
45
46
  `createCore()` returns a headless kernel — the event bus and handler registry, with no terminal, shell, LLM, or agent attached. `activateAgent(ctx)` attaches the agent surface (tools, LLM client, providers) and registers the built-in `ash` backend; `loadBuiltinExtensions(ctx)` adds the abstract backend registry, slash commands, and file autocomplete. `core:extensions-loaded` triggers provider resolution; `activateBackend()` then starts ash (or whichever backend is configured). Send queries by emitting `agent:submit` and consume responses by listening to bus events.
46
47
 
48
+ > **The LLM fields are backend config, not kernel config.** `createCore()` doesn't read `provider`/`apiKey`/`model`/`baseURL` — it stores the config object opaquely and re-exposes it through the `config:get-app-config` handler. The **ash** backend is the only consumer (`src/agent/index.ts`); it resolves the provider, key, and model from those fields. Under a different backend they're inert: `pi` reads `~/.pi/agent/settings.json`, `claude-code` uses its own SDK config — for those you pass `{ backend: "pi" }` (a real kernel field) and configure the model the backend's own way. The `AppConfig` type bundles kernel + agent + shell config into one object for convenience; the kernel only owns the `extensions` and `backend` keys (`CoreConfig`).
49
+
47
50
  Tools run without confirmation by default; to gate them, register tool advisors via `ctx.agent.adviseTool` (see examples/extensions/interactive-prompts.ts).
48
51
 
49
52
  ## AgentShellCore API
@@ -68,7 +71,7 @@ import { activateAgent } from "agent-sh/agent";
68
71
  import { loadBuiltinExtensions } from "agent-sh/extensions";
69
72
  import myTheme from "./my-theme";
70
73
 
71
- const core = createCore({ apiKey: "...", model: "gpt-4o" });
74
+ const core = createCore({ provider: "deepseek", apiKey: process.env.DEEPSEEK_API_KEY });
72
75
  const ctx = core.extensionContext({ quit: () => process.exit(0) });
73
76
 
74
77
  activateAgent(ctx);
@@ -18,7 +18,7 @@
18
18
 
19
19
  **Problem**: Tool calls not working (agent responds but doesn't use tools)
20
20
 
21
- **Solution**: Some models have limited or no tool/function calling support. Try a more capable model (e.g., gpt-4o, claude-sonnet-4-6 via OpenRouter).
21
+ **Solution**: Some models have limited or no tool/function calling support. Try a more capable model (e.g., deepseek-v4-flash, or a larger model via OpenRouter).
22
22
 
23
23
  **Problem**: Garbled output, startup banner overwritten, or messy prompt rendering
24
24
 
@@ -54,7 +54,7 @@ Your normal p10k prompt still works — only the "flash cached prompt then redra
54
54
  Enable debug mode for detailed protocol logging:
55
55
 
56
56
  ```bash
57
- DEBUG=1 agent-sh --api-key "$KEY" --model gpt-4o
57
+ DEBUG=1 DEEPSEEK_API_KEY="$KEY" agent-sh
58
58
  ```
59
59
 
60
60
  ## Getting Help
@@ -43,9 +43,11 @@ A `RenderSurface` is anything that can accept rendered output:
43
43
 
44
44
  ```typescript
45
45
  interface RenderSurface {
46
- write(text: string): void; // raw — supports \r, escape codes
46
+ write(text: string): void; // raw — supports \r, escape codes
47
47
  writeLine(line: string): void; // line + newline
48
48
  readonly columns: number; // available width
49
+ readonly rows: number; // available height
50
+ onResize(cb: (cols: number, rows: number) => void): () => void; // subscribe; returns unsubscribe
49
51
  }
50
52
  ```
51
53
 
@@ -71,6 +73,12 @@ const panelSurface: RenderSurface = {
71
73
  },
72
74
  writeLine(line) { panel.appendLine(line); },
73
75
  get columns() { return panel.computeGeometry().contentW; },
76
+ get rows() { return panel.computeGeometry().contentH; },
77
+ onResize(cb) {
78
+ const handler = () => { const g = panel.computeGeometry(); cb(g.contentW, g.contentH); };
79
+ process.stdout.on("resize", handler);
80
+ return () => process.stdout.off("resize", handler);
81
+ },
74
82
  };
75
83
  ```
76
84
 
@@ -94,7 +102,7 @@ interface Compositor {
94
102
  | `"query"` | User query display (the bordered input box) |
95
103
  | `"status"` | Info messages, errors, suggestions |
96
104
 
97
- The shell frontend (`src/shell/`) sets all three to `StdoutSurface` during `activateShell`. A library or web consumer that doesn't load the shell frontend has no defaults — it must call `compositor.setDefault(...)` itself.
105
+ The shell frontend (`src/shell/`) sets all three to a `surfaceFromTerminal(terminal)` surface (which writes through the host `Terminal`, ultimately stdout) during `activateShell`. A library or web consumer that doesn't load the shell frontend has no defaults — it must call `compositor.setDefault(...)` itself.
98
106
 
99
107
  ### Redirecting a stream
100
108
 
@@ -155,7 +163,7 @@ export default function activate(ctx: ExtensionContext): void {
155
163
  bus.emit("agent:submit", { query });
156
164
  });
157
165
 
158
- panel.handlers.advise("panel:dismiss", (next) => {
166
+ panel.handlers.advise("panel:hide", (next) => {
159
167
  next();
160
168
  restoreAgent?.(); restoreAgent = null;
161
169
  restoreQuery?.(); restoreQuery = null;
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
  }