@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/.memory/MEMORY.md +1 -0
- package/.memory/follow-up-queue.md +10 -6
- package/.memory/team-centric-design.md +3 -3
- package/README.md +48 -22
- package/package.json +2 -2
- package/references/agent-roles.md +8 -2
- package/src/agents.ts +100 -6
- package/src/follow-up-queue.ts +84 -21
- package/src/guidance.ts +141 -44
- package/src/index.ts +91 -56
- package/src/picker-keys.ts +30 -0
- package/src/spawner.ts +63 -3
- package/src/state.ts +266 -26
- package/src/statefile.ts +14 -7
- package/src/team-machine.ts +1012 -106
- package/src/tool-render.ts +48 -0
- package/src/tools.ts +120 -78
- package/src/types.ts +70 -7
- package/src/ui.ts +204 -52
- package/src/worker.ts +182 -19
- package/src/worktree.ts +46 -9
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
410
|
-
?
|
|
411
|
-
:
|
|
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
|
-
|
|
462
|
-
style
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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 === "
|
|
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
|
|
614
|
-
else ctx.ui
|
|
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 {
|