@jwilger/pi-development-system 0.50.0 → 0.51.1
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/package.json +2 -2
- package/skills/strict-lints/SKILL.md +2 -1
- package/src/core/spawn-overrides.ts +2 -2
- package/src/core/test-runner.ts +8 -2
- package/src/review/digest.ts +7 -1
- package/src/subagents/VENDORED.md +1 -1
- package/src/subagents/index.ts +144 -60
- package/src/subagents/orch/inherited-tools.ts +3 -3
- package/src/subagents/orch/mailbox.ts +11 -7
- package/src/subagents/orch/manager.ts +7 -1
- package/src/subagents/orch/runtime.ts +38 -33
- package/src/subagents/orch/tools.ts +12 -10
- package/src/subagents/prefs/agent-import.ts +13 -13
- package/src/subagents/prefs/config.ts +85 -185
- package/src/subagents/prefs/import-discovery.ts +6 -6
- package/src/subagents/prefs/models.ts +6 -10
- package/src/subagents/prefs/settings.ts +38 -38
- package/src/subagents/types.ts +22 -19
- package/src/subagents/ui/agent-navigation-editor.ts +20 -13
- package/src/subagents/ui/dialog.ts +99 -84
- package/src/subagents/ui/import-picker.ts +28 -11
- package/src/subagents/ui/live-agent-view.ts +17 -5
- package/src/subagents/ui/model-picker.ts +26 -17
- package/src/subagents/ui/settings-ui.ts +16 -34
- package/src/subagents/ui/status-ui.ts +140 -51
- package/src/subagents/ui/thread-tree.ts +3 -9
- package/src/subagents/ui/ui.ts +162 -265
package/src/subagents/ui/ui.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
//
|
|
1
|
+
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
|
+
// biome-ignore-all lint/style/noNestedTernary: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
3
|
+
// biome-ignore-all lint/suspicious/noControlCharactersInRegex: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
4
|
+
// biome-ignore-all lint/suspicious/noEmptyBlockStatements: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
5
|
+
// biome-ignore-all lint/suspicious/noShadow: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
6
|
+
// biome-ignore-all lint/suspicious/noUnnecessaryConditions: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
7
|
+
// biome-ignore-all lint/suspicious/useAwait: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
8
|
import { spawnSync } from "node:child_process";
|
|
3
9
|
import { existsSync } from "node:fs";
|
|
4
10
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
11
|
import { tmpdir } from "node:os";
|
|
6
12
|
import { join, resolve } from "node:path";
|
|
7
|
-
import { parse as parseShell } from "shell-quote";
|
|
8
13
|
import {
|
|
9
14
|
type ExtensionCommandContext,
|
|
10
15
|
type ExtensionContext,
|
|
@@ -14,40 +19,36 @@ import {
|
|
|
14
19
|
} from "@earendil-works/pi-coding-agent";
|
|
15
20
|
import {
|
|
16
21
|
Container,
|
|
17
|
-
SelectList,
|
|
18
|
-
Text,
|
|
19
22
|
colorToRgb,
|
|
20
23
|
rgbColor,
|
|
24
|
+
SelectList,
|
|
21
25
|
stripTerminalSequences,
|
|
26
|
+
Text,
|
|
22
27
|
truncateToWidth,
|
|
23
28
|
visibleWidth,
|
|
24
29
|
} from "@earendil-works/pi-tui";
|
|
30
|
+
import { parse as parseShell } from "shell-quote";
|
|
25
31
|
import {
|
|
26
32
|
AGENT_COLORS,
|
|
27
|
-
ConfigStore,
|
|
33
|
+
type ConfigStore,
|
|
28
34
|
diffAgentSettings,
|
|
29
35
|
parseAgentType,
|
|
30
36
|
serializeAgentType,
|
|
31
37
|
} from "../prefs/config.ts";
|
|
32
|
-
import {
|
|
38
|
+
import { getModelPreferences } from "../prefs/models.ts";
|
|
39
|
+
import type { WidgetMode } from "../prefs/settings.ts";
|
|
40
|
+
import { type AgentType, THINKING_LEVELS, type ThreadService, type ThreadView } from "../types.ts";
|
|
33
41
|
import {
|
|
34
42
|
canOpenDialog,
|
|
43
|
+
DIALOG_OPTIONS,
|
|
35
44
|
dialogEditor,
|
|
36
|
-
dialogMenu,
|
|
37
45
|
dialogHeight,
|
|
46
|
+
dialogMenu,
|
|
38
47
|
frameDialog,
|
|
39
|
-
DIALOG_OPTIONS,
|
|
40
48
|
withDialogSession,
|
|
41
49
|
} from "./dialog.ts";
|
|
42
|
-
import {
|
|
43
|
-
import type { WidgetMode } from "../prefs/settings.ts";
|
|
50
|
+
import { editModelPreferences, MODEL_EDITOR_CANCEL } from "./model-picker.ts";
|
|
44
51
|
import { buildStatusTree, type StatusRow } from "./thread-tree.ts";
|
|
45
|
-
import {
|
|
46
|
-
THINKING_LEVELS,
|
|
47
|
-
type AgentType,
|
|
48
|
-
type ThreadService,
|
|
49
|
-
type ThreadView,
|
|
50
|
-
} from "../types.ts";
|
|
51
52
|
|
|
52
53
|
export type ThreadController = Pick<
|
|
53
54
|
ThreadService,
|
|
@@ -69,19 +70,14 @@ function agentColorToken(color: string | undefined): ThemeColor {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/** Background pill with bold contrasting text and padding. Label is sanitized. */
|
|
72
|
-
function contrastPill(
|
|
73
|
-
label: string,
|
|
74
|
-
color: string | undefined,
|
|
75
|
-
theme: AgentBadgeTheme,
|
|
76
|
-
): string {
|
|
73
|
+
function contrastPill(label: string, color: string | undefined, theme: AgentBadgeTheme): string {
|
|
77
74
|
const background = theme.colors[agentColorToken(color)];
|
|
78
75
|
const { r, g, b } = colorToRgb(background);
|
|
79
76
|
const linear = (channel: number) => {
|
|
80
77
|
const value = channel / 255;
|
|
81
78
|
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
82
79
|
};
|
|
83
|
-
const luminance =
|
|
84
|
-
0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
|
|
80
|
+
const luminance = 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
|
|
85
81
|
// Pick whichever of black/white has the higher WCAG contrast ratio.
|
|
86
82
|
const foreground = luminance > Math.sqrt(0.0525) - 0.05 ? 0 : 255;
|
|
87
83
|
return theme.style(` ${sanitizeText(label)} `, {
|
|
@@ -93,8 +89,8 @@ function contrastPill(
|
|
|
93
89
|
|
|
94
90
|
/** Invalid/untrusted saved icons never reach the terminal. Names remain readable. */
|
|
95
91
|
export function agentTypeLabel(type: string, icon?: string, nerdFontIcons = false): string {
|
|
96
|
-
const prefix =
|
|
97
|
-
? `${icon} ` : "";
|
|
92
|
+
const prefix =
|
|
93
|
+
nerdFontIcons && typeof icon === "string" && /^\p{Co}$/u.test(icon) ? `${icon} ` : "";
|
|
98
94
|
return `${prefix}${sanitizeText(type)}`;
|
|
99
95
|
}
|
|
100
96
|
|
|
@@ -115,7 +111,8 @@ export function agentProgressIcon(
|
|
|
115
111
|
): string | undefined {
|
|
116
112
|
if (!nerdFontIcons) return undefined;
|
|
117
113
|
if (animate && (thread.state === "starting" || thread.state === "running")) {
|
|
118
|
-
const frame =
|
|
114
|
+
const frame =
|
|
115
|
+
Math.floor(Math.max(0, now) / AGENT_PROGRESS_INTERVAL) % AGENT_PROGRESS_FRAMES.length;
|
|
119
116
|
return AGENT_PROGRESS_FRAMES[frame];
|
|
120
117
|
}
|
|
121
118
|
return thread.icon;
|
|
@@ -131,18 +128,12 @@ export function agentTypeBadge(
|
|
|
131
128
|
return contrastPill(agentTypeLabel(type, icon, nerdFontIcons), color, theme);
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
function agentPath(
|
|
135
|
-
path: string,
|
|
136
|
-
color: string | undefined,
|
|
137
|
-
theme: AgentBadgeTheme,
|
|
138
|
-
): string {
|
|
131
|
+
function agentPath(path: string, color: string | undefined, theme: AgentBadgeTheme): string {
|
|
139
132
|
return theme.fg(agentColorToken(color), sanitizeText(path));
|
|
140
133
|
}
|
|
141
134
|
|
|
142
135
|
function metricCount(value: number | undefined): number {
|
|
143
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
144
|
-
? Math.max(0, Math.floor(value))
|
|
145
|
-
: 0;
|
|
136
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
146
137
|
}
|
|
147
138
|
|
|
148
139
|
/** Settled active time plus the current live run, only while starting or running. */
|
|
@@ -194,13 +185,10 @@ function fitLine(left: string, right: string, width: number): string {
|
|
|
194
185
|
}
|
|
195
186
|
|
|
196
187
|
function isLive(thread: ThreadView): boolean {
|
|
197
|
-
return (
|
|
198
|
-
thread.path !== "/root" &&
|
|
199
|
-
(thread.state === "starting" || thread.state === "running")
|
|
200
|
-
);
|
|
188
|
+
return thread.path !== "/root" && (thread.state === "starting" || thread.state === "running");
|
|
201
189
|
}
|
|
202
190
|
|
|
203
|
-
function
|
|
191
|
+
function _renderThreads(
|
|
204
192
|
threads: ThreadView[],
|
|
205
193
|
width: number,
|
|
206
194
|
theme: AgentBadgeTheme,
|
|
@@ -215,38 +203,28 @@ function renderThreads(
|
|
|
215
203
|
const visible = threads
|
|
216
204
|
.filter((thread) => thread.path !== "/root")
|
|
217
205
|
.sort(
|
|
218
|
-
(a, b) =>
|
|
219
|
-
priority(a) - priority(b) ||
|
|
220
|
-
b.updatedAt - a.updatedAt ||
|
|
221
|
-
b.createdAt - a.createdAt,
|
|
206
|
+
(a, b) => priority(a) - priority(b) || b.updatedAt - a.updatedAt || b.createdAt - a.createdAt,
|
|
222
207
|
);
|
|
223
208
|
const limit = visible.length > 8 ? 7 : 8;
|
|
224
209
|
const lines = visible.slice(0, limit).map((thread) => {
|
|
225
210
|
const stateColor =
|
|
226
|
-
thread.state === "failed"
|
|
227
|
-
? "error"
|
|
228
|
-
: thread.state === "paused"
|
|
229
|
-
? "warning"
|
|
230
|
-
: "accent";
|
|
211
|
+
thread.state === "failed" ? "error" : thread.state === "paused" ? "warning" : "accent";
|
|
231
212
|
const badge = agentTypeBadge(
|
|
232
|
-
thread.type,
|
|
213
|
+
thread.type,
|
|
214
|
+
thread.color,
|
|
215
|
+
theme,
|
|
216
|
+
agentProgressIcon(thread, nerdFontIcons),
|
|
217
|
+
nerdFontIcons,
|
|
233
218
|
);
|
|
234
219
|
const path = agentPath(thread.path, thread.color, theme);
|
|
235
220
|
const state = theme.fg(stateColor, `[${sanitizeText(thread.state)}]`);
|
|
236
221
|
const left = `${badge} ${path} ${state} ${sanitizeText(thread.status || thread.task)}`;
|
|
237
|
-
return fitLine(
|
|
238
|
-
left,
|
|
239
|
-
theme.fg("muted", threadMetrics(thread)),
|
|
240
|
-
Math.max(0, width),
|
|
241
|
-
);
|
|
222
|
+
return fitLine(left, theme.fg("muted", threadMetrics(thread)), Math.max(0, width));
|
|
242
223
|
});
|
|
243
224
|
if (visible.length > limit) {
|
|
244
225
|
lines.push(
|
|
245
226
|
truncateToWidth(
|
|
246
|
-
theme.fg(
|
|
247
|
-
"muted",
|
|
248
|
-
`+${visible.length - limit} more threads · /agents tree`,
|
|
249
|
-
),
|
|
227
|
+
theme.fg("muted", `+${visible.length - limit} more threads · /agents tree`),
|
|
250
228
|
Math.max(0, width),
|
|
251
229
|
),
|
|
252
230
|
);
|
|
@@ -281,7 +259,7 @@ function agentWidgetStatus(
|
|
|
281
259
|
].filter(Boolean);
|
|
282
260
|
return truncateToWidth(
|
|
283
261
|
theme.fg("accent", `${running} running`) +
|
|
284
|
-
(details.length ? theme.fg("muted", ` · ${details.join(" · ")}`) : ""),
|
|
262
|
+
(details.length > 0 ? theme.fg("muted", ` · ${details.join(" · ")}`) : ""),
|
|
285
263
|
width,
|
|
286
264
|
"",
|
|
287
265
|
);
|
|
@@ -293,26 +271,19 @@ function statePriority(state: ThreadView["state"] | undefined): number {
|
|
|
293
271
|
return 2;
|
|
294
272
|
}
|
|
295
273
|
|
|
296
|
-
function explicitParent(
|
|
297
|
-
path: string,
|
|
298
|
-
threadsByPath: Map<string, ThreadView>,
|
|
299
|
-
): string | null {
|
|
274
|
+
function explicitParent(path: string, threadsByPath: Map<string, ThreadView>): string | null {
|
|
300
275
|
if (path === WIDGET_ROOT) return null;
|
|
301
276
|
const thread = threadsByPath.get(path);
|
|
302
|
-
if (thread)
|
|
303
|
-
return thread.parent && thread.parent !== path ? thread.parent : null;
|
|
277
|
+
if (thread) return thread.parent && thread.parent !== path ? thread.parent : null;
|
|
304
278
|
const index = path.lastIndexOf("/");
|
|
305
279
|
return index > 0 ? path.slice(0, index) : null;
|
|
306
280
|
}
|
|
307
281
|
|
|
308
282
|
/** Lowest state in the branch. Parent links define ancestry; cycles are guarded. */
|
|
309
|
-
function branchPriorityComparator(
|
|
310
|
-
threads: ThreadView[],
|
|
311
|
-
): (a: string, b: string) => number {
|
|
283
|
+
function branchPriorityComparator(threads: ThreadView[]): (a: string, b: string) => number {
|
|
312
284
|
const threadsByPath = new Map<string, ThreadView>();
|
|
313
285
|
for (const thread of threads) {
|
|
314
|
-
if (thread?.path && !threadsByPath.has(thread.path))
|
|
315
|
-
threadsByPath.set(thread.path, thread);
|
|
286
|
+
if (thread?.path && !threadsByPath.has(thread.path)) threadsByPath.set(thread.path, thread);
|
|
316
287
|
}
|
|
317
288
|
const children = new Map<string, string[]>();
|
|
318
289
|
const link = (path: string, seen: Set<string>) => {
|
|
@@ -337,8 +308,7 @@ function branchPriorityComparator(
|
|
|
337
308
|
if (stack.has(path)) return own;
|
|
338
309
|
stack.add(path);
|
|
339
310
|
let best = own;
|
|
340
|
-
for (const child of children.get(path) ?? [])
|
|
341
|
-
best = Math.min(best, subtreeOf(child, stack));
|
|
311
|
+
for (const child of children.get(path) ?? []) best = Math.min(best, subtreeOf(child, stack));
|
|
342
312
|
stack.delete(path);
|
|
343
313
|
subtree.set(path, best);
|
|
344
314
|
return best;
|
|
@@ -363,14 +333,8 @@ function branchPriorityComparator(
|
|
|
363
333
|
(a < b ? -1 : a > b ? 1 : 0);
|
|
364
334
|
}
|
|
365
335
|
|
|
366
|
-
function agentStateColor(
|
|
367
|
-
state:
|
|
368
|
-
): "error" | "warning" | "accent" {
|
|
369
|
-
return state === "failed"
|
|
370
|
-
? "error"
|
|
371
|
-
: state === "paused"
|
|
372
|
-
? "warning"
|
|
373
|
-
: "accent";
|
|
336
|
+
function agentStateColor(state: ThreadView["state"]): "error" | "warning" | "accent" {
|
|
337
|
+
return state === "failed" ? "error" : state === "paused" ? "warning" : "accent";
|
|
374
338
|
}
|
|
375
339
|
|
|
376
340
|
function agentLine(
|
|
@@ -380,20 +344,20 @@ function agentLine(
|
|
|
380
344
|
nerdFontIcons: boolean,
|
|
381
345
|
animate: boolean,
|
|
382
346
|
): string {
|
|
383
|
-
const thread = row
|
|
347
|
+
const { thread } = row;
|
|
348
|
+
if (thread === undefined) return "";
|
|
384
349
|
const badge = agentTypeBadge(
|
|
385
|
-
thread.type,
|
|
386
|
-
|
|
350
|
+
thread.type,
|
|
351
|
+
thread.color,
|
|
352
|
+
theme,
|
|
353
|
+
agentProgressIcon(thread, nerdFontIcons, Date.now(), animate),
|
|
354
|
+
nerdFontIcons,
|
|
387
355
|
);
|
|
388
356
|
const left = `${row.prefix}${badge} ${agentPath(thread.path, thread.color, theme)} ${theme.fg(agentStateColor(thread.state), `[${sanitizeText(thread.state)}]`)} ${sanitizeText(thread.task)}`;
|
|
389
357
|
return fitLine(left, theme.fg("muted", threadMetrics(thread)), width);
|
|
390
358
|
}
|
|
391
359
|
|
|
392
|
-
function placeholderLine(
|
|
393
|
-
row: StatusRow,
|
|
394
|
-
width: number,
|
|
395
|
-
theme: AgentBadgeTheme,
|
|
396
|
-
): string {
|
|
360
|
+
function placeholderLine(row: StatusRow, width: number, theme: AgentBadgeTheme): string {
|
|
397
361
|
return truncateToWidth(
|
|
398
362
|
`${row.prefix}${theme.fg("muted", `${sanitizeText(row.path)} missing parent`)}`,
|
|
399
363
|
width,
|
|
@@ -405,33 +369,29 @@ function takeWidgetRows(rows: StatusRow[], budget: number): StatusRow[] {
|
|
|
405
369
|
const parents = new Map<number, number[]>();
|
|
406
370
|
const stack: number[] = [];
|
|
407
371
|
for (const [index, row] of rows.entries()) {
|
|
408
|
-
while (
|
|
409
|
-
stack.length &&
|
|
410
|
-
rows[stack[stack.length - 1]!]!.prefix.length >= row.prefix.length
|
|
411
|
-
)
|
|
372
|
+
while (stack.length > 0 && (rows[stack.at(-1) ?? -1]?.prefix.length ?? 0) >= row.prefix.length)
|
|
412
373
|
stack.pop();
|
|
413
374
|
parents.set(index, [...stack]);
|
|
414
375
|
stack.push(index);
|
|
415
376
|
}
|
|
416
|
-
const rowCost = (row: StatusRow) =>
|
|
417
|
-
row.path === WIDGET_ROOT ? 0 : 1;
|
|
377
|
+
const rowCost = (row: StatusRow) => (row.path === WIDGET_ROOT ? 0 : 1);
|
|
418
378
|
const candidates = rows
|
|
419
379
|
.map((row, index) => ({ row, index }))
|
|
420
380
|
.filter(({ row }) => row.thread && row.path !== WIDGET_ROOT)
|
|
421
381
|
.sort(
|
|
422
382
|
(a, b) =>
|
|
423
|
-
statePriority(a.row.thread
|
|
424
|
-
|
|
425
|
-
|
|
383
|
+
statePriority(a.row.thread?.state) - statePriority(b.row.thread?.state) ||
|
|
384
|
+
(b.row.thread?.createdAt ?? 0) - (a.row.thread?.createdAt ?? 0) ||
|
|
385
|
+
a.index - b.index,
|
|
426
386
|
);
|
|
427
387
|
const chosen = new Set<number>();
|
|
428
388
|
let used = 0;
|
|
429
389
|
for (const { index } of candidates) {
|
|
430
|
-
const needed = [...parents.get(index)
|
|
390
|
+
const needed = [...(parents.get(index) ?? []), index].filter(
|
|
431
391
|
(ancestor) => !chosen.has(ancestor),
|
|
432
392
|
);
|
|
433
393
|
const cost = needed.reduce(
|
|
434
|
-
(total, ancestor) => total + rowCost(rows[ancestor]
|
|
394
|
+
(total, ancestor) => total + (rows[ancestor] ? rowCost(rows[ancestor]) : 0),
|
|
435
395
|
0,
|
|
436
396
|
);
|
|
437
397
|
if (used + cost > budget) continue;
|
|
@@ -439,7 +399,7 @@ function takeWidgetRows(rows: StatusRow[], budget: number): StatusRow[] {
|
|
|
439
399
|
used += cost;
|
|
440
400
|
}
|
|
441
401
|
// If a missing-parent chain cannot fit its agent, retain a useful tree prefix.
|
|
442
|
-
if (
|
|
402
|
+
if (chosen.size === 0) {
|
|
443
403
|
for (const [index, row] of rows.entries()) {
|
|
444
404
|
const cost = rowCost(row);
|
|
445
405
|
if (used + cost > budget) break;
|
|
@@ -447,9 +407,7 @@ function takeWidgetRows(rows: StatusRow[], budget: number): StatusRow[] {
|
|
|
447
407
|
used += cost;
|
|
448
408
|
}
|
|
449
409
|
}
|
|
450
|
-
return rows.filter(
|
|
451
|
-
(row, index) => row.path !== WIDGET_ROOT && chosen.has(index),
|
|
452
|
-
);
|
|
410
|
+
return rows.filter((row, index) => row.path !== WIDGET_ROOT && chosen.has(index));
|
|
453
411
|
}
|
|
454
412
|
|
|
455
413
|
/** Themed agent tree above the editor. Synthetic /root is the main conversation and is omitted. */
|
|
@@ -460,18 +418,13 @@ function renderAgentTree(
|
|
|
460
418
|
{ showBrowserHint = true, nerdFontIcons = false, animate = true }: AgentWidgetRenderOptions = {},
|
|
461
419
|
): string[] {
|
|
462
420
|
const columns = Math.max(0, width);
|
|
463
|
-
const rows = buildStatusTree(
|
|
464
|
-
threads,
|
|
465
|
-
new Set(),
|
|
466
|
-
branchPriorityComparator(threads),
|
|
467
|
-
);
|
|
421
|
+
const rows = buildStatusTree(threads, new Set(), branchPriorityComparator(threads));
|
|
468
422
|
const agents = rows.filter((row) => row.thread && row.path !== WIDGET_ROOT);
|
|
469
|
-
if (
|
|
423
|
+
if (agents.length === 0) return [];
|
|
470
424
|
const live = agents.filter(
|
|
471
|
-
(row) =>
|
|
472
|
-
row.thread!.state === "starting" || row.thread!.state === "running",
|
|
425
|
+
(row) => row.thread?.state === "starting" || row.thread?.state === "running",
|
|
473
426
|
).length;
|
|
474
|
-
const paused = agents.filter((row) => row.thread
|
|
427
|
+
const paused = agents.filter((row) => row.thread?.state === "paused").length;
|
|
475
428
|
const heading = fitLine(
|
|
476
429
|
theme.fg("accent", "Agents"),
|
|
477
430
|
theme.fg("muted", `${live} live · ${paused} paused`),
|
|
@@ -504,7 +457,7 @@ function renderAgentSummary(
|
|
|
504
457
|
if (thread?.path && thread.path !== WIDGET_ROOT && !agents.has(thread.path))
|
|
505
458
|
agents.set(thread.path, thread);
|
|
506
459
|
}
|
|
507
|
-
if (
|
|
460
|
+
if (agents.size === 0) return [];
|
|
508
461
|
|
|
509
462
|
const counts: Record<Exclude<ThreadView["state"], "starting">, number> = {
|
|
510
463
|
running: 0,
|
|
@@ -529,14 +482,20 @@ function renderAgentSummary(
|
|
|
529
482
|
["paused", "warning"],
|
|
530
483
|
["completed", "success"],
|
|
531
484
|
];
|
|
532
|
-
const progress =
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
485
|
+
const progress =
|
|
486
|
+
counts.running > 0
|
|
487
|
+
? agentProgressIcon({ state: "running" }, nerdFontIcons, Date.now(), animate)
|
|
488
|
+
: undefined;
|
|
489
|
+
const left =
|
|
490
|
+
statuses
|
|
491
|
+
.filter(([state]) => state === "running" || counts[state] > 0)
|
|
492
|
+
.map(([state, color]) =>
|
|
493
|
+
theme.fg(
|
|
494
|
+
color,
|
|
495
|
+
`${state === "running" && progress ? `${progress} ` : ""}${counts[state]} ${state}`,
|
|
496
|
+
),
|
|
497
|
+
)
|
|
498
|
+
.join(theme.fg("muted", ", ")) +
|
|
540
499
|
(showBrowserHint && counts.running > 0 ? theme.fg("muted", " · ← browser") : "");
|
|
541
500
|
const right = theme.fg("muted", `↑${formatCount(input)} ↓${formatCount(output)}`);
|
|
542
501
|
return [fitLine(left, right, Math.max(0, width))];
|
|
@@ -572,14 +531,18 @@ export function updateWidget(
|
|
|
572
531
|
(tui) => {
|
|
573
532
|
let timer: ReturnType<typeof setInterval> | undefined;
|
|
574
533
|
if (snapshot.some(isLive) && (mode === "full" || nerdFontIcons)) {
|
|
575
|
-
timer = setInterval(
|
|
534
|
+
timer = setInterval(
|
|
535
|
+
() => tui.requestRender(),
|
|
536
|
+
nerdFontIcons ? AGENT_PROGRESS_INTERVAL : 1000,
|
|
537
|
+
);
|
|
576
538
|
timer.unref();
|
|
577
539
|
}
|
|
578
540
|
return {
|
|
579
|
-
render: (width) =>
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
541
|
+
render: (width) =>
|
|
542
|
+
render(snapshot, width, ctx.ui.theme, {
|
|
543
|
+
showBrowserHint: !settled,
|
|
544
|
+
nerdFontIcons,
|
|
545
|
+
}),
|
|
583
546
|
invalidate: () => {},
|
|
584
547
|
dispose: () => {
|
|
585
548
|
if (timer) clearInterval(timer);
|
|
@@ -601,10 +564,8 @@ export async function showThreads(
|
|
|
601
564
|
while (true) {
|
|
602
565
|
try {
|
|
603
566
|
if (!selectedPath) {
|
|
604
|
-
const threads = controller
|
|
605
|
-
|
|
606
|
-
.filter((thread) => thread.path !== "/root");
|
|
607
|
-
if (!threads.length) {
|
|
567
|
+
const threads = controller.list().filter((thread) => thread.path !== "/root");
|
|
568
|
+
if (threads.length === 0) {
|
|
608
569
|
ctx.ui.notify("No subagent threads.", "info");
|
|
609
570
|
return;
|
|
610
571
|
}
|
|
@@ -620,16 +581,14 @@ export async function showThreads(
|
|
|
620
581
|
if (selectedPath === "/root")
|
|
621
582
|
throw new Error("/root is the current Pi thread, not a subagent.");
|
|
622
583
|
const thread = controller.get(selectedPath);
|
|
623
|
-
const children = controller
|
|
624
|
-
.list()
|
|
625
|
-
.filter((child) => child.parent === selectedPath);
|
|
584
|
+
const children = controller.list().filter((child) => child.parent === selectedPath);
|
|
626
585
|
const title = `${sanitizeText(thread.path)} [${sanitizeText(thread.state)}] ${sanitizeText(thread.status)}`;
|
|
627
586
|
const action = await ctx.ui.select(title, [
|
|
628
587
|
"View output",
|
|
629
588
|
"View transcript",
|
|
630
589
|
"Send input / resume",
|
|
631
590
|
"Stop",
|
|
632
|
-
...(children.length ? ["Children"] : []),
|
|
591
|
+
...(children.length > 0 ? ["Children"] : []),
|
|
633
592
|
"Back",
|
|
634
593
|
]);
|
|
635
594
|
if (!action || action === "Back") {
|
|
@@ -648,10 +607,7 @@ export async function showThreads(
|
|
|
648
607
|
text.split("\n").map(sanitizeText).join("\n"),
|
|
649
608
|
);
|
|
650
609
|
} else if (action === "Send input / resume") {
|
|
651
|
-
const message = await ctx.ui.editor(
|
|
652
|
-
"Send input / resume retained session",
|
|
653
|
-
"",
|
|
654
|
-
);
|
|
610
|
+
const message = await ctx.ui.editor("Send input / resume retained session", "");
|
|
655
611
|
if (message?.trim()) await controller.steer(selectedPath, message);
|
|
656
612
|
} else if (action === "Stop") {
|
|
657
613
|
if (
|
|
@@ -668,14 +624,10 @@ export async function showThreads(
|
|
|
668
624
|
);
|
|
669
625
|
const selected = await ctx.ui.select("Children", labels);
|
|
670
626
|
if (selected !== undefined)
|
|
671
|
-
selectedPath =
|
|
672
|
-
children[labels.indexOf(selected)]?.path ?? selectedPath;
|
|
627
|
+
selectedPath = children[labels.indexOf(selected)]?.path ?? selectedPath;
|
|
673
628
|
}
|
|
674
629
|
} catch (error) {
|
|
675
|
-
ctx.ui.notify(
|
|
676
|
-
sanitizeText(error instanceof Error ? error.message : String(error)),
|
|
677
|
-
"error",
|
|
678
|
-
);
|
|
630
|
+
ctx.ui.notify(sanitizeText(error instanceof Error ? error.message : String(error)), "error");
|
|
679
631
|
return;
|
|
680
632
|
}
|
|
681
633
|
}
|
|
@@ -684,7 +636,7 @@ export async function showThreads(
|
|
|
684
636
|
/** Parse editor argv, rejecting shell operators instead of evaluating a shell command. */
|
|
685
637
|
function editorArguments(command: string): string[] {
|
|
686
638
|
const args = parseShell(command, process.env);
|
|
687
|
-
if (
|
|
639
|
+
if (args.length === 0 || args.some((arg) => typeof arg !== "string") || !args[0]) {
|
|
688
640
|
throw new Error(
|
|
689
641
|
"VISUAL/EDITOR must be an executable and arguments, without shell operators or globs.",
|
|
690
642
|
);
|
|
@@ -692,15 +644,10 @@ function editorArguments(command: string): string[] {
|
|
|
692
644
|
return args as string[];
|
|
693
645
|
}
|
|
694
646
|
|
|
695
|
-
async function externalEdit(
|
|
696
|
-
ctx
|
|
697
|
-
|
|
698
|
-
)
|
|
699
|
-
if (ctx.mode !== "tui")
|
|
700
|
-
throw new Error("External editing requires interactive TUI mode.");
|
|
701
|
-
const [command, ...args] = editorArguments(
|
|
702
|
-
process.env.VISUAL || process.env.EDITOR || "vi",
|
|
703
|
-
);
|
|
647
|
+
async function externalEdit(ctx: ExtensionCommandContext, text: string): Promise<string> {
|
|
648
|
+
if (ctx.mode !== "tui") throw new Error("External editing requires interactive TUI mode.");
|
|
649
|
+
const [command, ...args] = editorArguments(process.env.VISUAL || process.env.EDITOR || "vi");
|
|
650
|
+
if (command === undefined) throw new Error("No editor command is configured.");
|
|
704
651
|
const directory = await mkdtemp(join(tmpdir(), "pi-subagent-"));
|
|
705
652
|
const file = join(directory, "agent.md");
|
|
706
653
|
try {
|
|
@@ -708,15 +655,13 @@ async function externalEdit(
|
|
|
708
655
|
await ctx.ui.custom<void>((tui, _theme, _keys, done) => {
|
|
709
656
|
tui.stop();
|
|
710
657
|
try {
|
|
711
|
-
const result = spawnSync(command
|
|
658
|
+
const result = spawnSync(command, [...args, file], {
|
|
712
659
|
stdio: "inherit",
|
|
713
660
|
env: process.env,
|
|
714
661
|
});
|
|
715
662
|
if (result.error) throw result.error;
|
|
716
663
|
if (result.status !== 0)
|
|
717
|
-
throw new Error(
|
|
718
|
-
`Editor exited with status ${result.status ?? result.signal}.`,
|
|
719
|
-
);
|
|
664
|
+
throw new Error(`Editor exited with status ${result.status ?? result.signal}.`);
|
|
720
665
|
} finally {
|
|
721
666
|
tui.start();
|
|
722
667
|
tui.requestRender(true);
|
|
@@ -735,23 +680,14 @@ async function editToolList(
|
|
|
735
680
|
type: AgentType,
|
|
736
681
|
field: "allow" | "block",
|
|
737
682
|
): Promise<void> {
|
|
738
|
-
const modes = [
|
|
739
|
-
"Unset (use default policy)",
|
|
740
|
-
"Empty list",
|
|
741
|
-
"Enter exact tool names",
|
|
742
|
-
];
|
|
683
|
+
const modes = ["Unset (use default policy)", "Empty list", "Enter exact tool names"];
|
|
743
684
|
const current = type.tools?.[field];
|
|
744
685
|
const mode = await dialogMenu(
|
|
745
686
|
ctx,
|
|
746
687
|
`tools.${field} (${sanitizeText(toolListMenuValue(current))})`,
|
|
747
688
|
modes.map((label) => ({ id: label, label })),
|
|
748
689
|
{
|
|
749
|
-
selectedId:
|
|
750
|
-
current === undefined
|
|
751
|
-
? modes[0]
|
|
752
|
-
: current.length === 0
|
|
753
|
-
? modes[1]
|
|
754
|
-
: modes[2],
|
|
690
|
+
selectedId: current === undefined ? modes[0] : current.length === 0 ? modes[1] : modes[2],
|
|
755
691
|
},
|
|
756
692
|
);
|
|
757
693
|
if (!mode) return;
|
|
@@ -773,7 +709,7 @@ async function editToolList(
|
|
|
773
709
|
.map((name) => name.trim())
|
|
774
710
|
.filter(Boolean);
|
|
775
711
|
}
|
|
776
|
-
if (type.tools &&
|
|
712
|
+
if (type.tools && Object.keys(type.tools).length === 0) type.tools = undefined;
|
|
777
713
|
}
|
|
778
714
|
|
|
779
715
|
const AGENT_COLOR_DEFAULT = "__default__";
|
|
@@ -789,18 +725,29 @@ class AgentColorPickerComponent extends Container {
|
|
|
789
725
|
private readonly theme: Theme;
|
|
790
726
|
readonly selectList: SelectList;
|
|
791
727
|
|
|
728
|
+
private readonly tui: {
|
|
729
|
+
requestRender(force?: boolean): void;
|
|
730
|
+
terminal?: { rows: number };
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
private readonly agentName: string;
|
|
734
|
+
|
|
792
735
|
constructor(
|
|
793
|
-
|
|
736
|
+
tui: {
|
|
794
737
|
requestRender(force?: boolean): void;
|
|
795
738
|
terminal?: { rows: number };
|
|
796
739
|
},
|
|
797
740
|
theme: Theme,
|
|
798
741
|
currentColor: AgentType["color"] | undefined,
|
|
799
|
-
|
|
742
|
+
agentName: string,
|
|
800
743
|
onSelect: (color: AgentType["color"] | undefined) => void,
|
|
801
744
|
onCancel: () => void,
|
|
802
745
|
) {
|
|
803
746
|
super();
|
|
747
|
+
|
|
748
|
+
this.tui = tui;
|
|
749
|
+
|
|
750
|
+
this.agentName = agentName;
|
|
804
751
|
this.theme = theme;
|
|
805
752
|
this.items = [
|
|
806
753
|
{
|
|
@@ -827,17 +774,11 @@ class AgentColorPickerComponent extends Container {
|
|
|
827
774
|
this.updatePreview(tui, item.value);
|
|
828
775
|
};
|
|
829
776
|
this.selectList.onSelect = (item) => {
|
|
830
|
-
onSelect(
|
|
831
|
-
item.value === AGENT_COLOR_DEFAULT
|
|
832
|
-
? undefined
|
|
833
|
-
: (item.value as AgentType["color"]),
|
|
834
|
-
);
|
|
777
|
+
onSelect(item.value === AGENT_COLOR_DEFAULT ? undefined : (item.value as AgentType["color"]));
|
|
835
778
|
};
|
|
836
779
|
this.selectList.onCancel = onCancel;
|
|
837
|
-
this.
|
|
838
|
-
|
|
839
|
-
this.items[selectedIndex >= 0 ? selectedIndex : 0]!.value,
|
|
840
|
-
);
|
|
780
|
+
const initial = this.items[selectedIndex >= 0 ? selectedIndex : 0];
|
|
781
|
+
if (initial !== undefined) this.updatePreview(tui, initial.value);
|
|
841
782
|
}
|
|
842
783
|
|
|
843
784
|
getItems(): readonly {
|
|
@@ -854,10 +795,7 @@ class AgentColorPickerComponent extends Container {
|
|
|
854
795
|
const selected = this.items.findIndex(
|
|
855
796
|
(item) => item.value === this.selectList.getSelectedItem()?.value,
|
|
856
797
|
);
|
|
857
|
-
const start = Math.max(
|
|
858
|
-
0,
|
|
859
|
-
Math.min(selected - budget + 1, this.items.length - budget),
|
|
860
|
-
);
|
|
798
|
+
const start = Math.max(0, Math.min(selected - budget + 1, this.items.length - budget));
|
|
861
799
|
const body = [
|
|
862
800
|
` ${truncateToWidth(
|
|
863
801
|
this.preview
|
|
@@ -898,10 +836,7 @@ class AgentColorPickerComponent extends Container {
|
|
|
898
836
|
this.selectList.handleInput(keyData);
|
|
899
837
|
}
|
|
900
838
|
|
|
901
|
-
private updatePreview(
|
|
902
|
-
tui: { requestRender(force?: boolean): void },
|
|
903
|
-
value: string,
|
|
904
|
-
): void {
|
|
839
|
+
private updatePreview(tui: { requestRender(force?: boolean): void }, value: string): void {
|
|
905
840
|
const color = value === AGENT_COLOR_DEFAULT ? undefined : value;
|
|
906
841
|
const badge = agentTypeBadge(this.agentName, color, this.theme);
|
|
907
842
|
const path = agentPath("/root/example-task", color, this.theme);
|
|
@@ -956,13 +891,8 @@ async function editDocument(
|
|
|
956
891
|
source: type.source,
|
|
957
892
|
};
|
|
958
893
|
} catch (error) {
|
|
959
|
-
const diagnostic = sanitizeText(
|
|
960
|
-
|
|
961
|
-
);
|
|
962
|
-
ctx.ui.notify(
|
|
963
|
-
`Edit not accepted: ${diagnostic}. Original definition is unchanged.`,
|
|
964
|
-
"error",
|
|
965
|
-
);
|
|
894
|
+
const diagnostic = sanitizeText(error instanceof Error ? error.message : String(error));
|
|
895
|
+
ctx.ui.notify(`Edit not accepted: ${diagnostic}. Original definition is unchanged.`, "error");
|
|
966
896
|
const action = await dialogMenu(ctx, "Invalid edit — retry?", [
|
|
967
897
|
{
|
|
968
898
|
id: "Retry",
|
|
@@ -991,16 +921,12 @@ function assertSaveDestination(
|
|
|
991
921
|
kind: AgentEditMode = "fork",
|
|
992
922
|
): void {
|
|
993
923
|
if (kind === "override" && original && type.name !== original.name) {
|
|
994
|
-
throw new Error(
|
|
995
|
-
"Settings overrides cannot rename the agent; fork it instead.",
|
|
996
|
-
);
|
|
924
|
+
throw new Error("Settings overrides cannot rename the agent; fork it instead.");
|
|
997
925
|
}
|
|
998
926
|
const entries = store.list();
|
|
999
927
|
const sameName = entries.find((entry) => entry.name === type.name);
|
|
1000
928
|
if (sameName && (!original || type.name !== original.name)) {
|
|
1001
|
-
throw new Error(
|
|
1002
|
-
`Agent type ${type.name} already exists; choose a different name.`,
|
|
1003
|
-
);
|
|
929
|
+
throw new Error(`Agent type ${type.name} already exists; choose a different name.`);
|
|
1004
930
|
}
|
|
1005
931
|
const destination = store.destination(type.name, scope, original, kind);
|
|
1006
932
|
if (
|
|
@@ -1080,19 +1006,15 @@ const EDIT_FIELD_HELP: Record<EditMenuAction, string> = {
|
|
|
1080
1006
|
modelSuggestions:
|
|
1081
1007
|
"Advisory display names to help pick a model. Not provider/model pins; never selected or required. One name per line; empty clears.",
|
|
1082
1008
|
thinkingLevel: "Reasoning effort. Default follows the parent session.",
|
|
1083
|
-
"tools.allow":
|
|
1084
|
-
"Unset uses default policy; an empty list explicitly allows no tools.",
|
|
1009
|
+
"tools.allow": "Unset uses default policy; an empty list explicitly allows no tools.",
|
|
1085
1010
|
"tools.block": "Exact tool names to remove from the allowed set.",
|
|
1086
1011
|
color: "Thread widget color with live preview.",
|
|
1087
1012
|
icon: "Paste a single Nerd Font glyph from nerdfonts.com/cheat-sheet (not its name or codepoint).\nLeave blank to remove. Display requires [labs] Nerd Font icons in settings and a Nerd Font in your terminal.",
|
|
1088
1013
|
systemPrompt: "Edit the Markdown prompt body in a multiline dialog.",
|
|
1089
|
-
"Save scope":
|
|
1090
|
-
|
|
1091
|
-
Source:
|
|
1092
|
-
"Read-only source. Bundled definitions are copied, never overwritten.",
|
|
1014
|
+
"Save scope": "Project definitions override global definitions with the same name.",
|
|
1015
|
+
Source: "Read-only source. Bundled definitions are copied, never overwritten.",
|
|
1093
1016
|
"Edit frontmatter YAML": "Edit metadata as YAML, preserving the prompt body.",
|
|
1094
|
-
"External editor (entire Markdown)":
|
|
1095
|
-
"Edit the complete definition using $VISUAL / $EDITOR.",
|
|
1017
|
+
"External editor (entire Markdown)": "Edit the complete definition using $VISUAL / $EDITOR.",
|
|
1096
1018
|
Save: "Validate and save. Existing sessions keep their original configuration.",
|
|
1097
1019
|
Cancel: "Close without saving any edits.",
|
|
1098
1020
|
};
|
|
@@ -1102,10 +1024,7 @@ async function chooseSaveScope(
|
|
|
1102
1024
|
store: ConfigStore,
|
|
1103
1025
|
current: "user" | "project",
|
|
1104
1026
|
): Promise<"user" | "project" | undefined> {
|
|
1105
|
-
const choices = [
|
|
1106
|
-
"Global",
|
|
1107
|
-
...(store.canSaveProject() ? ["Trusted project"] : []),
|
|
1108
|
-
];
|
|
1027
|
+
const choices = ["Global", ...(store.canSaveProject() ? ["Trusted project"] : [])];
|
|
1109
1028
|
const value = await dialogMenu(
|
|
1110
1029
|
ctx,
|
|
1111
1030
|
"Save scope",
|
|
@@ -1131,10 +1050,7 @@ function modelSuggestionPrefill(suggestions: string[] | undefined): string {
|
|
|
1131
1050
|
return (suggestions ?? []).map((name) => sanitizeText(name)).join("\n");
|
|
1132
1051
|
}
|
|
1133
1052
|
|
|
1134
|
-
function customizationMenuValue(
|
|
1135
|
-
editMode: AgentEditMode | undefined,
|
|
1136
|
-
isNew: boolean,
|
|
1137
|
-
): string {
|
|
1053
|
+
function customizationMenuValue(editMode: AgentEditMode | undefined, isNew: boolean): string {
|
|
1138
1054
|
if (isNew) return "New full definition (.md)";
|
|
1139
1055
|
if (editMode === "override") return "Settings override (.yml)";
|
|
1140
1056
|
if (editMode === "fork") return "Full copy (.md)";
|
|
@@ -1228,14 +1144,14 @@ async function editAgentTypesDialog(
|
|
|
1228
1144
|
...types.map((type) => ({
|
|
1229
1145
|
id: type.name,
|
|
1230
1146
|
label: type.name,
|
|
1231
|
-
renderLabel: (label: string, theme: Theme) =>
|
|
1147
|
+
renderLabel: (label: string, theme: Theme) =>
|
|
1148
|
+
agentTypeBadge(label, type.color, theme, type.icon, nerdFontIcons),
|
|
1232
1149
|
value: type.description,
|
|
1233
1150
|
help: `${type.source ?? "user"} · ${type.filePath ?? ""}`,
|
|
1234
1151
|
})),
|
|
1235
1152
|
]);
|
|
1236
1153
|
if (selection === undefined) return;
|
|
1237
|
-
const original =
|
|
1238
|
-
selection === "Create new type" ? undefined : store.get(selection);
|
|
1154
|
+
const original = selection === "Create new type" ? undefined : store.get(selection);
|
|
1239
1155
|
const isNew = !original;
|
|
1240
1156
|
let draft: AgentType = original
|
|
1241
1157
|
? structuredClone(original)
|
|
@@ -1243,9 +1159,7 @@ async function editAgentTypesDialog(
|
|
|
1243
1159
|
const originalContent = serializeAgentType(draft);
|
|
1244
1160
|
let selectedId: string | undefined;
|
|
1245
1161
|
let saveScope: "user" | "project" =
|
|
1246
|
-
original?.source === "project" && store.canSaveProject()
|
|
1247
|
-
? "project"
|
|
1248
|
-
: "user";
|
|
1162
|
+
original?.source === "project" && store.canSaveProject() ? "project" : "user";
|
|
1249
1163
|
// Bundled definitions stay read-only until the user picks how to own them.
|
|
1250
1164
|
let editMode: AgentEditMode | undefined = isNew
|
|
1251
1165
|
? "fork"
|
|
@@ -1273,16 +1187,16 @@ async function editAgentTypesDialog(
|
|
|
1273
1187
|
return {
|
|
1274
1188
|
id: action,
|
|
1275
1189
|
label: EDIT_FIELD_LABELS[action],
|
|
1276
|
-
valueColor: action === "Save" && dirty ? "warning" as const : undefined,
|
|
1190
|
+
valueColor: action === "Save" && dirty ? ("warning" as const) : undefined,
|
|
1277
1191
|
value:
|
|
1278
1192
|
action === "Customization"
|
|
1279
1193
|
? customizationMenuValue(editMode, isNew)
|
|
1280
1194
|
: action === "Save" && dirty
|
|
1281
1195
|
? "(changes)"
|
|
1282
1196
|
: action === "Source"
|
|
1283
|
-
?
|
|
1197
|
+
? original?.customization
|
|
1284
1198
|
? `${original.customization.scope} ${original.customization.kind} over ${original.baseSource ?? "bundled"}`
|
|
1285
|
-
: (original?.source ?? "New draft")
|
|
1199
|
+
: (original?.source ?? "New draft")
|
|
1286
1200
|
: action === "Save scope"
|
|
1287
1201
|
? saveScope === "user"
|
|
1288
1202
|
? "Global"
|
|
@@ -1294,10 +1208,10 @@ async function editAgentTypesDialog(
|
|
|
1294
1208
|
: "",
|
|
1295
1209
|
help:
|
|
1296
1210
|
action === "Source"
|
|
1297
|
-
?
|
|
1211
|
+
? original?.customization
|
|
1298
1212
|
? `Settings file: ${original.customization.filePath}. Base prompt: ${original.baseFilePath ?? "(bundled)"}.`
|
|
1299
1213
|
: (original?.filePath ??
|
|
1300
|
-
"Not saved yet. Bundled definitions are copied, never overwritten.")
|
|
1214
|
+
"Not saved yet. Bundled definitions are copied, never overwritten.")
|
|
1301
1215
|
: EDIT_FIELD_HELP[action],
|
|
1302
1216
|
};
|
|
1303
1217
|
}),
|
|
@@ -1329,7 +1243,7 @@ async function editAgentTypesDialog(
|
|
|
1329
1243
|
editMode = mode;
|
|
1330
1244
|
continue;
|
|
1331
1245
|
}
|
|
1332
|
-
if (!isNew
|
|
1246
|
+
if (!(isNew || editMode)) {
|
|
1333
1247
|
ctx.ui.notify(
|
|
1334
1248
|
"Pick Customization first: tweak settings (.yml) or fork the agent (.md).",
|
|
1335
1249
|
"error",
|
|
@@ -1391,16 +1305,10 @@ async function editAgentTypesDialog(
|
|
|
1391
1305
|
continue;
|
|
1392
1306
|
}
|
|
1393
1307
|
if (field === "Source") {
|
|
1394
|
-
ctx.ui.notify(
|
|
1395
|
-
original?.filePath ?? "This definition has not been saved yet.",
|
|
1396
|
-
"info",
|
|
1397
|
-
);
|
|
1308
|
+
ctx.ui.notify(original?.filePath ?? "This definition has not been saved yet.", "info");
|
|
1398
1309
|
continue;
|
|
1399
1310
|
}
|
|
1400
|
-
if (
|
|
1401
|
-
field === "External editor (entire Markdown)" ||
|
|
1402
|
-
field === "Edit frontmatter YAML"
|
|
1403
|
-
) {
|
|
1311
|
+
if (field === "External editor (entire Markdown)" || field === "Edit frontmatter YAML") {
|
|
1404
1312
|
const edited = await editDocument(
|
|
1405
1313
|
ctx,
|
|
1406
1314
|
draft,
|
|
@@ -1410,11 +1318,7 @@ async function editAgentTypesDialog(
|
|
|
1410
1318
|
continue;
|
|
1411
1319
|
}
|
|
1412
1320
|
const candidate = structuredClone(draft);
|
|
1413
|
-
if (
|
|
1414
|
-
field === "name" ||
|
|
1415
|
-
field === "description" ||
|
|
1416
|
-
field === "systemPrompt"
|
|
1417
|
-
) {
|
|
1321
|
+
if (field === "name" || field === "description" || field === "systemPrompt") {
|
|
1418
1322
|
const originalValue = candidate[field] ?? "";
|
|
1419
1323
|
// Editor prefill must not carry terminal controls. An unmodified or
|
|
1420
1324
|
// SDK-trimmed submit keeps the original, including whitespace and CRLF.
|
|
@@ -1426,15 +1330,16 @@ async function editAgentTypesDialog(
|
|
|
1426
1330
|
.join("\n");
|
|
1427
1331
|
const value = await dialogEditor(ctx, `Agent ${field}`, prefill);
|
|
1428
1332
|
if (value === undefined) continue;
|
|
1429
|
-
candidate[field] =
|
|
1430
|
-
value === prefill || value === prefill.trim()
|
|
1431
|
-
? originalValue
|
|
1432
|
-
: value;
|
|
1333
|
+
candidate[field] = value === prefill || value === prefill.trim() ? originalValue : value;
|
|
1433
1334
|
} else if (field === "icon") {
|
|
1434
|
-
const value = await dialogEditor(
|
|
1335
|
+
const value = await dialogEditor(
|
|
1336
|
+
ctx,
|
|
1337
|
+
"Agent icon (Nerd Font glyph; blank to remove)",
|
|
1338
|
+
candidate.icon ?? "",
|
|
1339
|
+
);
|
|
1435
1340
|
if (value === undefined) continue;
|
|
1436
1341
|
const icon = value.trim();
|
|
1437
|
-
if (!icon)
|
|
1342
|
+
if (!icon) candidate.icon = undefined;
|
|
1438
1343
|
else candidate.icon = icon;
|
|
1439
1344
|
} else if (field === "models") {
|
|
1440
1345
|
const value = await editModelPreferences(
|
|
@@ -1444,11 +1349,11 @@ async function editAgentTypesDialog(
|
|
|
1444
1349
|
);
|
|
1445
1350
|
if (value === MODEL_EDITOR_CANCEL) continue;
|
|
1446
1351
|
if (value.length === 0) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1352
|
+
candidate.models = undefined;
|
|
1353
|
+
candidate.model = undefined;
|
|
1449
1354
|
} else {
|
|
1450
1355
|
candidate.models = [...value];
|
|
1451
|
-
|
|
1356
|
+
candidate.model = undefined;
|
|
1452
1357
|
}
|
|
1453
1358
|
} else if (field === "modelSuggestions") {
|
|
1454
1359
|
const prefill = modelSuggestionPrefill(candidate.modelSuggestions);
|
|
@@ -1463,7 +1368,7 @@ async function editAgentTypesDialog(
|
|
|
1463
1368
|
.split("\n")
|
|
1464
1369
|
.map((line) => line.trim())
|
|
1465
1370
|
.filter((line) => line.length > 0);
|
|
1466
|
-
if (names.length === 0)
|
|
1371
|
+
if (names.length === 0) candidate.modelSuggestions = undefined;
|
|
1467
1372
|
else candidate.modelSuggestions = [...names];
|
|
1468
1373
|
} else if (field === "thinkingLevel") {
|
|
1469
1374
|
const value = await dialogMenu(
|
|
@@ -1476,23 +1381,15 @@ async function editAgentTypesDialog(
|
|
|
1476
1381
|
{ selectedId: candidate.thinkingLevel ?? "Default (inherit)" },
|
|
1477
1382
|
);
|
|
1478
1383
|
if (!value) continue;
|
|
1479
|
-
if (value === "Default (inherit)")
|
|
1384
|
+
if (value === "Default (inherit)") candidate.thinkingLevel = undefined;
|
|
1480
1385
|
else candidate.thinkingLevel = value as AgentType["thinkingLevel"];
|
|
1481
1386
|
} else if (field === "color") {
|
|
1482
|
-
const value = await selectAgentColor(
|
|
1483
|
-
ctx,
|
|
1484
|
-
candidate.color,
|
|
1485
|
-
candidate.name,
|
|
1486
|
-
);
|
|
1387
|
+
const value = await selectAgentColor(ctx, candidate.color, candidate.name);
|
|
1487
1388
|
if (value === AGENT_COLOR_CANCEL) continue;
|
|
1488
|
-
if (value === undefined)
|
|
1389
|
+
if (value === undefined) candidate.color = undefined;
|
|
1489
1390
|
else candidate.color = value;
|
|
1490
1391
|
} else if (field === "tools.allow" || field === "tools.block") {
|
|
1491
|
-
await editToolList(
|
|
1492
|
-
ctx,
|
|
1493
|
-
candidate,
|
|
1494
|
-
field === "tools.allow" ? "allow" : "block",
|
|
1495
|
-
);
|
|
1392
|
+
await editToolList(ctx, candidate, field === "tools.allow" ? "allow" : "block");
|
|
1496
1393
|
}
|
|
1497
1394
|
parseAgentType(serializeAgentType(candidate));
|
|
1498
1395
|
draft = candidate;
|