@fradser/pi-agent-teams 0.7.0 → 0.8.2

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/src/ui.ts CHANGED
@@ -1,24 +1,39 @@
1
1
  /** Passive team widget and the /agent-teams management console
2
2
  * (session teammates + persistent agent roles + board). */
3
3
 
4
- import { truncateToWidth, Key, matchesKey } from "@earendil-works/pi-tui";
5
- import { createPiThemeStyle, PI_SPINNER_FRAMES, PI_SPINNER_INTERVAL_MS } from "@fradser/pi-kit";
4
+ import { truncateToWidth, Key, matchesKey, fuzzyFilter } from "@earendil-works/pi-tui";
5
+ import {
6
+ createPiThemeStyle,
7
+ createSearchPicker,
8
+ modelLabel,
9
+ modelSearchText,
10
+ PI_SPINNER_FRAMES,
11
+ PI_SPINNER_INTERVAL_MS,
12
+ notifyPi,
13
+ renderPiPanel,
14
+ renderPiWidgetRow,
15
+ sortModels,
16
+ type SearchPicker,
17
+ } from "@fradser/pi-kit";
6
18
  import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
7
19
  import {
8
20
  clampConsoleScroll, consoleScrollRange, maxConsoleBody, scrollConsoleDetail, wrapConsoleDetail,
9
21
  } from "./console-viewport.ts";
10
22
  import { fitTeammateRow, formatTeammateLabel, runningTeammateActivity } from "./activity.ts";
11
- import { discoverAgents, resolveAgent, type AgentDefinition } from "./agents.ts";
12
- import { getState, getTeammate, listTasks, listTeammates, livingTeammates } from "./state.ts";
23
+ import { MODEL_INHERIT_ALIAS, discoverAgents, resolveAgent, type AgentDefinition } from "./agents.ts";
24
+ import { getPeerDeliveryState, getState, getTeammate, getTeamDefaultModel, listTasks, listTeammates, livingTeammates, setTeamDefaultModel } from "./state.ts";
13
25
  import {
26
+ currentLeaderModelRef,
14
27
  ensureLivePoll,
15
28
  formatSilenceDuration,
29
+ resolveSpawnModel,
16
30
  runtimeDirPath,
17
31
  shutdownTeammate,
18
- STALL_NOTICE_MS,
19
32
  stallSilenceMs,
33
+ stallThresholdMs,
20
34
  } from "./team-machine.ts";
21
35
  import type { Teammate } from "./types.ts";
36
+ import { mapPickerKey } from "./picker-keys.ts";
22
37
  import { inboxPath } from "./statefile.ts";
23
38
  import * as fs from "node:fs";
24
39
  import * as path from "node:path";
