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
@@ -637,6 +637,13 @@ export class FloatingPanel {
637
637
  this.autocompleteItems = [];
638
638
  this.autocompleteIndex = 0;
639
639
  }
640
+ moveAutocomplete(delta) {
641
+ const n = this.autocompleteItems.length;
642
+ if (n === 0)
643
+ return;
644
+ this.autocompleteIndex = (this.autocompleteIndex + delta + n) % n;
645
+ this.render();
646
+ }
640
647
  // ── Input handling ──────────────────────────────────────────
641
648
  handleIntercept(payload) {
642
649
  const consumed = { ...payload, consumed: true };
@@ -746,6 +753,16 @@ export class FloatingPanel {
746
753
  }
747
754
  if (this.handleScroll(data, false))
748
755
  return;
756
+ if (data === "\x10" || data === "\x0e") {
757
+ const forward = data === "\x0e";
758
+ if (this.autocompleteActive) {
759
+ this.moveAutocomplete(forward ? 1 : -1);
760
+ }
761
+ else if (forward ? this.editor.historyForward() : this.editor.historyBack()) {
762
+ this.render();
763
+ }
764
+ return;
765
+ }
749
766
  const actions = this.editor.feed(data);
750
767
  for (const action of actions) {
751
768
  switch (action.action) {
@@ -760,6 +777,7 @@ export class FloatingPanel {
760
777
  this.editor.pushHistory(query);
761
778
  this.editor.clear();
762
779
  this.clearAutocomplete();
780
+ this.userScrolled = false;
763
781
  // Phase change is the submit handler's call — sync slash commands
764
782
  // (e.g. /model, /help) keep the user in input mode.
765
783
  this.handlers.call(`${this.prefix}:submit`, query);
@@ -782,34 +800,18 @@ export class FloatingPanel {
782
800
  case "shift+tab":
783
801
  this.render();
784
802
  break;
785
- case "arrow-up": {
786
- if (this.autocompleteActive) {
787
- this.autocompleteIndex = this.autocompleteIndex === 0
788
- ? this.autocompleteItems.length - 1
789
- : this.autocompleteIndex - 1;
790
- this.render();
791
- }
792
- else {
793
- const hist = this.editor.historyBack();
794
- if (hist)
795
- this.render();
796
- }
803
+ case "arrow-up":
804
+ if (this.autocompleteActive)
805
+ this.moveAutocomplete(-1);
806
+ else
807
+ this.scrollUp(1);
797
808
  break;
798
- }
799
- case "arrow-down": {
800
- if (this.autocompleteActive) {
801
- this.autocompleteIndex = this.autocompleteIndex === this.autocompleteItems.length - 1
802
- ? 0
803
- : this.autocompleteIndex + 1;
804
- this.render();
805
- }
806
- else {
807
- const hist = this.editor.historyForward();
808
- if (hist)
809
- this.render();
810
- }
809
+ case "arrow-down":
810
+ if (this.autocompleteActive)
811
+ this.moveAutocomplete(1);
812
+ else
813
+ this.scrollDown(1);
811
814
  break;
812
- }
813
815
  case "changed":
814
816
  case "delete-empty":
815
817
  this.updateAutocomplete();
@@ -78,6 +78,34 @@ export function wrapLine(text, maxWidth) {
78
78
  lineWidth = 0;
79
79
  lastVisibleIdx = -1;
80
80
  };
81
+ const hardBreak = (token) => {
82
+ let remaining = token;
83
+ while (remaining.length > 0) {
84
+ let fitLen = 0, fitWidth = 0;
85
+ for (const ch of remaining) {
86
+ const cw = charWidth(ch.codePointAt(0) ?? 0);
87
+ if (fitWidth + cw > maxWidth - lineWidth)
88
+ break;
89
+ fitWidth += cw;
90
+ fitLen += ch.length;
91
+ }
92
+ if (fitLen === 0) {
93
+ // Force one char on an empty line so an over-wide char can't loop forever.
94
+ if (lineWidth > 0) {
95
+ commit();
96
+ continue;
97
+ }
98
+ fitLen = remaining[0]?.length ?? 1;
99
+ }
100
+ const chunk = remaining.slice(0, fitLen);
101
+ remaining = remaining.slice(fitLen);
102
+ lineTokens.push(chunk);
103
+ lineWidth += visibleLen(chunk);
104
+ lastVisibleIdx = lineTokens.length - 1;
105
+ if (remaining.length > 0)
106
+ commit();
107
+ }
108
+ };
81
109
  for (const seg of segments) {
82
110
  if (seg.startsWith("\x1b[")) {
83
111
  lineTokens.push(seg);
@@ -102,26 +130,7 @@ export function wrapLine(text, maxWidth) {
102
130
  continue; // spaces at wrap points are dropped
103
131
  if (lineWidth === 0) {
104
132
  // Token longer than the entire line — hard-break by char width.
105
- let remaining = token;
106
- while (remaining.length > 0) {
107
- let fitLen = 0, fitWidth = 0;
108
- for (const ch of remaining) {
109
- const cw = charWidth(ch.codePointAt(0) ?? 0);
110
- if (fitWidth + cw > maxWidth)
111
- break;
112
- fitWidth += cw;
113
- fitLen += ch.length;
114
- }
115
- if (fitLen === 0)
116
- fitLen = remaining[0]?.length ?? 1;
117
- const chunk = remaining.slice(0, fitLen);
118
- remaining = remaining.slice(fitLen);
119
- lineTokens.push(chunk);
120
- lineWidth += visibleLen(chunk);
121
- lastVisibleIdx = lineTokens.length - 1;
122
- if (remaining.length > 0)
123
- commit();
124
- }
133
+ hardBreak(token);
125
134
  continue;
126
135
  }
127
136
  // Rule (a): closing punctuation must not start a line. Allow up to 2
@@ -146,9 +155,14 @@ export function wrapLine(text, maxWidth) {
146
155
  lineTokens.push(t);
147
156
  lineWidth += visibleLen(t);
148
157
  }
149
- lineTokens.push(token);
150
- lineWidth += tokenWidth;
151
- lastVisibleIdx = lineTokens.length - 1;
158
+ if (lineWidth + tokenWidth <= maxWidth) {
159
+ lineTokens.push(token);
160
+ lineWidth += tokenWidth;
161
+ lastVisibleIdx = lineTokens.length - 1;
162
+ }
163
+ else {
164
+ hardBreak(token);
165
+ }
152
166
  }
153
167
  }
154
168
  if (lineWidth > 0) {
@@ -318,27 +332,25 @@ export class MarkdownRenderer {
318
332
  renderLine(line) {
319
333
  if (line.trim() === "")
320
334
  return "";
321
- // Headings
322
- const h1 = line.match(/^# (.+)/);
323
- if (h1)
324
- return `${p.bold}${p.warning}${h1[1]}${p.reset}`;
325
- const h2 = line.match(/^## (.+)/);
326
- if (h2)
327
- return `${p.bold}${p.accent}${h2[1]}${p.reset}`;
328
- const h3 = line.match(/^### (.+)/);
329
- if (h3)
330
- return `${p.bold}${h3[1]}${p.reset}`;
331
- const h4 = line.match(/^#{4,} (.+)/);
332
- if (h4)
333
- return `${p.bold}${h4[1]}${p.reset}`;
334
- // Horizontal rule — subtle short separator, not full-width
335
+ // Headings — H3+ keep the `###` marker; H1/H2 don't
336
+ const heading = line.match(/^(#{1,6}) (.+)/);
337
+ if (heading) {
338
+ const level = heading[1].length;
339
+ const text = heading[2];
340
+ if (level === 1)
341
+ return `${p.bold}${p.underline}${p.mdHeading}${text}${p.reset}`;
342
+ if (level === 2)
343
+ return `${p.bold}${p.mdHeading}${text}${p.reset}`;
344
+ return `${p.bold}${p.mdHeading}${"#".repeat(level)} ${text}${p.reset}`;
345
+ }
346
+ // Horizontal rule
335
347
  if (/^(-{3,}|_{3,}|\*{3,})\s*$/.test(line)) {
336
- return "";
348
+ return `${p.mdHr}${"".repeat(Math.min(this.contentWidth, 80))}${p.reset}`;
337
349
  }
338
350
  // Blockquote
339
351
  const bq = line.match(/^>\s?(.*)/);
340
352
  if (bq)
341
- return `${p.muted}│${p.reset} ${p.dim}${p.italic}${this.renderInline(bq[1] || "")}${p.reset}`;
353
+ return `${p.mdQuoteBorder}│${p.reset} ${p.mdQuote}${p.italic}${this.renderInline(bq[1] || "")}${p.reset}`;
342
354
  // Task list (checkbox items) — must come before generic unordered list
343
355
  const task = line.match(/^(\s*)[*\-+]\s+\[([ xX])\]\s+(.*)/);
344
356
  if (task) {
@@ -353,21 +365,21 @@ export class MarkdownRenderer {
353
365
  const ul = line.match(/^(\s*)[*\-+]\s+(.*)/);
354
366
  if (ul) {
355
367
  const indent = ul[1] || "";
356
- return `${indent} ${p.accent}*${p.reset} ${this.renderInline(ul[2] || "")}`;
368
+ return `${indent} ${p.mdListBullet}-${p.reset} ${this.renderInline(ul[2] || "")}`;
357
369
  }
358
370
  // Ordered list
359
371
  const ol = line.match(/^(\s*)(\d+)[.)]\s+(.*)/);
360
372
  if (ol) {
361
373
  const indent = ol[1] || "";
362
- return `${indent} ${p.accent}${ol[2]}.${p.reset} ${this.renderInline(ol[3] || "")}`;
374
+ return `${indent} ${p.mdListBullet}${ol[2]}.${p.reset} ${this.renderInline(ol[3] || "")}`;
363
375
  }
364
376
  return this.renderInline(line);
365
377
  }
366
378
  renderInline(text) {
367
379
  // Links first — later subs inject `\x1b[…m` whose `[` would be eaten here.
368
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, `$1 ${p.muted}${p.underline}($2)${p.reset}`);
380
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, `${p.mdLink}${p.underline}$1${p.reset} ${p.mdLinkUrl}($2)${p.reset}`);
369
381
  // Inline code
370
- text = text.replace(/`([^`]+)`/g, `${p.accent}$1${p.reset}`);
382
+ text = text.replace(/`([^`]+)`/g, `${p.mdCode}$1${p.reset}`);
371
383
  // Bold + italic
372
384
  text = text.replace(/\*\*\*(.+?)\*\*\*/g, `${p.bold}${p.italic}$1${p.reset}`);
373
385
  // Bold
@@ -377,7 +389,7 @@ export class MarkdownRenderer {
377
389
  text = text.replace(/\*(.+?)\*/g, `${p.italic}$1${p.reset}`);
378
390
  text = text.replace(/(?<!\w)_(.+?)_(?!\w)/g, `${p.italic}$1${p.reset}`);
379
391
  // Strikethrough
380
- text = text.replace(/~~(.+?)~~/g, `${p.dim}$1${p.reset}`);
392
+ text = text.replace(/~~(.+?)~~/g, `${p.strikethrough}$1${p.reset}`);
381
393
  return text;
382
394
  }
383
395
  /**
@@ -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;