agent-sh 0.14.10 → 0.14.11
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/README.md +47 -20
- package/dist/agent/agent-loop.js +20 -15
- package/dist/agent/events.d.ts +2 -1
- package/dist/agent/index.js +38 -7
- package/dist/agent/live-view.d.ts +3 -3
- package/dist/agent/live-view.js +15 -7
- package/dist/agent/providers/openrouter.js +9 -0
- package/dist/agent/subagent.js +1 -1
- package/dist/cli/install.js +10 -1
- package/dist/shell/events.d.ts +3 -0
- package/dist/shell/shell.js +3 -0
- package/dist/utils/diff-renderer.d.ts +4 -0
- package/dist/utils/diff-renderer.js +15 -20
- package/examples/extensions/ads/SKILL.md +170 -0
- package/examples/extensions/ash-scheme/index.ts +332 -672
- package/examples/extensions/ash-scheme/package.json +1 -1
- package/examples/extensions/ashi/EXTENDING.md +116 -0
- package/examples/extensions/ashi/README.md +10 -54
- package/examples/extensions/ashi/package.json +6 -2
- package/examples/extensions/ashi/src/autocomplete-controller.ts +95 -0
- package/examples/extensions/ashi/src/autocomplete.ts +1 -23
- package/examples/extensions/ashi/src/capture.ts +9 -3
- package/examples/extensions/ashi/src/chat/assistant.ts +87 -0
- package/examples/extensions/ashi/src/chat/lines.ts +20 -0
- package/examples/extensions/ashi/src/chat/thinking.ts +42 -0
- package/examples/extensions/ashi/src/chat/tool-group.ts +84 -0
- package/examples/extensions/ashi/src/chat/user-message.ts +20 -0
- package/examples/extensions/ashi/src/cli.ts +56 -10
- package/examples/extensions/ashi/src/clipboard-image.ts +41 -0
- package/examples/extensions/ashi/src/commands.ts +11 -1
- package/examples/extensions/ashi/src/display-config.ts +9 -1
- package/examples/extensions/ashi/src/frontend.ts +340 -259
- package/examples/extensions/ashi/src/hooks.ts +33 -40
- package/examples/extensions/ashi/src/renderer.ts +222 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/app.ts +122 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/index.ts +23 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/nodes.ts +133 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +193 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/theme-adapters.ts +48 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +21 -0
- package/examples/extensions/ashi/src/schema.ts +43 -205
- package/examples/extensions/ashi/src/status-footer.ts +15 -23
- package/examples/extensions/ashi/src/terminal-mode.ts +9 -0
- package/examples/extensions/ashi/src/theme.ts +1 -47
- package/examples/extensions/ashi-ink/README.md +59 -0
- package/examples/extensions/ashi-ink/package.json +30 -0
- package/examples/extensions/ashi-ink/src/index.ts +6 -0
- package/examples/extensions/ashi-ink/src/ink-renderer.tsx +865 -0
- package/examples/extensions/ashi-ink/src/shims.d.ts +5 -0
- package/examples/extensions/ashi-ink/test/render.test.tsx +408 -0
- package/examples/extensions/ashi-ink/tsconfig.json +14 -0
- package/examples/extensions/ashi-scheme-render.ts +4 -10
- package/examples/extensions/ashi-shell-passthrough.ts +95 -0
- package/examples/extensions/latex-images.ts +22 -19
- package/examples/extensions/terminal-buffer.ts +4 -2
- package/package.json +3 -9
- package/examples/extensions/ashi/src/components.ts +0 -238
- package/examples/extensions/ollama.ts +0 -108
- package/examples/extensions/opencode-provider.ts +0 -251
- package/examples/extensions/zai-coding-plan.ts +0 -35
package/README.md
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
# agent-sh
|
|
2
2
|
|
|
3
|
-
A real shell with an AI agent one keystroke away.
|
|
4
|
-
|
|
5
3
|
[](https://www.npmjs.com/package/agent-sh)
|
|
6
4
|
[](https://github.com/guanyilun/agent-sh/blob/main/LICENSE)
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
A composable agent runtime — pair any frontend with any agent backend, over one shared extension layer.
|
|
7
|
+
|
|
8
|
+
## Three example apps built on agent-sh
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
agent-sh is small at its core and does its real work through extensions, so the same runtime drives very different apps. Three to start with — all sharing the same agent backends, tools, providers, and `~/.agent-sh/settings.json`:
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
### 1. A shell with the agent one keystroke away — bundled with agent-sh
|
|
13
|
+
|
|
14
|
+
A normal shell on top of node-pty — your rc config, your aliases, vim and tmux all just work. But at the start of any line, type `>` and you're talking to a small agent that already sees your cwd, your last command, and its output. Nothing to set up, no project to explain.
|
|
15
|
+
|
|
16
|
+

|
|
13
17
|
|
|
14
18
|
```
|
|
15
19
|
~ $ ls -la # real shell command
|
|
@@ -19,10 +23,47 @@ So I built agent-sh. Under the hood it's a normal shell on top of node-pty — y
|
|
|
19
23
|
~ $ > draft a commit message # agent reads your diff and shell history
|
|
20
24
|
```
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g agent-sh
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
[Quick Start ↓](#quick-start)
|
|
31
|
+
|
|
32
|
+
### 2. ashi — a standalone coding agent
|
|
33
|
+
|
|
34
|
+
[**`@guanyilun/ashi`**](examples/extensions/ashi/) is the same `ash` agent in a chat-style TUI, with no shell underneath — just the agent. Installed separately, it reuses agent-sh's backend, tools, slash commands, providers, and skills, and adds session history, in-session branching, and LLM-driven compaction.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install -g @guanyilun/ashi
|
|
38
|
+
ashi
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
ashi makes the runtime's **decoupled rendering** concrete: the frontend is itself an extension, and even *how* it draws tool calls and results is a swappable render extension. Same agent backend, same conversation — load a different render extension and the whole TUI restyles, no code changes:
|
|
42
|
+
|
|
43
|
+
| pi-style rendering | claude-code-style rendering |
|
|
44
|
+
|---|---|
|
|
45
|
+
|  |  |
|
|
46
|
+
|
|
47
|
+
### 3. asHub — a GUI coding agent
|
|
48
|
+
|
|
49
|
+
[**firslov/asHub**](https://github.com/firslov/asHub) is a third-party cross-platform desktop app (Electron) built on the agent-sh runtime: a multi-session sidebar, persistence across restarts, and a live-streaming interface with Markdown, syntax-highlighted code, diffs, and tool-call rendering. macOS / Windows / Linux.
|
|
50
|
+
|
|
51
|
+
It pushes the same decoupling one step further — the frontend isn't a terminal at all, but a full desktop GUI on the same runtime, backends, and tools:
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
## How it works
|
|
56
|
+
|
|
57
|
+
agent-sh is a **composable agent runtime**. At its center is a pure kernel — a typed event bus, a named-handler registry, and an extension loader — that knows nothing about terminals, LLMs, shells, or rendering. Everything else plugs into it: the agent backend, its tools, provider management, and the frontend that drives it.
|
|
58
|
+
|
|
59
|
+
The frontend and the agent backend are both just components on the bus, so you **mix and match** them freely — wire several frontends to one backend, or keep one frontend and swap the backend underneath — all sharing the **same extension layer** of tools, content transforms, slash commands, and themes. `import { createCore } from "agent-sh"` gives you the headless kernel; load the pieces you want and wire your own I/O.
|
|
60
|
+
|
|
61
|
+
For the kernel design in full — the bus, handlers, the compositor, and the shell ↔ agent boundary — see [Architecture](docs/architecture.md). To embed the runtime in your own frontend, see the [Library Guide](docs/library.md). The rest of this README covers the bundled shell.
|
|
23
62
|
|
|
24
63
|
## Quick Start
|
|
25
64
|
|
|
65
|
+
**This sets up the agent-sh shell** — the frontend bundled in the `agent-sh` package. (For the other frontends, install [ashi](examples/extensions/ashi/) or [asHub](https://github.com/firslov/asHub) instead.)
|
|
66
|
+
|
|
26
67
|
### Installation
|
|
27
68
|
|
|
28
69
|
Install from npm:
|
|
@@ -139,20 +180,6 @@ All three bridges receive agent-sh's per-query shell context (`<shell_events>`)
|
|
|
139
180
|
|
|
140
181
|
**Caveat:** pi, claude-code, and opencode each manage their own tool surfaces, so agent-sh extensions that register tools (or skills, instructions, etc.) for the built-in `ash` agent generally won't be visible to a hosted backend. Frontend extensions (themes, content transforms, slash commands, the TUI renderer) keep working — only the agent-side capabilities differ. Use the bridges when you want that agent's toolset; stay on `ash` when you want agent-sh's extension ecosystem.
|
|
141
182
|
|
|
142
|
-
## Key Features
|
|
143
|
-
|
|
144
|
-
**Real terminal, zero compromise.** Full PTY with your shell config, aliases, and environment. Shell starts instantly — the agent connects asynchronously in the background.
|
|
145
|
-
|
|
146
|
-
**One entry point, smart tool selection.** Type `>` and agent-sh figures out how to help. Scratchpad tools (`bash`, `read_file`, `grep`, `glob`) for investigation. Extensions add capabilities like running commands in your live shell. No modes to pick — the agent reasons about which tools to use based on your intent.
|
|
147
|
-
|
|
148
|
-
**Context that just works.** Every query includes your cwd, recent commands, and their output. Run a failing test, type `> fix this`, and agent-sh knows exactly what happened. Context management works like shell history — continuous, persistent across restarts, no sessions to manage. See [Context Management](docs/context-management.md).
|
|
149
|
-
|
|
150
|
-
**Any LLM, any backend.** agent-sh works with any OpenAI-compatible API out of the box. Define multiple providers in settings and switch models at runtime with `/model <name>`. Or swap in a completely different agent — bundled bridges run [pi](examples/extensions/pi-bridge/), [claude-code](examples/extensions/claude-code-bridge/), or [opencode](examples/extensions/opencode-bridge/) as a drop-in backend (see [Bring your own agent](#bring-your-own-agent)).
|
|
151
|
-
|
|
152
|
-
**Extensible by design.** The entire system is built on a typed event bus. Extensions can add custom input modes, content transforms (render LaTeX as images, Mermaid as diagrams), themes, slash commands, or replace the agent backend entirely. The built-in TUI renderer is itself just an extension.
|
|
153
|
-
|
|
154
|
-
**Embeddable as a library.** The core is a headless kernel — `import { createCore } from "agent-sh"` to build WebSocket servers, REST APIs, Electron apps, or test harnesses. No terminal required.
|
|
155
|
-
|
|
156
183
|
## Documentation
|
|
157
184
|
|
|
158
185
|
Start with **Usage** to get running, then **Architecture** for the mental model.
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -135,8 +135,8 @@ export class AgentLoop {
|
|
|
135
135
|
}
|
|
136
136
|
return acc;
|
|
137
137
|
});
|
|
138
|
-
on("agent:submit", ({ query }) => {
|
|
139
|
-
this.handleQuery(query).catch(() => { });
|
|
138
|
+
on("agent:submit", ({ query, images }) => {
|
|
139
|
+
this.handleQuery(query, images).catch(() => { });
|
|
140
140
|
});
|
|
141
141
|
on("agent:cancel-request", (e) => {
|
|
142
142
|
this.abortController?.abort(e.silent ? "silent" : undefined);
|
|
@@ -260,7 +260,7 @@ export class AgentLoop {
|
|
|
260
260
|
budgetTokens: this.currentMode.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
261
261
|
}));
|
|
262
262
|
onPipe("context:snapshot", (payload) => {
|
|
263
|
-
payload.messages = this.conversation.
|
|
263
|
+
payload.messages = this.conversation.get();
|
|
264
264
|
payload.contextWindow = this.currentMode.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
265
265
|
payload.activeTokens = this.conversation.estimateTokens();
|
|
266
266
|
return payload;
|
|
@@ -657,12 +657,10 @@ export class AgentLoop {
|
|
|
657
657
|
// filter, reorder, inject — whatever strategy fits.
|
|
658
658
|
h.define("conversation:prepare", (messages) => messages);
|
|
659
659
|
// ── Conversation primitives for compaction strategies ─────────
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
// `conversation:compact` use these to observe and mutate.
|
|
663
|
-
h.define("conversation:get-messages", () => this.conversation.getMessages());
|
|
660
|
+
// Canonical array (link/replace index space), not forLLM().
|
|
661
|
+
h.define("conversation:get-messages", () => this.conversation.get());
|
|
664
662
|
h.define("conversation:replace-messages", (msgs) => {
|
|
665
|
-
this.conversation.
|
|
663
|
+
this.conversation.replace(msgs);
|
|
666
664
|
});
|
|
667
665
|
h.define("conversation:estimate-tokens", () => this.conversation.estimateTokens());
|
|
668
666
|
h.define("conversation:estimate-prompt-tokens", () => this.conversation.estimatePromptTokens());
|
|
@@ -671,13 +669,13 @@ export class AgentLoop {
|
|
|
671
669
|
const strategy = opts.strategy;
|
|
672
670
|
if (strategy?.kind === "rewind" || strategy?.kind === "replace") {
|
|
673
671
|
const before = this.conversation.estimatePromptTokens();
|
|
674
|
-
const beforeLen = this.conversation.
|
|
672
|
+
const beforeLen = this.conversation.get().length;
|
|
675
673
|
const next = strategy.kind === "rewind"
|
|
676
|
-
? this.conversation.
|
|
674
|
+
? this.conversation.get().slice(0, strategy.toIndex)
|
|
677
675
|
: strategy.messages;
|
|
678
|
-
this.conversation.
|
|
676
|
+
this.conversation.replace(next);
|
|
679
677
|
const after = this.conversation.estimatePromptTokens();
|
|
680
|
-
const afterLen = this.conversation.
|
|
678
|
+
const afterLen = this.conversation.get().length;
|
|
681
679
|
return { before, after, evictedCount: Math.max(0, beforeLen - afterLen) };
|
|
682
680
|
}
|
|
683
681
|
return null;
|
|
@@ -755,7 +753,7 @@ export class AgentLoop {
|
|
|
755
753
|
return result;
|
|
756
754
|
});
|
|
757
755
|
}
|
|
758
|
-
async handleQuery(query) {
|
|
756
|
+
async handleQuery(query, images) {
|
|
759
757
|
// Cancel any in-flight loop (concurrent prompt handling)
|
|
760
758
|
if (this.abortController) {
|
|
761
759
|
this.abortController.abort();
|
|
@@ -778,7 +776,14 @@ export class AgentLoop {
|
|
|
778
776
|
const userContent = queryContext
|
|
779
777
|
? `<query_context>\n${queryContext}\n</query_context>\n\n${query}`
|
|
780
778
|
: query;
|
|
781
|
-
|
|
779
|
+
// Fail closed: an image sent to a non-vision model errors and leaves an
|
|
780
|
+
// unsendable message poisoning history, so require declared image support.
|
|
781
|
+
let userImages = images?.length ? images : undefined;
|
|
782
|
+
if (userImages && !this.currentMode.modalities?.includes("image")) {
|
|
783
|
+
this.bus.emit("ui:info", { message: `Current model has no declared image support — ${userImages.length} image(s) dropped.` });
|
|
784
|
+
userImages = undefined;
|
|
785
|
+
}
|
|
786
|
+
this.conversation.addUserMessage(userContent, userImages);
|
|
782
787
|
this.bus.emit("conversation:message-appended", { role: "user", content: query });
|
|
783
788
|
responseText = await this.executeLoop(signal);
|
|
784
789
|
}
|
|
@@ -1262,7 +1267,7 @@ export class AgentLoop {
|
|
|
1262
1267
|
// wrapTrailingWithDynamicContext for the cache-stability rationale.
|
|
1263
1268
|
const rawMessages = [
|
|
1264
1269
|
{ role: "system", content: systemPrompt },
|
|
1265
|
-
...wrapTrailingWithDynamicContext(this.conversation.
|
|
1270
|
+
...wrapTrailingWithDynamicContext(this.conversation.forLLM(), dynamicContext, toolPrompt),
|
|
1266
1271
|
];
|
|
1267
1272
|
// Let extensions transform the message array (compact, summarize, filter, etc.)
|
|
1268
1273
|
const messages = this.handlers.call("conversation:prepare", rawMessages);
|
package/dist/agent/events.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderRegistration } from "./host-types.js";
|
|
2
|
-
import type { ToolDefinition, ToolResultDisplay } from "./types.js";
|
|
2
|
+
import type { ImageContent, ToolDefinition, ToolResultDisplay } from "./types.js";
|
|
3
3
|
export interface AgentIdentity {
|
|
4
4
|
name: string;
|
|
5
5
|
version: string;
|
|
@@ -44,6 +44,7 @@ declare module "../core/event-bus.js" {
|
|
|
44
44
|
};
|
|
45
45
|
"agent:submit": {
|
|
46
46
|
query: string;
|
|
47
|
+
images?: ImageContent[];
|
|
47
48
|
};
|
|
48
49
|
"agent:cancel-request": {
|
|
49
50
|
silent?: boolean;
|
package/dist/agent/index.js
CHANGED
|
@@ -32,6 +32,12 @@ function persistedModelFor(providerName) {
|
|
|
32
32
|
return undefined;
|
|
33
33
|
return getSettings().providers?.[providerName]?.defaultModel;
|
|
34
34
|
}
|
|
35
|
+
/** The OpenAI SDK silently defaults an empty baseURL to api.openai.com, so a
|
|
36
|
+
* provider with a key but no endpoint would misroute its key there. `openai`
|
|
37
|
+
* is exempt: that default is its endpoint. */
|
|
38
|
+
function usableProvider(p) {
|
|
39
|
+
return !!p?.apiKey && (!!p.baseURL || p.id === "openai");
|
|
40
|
+
}
|
|
35
41
|
function defaultReasoningBuilder(level) {
|
|
36
42
|
if (level === "off")
|
|
37
43
|
return {};
|
|
@@ -279,6 +285,8 @@ export default function agentBackend(ctx) {
|
|
|
279
285
|
for (const [id, p] of resolvedProviders) {
|
|
280
286
|
if (!p.apiKey)
|
|
281
287
|
continue;
|
|
288
|
+
if (!usableProvider(p))
|
|
289
|
+
continue;
|
|
282
290
|
const shapeId = p.reasoningShape ?? id;
|
|
283
291
|
for (const model of p.models) {
|
|
284
292
|
const mc = p.modelCapabilities?.get(model);
|
|
@@ -347,13 +355,32 @@ export default function agentBackend(ctx) {
|
|
|
347
355
|
loadedExtensionNames = names;
|
|
348
356
|
resolvedProviders = computeResolvedProviders();
|
|
349
357
|
const settings = getSettings();
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
358
|
+
let providerName = config.provider ?? settings.defaultProvider;
|
|
359
|
+
let activeProvider = providerName ? resolvedProviders.get(providerName) ?? null : null;
|
|
360
|
+
// Inline CLI credentials carry their own endpoint, so they skip the
|
|
361
|
+
// usable-provider fallback that registry-driven selection needs.
|
|
362
|
+
if (!config.apiKey) {
|
|
363
|
+
if (!providerName) {
|
|
364
|
+
const first = [...resolvedProviders].find(([, p]) => usableProvider(p));
|
|
365
|
+
providerName = first?.[0];
|
|
366
|
+
activeProvider = first?.[1] ?? null;
|
|
367
|
+
}
|
|
368
|
+
else if (!usableProvider(activeProvider)) {
|
|
369
|
+
const reason = !activeProvider ? "is not registered"
|
|
370
|
+
: !activeProvider.apiKey ? "has no API key configured"
|
|
371
|
+
: "has no endpoint configured";
|
|
372
|
+
const next = [...resolvedProviders].find(([, p]) => usableProvider(p));
|
|
373
|
+
if (next) {
|
|
374
|
+
bus.emit("ui:error", { message: `Provider "${providerName}" ${reason}; falling back to "${next[0]}".` });
|
|
375
|
+
providerName = next[0];
|
|
376
|
+
activeProvider = next[1];
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
bus.emit("ui:error", { message: `Provider "${providerName}" ${reason}, and no other configured provider has both an API key and an endpoint. Run \`agent-sh auth\` to configure one.` });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
357
384
|
// Persisted defaultModel wins over openrouter's hardcoded DEFAULT_MODELS[0].
|
|
358
385
|
const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
|
|
359
386
|
const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
|
|
@@ -468,6 +495,10 @@ export default function agentBackend(ctx) {
|
|
|
468
495
|
bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
|
|
469
496
|
return;
|
|
470
497
|
}
|
|
498
|
+
if (!p.baseURL && p.id !== "openai") {
|
|
499
|
+
bus.emit("ui:error", { message: `Provider "${name}" has no endpoint configured` });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
471
502
|
const switchModel = p.defaultModel ?? p.models[0];
|
|
472
503
|
if (!switchModel) {
|
|
473
504
|
bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
|
|
@@ -19,7 +19,7 @@ export declare class LiveView {
|
|
|
19
19
|
constructor(handlers?: HandlerFunctions, instanceId?: string);
|
|
20
20
|
private getMessagesJson;
|
|
21
21
|
private invalidateMessagesCache;
|
|
22
|
-
addUserMessage(text: string): void;
|
|
22
|
+
addUserMessage(text: string, images?: ImageContent[]): void;
|
|
23
23
|
addAssistantMessage(content: string | null, toolCalls?: {
|
|
24
24
|
id: string;
|
|
25
25
|
function: {
|
|
@@ -34,9 +34,9 @@ export declare class LiveView {
|
|
|
34
34
|
appendUserMessage(text: string): void;
|
|
35
35
|
private hasOpenToolCalls;
|
|
36
36
|
private flushPendingMessages;
|
|
37
|
-
|
|
38
|
-
get(): AgentShMessage[];
|
|
37
|
+
/** Send-shaped; may be longer than get() (dangling calls stubbed) — never link()/replace() by these indices. */
|
|
39
38
|
forLLM(): ChatCompletionMessageParam[];
|
|
39
|
+
get(): AgentShMessage[];
|
|
40
40
|
replace(msgs: AgentShMessage[]): void;
|
|
41
41
|
link(index: number, entryId: string): void;
|
|
42
42
|
/** DeepSeek 400s on tool messages without a matching tool_call;
|
package/dist/agent/live-view.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { stripMeta } from "./llm-client.js";
|
|
2
1
|
export class LiveView {
|
|
3
2
|
messages = [];
|
|
4
3
|
messagesDirty = true;
|
|
@@ -26,8 +25,19 @@ export class LiveView {
|
|
|
26
25
|
this.messagesDirty = true;
|
|
27
26
|
this.cachedMessagesJson = null;
|
|
28
27
|
}
|
|
29
|
-
addUserMessage(text) {
|
|
30
|
-
|
|
28
|
+
addUserMessage(text, images) {
|
|
29
|
+
if (images?.length) {
|
|
30
|
+
const parts = [];
|
|
31
|
+
if (text)
|
|
32
|
+
parts.push({ type: "text", text });
|
|
33
|
+
for (const img of images) {
|
|
34
|
+
parts.push({ type: "image_url", image_url: { url: `data:${img.mimeType};base64,${img.data}` } });
|
|
35
|
+
}
|
|
36
|
+
this.messages.push({ role: "user", content: parts });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
this.messages.push({ role: "user", content: text });
|
|
40
|
+
}
|
|
31
41
|
this.invalidateMessagesCache();
|
|
32
42
|
}
|
|
33
43
|
addAssistantMessage(content, toolCalls, extras) {
|
|
@@ -131,15 +141,13 @@ export class LiveView {
|
|
|
131
141
|
}
|
|
132
142
|
this.invalidateMessagesCache();
|
|
133
143
|
}
|
|
134
|
-
|
|
144
|
+
/** Send-shaped; may be longer than get() (dangling calls stubbed) — never link()/replace() by these indices. */
|
|
145
|
+
forLLM() {
|
|
135
146
|
return this.normalizeReasoningConsistency(this.stubDanglingToolCalls(this.dropOrphanToolMessages(this.messages)));
|
|
136
147
|
}
|
|
137
148
|
get() {
|
|
138
149
|
return this.messages;
|
|
139
150
|
}
|
|
140
|
-
forLLM() {
|
|
141
|
-
return this.getMessages().map(stripMeta);
|
|
142
|
-
}
|
|
143
151
|
replace(msgs) {
|
|
144
152
|
this.replaceMessages(msgs);
|
|
145
153
|
}
|
|
@@ -14,6 +14,14 @@ function buildReasoningParams(level, _model) {
|
|
|
14
14
|
? { reasoning: { effort: "none" } }
|
|
15
15
|
: { reasoning: { effort: level } };
|
|
16
16
|
}
|
|
17
|
+
/** OpenRouter's input_modalities → the text/image subset; undefined when absent
|
|
18
|
+
* so the fail-closed image guard treats the model as text-only. */
|
|
19
|
+
function toModalities(input) {
|
|
20
|
+
if (!Array.isArray(input))
|
|
21
|
+
return undefined;
|
|
22
|
+
const out = input.filter((v) => v === "text" || v === "image");
|
|
23
|
+
return out.length ? out : undefined;
|
|
24
|
+
}
|
|
17
25
|
export default function activate(ctx) {
|
|
18
26
|
const apiKey = resolveApiKey("openrouter").key;
|
|
19
27
|
ctx.agent.providers.configure("openrouter", { reasoningParams: buildReasoningParams });
|
|
@@ -42,6 +50,7 @@ export default function activate(ctx) {
|
|
|
42
50
|
reasoning: m.supported_parameters?.includes("reasoning") ?? false,
|
|
43
51
|
contextWindow: m.context_length,
|
|
44
52
|
echoReasoning: userOverrides.get(m.id) ?? patterns.some((re) => re.test(m.id)),
|
|
53
|
+
modalities: toModalities(m.architecture?.input_modalities),
|
|
45
54
|
})),
|
|
46
55
|
});
|
|
47
56
|
}).catch(() => { });
|
package/dist/agent/subagent.js
CHANGED
|
@@ -109,7 +109,7 @@ async function streamOnce(llmClient, systemPrompt, conversation, apiTools, model
|
|
|
109
109
|
const stream = await llmClient.stream({
|
|
110
110
|
messages: [
|
|
111
111
|
{ role: "system", content: systemPrompt },
|
|
112
|
-
...wrapTrailingWithDynamicContext(conversation.
|
|
112
|
+
...wrapTrailingWithDynamicContext(conversation.forLLM(), dynamicContext ?? ""),
|
|
113
113
|
],
|
|
114
114
|
tools: apiTools.length > 0 ? apiTools : undefined,
|
|
115
115
|
model,
|
package/dist/cli/install.js
CHANGED
|
@@ -14,7 +14,16 @@ const EXT_DIR = path.join(CONFIG_DIR, "extensions");
|
|
|
14
14
|
export function listBundled() {
|
|
15
15
|
if (!fs.existsSync(BUNDLED_DIR))
|
|
16
16
|
return [];
|
|
17
|
-
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const d of fs.readdirSync(BUNDLED_DIR, { withFileTypes: true })) {
|
|
19
|
+
if (d.name.startsWith("."))
|
|
20
|
+
continue;
|
|
21
|
+
if (d.isDirectory())
|
|
22
|
+
out.push(d.name);
|
|
23
|
+
else if (SCRIPT_EXTS.some((ext) => d.name.endsWith(ext)))
|
|
24
|
+
out.push(d.name.replace(/\.[^.]+$/, ""));
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
18
27
|
}
|
|
19
28
|
/** Heuristic: a backend named "pi" is typically provided by an extension called "pi-bridge". */
|
|
20
29
|
export function suggestBridgeFor(backend) {
|
package/dist/shell/events.d.ts
CHANGED
package/dist/shell/shell.js
CHANGED
|
@@ -110,6 +110,9 @@ export class Shell {
|
|
|
110
110
|
this.bus.on("shell:pty-resize", ({ cols, rows }) => {
|
|
111
111
|
this.ptyProcess.resize(cols, rows);
|
|
112
112
|
});
|
|
113
|
+
this.bus.on("shell:host-write", ({ data }) => {
|
|
114
|
+
this.terminal.write(data);
|
|
115
|
+
});
|
|
113
116
|
// Compat shims for the bus-event API. shell:stdout-hold maps to hard
|
|
114
117
|
// mute so terminal_keys' stdout-show can't paint through the overlay.
|
|
115
118
|
let holdRefcount = 0;
|
|
@@ -13,6 +13,10 @@ export interface DiffRenderOptions {
|
|
|
13
13
|
trueColor?: boolean;
|
|
14
14
|
/** Enable syntax highlighting on diff lines. Default true. */
|
|
15
15
|
syntaxHighlight?: boolean;
|
|
16
|
+
/** Draw the `│` rule between the line number and the code (default true). Set false
|
|
17
|
+
* for a flush gutter: `<n> <sigil><code>`, the row background spans the line, and
|
|
18
|
+
* context code is left un-dimmed. */
|
|
19
|
+
gutterLine?: boolean;
|
|
16
20
|
}
|
|
17
21
|
export declare function detectLanguage(filePath?: string): string | undefined;
|
|
18
22
|
/**
|
|
@@ -239,25 +239,35 @@ function unifiedLayout(diff, opts) {
|
|
|
239
239
|
lineTextW: Math.max(1, textWidth - noW - 5),
|
|
240
240
|
textWidth,
|
|
241
241
|
useTrueColor: opts.trueColor !== false,
|
|
242
|
+
gutterLine: opts.gutterLine !== false,
|
|
242
243
|
lang: opts.syntaxHighlight !== false ? detectLanguage(opts.filePath) : undefined,
|
|
243
244
|
removedPalette: { rowBg: p.errorBg, emphBg: p.errorBgEmph },
|
|
244
245
|
addedPalette: { rowBg: p.successBg, emphBg: p.successBgEmph },
|
|
245
246
|
};
|
|
246
247
|
}
|
|
247
248
|
function renderUnifiedHunk(hunk, layout) {
|
|
248
|
-
const { noW, lineTextW, textWidth, useTrueColor, lang, removedPalette, addedPalette } = layout;
|
|
249
|
+
const { noW, lineTextW, textWidth, useTrueColor, gutterLine, lang, removedPalette, addedPalette } = layout;
|
|
249
250
|
const out = [];
|
|
250
251
|
const pairs = findChangePairs(hunk);
|
|
251
252
|
const renderedAsPartOfPair = new Set();
|
|
252
253
|
const bgWidth = Math.max(1, textWidth - noW - 3);
|
|
253
254
|
const gutter = (n) => `${p.dim}${n} │${p.reset} `;
|
|
255
|
+
const change = (no, sigil, bg, fg, text) => {
|
|
256
|
+
if (!gutterLine) {
|
|
257
|
+
return `${bg}${padToWidth(`${no} ${fg}${sigil}${preserveBg(text, bg)}`, textWidth)}${p.reset}`;
|
|
258
|
+
}
|
|
259
|
+
if (useTrueColor)
|
|
260
|
+
return gutter(no) + padToWidth(`${bg}${fg}${sigil} ${preserveBg(text, bg)}`, bgWidth) + p.reset;
|
|
261
|
+
return `${gutter(no)}${fg}${sigil} ${text}${p.reset}`;
|
|
262
|
+
};
|
|
254
263
|
for (let i = 0; i < hunk.lines.length; i++) {
|
|
255
264
|
const line = hunk.lines[i];
|
|
256
265
|
const no = String(line.type === "removed" ? (line.oldNo ?? "") : (line.newNo ?? line.oldNo ?? "")).padStart(noW);
|
|
257
266
|
if (line.type === "context") {
|
|
258
267
|
const raw = truncateText(line.text, lineTextW);
|
|
259
268
|
const text = lang ? highlightLine(raw, lang) : raw;
|
|
260
|
-
|
|
269
|
+
// The flush gutter dims only the line number; the code stays normal/highlighted.
|
|
270
|
+
out.push(!gutterLine ? `${p.dim}${no}${p.reset} ${text}` : `${gutter(no)} ${p.dim}${text}${p.reset}`);
|
|
261
271
|
continue;
|
|
262
272
|
}
|
|
263
273
|
if (line.type === "removed") {
|
|
@@ -276,19 +286,9 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
276
286
|
const raw = truncateText(line.text, lineTextW);
|
|
277
287
|
removedText = lang ? highlightLine(raw, lang) : raw;
|
|
278
288
|
}
|
|
279
|
-
|
|
280
|
-
out.push(gutter(no) + padToWidth(`${p.errorBg}${p.error}- ${preserveBg(removedText, p.errorBg)}`, bgWidth) + p.reset);
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
out.push(`${gutter(no)}${p.error}- ${removedText}${p.reset}`);
|
|
284
|
-
}
|
|
289
|
+
out.push(change(no, "-", p.errorBg, p.error, removedText));
|
|
285
290
|
if (addedText !== null && addedNo !== null) {
|
|
286
|
-
|
|
287
|
-
out.push(gutter(addedNo) + padToWidth(`${p.successBg}${p.success}+ ${preserveBg(addedText, p.successBg)}`, bgWidth) + p.reset);
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
out.push(`${gutter(addedNo)}${p.success}+ ${addedText}${p.reset}`);
|
|
291
|
-
}
|
|
291
|
+
out.push(change(addedNo, "+", p.successBg, p.success, addedText));
|
|
292
292
|
}
|
|
293
293
|
continue;
|
|
294
294
|
}
|
|
@@ -297,12 +297,7 @@ function renderUnifiedHunk(hunk, layout) {
|
|
|
297
297
|
continue;
|
|
298
298
|
const raw = truncateText(line.text, lineTextW);
|
|
299
299
|
const text = lang ? highlightLine(raw, lang) : raw;
|
|
300
|
-
|
|
301
|
-
out.push(gutter(no) + padToWidth(`${p.successBg}${p.success}+ ${preserveBg(text, p.successBg)}`, bgWidth) + p.reset);
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
out.push(`${gutter(no)}${p.success}+ ${text}${p.reset}`);
|
|
305
|
-
}
|
|
300
|
+
out.push(change(no, "+", p.successBg, p.success, text));
|
|
306
301
|
}
|
|
307
302
|
}
|
|
308
303
|
return out;
|