@@ -79,13 +94,13 @@ export function ensureTeamWidget(ctx?: { ui?: ExtensionUIContext; mode?: string
79
94
  if (working.length === 0) return [];
80
95
  const lines: string[] = [];
81
96
  for (const teammate of working) {
82
- lines.push(fitTeammateRow(
97
+ lines.push(renderPiWidgetRow(fitTeammateRow(
83
98
  PI_SPINNER_FRAMES[spinnerFrame],
84
99
  style.fg(colorFor(teammate.name), teammate.name),
85
100
  runningTeammateActivity(teammate) + stallSuffix(teammate),
86
- width,
101
+ Math.max(1, width - 1),
87
102
  (activity) => theme.bold(style.fg("accent", activity)),
88
- ));
103
+ ), width, truncateToWidth));
89
104
  }
90
105
  return lines;
91
106
  },
@@ -99,11 +114,12 @@ function isWorking(teammate: { status: string }): boolean {
99
114
  return teammate.status === "working" || teammate.status === "starting";
100
115
  }
101
116
 
102
- /** Silence marker appended once a working teammate passes the stall notice threshold. */
117
+ /** Silence marker appended once a working teammate passes its stall threshold. */
103
118
  function stallSuffix(teammate: Teammate): string {
104
119
  if (!isWorking(teammate)) return "";
105
120
  const silence = stallSilenceMs(teammate);
106
- return silence !== undefined && STALL_NOTICE_MS > 0 && silence >= STALL_NOTICE_MS
121
+ const threshold = stallThresholdMs(teammate);
122
+ return silence !== undefined && threshold > 0 && silence >= threshold
107
123
  ? ` · stalled for ${formatSilenceDuration(silence)}`
108
124
  : "";
109
125
  }
@@ -122,6 +138,7 @@ function cap(text: string | undefined, maxBytes = 2000): string {
122
138
  // ── Detail builders ───────────────────────────────────────────────
123
139
 
124
140
  interface PeerMailLine {
141
+ id: string;
125
142
  direction: "sent" | "received";
126
143
  counterpart: string;
127
144
  subject: string;
@@ -155,6 +172,7 @@ function readPeerMail(teammateName: string): PeerMailLine[] {
155
172
  if (!message.id || !message.from || !message.subject) continue;
156
173
  if (message.from !== teammateName && recipient !== teammateName) continue;
157
174
  lines.push({
175
+ id: message.id,
158
176
  direction: message.from === teammateName ? "sent" : "received",
159
177
  counterpart: message.from === teammateName ? recipient : message.from,
160
178
  subject: message.subject,
@@ -173,7 +191,8 @@ function indent(text: string): string[] {
173
191
  return text.split("\n").map((line) => ` ${line}`);
174
192
  }
175
193
 
176
- function displaySource(sourcePath: string): string {
194
+ function displaySource(sourcePath: string | undefined): string {
195
+ if (!sourcePath) return "(in-memory session role)";
177
196
  const relative = path.relative(process.cwd(), sourcePath);
178
197
  return relative.startsWith("..") ? sourcePath : `./${relative}`;
179
198
  }
@@ -188,6 +207,8 @@ function buildTeammateDetail(name: string): string[] {
188
207
  "== teammate ==",
189
208
  ` Status: ${teammate.status}${teammate.currentTaskId ? ` | Task: ${teammate.currentTaskId}` : ""}`,
190
209
  ` Spawn: ${teammate.pid > 0 ? `pid ${teammate.pid}` : "pid unknown"} | Isolation: ${teammate.isolation}`,
210
+ ...(teammate.model ? [` Launch model: ${teammate.model}`] : []),
211
+ ...(teammate.tools?.length ? [` Tools: ${teammate.tools.join(", ")}`] : []),
191
212
  ` Created: ${new Date(teammate.createdAt).toLocaleString()}`,
192
213
  ];
193
214
  if (teammate.stoppedAt) lines.push(` Stopped: ${new Date(teammate.stoppedAt).toLocaleString()}`);
@@ -203,7 +224,8 @@ function buildTeammateDetail(name: string): string[] {
203
224
  lines.push("", `== reports to leader (${reports.length}) ==`);
204
225
  if (reports.length === 0) lines.push(" (none)");
205
226
  for (const report of reports) {
206
- lines.push(` -> [${report.subject}] ${new Date(report.timestamp).toLocaleString()}`, ...indent(cap(report.body)), "");
227
+ const archive = report.archived ? " · archived after shutdown" : "";
228
+ lines.push(` ->${archive} [${report.subject}] ${new Date(report.timestamp).toLocaleString()}`, ...indent(cap(report.body)), "");
207
229
  }
208
230
 
209
231
  const peer = readPeerMail(name);
@@ -211,7 +233,10 @@ function buildTeammateDetail(name: string): string[] {
211
233
  if (peer.length === 0) lines.push(" (none)");
212
234
  for (const mail of peer) {
213
235
  const arrow = mail.direction === "sent" ? `to @${mail.counterpart}` : `from @${mail.counterpart}`;
214
- lines.push(` ${arrow} [${mail.subject}] ${new Date(mail.timestamp).toLocaleString()}`, ...indent(cap(mail.body)), "");
236
+ const routing = mail.direction === "sent"
237
+ ? getPeerDeliveryState(mail.id) ?? "written"
238
+ : "received";
239
+ lines.push(` ${arrow} · ${routing} [${mail.subject}] ${new Date(mail.timestamp).toLocaleString()}`, ...indent(cap(mail.body)), "");
215
240
  }
216
241
  return lines;
217
242
  }
@@ -225,7 +250,8 @@ function buildTaskDetail(taskId: string): string[] {
225
250
  `[${task.id}] ${task.subject}`,
226
251
  "",
227
252
  "== task ==",
228
- ` Status: ${task.status}${task.claimedBy ? ` (@${task.claimedBy})` : ""}`,
253
+ ` Status: ${task.status}${task.claimedBy ? ` (@${task.claimedBy})` : ""}${task.supersededBy ? ` → superseded by ${task.supersededBy}` : ""}`,
254
+ ...((task.resources?.length ?? 0) > 0 ? [` Resources: ${task.resources.join(", ")}`] : []),
229
255
  ...(task.description ? [` Description:`, ...indent(cap(task.description))] : []),
230
256
  ...(task.dependsOn.length > 0
231
257
  ? [` Depends on: ${task.dependsOn.join(", ")} (${task.dependsOn.every((dep) => byId.get(dep)?.status === "completed") ? "met" : "unmet"})`]
@@ -242,13 +268,13 @@ function buildRoleDetail(name: string): string[] {
242
268
  const def = resolveAgent(name);
243
269
  if (!def) return ["(agent definition not found)"];
244
270
  return [
245
- `@${def.name} (${def.scope})${def.gitManaged ? " [git-managed]" : " [local]"}`,
271
+ `@${def.name} (${def.scope})${def.scope === "session" ? " [in-memory]" : def.gitManaged ? " [git-managed]" : " [local]"}`,
246
272
  "",
247
273
  "== role ==",
248
274
  ` Source: ${displaySource(def.source)}`,
249
275
  ` Description: ${def.description || "(none)"}`,
250
276
  ` Tools: ${def.tools.join(", ") || "(role defaults)"}`,
251
- ` Model: ${def.model ?? "(session default)"}`,
277
+ ` Model: ${describeSpawnModel(def.model)}`,
252
278
  ` Verify: ${def.verify ?? "(none)"}`,
253
279
  ` Worktree: ${def.worktree ? "true" : "false"}`,
254
280
  ` Living instances: ${livingTeammates().filter((teammate) => teammate.agent === def.name).length}`,
@@ -258,6 +284,17 @@ function buildRoleDetail(name: string): string[] {
258
284
  ];
259
285
  }
260
286
 
287
+ /** Human-readable effective launch model for a role definition. */
288
+ function describeSpawnModel(pinned: string | undefined): string {
289
+ const resolved = resolveSpawnModel(pinned, getTeamDefaultModel(), currentLeaderModelRef());
290
+ if (pinned && pinned.trim().toLowerCase() !== MODEL_INHERIT_ALIAS) return pinned.trim();
291
+ if (resolved.model) {
292
+ const suffix = pinned ? "inherit -> " : "[team default] ";
293
+ return `${suffix}${resolved.model}`;
294
+ }
295
+ return pinned ? "inherit -> (unavailable at spawn)" : "(Pi default)";
296
+ }
297
+
261
298
  // ── Full-screen console ───────────────────────────────────────────
262
299
 
263
300
  type ConsolePage = "roster" | "board";
@@ -292,11 +329,15 @@ function wheelDelta(data: string): number | undefined {
292
329
  const LIST_CHROME_LINES = 5;
293
330
 
294
331
  /** Full-screen Team Console — owns input via ctx.ui.custom. Pages: roster /
295
- * board; each row opens a scrolling detail view. */
296
- export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void> {
332
+ * board; each row opens a scrolling detail view; m opens the searchable
333
+ * teammate-model picker. */
334
+ export function openTeamConsole(ctx: {
335
+ ui: ExtensionUIContext;
336
+ modelRegistry?: { getAvailable(): Array<{ provider: string; id: string; name?: string }> };
337
+ }): Promise<void> {
297
338
  return ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
298
339
  let page: ConsolePage = "roster";
299
- let mode: "list" | "detail" = "list";
340
+ let mode: "list" | "detail" | "picker" = "list";
300
341
  let selectedRoster = 0;
301
342
  let selectedBoard = 0;
302
343
  let listOffset = 0;
@@ -341,6 +382,109 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
341
382
 
342
383
  const style = createPiThemeStyle(theme);
343
384
 
385
+ // ── Teammate-model picker (type-to-filter) ──────────────────
386
+ let picker: SearchPicker<{ provider: string; id: string; name?: string }> | undefined;
387
+ let pickerOffset = 0;
388
+
389
+ /** Pinned pseudo-entry so clearing never collides with typed search text. */
390
+ const CLEAR_TEAM_MODEL_ENTRY: { provider: string; id: string; name?: string } = {
391
+ provider: "",
392
+ id: "__clear-team-default__",
393
+ name: "Use Pi default",
394
+ };
395
+ const isClearEntry = (model: { id: string } | undefined): boolean =>
396
+ model?.id === CLEAR_TEAM_MODEL_ENTRY.id;
397
+
398
+ const openModelPicker = (): void => {
399
+ const models = sortModels([...(ctx.modelRegistry?.getAvailable() ?? [])]);
400
+ if (models.length === 0) return;
401
+ picker = createSearchPicker([CLEAR_TEAM_MODEL_ENTRY, ...models], {
402
+ filter: fuzzyFilter,
403
+ getText: (model) => (isClearEntry(model) ? "clear team default use pi default" : modelSearchText(model)),
404
+ });
405
+ pickerOffset = 0;
406
+ mode = "picker";
407
+ requestRender();
408
+ };
409
+
410
+ const closeModelPicker = (): void => {
411
+ picker = undefined;
412
+ mode = "list";
413
+ requestRender();
414
+ };
415
+
416
+ const PICKER_CHROME_LINES = 7;
417
+ const pickerViewport = (): number => Math.max(1, tui.terminal.rows - PICKER_CHROME_LINES);
418
+
419
+ const renderPicker = (width: number): string[] => {
420
+ if (!picker) return [];
421
+ const results = picker.results();
422
+ const selected = picker.selectedIndex();
423
+ if (selected < pickerOffset) pickerOffset = selected;
424
+ else if (selected >= pickerOffset + pickerViewport()) pickerOffset = selected - pickerViewport() + 1;
425
+ pickerOffset = clampConsoleScroll(pickerOffset, results.length, pickerViewport());
426
+ const leaderRef = currentLeaderModelRef();
427
+ const teamDefault = getTeamDefaultModel();
428
+ const rows = results.slice(pickerOffset, pickerOffset + pickerViewport()).map((model, index) => {
429
+ const absolute = pickerOffset + index;
430
+ const marker = absolute === selected ? style.accent("❯ ") : " ";
431
+ if (isClearEntry(model)) {
432
+ return truncateToWidth(`${marker}${theme.fg("warning", "✕ clear team default")} ${style.dim("· use Pi default")}`, Math.max(10, width - 1));
433
+ }
434
+ const tags = [
435
+ modelLabel(model) === leaderRef ? style.dim("(leader)") : "",
436
+ modelLabel(model) === teamDefault ? style.success("(default)") : "",
437
+ ].filter(Boolean).join(" ");
438
+ const display = model.name && model.name !== model.id ? style.dim(model.name) : "";
439
+ return truncateToWidth(`${marker}${theme.fg("customMessageText", modelLabel(model))} ${display} ${tags}`.replace(/\s+$/g, ""), Math.max(10, width - 1));
440
+ });
441
+ return renderPiPanel({
442
+ width,
443
+ style,
444
+ fit: truncateToWidth,
445
+ title: `teammate model ${teamDefault ?? "auto (Pi default)"}`,
446
+ body: [
447
+ "",
448
+ `${style.accent("❯ ")}${picker.query()}▏ ${style.dim(`${results.length} models`)}`,
449
+ ...rows,
450
+ "",
451
+ ],
452
+ footer: "type to filter · ↑↓ select · enter set/clear · esc cancel",
453
+ });
454
+ };
455
+
456
+ function handlePickerInput(data: string): void {
457
+ if (!picker) {
458
+ mode = "list";
459
+ return;
460
+ }
461
+ const action = mapPickerKey(data);
462
+ if (!action) return;
463
+ if (action.kind === "cancel") {
464
+ closeModelPicker();
465
+ return;
466
+ }
467
+ if (action.kind === "confirm") {
468
+ const picked = picker.selected();
469
+ // An empty filtered list must not clobber the current default.
470
+ if (!picked) return;
471
+ if (isClearEntry(picked)) {
472
+ setTeamDefaultModel(undefined);
473
+ notifyPi(ctx.ui, "Teammate model cleared — Pi picks its default", "info");
474
+ } else {
475
+ setTeamDefaultModel(modelLabel(picked));
476
+ notifyPi(ctx.ui, `Teammate model set to ${modelLabel(picked)} for this session`, "info");
477
+ }
478
+ closeModelPicker();
479
+ return;
480
+ }
481
+ if (action.kind === "up") picker.up();
482
+ else if (action.kind === "down") picker.down();
483
+ else if (action.kind === "backspace") picker.backspace();
484
+ else picker.type(action.text);
485
+ requestRender();
486
+ }
487
+
344
488
  const currentRows = (): ConsoleRow[] =>
345
489
  page === "roster"
346
490
  // The console is the management surface: idle and stopped stay
@@ -364,7 +508,7 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
364
508
  const headerLine = (): string => {
365
509
  const alive = livingTeammates();
366
510
  const tasks = listTasks();
367
- return `team ${alive.length} alive · ${alive.filter(isWorking).length} working · ${getRoles().length} roles · board ${tasks.filter((task) => task.status === "pending").length}p/${tasks.filter((task) => task.status === "claimed").length}c/${tasks.filter((task) => task.status === "completed").length}d · ${page}`;
511
+ return `team ${alive.length} alive · ${alive.filter(isWorking).length} working · ${getRoles().length} roles · board ${tasks.filter((task) => task.status === "pending").length}p/${tasks.filter((task) => task.status === "claimed").length}c/${tasks.filter((task) => task.status === "completed").length}d/${tasks.filter((task) => task.status === "superseded").length}s · model ${getTeamDefaultModel() ?? "auto"} · ${page}`;
368
512
  };
369
513
 
370
514
  interface ContentLine {
@@ -396,7 +540,8 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
396
540
  const name = theme.bold(theme.fg(colorFor(key), `@${key}`));
397
541
  const live = livingTeammates().filter((teammate) => teammate.agent === key && teammate.status !== "stopped").length;
398
542
  const liveTag = live > 0 ? style.success(`[${live} live]`) : style.dim("[0 live]");
399
- return `${marker}${name} ${style.dim(def.scope)} ${style.dim(displaySource(def.source))} ${liveTag}`;
543
+ const provenance = def.scope === "session" ? "in-memory" : displaySource(def.source);
544
+ return `${marker}${name} ${style.dim(def.scope)} ${style.dim(provenance)} ${liveTag}`;
400
545
  };
401
546
 
402
547
  const taskRowText = (key: string, selected: boolean): string => {
@@ -406,9 +551,11 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
406
551
  const holder = task.claimedBy ? style.dim(` @${task.claimedBy}`) : "";
407
552
  const statusText = task.status === "completed"
408
553
  ? style.success("✓")
409
- : task.status === "claimed"
410
- ? theme.fg("warning", "◐ claimed")
411
- : style.dim("○ pending");
554
+ : task.status === "superseded"
555
+ ? style.dim("⊘ superseded")
556
+ : task.status === "claimed"
557
+ ? theme.fg("warning", "◐ claimed")
558
+ : style.dim("○ pending");
412
559
  return `${marker}${label} ${statusText} ${theme.fg("customMessageText", task.subject)}${holder}`;
413
560
  };
414
561
 
@@ -431,7 +578,7 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
431
578
  }
432
579
  teammates.forEach((row, index) => lines.push({ text: teammateRowText(row.key, index === selection), select: index }));
433
580
  lines.push({ text: "", select: -1 });
434
- lines.push({ text: style.dim("== agent roles (persistent definitions) =="), select: -1 });
581
+ lines.push({ text: style.dim("== agent roles (persistent and session definitions) =="), select: -1 });
435
582
  if (roles.length === 0) {
436
583
  lines.push({ text: style.dim("No agent definitions discovered."), select: -1 });
437
584
  }
@@ -442,7 +589,6 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
442
589
  const listViewport = (): number => Math.max(1, tui.terminal.rows - LIST_CHROME_LINES);
443
590
 
444
591
  const renderList = (width: number): string[] => {
445
- const border = style.border("─".repeat(Math.max(1, width)));
446
592
  const content = buildContent();
447
593
  const viewport = listViewport();
448
594
  const selection = currentPageSelection(currentRows().length);
@@ -455,18 +601,16 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
455
601
  const footer = confirmName
456
602
  ? theme.fg("warning", `Shut down @${confirmName}? Its claimed task returns to the board · y yes / n no`)
457
603
  : style.dim(page === "roster"
458
- ? "↑↓ select · enter open · tab board · x shutdown · esc/q close"
604
+ ? "↑↓ select · enter open · tab board · x shutdown · m model · esc/q close"
459
605
  : "↑↓ select · enter open · tab roster · esc/q close");
460
- return [
461
- border,
462
- style.accent(truncateToWidth(headerLine(), width)),
463
- "",
464
- ...content.slice(listOffset, listOffset + viewport)
465
- .map((line) => truncateToWidth(line.text, Math.max(10, width - 1))),
466
- "",
467
- truncateToWidth(footer, Math.max(10, width - 1)),
468
- border,
469
- ];
606
+ return renderPiPanel({
607
+ width,
608
+ style,
609
+ fit: truncateToWidth,
610
+ title: headerLine(),
611
+ body: content.slice(listOffset, listOffset + viewport).map((line) => line.text),
612
+ footer,
613
+ });
470
614
  };
471
615
 
472
616
  const currentPageSelection = (rowCount: number): number => {
@@ -478,20 +622,16 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
478
622
  detailKind === "task" ? `[${detailKey}]` : detailKind === "role" ? `@${detailKey} · role` : `@${detailKey}`;
479
623
 
480
624
  const renderDetail = (width: number): string[] => {
481
- const border = style.border("─".repeat(Math.max(1, width)));
482
625
  const source = detailSource();
483
626
  const detail = windowLines(source, width);
484
- const footer = style.dim(` ${detail.range} · ↑↓ scroll · pgup/pgdn page · home/end jump · esc back · q close`);
485
- const lines = [
486
- border,
487
- style.accent(truncateToWidth(`agent-teams ${detailTitle()}`, width)),
488
- "",
489
- ...detail.lines.map((line) => ` ${line}`),
490
- "",
491
- footer,
492
- border,
493
- ];
494
- return lines.map((line) => truncateToWidth(line, Math.max(10, width - 1)));
627
+ return renderPiPanel({
628
+ width,
629
+ style,
630
+ fit: truncateToWidth,
631
+ title: `agent-teams ${detailTitle()}`,
632
+ body: detail.lines,
633
+ footer: `${detail.range} · ↑↓ scroll · pgup/pgdn page · home/end jump · esc back · q close`,
634
+ });
495
635
  };
496
636
 
497
637
  const detailSource = (): string[] => {
@@ -501,8 +641,12 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
501
641
  };
502
642
 
503
643
  return {
504
- render: (width) => (mode === "list" ? renderList(width) : renderDetail(width)),
644
+ render: (width) => (mode === "picker" ? renderPicker(width) : mode === "detail" ? renderDetail(width) : renderList(width)),
505
645
  handleInput: (data: string) => {
646
+ if (mode === "picker") {
647
+ handlePickerInput(data);
648
+ return;
649
+ }
506
650
  if (mode !== "list") {
507
651
  handleDetailInput(data);
508
652
  return;
@@ -546,6 +690,14 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
546
690
  }
547
691
  return;
548
692
  }
693
+ if (data === "m" || data === "M") {
694
+ if (ctx.modelRegistry && ctx.modelRegistry.getAvailable().length > 0) {
695
+ openModelPicker();
696
+ } else {
697
+ notifyPi(ctx.ui, "No models are available in the model registry.", "warning");
698
+ }
699
+ return;
700
+ }
549
701
  const wheel = wheelDelta(data);
550
702
  if (wheel !== undefined) {
551
703
  scrollList(wheel * 3);
@@ -610,8 +762,8 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
610
762
 
611
763
  async function shutdownFromConsole(ctx: { ui: ExtensionUIContext }, name: string): Promise<void> {
612
764
  const result = await shutdownTeammate(name);
613
- if (result.ok) ctx.ui.notify(result.body, "info");
614
- else ctx.ui.notify(result.error, "error");
765
+ if (result.ok) notifyPi(ctx.ui, result.body, "info");
766
+ else notifyPi(ctx.ui, result.error, "error");
615
767
  }
616
768
 
617
769
  function clampIndex(index: number, rowCount: number): number {