@fradser/pi-agent-teams 0.7.0 → 0.8.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/.memory/team-centric-design.md +1 -1
- package/README.md +26 -10
- package/package.json +2 -2
- package/src/agents.ts +100 -6
- package/src/guidance.ts +28 -16
- package/src/index.ts +4 -24
- package/src/picker-keys.ts +30 -0
- package/src/state.ts +42 -5
- package/src/team-machine.ts +381 -51
- package/src/tools.ts +7 -2
- package/src/types.ts +23 -4
- package/src/ui.ts +158 -15
package/src/types.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface Teammate {
|
|
|
30
30
|
isolation: "worktree" | "none";
|
|
31
31
|
/** Board task currently claimed by this teammate, if any. */
|
|
32
32
|
currentTaskId?: string;
|
|
33
|
+
/** Effective launch model reference ("provider/model"); absent when Pi picks its default. */
|
|
34
|
+
model?: string;
|
|
33
35
|
/** Live assistant text assembled from the RPC stream. */
|
|
34
36
|
liveText?: string;
|
|
35
37
|
/** Current child tool name, if a tool is executing. */
|
|
@@ -42,6 +44,8 @@ export interface Teammate {
|
|
|
42
44
|
sequenceEnded?: boolean;
|
|
43
45
|
/** When the harness last sent a claimable-task notice to this teammate. */
|
|
44
46
|
lastNoticeAt?: number;
|
|
47
|
+
/** Claimable task ids already announced to this teammate; one notice per id until it re-arms. */
|
|
48
|
+
noticedTaskIds?: string[];
|
|
45
49
|
usage?: WorkerUsage;
|
|
46
50
|
error?: string;
|
|
47
51
|
createdAt: number;
|
|
@@ -72,7 +76,8 @@ export interface BoardTask {
|
|
|
72
76
|
description?: string;
|
|
73
77
|
/** Task ids that must complete before this task is claimable. */
|
|
74
78
|
dependsOn: string[];
|
|
75
|
-
/**
|
|
79
|
+
/** Completion gate: a review prompt a fresh one-shot reviewer answers with
|
|
80
|
+
* VERDICT: PASS or FAIL; overrides the agent-role default. */
|
|
76
81
|
verify?: string;
|
|
77
82
|
status: TaskStatus;
|
|
78
83
|
claimedBy?: string;
|
|
@@ -165,8 +170,21 @@ export interface InboxMessage {
|
|
|
165
170
|
/** Spawn one named resident teammate. */
|
|
166
171
|
export const TeammateSpawnParams = Type.Object({
|
|
167
172
|
name: Type.String({ minLength: 1, description: "Teammate name, unique among living teammates; used for messaging and claiming" }),
|
|
168
|
-
agent: Type.String({ description: "Agent definition name
|
|
173
|
+
agent: Type.String({ description: "Agent definition name; an inline definition may create this role in memory for the current session" }),
|
|
169
174
|
prompt: Type.Optional(Type.String({ description: "Optional kickoff prompt delivered as the teammate's first turn; omit to let it wait for messages or board claims" })),
|
|
175
|
+
definition: Type.Optional(Type.Object({
|
|
176
|
+
description: Type.String({ description: "Routing contract for the generated role" }),
|
|
177
|
+
tools: Type.Optional(Type.Array(Type.String(), { description: "Pi tool ids for the generated role" })),
|
|
178
|
+
model: Type.Optional(Type.String({ description: 'Optional provider/model pin, or "inherit" to run on the leader\'s current model' })),
|
|
179
|
+
verify: Type.Optional(Type.String({ description: "Role-default completion gate: a review prompt a fresh reviewer answers with VERDICT: PASS or FAIL" })),
|
|
180
|
+
worktree: Type.Optional(Type.Boolean({ description: "Whether this role receives a dedicated Git worktree" })),
|
|
181
|
+
prompt: Type.String({ minLength: 1, description: "Role prompt for this generated teammate" }),
|
|
182
|
+
persist: Type.Optional(Type.Boolean({ description: "Persist only when the user explicitly asks to keep this role for future sessions" })),
|
|
183
|
+
persistScope: Type.Optional(Type.Union([
|
|
184
|
+
Type.Literal("project"),
|
|
185
|
+
Type.Literal("project-local"),
|
|
186
|
+
], { description: "Persistence scope; defaults to project-local when persist is true" })),
|
|
187
|
+
}, { description: "Optional generated role definition; kept in memory unless explicitly persisted" })),
|
|
170
188
|
});
|
|
171
189
|
|
|
172
190
|
/** Shut down one living teammate. */
|
|
@@ -180,7 +198,7 @@ export const TaskCreateParams = Type.Object({
|
|
|
180
198
|
description: Type.Optional(Type.String({ description: "Full task description for the claiming teammate" }),
|
|
181
199
|
),
|
|
182
200
|
dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Task ids that must complete before this task is claimable" })),
|
|
183
|
-
verify: Type.Optional(Type.String({ description: "
|
|
201
|
+
verify: Type.Optional(Type.String({ description: "Completion gate: a review prompt a fresh reviewer answers with VERDICT: PASS or FAIL. Overrides any agent-role default verify." })),
|
|
184
202
|
});
|
|
185
203
|
|
|
186
204
|
/** Shared leader/worker read-only board view. */
|
|
@@ -218,10 +236,11 @@ export const TaskSubmitParams = Type.Object({
|
|
|
218
236
|
export interface TeamState {
|
|
219
237
|
teammates: Record<string, Teammate>;
|
|
220
238
|
tasks: Record<string, BoardTask>;
|
|
239
|
+
/** Unified teammate model for this session; spawns without a role pin use it. */
|
|
240
|
+
defaultModel?: string;
|
|
221
241
|
/** Single leader inbox for teammate reports and harness diagnostics. */
|
|
222
242
|
leaderMailbox: MailboxMessage[];
|
|
223
243
|
messageCounter: number;
|
|
224
|
-
taskCounter: number;
|
|
225
244
|
/** Byte offsets consumed by the parent from each teammate's outbox. */
|
|
226
245
|
workerEventOffsets: Record<string, number>;
|
|
227
246
|
/** Report event ids already applied, keyed by teammate spawn. */
|
package/src/ui.ts
CHANGED
|
@@ -1,24 +1,36 @@
|
|
|
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 {
|
|
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
|
+
sortModels,
|
|
13
|
+
type SearchPicker,
|
|
14
|
+
} from "@fradser/pi-kit";
|
|
6
15
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
7
16
|
import {
|
|
8
17
|
clampConsoleScroll, consoleScrollRange, maxConsoleBody, scrollConsoleDetail, wrapConsoleDetail,
|
|
9
18
|
} from "./console-viewport.ts";
|
|
10
19
|
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";
|
|
20
|
+
import { MODEL_INHERIT_ALIAS, discoverAgents, resolveAgent, type AgentDefinition } from "./agents.ts";
|
|
21
|
+
import { getState, getTeammate, getTeamDefaultModel, listTasks, listTeammates, livingTeammates, setTeamDefaultModel } from "./state.ts";
|
|
13
22
|
import {
|
|
23
|
+
currentLeaderModelRef,
|
|
14
24
|
ensureLivePoll,
|
|
15
25
|
formatSilenceDuration,
|
|
26
|
+
resolveSpawnModel,
|
|
16
27
|
runtimeDirPath,
|
|
17
28
|
shutdownTeammate,
|
|
18
29
|
STALL_NOTICE_MS,
|
|
19
30
|
stallSilenceMs,
|
|
20
31
|
} from "./team-machine.ts";
|
|
21
32
|
import type { Teammate } from "./types.ts";
|
|
33
|
+
import { mapPickerKey } from "./picker-keys.ts";
|
|
22
34
|
import { inboxPath } from "./statefile.ts";
|
|
23
35
|
import * as fs from "node:fs";
|
|
24
36
|
import * as path from "node:path";
|
|
@@ -173,7 +185,8 @@ function indent(text: string): string[] {
|
|
|
173
185
|
return text.split("\n").map((line) => ` ${line}`);
|
|
174
186
|
}
|
|
175
187
|
|
|
176
|
-
function displaySource(sourcePath: string): string {
|
|
188
|
+
function displaySource(sourcePath: string | undefined): string {
|
|
189
|
+
if (!sourcePath) return "(in-memory session role)";
|
|
177
190
|
const relative = path.relative(process.cwd(), sourcePath);
|
|
178
191
|
return relative.startsWith("..") ? sourcePath : `./${relative}`;
|
|
179
192
|
}
|
|
@@ -188,6 +201,7 @@ function buildTeammateDetail(name: string): string[] {
|
|
|
188
201
|
"== teammate ==",
|
|
189
202
|
` Status: ${teammate.status}${teammate.currentTaskId ? ` | Task: ${teammate.currentTaskId}` : ""}`,
|
|
190
203
|
` Spawn: ${teammate.pid > 0 ? `pid ${teammate.pid}` : "pid unknown"} | Isolation: ${teammate.isolation}`,
|
|
204
|
+
...(teammate.model ? [` Launch model: ${teammate.model}`] : []),
|
|
191
205
|
` Created: ${new Date(teammate.createdAt).toLocaleString()}`,
|
|
192
206
|
];
|
|
193
207
|
if (teammate.stoppedAt) lines.push(` Stopped: ${new Date(teammate.stoppedAt).toLocaleString()}`);
|
|
@@ -242,13 +256,13 @@ function buildRoleDetail(name: string): string[] {
|
|
|
242
256
|
const def = resolveAgent(name);
|
|
243
257
|
if (!def) return ["(agent definition not found)"];
|
|
244
258
|
return [
|
|
245
|
-
`@${def.name} (${def.scope})${def.gitManaged ? " [git-managed]" : " [local]"}`,
|
|
259
|
+
`@${def.name} (${def.scope})${def.scope === "session" ? " [in-memory]" : def.gitManaged ? " [git-managed]" : " [local]"}`,
|
|
246
260
|
"",
|
|
247
261
|
"== role ==",
|
|
248
262
|
` Source: ${displaySource(def.source)}`,
|
|
249
263
|
` Description: ${def.description || "(none)"}`,
|
|
250
264
|
` Tools: ${def.tools.join(", ") || "(role defaults)"}`,
|
|
251
|
-
` Model: ${def.model
|
|
265
|
+
` Model: ${describeSpawnModel(def.model)}`,
|
|
252
266
|
` Verify: ${def.verify ?? "(none)"}`,
|
|
253
267
|
` Worktree: ${def.worktree ? "true" : "false"}`,
|
|
254
268
|
` Living instances: ${livingTeammates().filter((teammate) => teammate.agent === def.name).length}`,
|
|
@@ -258,6 +272,17 @@ function buildRoleDetail(name: string): string[] {
|
|
|
258
272
|
];
|
|
259
273
|
}
|
|
260
274
|
|
|
275
|
+
/** Human-readable effective launch model for a role definition. */
|
|
276
|
+
function describeSpawnModel(pinned: string | undefined): string {
|
|
277
|
+
const resolved = resolveSpawnModel(pinned, getTeamDefaultModel(), currentLeaderModelRef());
|
|
278
|
+
if (pinned && pinned.trim().toLowerCase() !== MODEL_INHERIT_ALIAS) return pinned.trim();
|
|
279
|
+
if (resolved.model) {
|
|
280
|
+
const suffix = pinned ? "inherit -> " : "[team default] ";
|
|
281
|
+
return `${suffix}${resolved.model}`;
|
|
282
|
+
}
|
|
283
|
+
return pinned ? "inherit -> (unavailable at spawn)" : "(Pi default)";
|
|
284
|
+
}
|
|
285
|
+
|
|
261
286
|
// ── Full-screen console ───────────────────────────────────────────
|
|
262
287
|
|
|
263
288
|
type ConsolePage = "roster" | "board";
|
|
@@ -292,11 +317,15 @@ function wheelDelta(data: string): number | undefined {
|
|
|
292
317
|
const LIST_CHROME_LINES = 5;
|
|
293
318
|
|
|
294
319
|
/** Full-screen Team Console — owns input via ctx.ui.custom. Pages: roster /
|
|
295
|
-
* board; each row opens a scrolling detail view
|
|
296
|
-
|
|
320
|
+
* board; each row opens a scrolling detail view; m opens the searchable
|
|
321
|
+
* teammate-model picker. */
|
|
322
|
+
export function openTeamConsole(ctx: {
|
|
323
|
+
ui: ExtensionUIContext;
|
|
324
|
+
modelRegistry?: { getAvailable(): Array<{ provider: string; id: string; name?: string }> };
|
|
325
|
+
}): Promise<void> {
|
|
297
326
|
return ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
298
327
|
let page: ConsolePage = "roster";
|
|
299
|
-
let mode: "list" | "detail" = "list";
|
|
328
|
+
let mode: "list" | "detail" | "picker" = "list";
|
|
300
329
|
let selectedRoster = 0;
|
|
301
330
|
let selectedBoard = 0;
|
|
302
331
|
let listOffset = 0;
|
|
@@ -341,6 +370,107 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
341
370
|
|
|
342
371
|
const style = createPiThemeStyle(theme);
|
|
343
372
|
|
|
373
|
+
// ── Teammate-model picker (type-to-filter) ──────────────────
|
|
374
|
+
let picker: SearchPicker<{ provider: string; id: string; name?: string }> | undefined;
|
|
375
|
+
let pickerOffset = 0;
|
|
376
|
+
|
|
377
|
+
/** Pinned pseudo-entry so clearing never collides with typed search text. */
|
|
378
|
+
const CLEAR_TEAM_MODEL_ENTRY: { provider: string; id: string; name?: string } = {
|
|
379
|
+
provider: "",
|
|
380
|
+
id: "__clear-team-default__",
|
|
381
|
+
name: "Use Pi default",
|
|
382
|
+
};
|
|
383
|
+
const isClearEntry = (model: { id: string } | undefined): boolean =>
|
|
384
|
+
model?.id === CLEAR_TEAM_MODEL_ENTRY.id;
|
|
385
|
+
|
|
386
|
+
const openModelPicker = (): void => {
|
|
387
|
+
const models = sortModels([...(ctx.modelRegistry?.getAvailable() ?? [])]);
|
|
388
|
+
if (models.length === 0) return;
|
|
389
|
+
picker = createSearchPicker([CLEAR_TEAM_MODEL_ENTRY, ...models], {
|
|
390
|
+
filter: fuzzyFilter,
|
|
391
|
+
getText: (model) => (isClearEntry(model) ? "clear team default use pi default" : modelSearchText(model)),
|
|
392
|
+
});
|
|
393
|
+
pickerOffset = 0;
|
|
394
|
+
mode = "picker";
|
|
395
|
+
requestRender();
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const closeModelPicker = (): void => {
|
|
399
|
+
picker = undefined;
|
|
400
|
+
mode = "list";
|
|
401
|
+
requestRender();
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const PICKER_CHROME_LINES = 7;
|
|
405
|
+
const pickerViewport = (): number => Math.max(1, tui.terminal.rows - PICKER_CHROME_LINES);
|
|
406
|
+
|
|
407
|
+
const renderPicker = (width: number): string[] => {
|
|
408
|
+
if (!picker) return [];
|
|
409
|
+
const border = style.border("─".repeat(Math.max(1, width)));
|
|
410
|
+
const results = picker.results();
|
|
411
|
+
const selected = picker.selectedIndex();
|
|
412
|
+
if (selected < pickerOffset) pickerOffset = selected;
|
|
413
|
+
else if (selected >= pickerOffset + pickerViewport()) pickerOffset = selected - pickerViewport() + 1;
|
|
414
|
+
pickerOffset = clampConsoleScroll(pickerOffset, results.length, pickerViewport());
|
|
415
|
+
const leaderRef = currentLeaderModelRef();
|
|
416
|
+
const teamDefault = getTeamDefaultModel();
|
|
417
|
+
const rows = results.slice(pickerOffset, pickerOffset + pickerViewport()).map((model, index) => {
|
|
418
|
+
const absolute = pickerOffset + index;
|
|
419
|
+
const marker = absolute === selected ? style.accent("❯ ") : " ";
|
|
420
|
+
if (isClearEntry(model)) {
|
|
421
|
+
return truncateToWidth(`${marker}${theme.fg("warning", "✕ clear team default")} ${style.dim("· use Pi default")}`, Math.max(10, width - 1));
|
|
422
|
+
}
|
|
423
|
+
const tags = [
|
|
424
|
+
modelLabel(model) === leaderRef ? style.dim("(leader)") : "",
|
|
425
|
+
modelLabel(model) === teamDefault ? style.success("(default)") : "",
|
|
426
|
+
].filter(Boolean).join(" ");
|
|
427
|
+
const display = model.name && model.name !== model.id ? style.dim(model.name) : "";
|
|
428
|
+
return truncateToWidth(`${marker}${theme.fg("customMessageText", modelLabel(model))} ${display} ${tags}`.replace(/\s+$/g, ""), Math.max(10, width - 1));
|
|
429
|
+
});
|
|
430
|
+
return [
|
|
431
|
+
border,
|
|
432
|
+
style.accent(truncateToWidth(`teammate model ${teamDefault ?? "auto (Pi default)"}`, width)),
|
|
433
|
+
"",
|
|
434
|
+
truncateToWidth(`${style.accent("❯ ")}${picker.query()}▏ ${style.dim(`${results.length} models`)}`, Math.max(10, width - 1)),
|
|
435
|
+
...rows,
|
|
436
|
+
"",
|
|
437
|
+
truncateToWidth(style.dim("type to filter · ↑↓ select · enter set/clear · esc cancel"), Math.max(10, width - 1)),
|
|
438
|
+
border,
|
|
439
|
+
];
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
function handlePickerInput(data: string): void {
|
|
443
|
+
if (!picker) {
|
|
444
|
+
mode = "list";
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const action = mapPickerKey(data);
|
|
448
|
+
if (!action) return;
|
|
449
|
+
if (action.kind === "cancel") {
|
|
450
|
+
closeModelPicker();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (action.kind === "confirm") {
|
|
454
|
+
const picked = picker.selected();
|
|
455
|
+
// An empty filtered list must not clobber the current default.
|
|
456
|
+
if (!picked) return;
|
|
457
|
+
if (isClearEntry(picked)) {
|
|
458
|
+
setTeamDefaultModel(undefined);
|
|
459
|
+
ctx.ui.notify("Teammate model cleared — Pi picks its default", "info");
|
|
460
|
+
} else {
|
|
461
|
+
setTeamDefaultModel(modelLabel(picked));
|
|
462
|
+
ctx.ui.notify(`Teammate model set to ${modelLabel(picked)} for this session`, "info");
|
|
463
|
+
}
|
|
464
|
+
closeModelPicker();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (action.kind === "up") picker.up();
|
|
468
|
+
else if (action.kind === "down") picker.down();
|
|
469
|
+
else if (action.kind === "backspace") picker.backspace();
|
|
470
|
+
else picker.type(action.text);
|
|
471
|
+
requestRender();
|
|
472
|
+
}
|
|
473
|
+
|
|
344
474
|
const currentRows = (): ConsoleRow[] =>
|
|
345
475
|
page === "roster"
|
|
346
476
|
// The console is the management surface: idle and stopped stay
|
|
@@ -364,7 +494,7 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
364
494
|
const headerLine = (): string => {
|
|
365
495
|
const alive = livingTeammates();
|
|
366
496
|
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}`;
|
|
497
|
+
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 · model ${getTeamDefaultModel() ?? "auto"} · ${page}`;
|
|
368
498
|
};
|
|
369
499
|
|
|
370
500
|
interface ContentLine {
|
|
@@ -396,7 +526,8 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
396
526
|
const name = theme.bold(theme.fg(colorFor(key), `@${key}`));
|
|
397
527
|
const live = livingTeammates().filter((teammate) => teammate.agent === key && teammate.status !== "stopped").length;
|
|
398
528
|
const liveTag = live > 0 ? style.success(`[${live} live]`) : style.dim("[0 live]");
|
|
399
|
-
|
|
529
|
+
const provenance = def.scope === "session" ? "in-memory" : displaySource(def.source);
|
|
530
|
+
return `${marker}${name} ${style.dim(def.scope)} ${style.dim(provenance)} ${liveTag}`;
|
|
400
531
|
};
|
|
401
532
|
|
|
402
533
|
const taskRowText = (key: string, selected: boolean): string => {
|
|
@@ -431,7 +562,7 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
431
562
|
}
|
|
432
563
|
teammates.forEach((row, index) => lines.push({ text: teammateRowText(row.key, index === selection), select: index }));
|
|
433
564
|
lines.push({ text: "", select: -1 });
|
|
434
|
-
lines.push({ text: style.dim("== agent roles (persistent definitions) =="), select: -1 });
|
|
565
|
+
lines.push({ text: style.dim("== agent roles (persistent and session definitions) =="), select: -1 });
|
|
435
566
|
if (roles.length === 0) {
|
|
436
567
|
lines.push({ text: style.dim("No agent definitions discovered."), select: -1 });
|
|
437
568
|
}
|
|
@@ -455,7 +586,7 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
455
586
|
const footer = confirmName
|
|
456
587
|
? theme.fg("warning", `Shut down @${confirmName}? Its claimed task returns to the board · y yes / n no`)
|
|
457
588
|
: style.dim(page === "roster"
|
|
458
|
-
? "↑↓ select · enter open · tab board · x shutdown · esc/q close"
|
|
589
|
+
? "↑↓ select · enter open · tab board · x shutdown · m model · esc/q close"
|
|
459
590
|
: "↑↓ select · enter open · tab roster · esc/q close");
|
|
460
591
|
return [
|
|
461
592
|
border,
|
|
@@ -501,8 +632,12 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
501
632
|
};
|
|
502
633
|
|
|
503
634
|
return {
|
|
504
|
-
render: (width) => (mode === "
|
|
635
|
+
render: (width) => (mode === "picker" ? renderPicker(width) : mode === "detail" ? renderDetail(width) : renderList(width)),
|
|
505
636
|
handleInput: (data: string) => {
|
|
637
|
+
if (mode === "picker") {
|
|
638
|
+
handlePickerInput(data);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
506
641
|
if (mode !== "list") {
|
|
507
642
|
handleDetailInput(data);
|
|
508
643
|
return;
|
|
@@ -546,6 +681,14 @@ export function openTeamConsole(ctx: { ui: ExtensionUIContext }): Promise<void>
|
|
|
546
681
|
}
|
|
547
682
|
return;
|
|
548
683
|
}
|
|
684
|
+
if (data === "m" || data === "M") {
|
|
685
|
+
if (ctx.modelRegistry && ctx.modelRegistry.getAvailable().length > 0) {
|
|
686
|
+
openModelPicker();
|
|
687
|
+
} else {
|
|
688
|
+
ctx.ui.notify("No models are available in the model registry.", "warning");
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
549
692
|
const wheel = wheelDelta(data);
|
|
550
693
|
if (wheel !== undefined) {
|
|
551
694
|
scrollList(wheel * 3);
|