agent-sh 0.14.11 → 0.15.0
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 +38 -42
- package/dist/agent/agent-loop.d.ts +9 -17
- package/dist/agent/agent-loop.js +104 -136
- package/dist/agent/events.d.ts +8 -11
- package/dist/agent/host-types.d.ts +17 -11
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +38 -22
- package/dist/agent/providers/deepseek.js +9 -1
- package/dist/agent/session-store.js +1 -1
- package/dist/agent/system-prompt.d.ts +7 -3
- package/dist/agent/system-prompt.js +11 -14
- package/dist/agent/tool-protocol.js +0 -7
- package/dist/cli/args.js +2 -1
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.js +29 -1
- package/dist/cli/subcommands.js +1 -0
- package/dist/core/event-bus.js +0 -2
- package/dist/core/extension-loader.js +3 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +3 -2
- package/dist/extensions/slash-commands/index.js +16 -11
- package/dist/shell/index.js +9 -0
- package/dist/shell/shell-context.d.ts +2 -2
- package/dist/shell/shell-context.js +26 -11
- package/dist/shell/tui-renderer.js +0 -1
- package/dist/utils/diff-renderer.js +2 -9
- package/dist/utils/handler-registry.d.ts +1 -6
- package/dist/utils/handler-registry.js +1 -6
- package/dist/utils/line-editor.js +0 -2
- package/dist/utils/palette.js +4 -4
- package/dist/utils/terminal-buffer.d.ts +2 -0
- package/dist/utils/terminal-buffer.js +4 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +11 -7
- package/examples/extensions/ash-scheme/index.ts +104 -74
- package/examples/extensions/ashi/EXTENDING.md +2 -0
- package/examples/extensions/ashi/README.md +17 -1
- package/examples/extensions/ashi/docs/ui-surface-protocol.md +163 -0
- package/examples/extensions/ashi/package.json +9 -1
- package/examples/extensions/ashi/src/capture.ts +45 -7
- package/examples/extensions/ashi/src/chat/assistant.ts +23 -43
- package/examples/extensions/ashi/src/chat/lines.ts +20 -1
- package/examples/extensions/ashi/src/cli.ts +25 -3
- package/examples/extensions/ashi/src/clipboard-image.ts +1 -1
- package/examples/extensions/ashi/src/dialogs.ts +67 -0
- package/examples/extensions/ashi/src/display-config.ts +7 -0
- package/examples/extensions/ashi/src/docks.ts +31 -0
- package/examples/extensions/ashi/src/events.ts +16 -0
- package/examples/extensions/ashi/src/frontend.ts +134 -27
- package/examples/extensions/ashi/src/hooks.ts +6 -12
- package/examples/extensions/ashi/src/input-prompt.ts +64 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/index.ts +7 -3
- package/examples/extensions/ashi/src/renderers/pi-tui/nodes.ts +67 -10
- package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +11 -1
- package/examples/extensions/ashi/src/schema.ts +3 -0
- package/examples/extensions/ashi/src/session-commands.ts +2 -1
- package/examples/extensions/ashi/src/status-footer.ts +21 -3
- package/examples/extensions/ashi/src/ui.ts +88 -0
- package/examples/extensions/ashi-ink/README.md +2 -0
- package/examples/extensions/ashi-scheme-render.ts +8 -2
- package/examples/extensions/ashi-ui-demo.ts +63 -0
- package/examples/extensions/latex-images.ts +57 -9
- package/examples/extensions/overlay-agent.ts +5 -5
- package/examples/extensions/pi-bridge/index.ts +7 -12
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
2
|
+
import type { RenderNode, RenderNodes } from "./renderer.js";
|
|
3
|
+
import type { StatusSegment } from "./status-footer.js";
|
|
4
|
+
import type { SelectOpts, ConfirmOpts } from "./dialogs.js";
|
|
5
|
+
import type { InputOpts } from "./input-prompt.js";
|
|
6
|
+
|
|
7
|
+
export type { SelectChoice, SelectOpts, ConfirmOpts } from "./dialogs.js";
|
|
8
|
+
export type { InputOpts } from "./input-prompt.js";
|
|
9
|
+
export type { StatusSegment } from "./status-footer.js";
|
|
10
|
+
|
|
11
|
+
export type NoticeLevel = "info" | "warn" | "error" | "success";
|
|
12
|
+
|
|
13
|
+
export interface Contribution {
|
|
14
|
+
/** Re-pull this surface (call after the content it depends on changes). */
|
|
15
|
+
refresh(): void;
|
|
16
|
+
remove(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Typed sugar over ashi's UI protocol. Wraps the bus events and named handlers so call
|
|
21
|
+
* sites read like a UI object, with real types and no magic strings — without a `ui` field
|
|
22
|
+
* on the kernel context. Request/response surfaces degrade when no frontend answers them
|
|
23
|
+
* (select/input → undefined, confirm → false, getEditorText → "").
|
|
24
|
+
*/
|
|
25
|
+
export interface Ui {
|
|
26
|
+
notify(message: string, level?: NoticeLevel): void;
|
|
27
|
+
select(opts: SelectOpts): Promise<string | undefined>;
|
|
28
|
+
confirm(opts: ConfirmOpts): Promise<boolean>;
|
|
29
|
+
input(opts?: InputOpts): Promise<string | undefined>;
|
|
30
|
+
getEditorText(): string;
|
|
31
|
+
setEditorText(text: string): void;
|
|
32
|
+
/** Contribute a status-bar segment. `get` runs on each repaint; return null to show nothing. */
|
|
33
|
+
status(get: () => StatusSegment | null): Contribution;
|
|
34
|
+
/** Contribute a pinned widget above the input, built from the renderer's node factory. */
|
|
35
|
+
dock(build: (nodes: RenderNodes) => RenderNode | null): Contribution;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createUi(ctx: ExtensionContext): Ui {
|
|
39
|
+
const has = (name: string): boolean => ctx.list().includes(name);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
notify(message, level) {
|
|
43
|
+
ctx.bus.emit("ui:notify", { message, level });
|
|
44
|
+
},
|
|
45
|
+
select(opts) {
|
|
46
|
+
if (!has("ui:select")) return Promise.resolve(undefined);
|
|
47
|
+
return ctx.call("ui:select", opts) as Promise<string | undefined>;
|
|
48
|
+
},
|
|
49
|
+
confirm(opts) {
|
|
50
|
+
if (!has("ui:confirm")) return Promise.resolve(false);
|
|
51
|
+
return ctx.call("ui:confirm", opts) as Promise<boolean>;
|
|
52
|
+
},
|
|
53
|
+
input(opts = {}) {
|
|
54
|
+
if (!has("ui:input")) return Promise.resolve(undefined);
|
|
55
|
+
return ctx.call("ui:input", opts) as Promise<string | undefined>;
|
|
56
|
+
},
|
|
57
|
+
getEditorText() {
|
|
58
|
+
return has("ui:editor:get-text") ? (ctx.call("ui:editor:get-text") as string) : "";
|
|
59
|
+
},
|
|
60
|
+
setEditorText(text) {
|
|
61
|
+
if (has("ui:editor:set-text")) ctx.call("ui:editor:set-text", text);
|
|
62
|
+
},
|
|
63
|
+
status(get) {
|
|
64
|
+
const contribute = (p: { segments: StatusSegment[] }): { segments: StatusSegment[] } => {
|
|
65
|
+
const seg = get();
|
|
66
|
+
return seg ? { segments: [...p.segments, seg] } : p;
|
|
67
|
+
};
|
|
68
|
+
ctx.bus.onPipe("ui:status", contribute);
|
|
69
|
+
return {
|
|
70
|
+
refresh: () => ctx.bus.emit("ui:status:invalidate", {}),
|
|
71
|
+
remove: () => ctx.bus.offPipe("ui:status", contribute),
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
dock(build) {
|
|
75
|
+
const contribute = (
|
|
76
|
+
p: { nodes: RenderNodes; views: RenderNode[] },
|
|
77
|
+
): { nodes: RenderNodes; views: RenderNode[] } => {
|
|
78
|
+
const view = build(p.nodes);
|
|
79
|
+
return view ? { ...p, views: [...p.views, view] } : p;
|
|
80
|
+
};
|
|
81
|
+
ctx.bus.onPipe("ashi:dock:above-input", contribute);
|
|
82
|
+
return {
|
|
83
|
+
refresh: () => ctx.bus.emit("ashi:dock:invalidate", {}),
|
|
84
|
+
remove: () => ctx.bus.offPipe("ashi:dock:above-input", contribute),
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -10,6 +10,8 @@ The look follows Claude Code's chat design: a `❯` user prompt on a faint band,
|
|
|
10
10
|
read & search groups that collapse to `Read N files`, and box-less diffs with a
|
|
11
11
|
line-numbered green/red gutter.
|
|
12
12
|
|
|
13
|
+

|
|
14
|
+
|
|
13
15
|
> Requires `@guanyilun/ashi` ≥ 0.2.0.
|
|
14
16
|
|
|
15
17
|
## Use
|
|
@@ -29,16 +29,22 @@ const model: RenderModel<SchemeInit> = {
|
|
|
29
29
|
{ text: "scheme ", style: { bold: true, color: "toolTitle" } },
|
|
30
30
|
{ text: env.expanded ? s.source : compact(s.source), highlight: "scheme" },
|
|
31
31
|
];
|
|
32
|
+
const summaryOnResult = env.expanded && env.finalized && !failed && !!s.status;
|
|
32
33
|
return {
|
|
33
34
|
titleIcon: "scheme",
|
|
34
35
|
title,
|
|
35
36
|
status: s.status,
|
|
36
|
-
|
|
37
|
+
hideTitleStatus: summaryOnResult,
|
|
37
38
|
body: failed
|
|
38
39
|
? { kind: "text", segments: [
|
|
39
40
|
{ text: `✗ ${s.output.trim()}`, style: { color: "error" } },
|
|
40
41
|
] }
|
|
41
|
-
:
|
|
42
|
+
: summaryOnResult
|
|
43
|
+
? { kind: "text", segments: [
|
|
44
|
+
{ text: "✓", style: { color: "success" } },
|
|
45
|
+
{ text: ` ${s.status!.summary ?? "ok"}`, style: { color: "muted" } },
|
|
46
|
+
] }
|
|
47
|
+
: { kind: "stream", text: s.output },
|
|
42
48
|
expandable: true,
|
|
43
49
|
defaultExpanded: failed,
|
|
44
50
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ashi UI-surface demo — exercises every surface via the typed `@guanyilun/ashi/ui` helper.
|
|
3
|
+
*
|
|
4
|
+
* Usage: ashi -e ashi-ui-demo (or -e examples/extensions/ashi-ui-demo.ts)
|
|
5
|
+
*
|
|
6
|
+
* Adds a pinned dock line and a status segment, plus commands:
|
|
7
|
+
* /ui-demo walk the dialogs (select → confirm → input) + notify
|
|
8
|
+
* /ui-demo-bump bump the status segment and re-pull the footer
|
|
9
|
+
* /ui-demo-dock toggle the dock widget and re-pull the dock
|
|
10
|
+
*/
|
|
11
|
+
import { createUi } from "@guanyilun/ashi/ui";
|
|
12
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
13
|
+
|
|
14
|
+
export default function activate(ctx: ExtensionContext): void {
|
|
15
|
+
const ui = createUi(ctx);
|
|
16
|
+
let bumps = 0;
|
|
17
|
+
let dockOn = true;
|
|
18
|
+
|
|
19
|
+
const status = ui.status(() => ({ id: "demo", text: `✦ demo ${bumps}`, color: "accent" }));
|
|
20
|
+
const dock = ui.dock((nodes) => {
|
|
21
|
+
if (!dockOn) return null;
|
|
22
|
+
const line = nodes.text({ paddingX: 1 });
|
|
23
|
+
line.setText("📌 ui-demo: a pinned dock widget");
|
|
24
|
+
return line.node;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
ctx.registerCommand("ui-demo-bump", "Bump the demo status segment", () => {
|
|
28
|
+
bumps++;
|
|
29
|
+
status.refresh();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
ctx.registerCommand("ui-demo-dock", "Toggle the demo dock widget", () => {
|
|
33
|
+
dockOn = !dockOn;
|
|
34
|
+
dock.refresh();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
ctx.registerCommand("ui-demo", "Walk the ashi UI dialogs (select/confirm/input)", async () => {
|
|
38
|
+
ui.notify("ui-demo: starting…");
|
|
39
|
+
|
|
40
|
+
const fruit = await ui.select({
|
|
41
|
+
title: "Pick a fruit",
|
|
42
|
+
items: [
|
|
43
|
+
{ value: "apple", label: "Apple", description: "crisp" },
|
|
44
|
+
{ value: "banana", label: "Banana", description: "soft" },
|
|
45
|
+
{ value: "cherry", label: "Cherry" },
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
if (!fruit) {
|
|
49
|
+
ui.notify("ui-demo: cancelled", "warn");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!(await ui.confirm({ title: `Really pick ${fruit}?` }))) {
|
|
54
|
+
ui.notify("ui-demo: not confirmed", "warn");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const note = await ui.input({ title: "Add a note — enter to submit, esc to skip" });
|
|
59
|
+
const summary = `picked ${fruit}${note ? ` — ${note}` : ""}`;
|
|
60
|
+
ui.setEditorText(`/* ${summary} */`);
|
|
61
|
+
ui.notify(`ui-demo: ${summary}`, "success");
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -96,6 +96,42 @@ function renderEquation(equation: string): Buffer | null {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
const equationCache = new Map<string, Buffer | null>();
|
|
100
|
+
function renderEquationCached(equation: string): Buffer | null {
|
|
101
|
+
if (!equationCache.has(equation)) {
|
|
102
|
+
equationCache.set(equation, renderEquation(equation));
|
|
103
|
+
}
|
|
104
|
+
return equationCache.get(equation) ?? null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const EQ_DELIM = "$$";
|
|
108
|
+
|
|
109
|
+
type Block =
|
|
110
|
+
| { type: "text"; text: string }
|
|
111
|
+
| { type: "image"; data: Buffer }
|
|
112
|
+
| { type: "code-block"; language: string; code: string };
|
|
113
|
+
type ContentPipe = { blocks: Block[]; images?: boolean };
|
|
114
|
+
|
|
115
|
+
function splitEquations(text: string): Block[] {
|
|
116
|
+
const out: Block[] = [];
|
|
117
|
+
let i = 0;
|
|
118
|
+
while (i < text.length) {
|
|
119
|
+
const open = text.indexOf(EQ_DELIM, i);
|
|
120
|
+
if (open === -1) {
|
|
121
|
+
const rest = text.slice(i);
|
|
122
|
+
if (rest) out.push({ type: "text", text: rest });
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
const close = text.indexOf(EQ_DELIM, open + EQ_DELIM.length);
|
|
126
|
+
if (close === -1) { out.push({ type: "text", text: text.slice(i) }); break; }
|
|
127
|
+
if (open > i) out.push({ type: "text", text: text.slice(i, open) });
|
|
128
|
+
const png = renderEquationCached(text.slice(open + EQ_DELIM.length, close).trim());
|
|
129
|
+
out.push(png ? { type: "image", data: png } : { type: "text", text: text.slice(open, close + EQ_DELIM.length) });
|
|
130
|
+
i = close + EQ_DELIM.length;
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
99
135
|
// ── Extension entry point ────────────────────────────────────────
|
|
100
136
|
|
|
101
137
|
export default function activate(ctx: ExtensionContext) {
|
|
@@ -115,26 +151,38 @@ export default function activate(ctx: ExtensionContext) {
|
|
|
115
151
|
return;
|
|
116
152
|
}
|
|
117
153
|
|
|
118
|
-
ctx.define("latex:render-equation", (equation: string): Buffer | null =>
|
|
154
|
+
ctx.define("latex:render-equation", (equation: string): Buffer | null => renderEquationCached(equation));
|
|
119
155
|
|
|
120
|
-
// Shell-only — ashi has no shell surface; it uses the capability above instead.
|
|
121
156
|
if (ctx.shell) {
|
|
157
|
+
// Shell streams output, so it buffers $$ spanning chunks; finalized-content
|
|
158
|
+
// renderers (below) get whole blocks instead.
|
|
122
159
|
ctx.shell.createBlockTransform({
|
|
123
|
-
open:
|
|
124
|
-
close:
|
|
125
|
-
transform(latex) {
|
|
126
|
-
const png =
|
|
127
|
-
|
|
128
|
-
return { type: "image", data: png };
|
|
160
|
+
open: EQ_DELIM,
|
|
161
|
+
close: EQ_DELIM,
|
|
162
|
+
transform(latex: string) {
|
|
163
|
+
const png = renderEquationCached(latex);
|
|
164
|
+
return png ? { type: "image" as const, data: png } : null;
|
|
129
165
|
},
|
|
130
166
|
});
|
|
131
167
|
|
|
132
168
|
ctx.advise("render:code-block", (next, language: string, code: string, width: number) => {
|
|
133
169
|
if (language !== "latex" && language !== "tex") return next(language, code, width);
|
|
134
|
-
const png =
|
|
170
|
+
const png = renderEquationCached(code);
|
|
135
171
|
if (!png) return next(language, code, width);
|
|
136
172
|
ctx.call("render:image", png);
|
|
137
173
|
});
|
|
174
|
+
} else {
|
|
175
|
+
(bus.onPipe as unknown as (e: string, fn: (p: ContentPipe) => ContentPipe) => void)(
|
|
176
|
+
"render:assistant-content",
|
|
177
|
+
(payload) => {
|
|
178
|
+
// Can't show images reliably → leave $$…$$ as text.
|
|
179
|
+
if (!payload.images) return payload;
|
|
180
|
+
return {
|
|
181
|
+
...payload,
|
|
182
|
+
blocks: payload.blocks.flatMap((b) => (b.type === "text" ? splitEquations(b.text) : [b])),
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
);
|
|
138
186
|
}
|
|
139
187
|
|
|
140
188
|
process.on("exit", () => {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { AgentContext, ShellContext, RemoteSession } from "agent-sh/types";
|
|
24
24
|
import type { RenderSurface } from "agent-sh/utils/compositor";
|
|
25
|
-
import { FloatingPanel } from "agent-sh/utils/floating-panel";
|
|
26
|
-
import {
|
|
25
|
+
import type { FloatingPanel, FloatingPanelConfig } from "agent-sh/utils/floating-panel";
|
|
26
|
+
import type { TerminalBuffer } from "agent-sh/utils/terminal-buffer";
|
|
27
27
|
|
|
28
28
|
/** Adapt a FloatingPanel to the RenderSurface interface. */
|
|
29
29
|
function createPanelSurface(panel: FloatingPanel): RenderSurface {
|
|
@@ -72,11 +72,11 @@ export default function activate(ctx: AgentContext & ShellContext): void {
|
|
|
72
72
|
const { createRemoteSession } = ctx.shell;
|
|
73
73
|
const terminalBuffer: TerminalBuffer | null = ctx.call("terminal-buffer");
|
|
74
74
|
|
|
75
|
-
const panel =
|
|
75
|
+
const panel: FloatingPanel = ctx.call("floating-panel:create", {
|
|
76
76
|
trigger: "\x1c", // Ctrl+\
|
|
77
77
|
dimBackground: true,
|
|
78
78
|
terminalBuffer: terminalBuffer ?? undefined,
|
|
79
|
-
});
|
|
79
|
+
} satisfies FloatingPanelConfig, bus);
|
|
80
80
|
|
|
81
81
|
const panelSurface = createPanelSurface(panel);
|
|
82
82
|
let session: RemoteSession | null = null;
|
|
@@ -89,7 +89,7 @@ export default function activate(ctx: AgentContext & ShellContext): void {
|
|
|
89
89
|
// `<shell_events>` already covers the visible scrollback — skip to dedupe.
|
|
90
90
|
ctx.agent.registerContextProducer("terminal-screen", () => {
|
|
91
91
|
if (!session?.active || !terminalBuffer?.altScreen) return null;
|
|
92
|
-
return
|
|
92
|
+
return terminalBuffer.formatScreen(80);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
registerInstruction("Interactive Overlay Sessions", [
|
|
@@ -331,34 +331,29 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
331
331
|
const all = modelRegistry.getAvailable() as Array<{ id: string; provider: string }>;
|
|
332
332
|
const cur = session.model;
|
|
333
333
|
return {
|
|
334
|
-
models: all.map((m) => ({
|
|
335
|
-
active: cur ? {
|
|
334
|
+
models: all.map((m) => ({ id: m.id, provider: m.provider })),
|
|
335
|
+
active: cur ? { id: cur.id, provider: cur.provider } : null,
|
|
336
336
|
};
|
|
337
337
|
};
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
const onSwitchModel = async ({ model: target }: { model: string }) => {
|
|
339
|
+
const onSwitchModel = async ({ id, provider }: { id: string; provider: string }) => {
|
|
341
340
|
if (!session || !modelRegistry) return;
|
|
342
|
-
const atIdx = target.lastIndexOf("@");
|
|
343
|
-
const modelId = atIdx > 0 ? target.slice(0, atIdx) : target;
|
|
344
|
-
const providerHint = atIdx > 0 ? target.slice(atIdx + 1) : undefined;
|
|
345
|
-
|
|
346
341
|
const candidates = (modelRegistry.getAvailable() as Array<{ id: string; provider: string }>)
|
|
347
|
-
.filter((m) => m.id ===
|
|
342
|
+
.filter((m) => m.id === id && (!provider || m.provider === provider));
|
|
348
343
|
|
|
349
344
|
if (candidates.length === 0) {
|
|
350
|
-
bus.emit("ui:error", { message: `Unknown model: ${
|
|
345
|
+
bus.emit("ui:error", { message: `Unknown model: ${provider}:${id}` });
|
|
351
346
|
return;
|
|
352
347
|
}
|
|
353
348
|
if (candidates.length > 1) {
|
|
354
349
|
const opts = candidates.map((m) => `${m.id}@${m.provider}`).join(", ");
|
|
355
|
-
bus.emit("ui:error", { message: `Ambiguous model "${
|
|
350
|
+
bus.emit("ui:error", { message: `Ambiguous model "${id}". Use one of: ${opts}` });
|
|
356
351
|
return;
|
|
357
352
|
}
|
|
358
353
|
const picked = candidates[0]!;
|
|
359
354
|
const full = modelRegistry.find(picked.provider, picked.id);
|
|
360
355
|
if (!full) {
|
|
361
|
-
bus.emit("ui:error", { message: `Model not found: ${
|
|
356
|
+
bus.emit("ui:error", { message: `Model not found: ${picked.provider}:${picked.id}` });
|
|
362
357
|
return;
|
|
363
358
|
}
|
|
364
359
|
try {
|