agent-sh 0.15.5 → 0.15.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/agent/agent-loop.js +2 -5
- package/dist/agent/extensions/rolling-history/index.js +20 -8
- package/dist/agent/extensions/rolling-history/recall.d.ts +2 -2
- package/dist/agent/extensions/rolling-history/recall.js +17 -7
- package/dist/agent/providers/openai-compatible.d.ts +8 -0
- package/dist/agent/providers/openai-compatible.js +9 -2
- package/dist/agent/store.js +6 -1
- package/dist/agent/token-budget.d.ts +2 -1
- package/dist/agent/token-budget.js +6 -1
- package/dist/agent/types.d.ts +4 -1
- package/dist/cli/index.js +1 -1
- package/dist/core/event-bus.d.ts +16 -1
- package/dist/core/event-bus.js +73 -11
- package/dist/core/index.js +18 -0
- package/dist/shell/tui-renderer.js +116 -174
- package/dist/utils/diff-renderer.js +65 -30
- package/dist/utils/executor.js +19 -11
- package/dist/utils/floating-panel.d.ts +1 -0
- package/dist/utils/floating-panel.js +28 -26
- package/dist/utils/markdown.js +56 -44
- package/dist/utils/palette.d.ts +11 -0
- package/dist/utils/palette.js +11 -0
- package/docs/agent.md +13 -11
- package/docs/architecture.md +3 -5
- package/docs/extensions.md +21 -20
- package/docs/library.md +6 -3
- package/docs/troubleshooting.md +2 -2
- package/docs/tui-composition.md +11 -3
- package/docs/usage.md +70 -50
- package/examples/extensions/ashi/src/chat/assistant.ts +6 -4
- package/examples/extensions/ashi/src/compaction.ts +4 -7
- package/examples/extensions/ashi/src/frontend.ts +2 -0
- package/examples/extensions/ashi/src/schema.ts +8 -2
- package/examples/extensions/command-suggest.ts +90 -0
- package/examples/extensions/solarized-theme.ts +11 -0
- package/package.json +5 -5
- package/src/agent/agent-loop.ts +2 -5
- package/src/agent/extensions/rolling-history/index.ts +20 -8
- package/src/agent/extensions/rolling-history/recall.ts +28 -7
- package/src/agent/providers/openai-compatible.ts +19 -4
- package/src/agent/store.ts +5 -1
- package/src/agent/token-budget.ts +10 -1
- package/src/agent/types.ts +4 -1
- package/src/cli/index.ts +1 -1
- package/src/core/event-bus.ts +67 -12
- package/src/core/index.ts +18 -0
- package/src/shell/tui-renderer.ts +131 -207
- package/src/utils/diff-renderer.ts +62 -29
- package/src/utils/executor.ts +17 -14
- package/src/utils/floating-panel.ts +24 -22
- package/src/utils/markdown.ts +49 -40
- package/src/utils/palette.ts +30 -5
|
@@ -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.
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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();
|
package/dist/utils/markdown.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return `${p.bold}${
|
|
331
|
-
|
|
332
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
392
|
+
text = text.replace(/~~(.+?)~~/g, `${p.strikethrough}$1${p.reset}`);
|
|
381
393
|
return text;
|
|
382
394
|
}
|
|
383
395
|
/**
|
package/dist/utils/palette.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/palette.js
CHANGED
|
@@ -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. **
|
|
48
|
-
3. **
|
|
49
|
-
4. **
|
|
50
|
-
5. **
|
|
51
|
-
6. **
|
|
52
|
-
7. **
|
|
53
|
-
8. **
|
|
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
|
|
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": "
|
|
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. "
|
|
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;
|
package/docs/architecture.md
CHANGED
|
@@ -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,
|
|
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, /
|
|
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
|
package/docs/extensions.md
CHANGED
|
@@ -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: "
|
|
479
|
-
baseURL: "http://localhost:
|
|
480
|
-
defaultModel: "
|
|
481
|
-
models: ["
|
|
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`.
|
|
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
|
-
|
|
732
|
-
|
|
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`
|
|
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-
|
|
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
|
|
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-
|
|
874
|
+
### `shell:on-processing-redraw`
|
|
878
875
|
|
|
879
|
-
Default:
|
|
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-
|
|
880
|
+
ctx.advise("shell:on-processing-redraw", (next) => {
|
|
884
881
|
if (mySessionActive) return; // skip
|
|
885
|
-
return next(); // default:
|
|
882
|
+
return next(); // default: redraw / re-enter input mode
|
|
886
883
|
});
|
|
887
884
|
```
|
|
888
885
|
|
|
889
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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({
|
|
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);
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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.,
|
|
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
|
|
57
|
+
DEBUG=1 DEEPSEEK_API_KEY="$KEY" agent-sh
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
## Getting Help
|
package/docs/tui-composition.md
CHANGED
|
@@ -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;
|
|
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 `
|
|
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:
|
|
166
|
+
panel.handlers.advise("panel:hide", (next) => {
|
|
159
167
|
next();
|
|
160
168
|
restoreAgent?.(); restoreAgent = null;
|
|
161
169
|
restoreQuery?.(); restoreQuery = null;
|