decorated-pi 0.4.0 → 0.5.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.
@@ -1,102 +1,332 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { Type } from "typebox";
1
+ import { keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
3
  import { McpConnection } from "./client.js";
4
- import { resolveMcpConfigs } from "./builtin.js";
4
+ import {
5
+ resolveMcpConfigs, saveProjectMcpDescription,
6
+ loadMcpCache, updateServerCache, cleanupStaleCache,
7
+ type McpServerConfig, type McpToolCache,
8
+ } from "./builtin.js";
9
+ import {
10
+ getMcpBrokerModelKey, getCompactModelKey,
11
+ getMcpDescription, setMcpDescription,
12
+ parseModelKey,
13
+ } from "../settings.js";
5
14
 
6
15
  export interface McpServerStatus {
7
16
  name: string;
8
17
  url: string;
9
18
  source: string;
10
- state: "connecting" | "connected" | "failed";
19
+ description?: string;
20
+ state: "connecting" | "connected" | "failed" | "disabled";
11
21
  toolCount: number;
12
- tools: Array<{ name: string; description: string }>;
22
+ tools: Array<{ name: string; description?: string; inputSchema?: Record<string, unknown> }>;
13
23
  error?: string;
14
24
  }
15
25
 
16
26
  let activeConnections: McpConnection[] = [];
17
27
  let allServers = new Map<string, McpServerStatus>();
28
+ let cachedConfigs: McpServerConfig[] = [];
18
29
  let connectPromise: Promise<void> | null = null;
30
+ let cachedCwd = "";
19
31
 
20
- export function setupMcp(pi: ExtensionAPI) {
21
- pi.on("session_start", (_event, ctx: ExtensionContext) => {
22
- void (async () => {
23
- await teardownMcp();
24
-
25
- const configs = resolveMcpConfigs(ctx.cwd);
26
- if (configs.length === 0) return;
27
-
28
- // Initialise every target server as "connecting"
29
- allServers = new Map(
30
- configs.map((s) => [
31
- s.name,
32
- {
33
- name: s.name,
34
- url: s.url,
35
- source: s.source,
36
- state: "connecting" as const,
37
- toolCount: 0,
38
- tools: [],
39
- },
40
- ]),
41
- );
42
-
43
- connectPromise = Promise.all(
44
- configs.map(async (server) => {
45
- const conn = new McpConnection(server.name, server.url);
46
- conn.source = server.source;
32
+ const MCP_RESULT_FOLD_LINES = 45;
33
+
34
+ function trimTrailingEmptyLines(lines: string[]): string[] {
35
+ let end = lines.length;
36
+ while (end > 0 && lines[end - 1] === "") end -= 1;
37
+ return lines.slice(0, end);
38
+ }
39
+
40
+ function collapseMcpText(text: string, maxLines = MCP_RESULT_FOLD_LINES) {
41
+ const lines = trimTrailingEmptyLines(text.split("\n"));
42
+ const totalLines = lines.length;
43
+ const displayLines = lines.slice(0, maxLines);
44
+ const remainingLines = Math.max(0, totalLines - maxLines);
45
+ return { totalLines, displayLines, remainingLines };
46
+ }
47
+
48
+ function getTextContent(result: { content?: Array<{ type: string; text?: string }> }): string {
49
+ return (result.content ?? [])
50
+ .filter((c): c is { type: "text"; text?: string } => c.type === "text")
51
+ .map((c) => c.text ?? "")
52
+ .join("\n");
53
+ }
54
+
55
+ function formatMcpResultText(text: string, expanded: boolean, theme: any): string {
56
+ const { totalLines, displayLines, remainingLines } = collapseMcpText(
57
+ text,
58
+ expanded ? Number.MAX_SAFE_INTEGER : MCP_RESULT_FOLD_LINES,
59
+ );
60
+ let rendered = displayLines.join("\n") ? theme.fg("toolOutput", displayLines.join("\n")) : "";
61
+ if (!expanded && remainingLines > 0) {
62
+ rendered += `${theme.fg("muted", `\n... (${remainingLines} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")})`;
63
+ }
64
+ return rendered;
65
+ }
66
+
67
+ function renderMcpResult(result: any, options: { expanded: boolean }, theme: any, context: any) {
68
+ const component = context.lastComponent ?? new Text("", 0, 0);
69
+ component.setText(formatMcpResultText(getTextContent(result), options.expanded, theme));
70
+ return component;
71
+ }
72
+
73
+ // ── config helpers ────────────────────────────────────────────────────────
74
+
75
+ export function updateConfigEnabled(serverName: string, enabled: boolean): void {
76
+ const config = cachedConfigs.find(c => c.name === serverName);
77
+ if (config) config.enabled = enabled;
78
+ const server = allServers.get(serverName);
79
+ if (server) {
80
+ if (!enabled) {
81
+ // Stash the real connection state, set to disabled
82
+ server.state = "disabled";
83
+ } else {
84
+ // Re-enable: if there's still an active connection, restore it
85
+ const conn = activeConnections.find(c => c.serverName === serverName);
86
+ server.state = conn ? "connected" : "connecting";
87
+ }
88
+ }
89
+ }
90
+
91
+ // ── helpers ───────────────────────────────────────────────────────────────
92
+
93
+ function serverDescription(s: McpServerConfig): string | undefined {
94
+ return s.description || getMcpDescription(s.name, cachedCwd);
95
+ }
96
+
97
+ function makeToolName(serverName: string, toolName: string): string {
98
+ return `${serverName}_${toolName}`;
99
+ }
100
+
101
+ function makeToolLabel(serverName: string, toolName: string, desc?: string): string {
102
+ return `MCP ${serverName}: ${toolName}${desc ? ` (${desc.slice(0, 20)})` : ""}`;
103
+ }
104
+
105
+ // ── cache helpers ─────────────────────────────────────────────────────────
106
+
107
+ function cacheScopeForSource(source: string): "global" | "project" {
108
+ return source === "project" ? "project" : "global";
109
+ }
110
+
111
+ // ── auto-summary ──────────────────────────────────────────────────────────
112
+
113
+ async function autoDescribeServer(
114
+ conn: McpConnection,
115
+ serverName: string,
116
+ registry: any,
117
+ ): Promise<string> {
118
+ const descs = conn.tools.map(t => `- ${t.name}: ${t.description || "(no description)"}`).join("\n");
119
+
120
+ const prompt = `Describe what this MCP server is and what it does, based on the tools it exposes. Start with action verbs directly, like a capability summary.
121
+
122
+ Server: "${serverName}"
123
+ Tools:
124
+ ${descs}
125
+
126
+ Respond with ONLY one short sentence. No quotes.`;
127
+
128
+ return await summarizeWithBroker(registry, prompt) || `${serverName} MCP server (${conn.tools.length} tools)`;
129
+ }
130
+
131
+ async function summarizeWithBroker(registry: any, prompt: string): Promise<string | undefined> {
132
+ if (!registry) return undefined;
133
+
134
+ const brokerKey = getMcpBrokerModelKey() || getCompactModelKey();
135
+ const model = brokerKey
136
+ ? (() => {
137
+ const parsed = parseModelKey(brokerKey);
138
+ return parsed ? registry.find(parsed.provider, parsed.modelId) : undefined;
139
+ })()
140
+ : undefined;
141
+
142
+ if (!model) return undefined;
143
+
144
+ try {
145
+ const auth = await registry.getApiKeyAndHeaders(model);
146
+ if (!auth.ok) return undefined;
147
+
148
+ const { complete } = await import("@earendil-works/pi-ai");
149
+ const resp = await complete(model, {
150
+ systemPrompt: "You are a concise MCP server description generator.",
151
+ messages: [{
152
+ role: "user" as const,
153
+ content: [{ type: "text" as const, text: prompt }],
154
+ timestamp: Date.now(),
155
+ }],
156
+ }, {
157
+ maxTokens: 128,
158
+ apiKey: auth.apiKey ?? "",
159
+ headers: auth.headers,
160
+ signal: AbortSignal.timeout(15_000),
161
+ });
162
+
163
+ if (resp.stopReason === "error") return undefined;
164
+ return resp.content
165
+ .filter((c: any): c is { type: "text"; text: string } => c.type === "text")
166
+ .map((c: any) => c.text).join(" ").trim() || undefined;
167
+ } catch {
168
+ return undefined;
169
+ }
170
+ }
171
+
172
+ // ── register cached tools ─────────────────────────────────────────────────
47
173
 
174
+ function registerCachedTools(pi: ExtensionAPI, configs: McpServerConfig[]): void {
175
+ const cache = loadMcpCache(cachedCwd);
176
+ if (!cache) return;
177
+
178
+ for (const config of configs) {
179
+ if (!config.enabled) continue;
180
+ const entry = cache.servers[config.name];
181
+ if (!entry || entry.tools.length === 0) continue;
182
+
183
+ for (const t of entry.tools) {
184
+ const toolName = makeToolName(config.name, t.name);
185
+ const desc = t.description || `${t.name} (MCP tool)`;
186
+ pi.registerTool({
187
+ name: toolName,
188
+ label: makeToolLabel(config.name, t.name, t.description),
189
+ description: desc,
190
+ promptSnippet: desc || `MCP tool ${config.name}/${t.name}`,
191
+ renderResult: renderMcpResult,
192
+ parameters: t.inputSchema,
193
+ execute: async (_id, params, _signal, _update, _ctx) => {
194
+ const conn = activeConnections.find(c => c.serverName === config.name);
195
+ if (!conn) {
196
+ return {
197
+ content: [{ type: "text", text: `MCP server "${config.name}" is not connected.` }],
198
+ isError: true,
199
+ details: {},
200
+ };
201
+ }
48
202
  try {
49
- await conn.connect();
50
- activeConnections.push(conn);
51
-
52
- for (const tool of conn.tools) {
53
- const prefixedName = `${server.name}_${tool.name}`;
54
- pi.registerTool({
55
- name: prefixedName,
56
- label: `MCP: ${tool.name}`,
57
- description: tool.description,
58
- promptSnippet: tool.description.slice(0, 120),
59
- parameters: Type.Unsafe(tool.inputSchema as never),
60
- execute: async (_toolCallId, params, _signal, _onUpdate, _ctx2) => {
61
- const text = await conn.callTool(
62
- tool.name,
63
- params as Record<string, unknown>,
64
- );
65
- return {
66
- content: [{ type: "text" as const, text }],
67
- isError: false,
68
- details: { server: server.name, tool: tool.name },
69
- };
70
- },
71
- });
72
- }
73
-
74
- allServers.set(server.name, {
75
- name: server.name,
76
- url: server.url,
77
- source: server.source,
78
- state: "connected",
79
- toolCount: conn.tools.length,
80
- tools: conn.tools.map((t) => ({ name: t.name, description: t.description })),
81
- });
203
+ const text = await conn.callTool(t.name, params as Record<string, unknown>);
204
+ return { content: [{ type: "text", text }], isError: false, details: {} };
82
205
  } catch (err) {
83
206
  const msg = err instanceof Error ? err.message : String(err);
84
- allServers.set(server.name, {
85
- name: server.name,
86
- url: server.url,
87
- source: server.source,
88
- state: "failed",
89
- toolCount: 0,
90
- tools: [],
91
- error: msg,
92
- });
207
+ return {
208
+ content: [{ type: "text", text: `MCP call failed on "${config.name}/${t.name}": ${msg}` }],
209
+ isError: true,
210
+ details: {},
211
+ };
212
+ }
213
+ },
214
+ });
215
+ }
216
+ }
217
+ }
218
+
219
+ // ── connect ───────────────────────────────────────────────────────────────
220
+
221
+ async function connectAll(configs: McpServerConfig[], registry: any): Promise<void> {
222
+ allServers = new Map(
223
+ configs.map((s) => [
224
+ s.name,
225
+ {
226
+ name: s.name,
227
+ url: s.url ?? s.command ?? "(unknown)",
228
+ source: s.source,
229
+ description: serverDescription(s),
230
+ state: "connecting" as const,
231
+ toolCount: 0,
232
+ tools: [],
233
+ },
234
+ ]),
235
+ );
236
+
237
+ connectPromise = Promise.all(
238
+ configs.map(async (server) => {
239
+ const conn = new McpConnection(server.name, server);
240
+ conn.source = server.source;
241
+
242
+ try {
243
+ await conn.connect(30_000);
244
+ activeConnections.push(conn);
245
+
246
+ let desc = serverDescription(server);
247
+ if (!desc) {
248
+ desc = await autoDescribeServer(conn, server.name, registry);
249
+ if (desc) {
250
+ if (server.source === "project") saveProjectMcpDescription(cachedCwd, server.name, desc);
251
+ else setMcpDescription(server.name, desc);
93
252
  }
94
- }),
95
- ).then(() => undefined);
253
+ }
254
+
255
+ allServers.set(server.name, {
256
+ name: server.name,
257
+ url: server.url ?? server.command ?? "(unknown)",
258
+ source: server.source,
259
+ description: desc,
260
+ state: "connected",
261
+ toolCount: conn.tools.length,
262
+ tools: conn.tools.map((t) => ({
263
+ name: t.name,
264
+ description: t.description,
265
+ inputSchema: t.inputSchema,
266
+ })),
267
+ });
268
+
269
+ // Update cache with this server's tools
270
+ const tools: McpToolCache[] = conn.tools.map(t => ({
271
+ name: t.name,
272
+ description: t.description,
273
+ inputSchema: t.inputSchema,
274
+ }));
275
+ updateServerCache(
276
+ server.name,
277
+ { description: desc, tools, cachedAt: Date.now() },
278
+ cacheScopeForSource(server.source),
279
+ cachedCwd || undefined,
280
+ );
281
+ } catch (err) {
282
+ const msg = err instanceof Error ? err.message : String(err);
283
+ allServers.set(server.name, {
284
+ name: server.name,
285
+ url: server.url ?? server.command ?? "(unknown)",
286
+ source: server.source,
287
+ description: serverDescription(server),
288
+ state: "failed",
289
+ toolCount: 0,
290
+ tools: [],
291
+ error: msg,
292
+ });
293
+ }
294
+ }),
295
+ ).then(() => undefined);
296
+
297
+ await connectPromise;
298
+ connectPromise = null;
299
+ }
96
300
 
97
- await connectPromise;
98
- connectPromise = null;
99
- })();
301
+ // ── setup ─────────────────────────────────────────────────────────────────
302
+
303
+ export function setupMcp(pi: ExtensionAPI) {
304
+ pi.on("session_start", async (_event, ctx: ExtensionContext) => {
305
+ await teardownMcp();
306
+ cachedCwd = ctx.cwd;
307
+
308
+ const configs = resolveMcpConfigs(ctx.cwd).sort((a, b) => a.name.localeCompare(b.name));
309
+ cachedConfigs = configs;
310
+ if (configs.length === 0) return;
311
+
312
+ // Clean stale cache entries for removed servers
313
+ cleanupStaleCache(configs, cachedCwd);
314
+
315
+ const enabledConfigs = configs.filter(s => s.enabled);
316
+
317
+ // Register tools from cache — prompt-stable, works even if MCP is down
318
+ registerCachedTools(pi, enabledConfigs);
319
+
320
+ const needSummary = enabledConfigs.filter(s => !serverDescription(s));
321
+
322
+ if (needSummary.length === 0) {
323
+ // All servers have descriptions — connect in background, update cache
324
+ void connectAll(enabledConfigs, ctx.modelRegistry);
325
+ return;
326
+ }
327
+
328
+ // Some servers lack description — connect and auto-summarize synchronously
329
+ await connectAll(enabledConfigs, ctx.modelRegistry);
100
330
  });
101
331
 
102
332
  pi.on("session_shutdown", () => {
@@ -105,7 +335,103 @@ export function setupMcp(pi: ExtensionAPI) {
105
335
  }
106
336
 
107
337
  export function getMcpStatus(): McpServerStatus[] {
108
- return [...allServers.values()];
338
+ const cache = loadMcpCache(cachedCwd);
339
+ const result: McpServerStatus[] = [];
340
+ for (const config of cachedConfigs) {
341
+ const connected = allServers.get(config.name);
342
+ if (connected) {
343
+ result.push(connected);
344
+ } else {
345
+ const cachedEntry = cache?.servers[config.name];
346
+ result.push({
347
+ name: config.name,
348
+ url: config.url ?? config.command ?? "(unknown)",
349
+ source: config.source,
350
+ description: serverDescription(config),
351
+ state: config.enabled ? "connecting" : "disabled",
352
+ toolCount: cachedEntry?.tools.length ?? 0,
353
+ tools: cachedEntry?.tools ?? [],
354
+ });
355
+ }
356
+ }
357
+ return result;
358
+ }
359
+
360
+ // ── refresh single server cache ───────────────────────────────────────────
361
+
362
+ export const __mcpIndexTest = { collapseMcpText };
363
+
364
+ export async function refreshServerCache(
365
+ serverName: string,
366
+ registry: any,
367
+ ): Promise<{ ok: boolean; error?: string }> {
368
+ const config = resolveMcpConfigs(cachedCwd).find(s => s.name === serverName);
369
+ if (!config) return { ok: false, error: `Server "${serverName}" not found in config.` };
370
+
371
+ // Disconnect existing connection for this server
372
+ const existing = activeConnections.find(c => c.serverName === serverName);
373
+ if (existing) {
374
+ try { await existing.disconnect(); } catch { /* ignore */ }
375
+ activeConnections = activeConnections.filter(c => c.serverName !== serverName);
376
+ }
377
+
378
+ const conn = new McpConnection(config.name, config);
379
+ conn.source = config.source;
380
+
381
+ try {
382
+ await conn.connect(30_000);
383
+ activeConnections.push(conn);
384
+
385
+ let desc = serverDescription(config);
386
+ if (!desc) {
387
+ desc = await autoDescribeServer(conn, config.name, registry);
388
+ if (desc) {
389
+ if (config.source === "project") saveProjectMcpDescription(cachedCwd, config.name, desc);
390
+ else setMcpDescription(config.name, desc);
391
+ }
392
+ }
393
+
394
+ allServers.set(config.name, {
395
+ name: config.name,
396
+ url: config.url ?? config.command ?? "(unknown)",
397
+ source: config.source,
398
+ description: desc,
399
+ state: "connected",
400
+ toolCount: conn.tools.length,
401
+ tools: conn.tools.map(t => ({
402
+ name: t.name,
403
+ description: t.description,
404
+ inputSchema: t.inputSchema,
405
+ })),
406
+ });
407
+
408
+ const tools: McpToolCache[] = conn.tools.map(t => ({
409
+ name: t.name,
410
+ description: t.description,
411
+ inputSchema: t.inputSchema,
412
+ }));
413
+ updateServerCache(
414
+ config.name,
415
+ { description: desc, tools, cachedAt: Date.now() },
416
+ cacheScopeForSource(config.source),
417
+ cachedCwd || undefined,
418
+ );
419
+
420
+ return { ok: true };
421
+ } catch (err) {
422
+ const msg = err instanceof Error ? err.message : String(err);
423
+ allServers.set(config.name, {
424
+ name: config.name,
425
+ url: config.url ?? config.command ?? "(unknown)",
426
+ source: config.source,
427
+ description: serverDescription(config),
428
+ state: "failed",
429
+ toolCount: 0,
430
+ tools: [],
431
+ error: msg,
432
+ });
433
+ return { ok: false, error: msg };
434
+ }
109
435
  }
110
436
 
111
437
  async function teardownMcp(): Promise<void> {
@@ -120,4 +446,5 @@ async function teardownMcp(): Promise<void> {
120
446
  );
121
447
  activeConnections = [];
122
448
  allServers = new Map();
449
+ cachedConfigs = [];
123
450
  }
@@ -25,8 +25,8 @@ import { fileTypeFromFile } from "file-type";
25
25
  import { isContextOverflow, type Model } from "@earendil-works/pi-ai";
26
26
  import {
27
27
  loadConfig, saveConfig, parseModelKey, formatModelKey,
28
- getImageModelKey, getCompactModelKey,
29
- setImageModelKey, setCompactModelKey,
28
+ getImageModelKey, getCompactModelKey, getMcpBrokerModelKey,
29
+ setImageModelKey, setCompactModelKey, setMcpBrokerModelKey,
30
30
  } from "./settings.js";
31
31
  import * as fs from "node:fs";
32
32
  import * as os from "node:os";
@@ -172,6 +172,7 @@ function setupImageReadFallback(pi: ExtensionAPI) {
172
172
 
173
173
  const TAB_IMAGE = 0;
174
174
  const TAB_COMPACT = 1;
175
+ const TAB_BROKER = 2;
175
176
 
176
177
  export class ModelPickerComponent extends Container {
177
178
  private searchInput: Input;
@@ -182,6 +183,7 @@ export class ModelPickerComponent extends Container {
182
183
  private activeTab = TAB_IMAGE;
183
184
  private imageKey: string | null;
184
185
  private compactKey: string | null;
186
+ private brokerKey: string | null;
185
187
  private allItems: { label: string; desc: string; model: Model<any> | null; modelName?: string }[] = [];
186
188
  private filtered: typeof this.allItems = [];
187
189
  private selectedIndex = 0;
@@ -197,6 +199,7 @@ export class ModelPickerComponent extends Container {
197
199
  this.onDone = onDone;
198
200
  this.imageKey = getImageModelKey();
199
201
  this.compactKey = getCompactModelKey();
202
+ this.brokerKey = getMcpBrokerModelKey();
200
203
 
201
204
  this.addChild(new DynamicBorder());
202
205
  this.addChild(new Spacer(1));
@@ -232,8 +235,8 @@ export class ModelPickerComponent extends Container {
232
235
  }
233
236
  }
234
237
 
235
- private currentKey() { return this.activeTab === TAB_IMAGE ? this.imageKey : this.compactKey; }
236
- private currentKind() { return this.activeTab === TAB_IMAGE ? "image" : "compact"; }
238
+ private currentKey() { return this.activeTab === TAB_IMAGE ? this.imageKey : this.activeTab === TAB_BROKER ? this.brokerKey : this.compactKey; }
239
+ private currentKind() { return this.activeTab === TAB_IMAGE ? "image" : this.activeTab === TAB_BROKER ? "broker" : "compact"; }
237
240
 
238
241
  private switchTab(tab: number) {
239
242
  this.activeTab = tab;
@@ -260,16 +263,21 @@ export class ModelPickerComponent extends Container {
260
263
  const t = this.theme;
261
264
  const im = this.activeTab === TAB_IMAGE ? t.fg("accent", "●") : "○";
262
265
  const cm = this.activeTab === TAB_COMPACT ? t.fg("accent", "●") : "○";
263
- const il = this.activeTab === TAB_IMAGE ? t.bold("Image Model") : t.fg("dim", "Image Model");
264
- const cl = this.activeTab === TAB_COMPACT ? t.bold("Compact Model") : t.fg("dim", "Compact Model");
265
- this.tabTitle.setText(`${im} ${il} | ${cm} ${cl}`);
266
+ const bm = this.activeTab === TAB_BROKER ? t.fg("accent", "●") : "";
267
+ const il = this.activeTab === TAB_IMAGE ? t.bold("Image") : t.fg("dim", "Image");
268
+ const cl = this.activeTab === TAB_COMPACT ? t.bold("Compact") : t.fg("dim", "Compact");
269
+ const bl = this.activeTab === TAB_BROKER ? t.bold("Broker") : t.fg("dim", "Broker");
270
+ this.tabTitle.setText(`${im} ${il} | ${cm} ${cl} | ${bm} ${bl}`);
266
271
  const key = this.currentKey();
267
272
  this.subtitleText.setText(key ? t.fg("warning", `Current ${this.currentKind()} model: ${key}`) : t.fg("warning", `No ${this.currentKind()} model set`));
268
273
  }
269
274
 
270
275
  handleInput(keyData: string) {
271
276
  const kb = getKeybindings();
272
- if (kb.matches(keyData, "tui.input.tab")) { this.switchTab(this.activeTab === TAB_IMAGE ? TAB_COMPACT : TAB_IMAGE); this.tui.requestRender(); return; }
277
+ if (kb.matches(keyData, "tui.input.tab")) {
278
+ const next = this.activeTab === TAB_IMAGE ? TAB_COMPACT : this.activeTab === TAB_COMPACT ? TAB_BROKER : TAB_IMAGE;
279
+ this.switchTab(next); this.tui.requestRender(); return;
280
+ }
273
281
  if (kb.matches(keyData, "tui.select.up")) { this.selectedIndex = this.selectedIndex === 0 ? this.filtered.length - 1 : this.selectedIndex - 1; this.updateList(); return; }
274
282
  if (kb.matches(keyData, "tui.select.down")) { this.selectedIndex = this.selectedIndex === this.filtered.length - 1 ? 0 : this.selectedIndex + 1; this.updateList(); return; }
275
283
  if (kb.matches(keyData, "tui.select.confirm")) { const s = this.filtered[this.selectedIndex]; if (s) this.selectModel(s.model); return; }
@@ -289,12 +297,15 @@ export class ModelPickerComponent extends Container {
289
297
  const kind = this.currentKind();
290
298
  if (model) {
291
299
  if (kind === "image") setImageModelKey(formatModelKey(model));
300
+ else if (kind === "broker") setMcpBrokerModelKey(formatModelKey(model));
292
301
  else setCompactModelKey(formatModelKey(model));
293
302
  } else {
294
303
  if (kind === "image") setImageModelKey(null);
304
+ else if (kind === "broker") setMcpBrokerModelKey(null);
295
305
  else setCompactModelKey(null);
296
306
  }
297
307
  if (kind === "image") this.imageKey = model ? formatModelKey(model) : null;
308
+ else if (kind === "broker") this.brokerKey = model ? formatModelKey(model) : null;
298
309
  else this.compactKey = model ? formatModelKey(model) : null;
299
310
  this.switchTab(this.activeTab); this.tui.requestRender();
300
311
  }