pi-subagents-lite 1.3.0 → 1.4.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/README.md +184 -235
- package/package.json +1 -1
- package/src/{agent-discovery.ts → agents/agent-discovery.ts} +8 -5
- package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
- package/src/{agent-runner.ts → agents/agent-runner.ts} +115 -173
- package/src/{agent-status.ts → agents/agent-status.ts} +4 -4
- package/src/agents/agent-types.ts +339 -0
- package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
- package/src/{output-file.ts → agents/output-file.ts} +68 -1
- package/src/{tool-execution.ts → agents/tool-execution.ts} +60 -222
- package/src/agents/types.ts +54 -0
- package/src/{usage.ts → agents/usage.ts} +7 -0
- package/src/{config-io.ts → config/config-io.ts} +20 -3
- package/src/config/config-store.ts +472 -0
- package/src/config/types.ts +26 -0
- package/src/events.ts +185 -0
- package/src/index.ts +8 -281
- package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
- package/src/{model-selector.ts → models/model-selector.ts} +1 -1
- package/src/{context.ts → prompt/context.ts} +1 -1
- package/src/prompt/prompts.ts +180 -0
- package/src/prompt/skill-loader.ts +195 -0
- package/src/registration.ts +101 -0
- package/src/shell.ts +101 -0
- package/src/spawn/spawn-coordinator.ts +232 -0
- package/src/status-note.ts +10 -0
- package/src/types.ts +47 -71
- package/src/ui/agent-widget.ts +61 -49
- package/src/{format.ts → ui/format.ts} +64 -26
- package/src/ui/menu/helpers.ts +93 -0
- package/src/ui/menu/menu-concurrency.ts +192 -0
- package/src/ui/menu/menu-debug.ts +125 -0
- package/src/ui/menu/menu-model-settings.ts +208 -0
- package/src/ui/menu/menu-running-agents.ts +224 -0
- package/src/ui/menu/menu-spawn-options.ts +87 -0
- package/src/ui/menu/menu-spawn-wizard.ts +418 -0
- package/src/ui/menu/menu-system-prompt.ts +109 -0
- package/src/ui/menu/menu-widget-settings.ts +130 -0
- package/src/ui/menu/menus.ts +101 -0
- package/src/ui/menu/submenus/confirm.ts +47 -0
- package/src/ui/menu/submenus/model-select.ts +70 -0
- package/src/ui/menu/submenus/numeric-input.ts +98 -0
- package/src/ui/menu/wrappers/settings-list.ts +205 -0
- package/src/{renderer.ts → ui/renderer.ts} +7 -6
- package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
- package/src/ui/types.ts +11 -0
- package/src/agent-types.ts +0 -184
- package/src/config-mutator.ts +0 -183
- package/src/menus.ts +0 -1333
- package/src/prompts.ts +0 -94
- package/src/skill-loader.ts +0 -178
- package/src/state.ts +0 -83
- /package/src/{worktree-validator.ts → spawn/worktree-validator.ts} +0 -0
package/src/menus.ts
DELETED
|
@@ -1,1333 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* menus.ts — /agents command menu system.
|
|
3
|
-
*
|
|
4
|
-
* All menu-related functions extracted from index.ts.
|
|
5
|
-
* Imports shared state (config, manager, piInstance) from state.ts.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { getAgentConfig, getAvailableTypes, getAllTypes, resolveType, discoverNewAgents } from "./agent-types.js";
|
|
10
|
-
import type { AgentRecord, ThinkingLevel } from "./types.js";
|
|
11
|
-
import { SHORT_ID_LENGTH, CONFIG_AGENT_NON_MODEL_KEYS } from "./types.js";
|
|
12
|
-
import type { SpawnOptions } from "./agent-manager.js";
|
|
13
|
-
import { ModelSelectorDialog, type ModelOption } from "./model-selector.js";
|
|
14
|
-
import { ResultViewer, type ResultViewerStats } from "./result-viewer.js";
|
|
15
|
-
import { getDisplayName } from "./format.js";
|
|
16
|
-
import { buildSnapshotMarkdown } from "./context.js";
|
|
17
|
-
|
|
18
|
-
import { parseModelKey, findModelInRegistry } from "./utils.js";
|
|
19
|
-
import {
|
|
20
|
-
__config,
|
|
21
|
-
sessionOverrides,
|
|
22
|
-
piInstance,
|
|
23
|
-
sessionCtx,
|
|
24
|
-
agentActivity,
|
|
25
|
-
getManager,
|
|
26
|
-
getWidget,
|
|
27
|
-
} from "./state.js";
|
|
28
|
-
import { resolveModel } from "./model-precedence.js";
|
|
29
|
-
import { createActivityTracker, backgroundAgentIds } from "./tool-execution.js";
|
|
30
|
-
import {
|
|
31
|
-
setModelOverride,
|
|
32
|
-
setDefaultModel,
|
|
33
|
-
clearModelOverride,
|
|
34
|
-
clearAllModelOverrides,
|
|
35
|
-
setForceBackground,
|
|
36
|
-
setShowCost,
|
|
37
|
-
setGraceTurns,
|
|
38
|
-
setWidgetCompact,
|
|
39
|
-
setWidgetMaxLines,
|
|
40
|
-
setWidgetMaxLinesCompact,
|
|
41
|
-
setWidgetShortcut,
|
|
42
|
-
setAgent,
|
|
43
|
-
setConcurrencyDefault,
|
|
44
|
-
setConcurrencyProvider,
|
|
45
|
-
setConcurrencyModel,
|
|
46
|
-
removeConcurrencyProvider,
|
|
47
|
-
removeConcurrencyModel,
|
|
48
|
-
resetConcurrency,
|
|
49
|
-
} from "./config-mutator.js";
|
|
50
|
-
|
|
51
|
-
// ============================================================================
|
|
52
|
-
// Helpers
|
|
53
|
-
// ============================================================================
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Build ModelOption[] from raw "provider/model-id" strings.
|
|
57
|
-
* Includes "(inherits parent)" as the first option.
|
|
58
|
-
*/
|
|
59
|
-
function buildModelOptions(rawOptions: string[]): ModelOption[] {
|
|
60
|
-
const items: ModelOption[] = [
|
|
61
|
-
{ value: "(inherits parent)", label: "(inherits parent)", provider: "" },
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
for (const opt of rawOptions) {
|
|
65
|
-
const parsed = parseModelKey(opt);
|
|
66
|
-
if (!parsed) continue;
|
|
67
|
-
items.push({ value: opt, label: parsed.modelId, provider: parsed.provider });
|
|
68
|
-
}
|
|
69
|
-
return items;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Show the ModelSelectorDialog and return the chosen model string, or null.
|
|
74
|
-
*/
|
|
75
|
-
async function promptModelSelection(
|
|
76
|
-
ctx: ExtensionCommandContext,
|
|
77
|
-
modelOptions: string[],
|
|
78
|
-
currentValue: string,
|
|
79
|
-
): Promise<string | null> {
|
|
80
|
-
return ctx.ui.custom<string | null>(
|
|
81
|
-
(_tui, theme, _kb, done) => {
|
|
82
|
-
const opts = buildModelOptions(modelOptions);
|
|
83
|
-
return new ModelSelectorDialog(opts, currentValue, {
|
|
84
|
-
onSelect: (m) => done(m),
|
|
85
|
-
onCancel: () => done(null),
|
|
86
|
-
}, theme);
|
|
87
|
-
}, // no overlay — renders inline below editor, matching pi's model selector look and feel
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Prompt user to choose between session-only or permanent persistence.
|
|
93
|
-
* When showClear is true, also offers "Clear".
|
|
94
|
-
* Returns "session", "permanent", "clear", or null if cancelled.
|
|
95
|
-
*/
|
|
96
|
-
async function promptOverrideMode(
|
|
97
|
-
ctx: ExtensionCommandContext,
|
|
98
|
-
showClear: boolean = false,
|
|
99
|
-
): Promise<"session" | "permanent" | "clear" | null> {
|
|
100
|
-
const choices: string[] = [
|
|
101
|
-
"Set for this session (not saved)",
|
|
102
|
-
"Set permanently (saved to config)",
|
|
103
|
-
];
|
|
104
|
-
if (showClear) {
|
|
105
|
-
choices.push("Clear");
|
|
106
|
-
}
|
|
107
|
-
const choice = await ctx.ui.select("Save mode", choices);
|
|
108
|
-
if (choice === undefined) return null;
|
|
109
|
-
if (choice.startsWith("Set for this session")) return "session";
|
|
110
|
-
if (choice.startsWith("Set permanently")) return "permanent";
|
|
111
|
-
return "clear";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Prompt for a model selection and apply it as an override.
|
|
116
|
-
* "(inherits parent)" clears the override (sets to null).
|
|
117
|
-
* The caller is responsible for persistence (saveConfigAtomic).
|
|
118
|
-
*/
|
|
119
|
-
async function applyModelOverride(
|
|
120
|
-
ctx: ExtensionCommandContext,
|
|
121
|
-
modelOptions: string[],
|
|
122
|
-
label: string,
|
|
123
|
-
currentValue: string,
|
|
124
|
-
apply: (chosen: string | null) => void,
|
|
125
|
-
): Promise<void> {
|
|
126
|
-
const chosen = await promptModelSelection(ctx, modelOptions, currentValue);
|
|
127
|
-
if (chosen === null) return;
|
|
128
|
-
|
|
129
|
-
const effective = chosen === "(inherits parent)" ? null : chosen;
|
|
130
|
-
apply(effective);
|
|
131
|
-
ctx.ui.notify(
|
|
132
|
-
effective === null
|
|
133
|
-
? `${label} inherits parent model`
|
|
134
|
-
: `${label} model set to ${effective}`,
|
|
135
|
-
"info",
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Prompt for numeric input, validate (integer ≥ min), return parsed value or undefined.
|
|
141
|
-
* Returns undefined if the user cancels or the value is invalid.
|
|
142
|
-
*/
|
|
143
|
-
async function parseNumericInput(
|
|
144
|
-
ctx: ExtensionCommandContext,
|
|
145
|
-
label: string,
|
|
146
|
-
initialValue: string,
|
|
147
|
-
min: number,
|
|
148
|
-
minLabel: string,
|
|
149
|
-
): Promise<number | undefined> {
|
|
150
|
-
const input = await ctx.ui.input(label, initialValue);
|
|
151
|
-
if (input === undefined) return undefined;
|
|
152
|
-
const parsed = parseInt(input.trim(), 10);
|
|
153
|
-
if (isNaN(parsed) || parsed < min) {
|
|
154
|
-
ctx.ui.notify(`Invalid value — must be a number ${minLabel}`, "error");
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
return parsed;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Parse a concurrency input: prompt, validate (integer ≥ 1), return parsed value or undefined.
|
|
162
|
-
*/
|
|
163
|
-
async function parseConcurrencyInput(
|
|
164
|
-
ctx: ExtensionCommandContext,
|
|
165
|
-
label: string,
|
|
166
|
-
initialValue: string,
|
|
167
|
-
): Promise<number | undefined> {
|
|
168
|
-
return parseNumericInput(ctx, label, initialValue, 1, "≥ 1");
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Prompt for a concurrency value, validate, and apply via setter.
|
|
173
|
-
* The setter handles save + sync internally.
|
|
174
|
-
*/
|
|
175
|
-
async function promptConcurrencyInput(
|
|
176
|
-
ctx: ExtensionCommandContext,
|
|
177
|
-
label: string,
|
|
178
|
-
currentValue: number,
|
|
179
|
-
setter: (value: number) => void,
|
|
180
|
-
): Promise<void> {
|
|
181
|
-
const parsed = await parseConcurrencyInput(ctx, label, String(currentValue));
|
|
182
|
-
if (parsed === undefined) return;
|
|
183
|
-
setter(parsed);
|
|
184
|
-
ctx.ui.notify(
|
|
185
|
-
`${label.replace("Concurrency slots for ", "")} concurrency set to ${parsed}`,
|
|
186
|
-
"info",
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Prompt to add a new concurrency limit for a named entity.
|
|
192
|
-
* Calls the setter which handles save + sync internally.
|
|
193
|
-
*/
|
|
194
|
-
async function promptAddConcurrencyLimit(
|
|
195
|
-
ctx: ExtensionCommandContext,
|
|
196
|
-
label: string,
|
|
197
|
-
setter: (key: string, value: number) => void,
|
|
198
|
-
): Promise<void> {
|
|
199
|
-
const parsed = await parseConcurrencyInput(ctx, "Concurrency slots", "1");
|
|
200
|
-
if (parsed === undefined) return;
|
|
201
|
-
setter(label, parsed);
|
|
202
|
-
ctx.ui.notify(`${label} concurrency set to ${parsed}`, "info");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Show a select menu once, dispatch the chosen action.
|
|
207
|
-
* Used by the per-agent action sub-menu (single-shot, not a loop).
|
|
208
|
-
*/
|
|
209
|
-
async function runMenu(
|
|
210
|
-
ctx: ExtensionCommandContext,
|
|
211
|
-
title: string,
|
|
212
|
-
items: string[],
|
|
213
|
-
actions: Array<() => Promise<void>>,
|
|
214
|
-
): Promise<void> {
|
|
215
|
-
const choice = await ctx.ui.select(title, items);
|
|
216
|
-
if (choice === undefined) return;
|
|
217
|
-
const idx = items.indexOf(choice);
|
|
218
|
-
if (idx >= 0 && idx < actions.length) {
|
|
219
|
-
await actions[idx]();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Loop a menu until the user presses Escape or selects "Back".
|
|
225
|
-
* Rebuilds items/actions each iteration so the display stays fresh.
|
|
226
|
-
* Appends blank spacer + "Back" automatically.
|
|
227
|
-
* Used by model settings, concurrency settings, and running agents menus.
|
|
228
|
-
*/
|
|
229
|
-
async function runMenuLoop(
|
|
230
|
-
ctx: ExtensionCommandContext,
|
|
231
|
-
title: string,
|
|
232
|
-
build: () => { items: string[]; actions: Array<() => Promise<void>> },
|
|
233
|
-
): Promise<void> {
|
|
234
|
-
while (true) {
|
|
235
|
-
const { items, actions } = build();
|
|
236
|
-
items.push("");
|
|
237
|
-
actions.push(async () => {});
|
|
238
|
-
items.push("Back");
|
|
239
|
-
actions.push(async () => {});
|
|
240
|
-
|
|
241
|
-
const choice = await ctx.ui.select(title, items);
|
|
242
|
-
if (choice === undefined || choice === "Back") return;
|
|
243
|
-
const idx = items.indexOf(choice);
|
|
244
|
-
if (idx >= 0 && idx < actions.length) {
|
|
245
|
-
await actions[idx]();
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// ============================================================================
|
|
251
|
-
// Worktree picker helpers
|
|
252
|
-
// ============================================================================
|
|
253
|
-
|
|
254
|
-
/** Timeout for git worktree list command (ms). */
|
|
255
|
-
const WORKTREE_LIST_TIMEOUT_MS = 5000;
|
|
256
|
-
|
|
257
|
-
/** Max display length for a worktree path before truncation. */
|
|
258
|
-
const WORKTREE_PATH_TRUNCATE_LEN = 60;
|
|
259
|
-
|
|
260
|
-
interface WorktreeEntry {
|
|
261
|
-
path: string;
|
|
262
|
-
branch: string | null;
|
|
263
|
-
isDetached: boolean;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Parse `git worktree list --porcelain` output into structured entries.
|
|
268
|
-
*
|
|
269
|
-
* Format (one block per worktree, separated by blank lines):
|
|
270
|
-
* worktree /path/to/worktree
|
|
271
|
-
* HEAD <sha>
|
|
272
|
-
* branch refs/heads/<name> (or: (detached))
|
|
273
|
-
*/
|
|
274
|
-
function parseWorktreeList(output: string): WorktreeEntry[] {
|
|
275
|
-
const entries: WorktreeEntry[] = [];
|
|
276
|
-
const blocks = output.split(/\n\n+/);
|
|
277
|
-
for (const block of blocks) {
|
|
278
|
-
if (!block.trim()) continue;
|
|
279
|
-
const lines = block.split("\n");
|
|
280
|
-
let path = "";
|
|
281
|
-
let branch: string | null = null;
|
|
282
|
-
let isDetached = false;
|
|
283
|
-
for (const line of lines) {
|
|
284
|
-
if (line.startsWith("worktree ")) {
|
|
285
|
-
path = line.slice("worktree ".length);
|
|
286
|
-
} else if (line.startsWith("branch refs/heads/")) {
|
|
287
|
-
branch = line.slice("branch refs/heads/".length);
|
|
288
|
-
} else if (line === "detached") {
|
|
289
|
-
isDetached = true;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (path) {
|
|
293
|
-
entries.push({ path, branch, isDetached });
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return entries;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Truncate a path for display, keeping the tail. */
|
|
300
|
-
function truncatePath(p: string): string {
|
|
301
|
-
if (p.length <= WORKTREE_PATH_TRUNCATE_LEN) return p;
|
|
302
|
-
return "..." + p.slice(p.length - WORKTREE_PATH_TRUNCATE_LEN + 3);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Fetch worktrees via `git worktree list --porcelain`.
|
|
307
|
-
* Returns null if git is unavailable or the command fails.
|
|
308
|
-
*/
|
|
309
|
-
async function listWorktrees(cwd: string): Promise<WorktreeEntry[] | null> {
|
|
310
|
-
try {
|
|
311
|
-
const result = await piInstance.exec(
|
|
312
|
-
"git",
|
|
313
|
-
["worktree", "list", "--porcelain"],
|
|
314
|
-
{ cwd, timeout: WORKTREE_LIST_TIMEOUT_MS },
|
|
315
|
-
);
|
|
316
|
-
if (result.code !== 0) return null;
|
|
317
|
-
return parseWorktreeList(result.stdout);
|
|
318
|
-
} catch {
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Check whether a directory is inside a git repository.
|
|
325
|
-
* Uses `git rev-parse --git-common-dir` — the same strategy as the worktree validator.
|
|
326
|
-
*/
|
|
327
|
-
async function isInGitRepo(cwd: string): Promise<boolean> {
|
|
328
|
-
try {
|
|
329
|
-
const result = await piInstance.exec(
|
|
330
|
-
"git",
|
|
331
|
-
["rev-parse", "--git-common-dir"],
|
|
332
|
-
{ cwd, timeout: WORKTREE_LIST_TIMEOUT_MS },
|
|
333
|
-
);
|
|
334
|
-
return result.code === 0 && result.stdout.trim() !== "";
|
|
335
|
-
} catch {
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ============================================================================
|
|
341
|
-
// /agents command handler
|
|
342
|
-
// ============================================================================
|
|
343
|
-
|
|
344
|
-
export async function showModelSettingsMenu(
|
|
345
|
-
ctx: ExtensionCommandContext,
|
|
346
|
-
modelOptions: string[],
|
|
347
|
-
): Promise<void> {
|
|
348
|
-
return runMenuLoop(ctx, "Model Settings", () => {
|
|
349
|
-
const items: string[] = [];
|
|
350
|
-
const actions: Array<() => Promise<void>> = [];
|
|
351
|
-
|
|
352
|
-
// ── Session overrides section ──
|
|
353
|
-
const hasSessionOverrides = Object.entries(sessionOverrides).some(
|
|
354
|
-
([, v]) => v != null,
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
const buildOverrideAction = (
|
|
358
|
-
label: string,
|
|
359
|
-
targetKey: string,
|
|
360
|
-
currentValue: string,
|
|
361
|
-
hasPermanentOverride: boolean = false,
|
|
362
|
-
) => async () => {
|
|
363
|
-
const mode = await promptOverrideMode(ctx, hasPermanentOverride);
|
|
364
|
-
if (mode === null) return;
|
|
365
|
-
|
|
366
|
-
// Handle "clear" — remove all overrides (session + config) and save
|
|
367
|
-
if (mode === "clear") {
|
|
368
|
-
clearModelOverride(targetKey);
|
|
369
|
-
if (targetKey !== "default") {
|
|
370
|
-
delete sessionOverrides[targetKey];
|
|
371
|
-
} else {
|
|
372
|
-
sessionOverrides.default = null;
|
|
373
|
-
}
|
|
374
|
-
ctx.ui.notify(`${label} overrides cleared`, "info");
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const isSession = mode === "session";
|
|
379
|
-
await applyModelOverride(
|
|
380
|
-
ctx, modelOptions, label,
|
|
381
|
-
currentValue,
|
|
382
|
-
isSession
|
|
383
|
-
? (chosen) => { sessionOverrides[targetKey] = chosen; }
|
|
384
|
-
: (chosen) => {
|
|
385
|
-
setModelOverride(targetKey, chosen);
|
|
386
|
-
},
|
|
387
|
-
);
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
// Global default — show session value if present
|
|
391
|
-
const hasSessionGlobal = sessionOverrides.default != null;
|
|
392
|
-
const globalLabel = hasSessionGlobal
|
|
393
|
-
? `Global default model · ${sessionOverrides.default} [session]`
|
|
394
|
-
: __config.agent.default
|
|
395
|
-
? `Global default model · ${__config.agent.default}`
|
|
396
|
-
: "Global default model · (inherits parent)";
|
|
397
|
-
items.push(globalLabel);
|
|
398
|
-
actions.push(buildOverrideAction(
|
|
399
|
-
"Global default", "default",
|
|
400
|
-
hasSessionGlobal
|
|
401
|
-
? sessionOverrides.default!
|
|
402
|
-
: __config.agent.default ?? "(inherits parent)",
|
|
403
|
-
));
|
|
404
|
-
|
|
405
|
-
// Force background toggle
|
|
406
|
-
const forceBgLabel = __config.agent.forceBackground
|
|
407
|
-
? "Force background · ON"
|
|
408
|
-
: "Force background · OFF";
|
|
409
|
-
items.push(forceBgLabel);
|
|
410
|
-
actions.push(async () => {
|
|
411
|
-
setForceBackground(!__config.agent.forceBackground);
|
|
412
|
-
ctx.ui.notify(
|
|
413
|
-
`Force background ${__config.agent.forceBackground ? "ON" : "OFF"}`,
|
|
414
|
-
"info",
|
|
415
|
-
);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// Cost display toggle
|
|
419
|
-
const showCost = __config.agent.showCost === true; // default false
|
|
420
|
-
items.push(`Cost display · ${showCost ? "ON" : "OFF"}`);
|
|
421
|
-
actions.push(async () => {
|
|
422
|
-
setShowCost(!showCost);
|
|
423
|
-
ctx.ui.notify(`Cost display ${showCost ? "OFF" : "ON"}`, "info");
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
// Grace turns setting
|
|
427
|
-
const graceTurns = __config.agent.graceTurns ?? 6;
|
|
428
|
-
items.push(`Grace turns · ${graceTurns}`);
|
|
429
|
-
actions.push(async () => {
|
|
430
|
-
const parsed = await parseNumericInput(ctx, "Grace turns (≥ 0)", String(graceTurns), 0, "≥ 0");
|
|
431
|
-
if (parsed === undefined) return;
|
|
432
|
-
setGraceTurns(parsed);
|
|
433
|
-
ctx.ui.notify(`Grace turns set to ${parsed}`, "info");
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
items.push("");
|
|
437
|
-
actions.push(async () => {});
|
|
438
|
-
items.push("─── per-type overrides ───");
|
|
439
|
-
actions.push(async () => {}); // separator
|
|
440
|
-
|
|
441
|
-
// Per-type overrides — show only types with an explicit override (session or config)
|
|
442
|
-
// All others inherit the global default; accessible via "Override another type..."
|
|
443
|
-
const types = getAllTypes();
|
|
444
|
-
const typeEntries = types.map((typeName) => {
|
|
445
|
-
const cfg = getAgentConfig(typeName);
|
|
446
|
-
const sessionOverride = sessionOverrides[typeName];
|
|
447
|
-
const configOverride = __config.agent[typeName];
|
|
448
|
-
const hasSession = sessionOverride != null;
|
|
449
|
-
const hasConfigOverride = configOverride != null && typeof configOverride === "string";
|
|
450
|
-
const effectiveModel = resolveModel({
|
|
451
|
-
subagentType: typeName,
|
|
452
|
-
agentConfig: cfg,
|
|
453
|
-
config: __config,
|
|
454
|
-
parentModelId: "(inherits parent)",
|
|
455
|
-
sessionOverrides,
|
|
456
|
-
});
|
|
457
|
-
return { typeName, cfg, sessionOverride, configOverride, hasSession, hasConfigOverride, effectiveModel };
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
const overridden = typeEntries.filter(e => e.hasSession || e.hasConfigOverride);
|
|
461
|
-
const nonOverridden = typeEntries.filter(e => !e.hasSession && !e.hasConfigOverride);
|
|
462
|
-
|
|
463
|
-
if (overridden.length === 0) {
|
|
464
|
-
items.push(" (all inherit global default)");
|
|
465
|
-
actions.push(async () => {}); // no-op
|
|
466
|
-
} else {
|
|
467
|
-
overridden.sort((a, b) => a.effectiveModel.localeCompare(b.effectiveModel));
|
|
468
|
-
const padLen = Math.max(...types.map(t => t.length));
|
|
469
|
-
for (const { typeName, cfg, sessionOverride, configOverride, hasSession, effectiveModel } of overridden) {
|
|
470
|
-
const frontmatterHint = !hasSession && configOverride && cfg?.model ? `${cfg.model} → ` : "";
|
|
471
|
-
const displayModel = hasSession ? `${sessionOverride} [session]` : effectiveModel;
|
|
472
|
-
items.push(`${typeName.padEnd(padLen)} · ${frontmatterHint}${displayModel}`);
|
|
473
|
-
|
|
474
|
-
const currentValue = hasSession ? sessionOverride! : effectiveModel;
|
|
475
|
-
actions.push(buildOverrideAction(typeName, typeName, currentValue, !!configOverride));
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Add override for a type that currently inherits
|
|
480
|
-
if (nonOverridden.length > 0) {
|
|
481
|
-
items.push("Override another type...");
|
|
482
|
-
actions.push(async () => {
|
|
483
|
-
const typeNames = nonOverridden.map(e => e.typeName);
|
|
484
|
-
const chosen = await ctx.ui.select("Select agent type", typeNames);
|
|
485
|
-
if (chosen === undefined) return;
|
|
486
|
-
const entry = nonOverridden.find(e => e.typeName === chosen)!;
|
|
487
|
-
const action = buildOverrideAction(chosen, chosen, entry.effectiveModel, false);
|
|
488
|
-
await action();
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Clear session overrides
|
|
493
|
-
if (hasSessionOverrides) {
|
|
494
|
-
items.push("Clear session overrides");
|
|
495
|
-
actions.push(async () => {
|
|
496
|
-
sessionOverrides.default = null;
|
|
497
|
-
for (const key of Object.keys(sessionOverrides)) {
|
|
498
|
-
if (key !== "default") {
|
|
499
|
-
delete sessionOverrides[key];
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
ctx.ui.notify("Session overrides cleared", "info");
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Clear all overrides
|
|
507
|
-
items.push("Clear all overrides");
|
|
508
|
-
actions.push(async () => {
|
|
509
|
-
const hasOverrides = Object.entries(__config.agent).some(
|
|
510
|
-
([k, v]) => !CONFIG_AGENT_NON_MODEL_KEYS.includes(k) && v != null,
|
|
511
|
-
);
|
|
512
|
-
if (!hasOverrides && __config.agent.default === null) {
|
|
513
|
-
ctx.ui.notify("No overrides to clear", "info");
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
clearAllModelOverrides();
|
|
517
|
-
ctx.ui.notify("All model overrides cleared", "info");
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
return { items, actions };
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/** Map menu choice to handler. Matches by number prefix or first word. */
|
|
525
|
-
function matchMenuChoice(
|
|
526
|
-
choice: string,
|
|
527
|
-
handlers: Record<string, () => Promise<void>>,
|
|
528
|
-
): (() => Promise<void>) | undefined {
|
|
529
|
-
// Try number prefix first (e.g., "1." from "1. Running agents")
|
|
530
|
-
const numMatch = choice.match(/^(\d+)/);
|
|
531
|
-
if (numMatch) return handlers[numMatch[1]];
|
|
532
|
-
// Fall back to first word
|
|
533
|
-
const key = choice.split(" ")[0].toLowerCase();
|
|
534
|
-
return handlers[key];
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// ============================================================================
|
|
538
|
-
// Spawn agent menu
|
|
539
|
-
// ============================================================================
|
|
540
|
-
|
|
541
|
-
const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Show the spawn agent flow: type selection → prompt → options sub-menu → spawn.
|
|
545
|
-
* Escape at any step aborts the flow and returns to the main menu.
|
|
546
|
-
*/
|
|
547
|
-
export async function showSpawnAgentMenu(
|
|
548
|
-
ctx: ExtensionCommandContext,
|
|
549
|
-
modelOptions: string[],
|
|
550
|
-
): Promise<void> {
|
|
551
|
-
// Step 1: Type selection loop (unknown type → error → retry)
|
|
552
|
-
let selectedType: string;
|
|
553
|
-
while (true) {
|
|
554
|
-
const types = getAvailableTypes();
|
|
555
|
-
if (types.length === 0) {
|
|
556
|
-
ctx.ui.notify("No agent types available", "error");
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
const type = await ctx.ui.select("Select agent type", types);
|
|
560
|
-
if (type === undefined) return; // Escape → main menu
|
|
561
|
-
|
|
562
|
-
const config = getAgentConfig(type);
|
|
563
|
-
if (!config) {
|
|
564
|
-
ctx.ui.notify(`Unknown agent type: ${type}`, "error");
|
|
565
|
-
continue; // Loop back to type selection
|
|
566
|
-
}
|
|
567
|
-
selectedType = type;
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const agentConfig = getAgentConfig(selectedType)!;
|
|
572
|
-
|
|
573
|
-
// Step 2: Prompt entry loop (empty prompt → error → retry)
|
|
574
|
-
let prompt: string;
|
|
575
|
-
while (true) {
|
|
576
|
-
const input = await ctx.ui.input("Agent prompt");
|
|
577
|
-
if (input === undefined) return; // Escape → main menu
|
|
578
|
-
|
|
579
|
-
if (!input.trim()) {
|
|
580
|
-
ctx.ui.notify("Prompt cannot be empty", "error");
|
|
581
|
-
continue; // Loop back to prompt input
|
|
582
|
-
}
|
|
583
|
-
prompt = input.trim();
|
|
584
|
-
break;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Step 3: Options sub-menu with spawn action
|
|
588
|
-
const autoDescription = prompt.length > 50 ? prompt.slice(0, 50) : prompt;
|
|
589
|
-
let description = autoDescription;
|
|
590
|
-
|
|
591
|
-
// Check if parent's cwd is inside a git repo (for worktree picker visibility)
|
|
592
|
-
const parentCwd = sessionCtx?.cwd ?? "";
|
|
593
|
-
const inGitRepo = parentCwd ? await isInGitRepo(parentCwd) : false;
|
|
594
|
-
|
|
595
|
-
// Worktree picker state
|
|
596
|
-
let currentWorktreePath: string | undefined;
|
|
597
|
-
let currentWorktreeLabel = "Inherits parent cwd";
|
|
598
|
-
|
|
599
|
-
// Pre-fill model from precedence chain
|
|
600
|
-
const parentModelId = sessionCtx?.model
|
|
601
|
-
? `${sessionCtx.model.provider}/${sessionCtx.model.id}`
|
|
602
|
-
: "";
|
|
603
|
-
const effectiveModelStr = resolveModel({
|
|
604
|
-
subagentType: selectedType,
|
|
605
|
-
agentConfig,
|
|
606
|
-
config: __config,
|
|
607
|
-
parentModelId,
|
|
608
|
-
sessionOverrides,
|
|
609
|
-
});
|
|
610
|
-
let currentModelStr = effectiveModelStr || ""; // "" means inherit parent
|
|
611
|
-
let currentThinking: ThinkingLevel | undefined = agentConfig.thinking;
|
|
612
|
-
let currentMaxTurns: number | undefined = agentConfig.maxTurns;
|
|
613
|
-
let currentGraceTurns: number | undefined = __config.agent.graceTurns ?? 6;
|
|
614
|
-
let currentBackground: boolean = __config.agent.forceBackground ?? false;
|
|
615
|
-
|
|
616
|
-
while (true) {
|
|
617
|
-
const displayModel = currentModelStr || "(inherits parent)";
|
|
618
|
-
const displayThinking = currentThinking ?? "inherit";
|
|
619
|
-
const displayMaxTurns = currentMaxTurns != null ? String(currentMaxTurns) : "unlimited";
|
|
620
|
-
const displayGraceTurns = String(currentGraceTurns ?? 6);
|
|
621
|
-
const displayBackground = currentBackground ? "ON" : "OFF";
|
|
622
|
-
|
|
623
|
-
const items = [
|
|
624
|
-
"Spawn",
|
|
625
|
-
"",
|
|
626
|
-
`Model · ${displayModel}`,
|
|
627
|
-
`Background · ${displayBackground}`,
|
|
628
|
-
`Thinking · ${displayThinking}`,
|
|
629
|
-
`Max turns · ${displayMaxTurns}`,
|
|
630
|
-
`Grace turns · ${displayGraceTurns}`,
|
|
631
|
-
`Description · ${description}`,
|
|
632
|
-
];
|
|
633
|
-
|
|
634
|
-
if (inGitRepo) {
|
|
635
|
-
items.push(`Worktree · ${currentWorktreeLabel}`);
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
const choice = await ctx.ui.select("Spawn Options", items);
|
|
639
|
-
if (choice === undefined) return; // Escape → main menu
|
|
640
|
-
|
|
641
|
-
if (choice === "Spawn") {
|
|
642
|
-
// Resolve model string to Model object
|
|
643
|
-
let model: ReturnType<typeof findModelInRegistry> = undefined;
|
|
644
|
-
let modelKey: string | undefined;
|
|
645
|
-
|
|
646
|
-
if (currentModelStr) {
|
|
647
|
-
const registry = sessionCtx?.modelRegistry ?? ctx.modelRegistry;
|
|
648
|
-
model = findModelInRegistry(currentModelStr, registry, undefined);
|
|
649
|
-
if (!model) {
|
|
650
|
-
ctx.ui.notify(`Model not found: ${currentModelStr}`, "error");
|
|
651
|
-
continue; // Return to options sub-menu
|
|
652
|
-
}
|
|
653
|
-
modelKey = `${model.provider}/${model.id}`;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Discover worktree-local agent types before spawn
|
|
657
|
-
if (currentWorktreePath) {
|
|
658
|
-
await discoverNewAgents(`${currentWorktreePath}/.pi/agents`);
|
|
659
|
-
}
|
|
660
|
-
// Resolve type (may have been discovered from worktree)
|
|
661
|
-
const resolvedType = resolveType(selectedType) ?? selectedType;
|
|
662
|
-
|
|
663
|
-
const spawnOptions: SpawnOptions = {
|
|
664
|
-
description,
|
|
665
|
-
model,
|
|
666
|
-
maxTurns: currentMaxTurns,
|
|
667
|
-
thinkingLevel: currentThinking,
|
|
668
|
-
isBackground: currentBackground,
|
|
669
|
-
modelKey,
|
|
670
|
-
invocation: {
|
|
671
|
-
modelName: model?.id,
|
|
672
|
-
thinking: currentThinking,
|
|
673
|
-
maxTurns: currentMaxTurns,
|
|
674
|
-
runInBackground: currentBackground,
|
|
675
|
-
},
|
|
676
|
-
graceTurns: currentGraceTurns,
|
|
677
|
-
worktreePath: currentWorktreePath,
|
|
678
|
-
worktreeLabel: currentWorktreePath ? currentWorktreeLabel : undefined,
|
|
679
|
-
};
|
|
680
|
-
|
|
681
|
-
const { state: activityState, callbacks } = createActivityTracker(currentMaxTurns);
|
|
682
|
-
|
|
683
|
-
let agentId: string;
|
|
684
|
-
try {
|
|
685
|
-
agentId = getManager().spawn(piInstance, sessionCtx, resolvedType, prompt, {
|
|
686
|
-
...spawnOptions,
|
|
687
|
-
...callbacks,
|
|
688
|
-
});
|
|
689
|
-
} catch (err) {
|
|
690
|
-
ctx.ui.notify(
|
|
691
|
-
`Spawn failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
692
|
-
"error",
|
|
693
|
-
);
|
|
694
|
-
return; // Return to main menu
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Wire activity tracking for widget
|
|
698
|
-
agentActivity.set(agentId, activityState);
|
|
699
|
-
// Set UI context so widget can render (same as tool_execution_start handler)
|
|
700
|
-
const widget = getWidget();
|
|
701
|
-
if (widget) {
|
|
702
|
-
widget.setUICtx(ctx.ui as unknown as import("./ui/agent-widget.js").UICtx);
|
|
703
|
-
widget.ensureTimer();
|
|
704
|
-
widget.update();
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (currentBackground) {
|
|
708
|
-
backgroundAgentIds.add(agentId);
|
|
709
|
-
return; // Background: return to main menu immediately
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Foreground: block until completion
|
|
713
|
-
const fgRecord = getManager().getRecord(agentId);
|
|
714
|
-
if (fgRecord?.execution?.promise) {
|
|
715
|
-
await fgRecord.execution.promise;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
agentActivity.delete(agentId);
|
|
719
|
-
getWidget()?.markFinished(agentId);
|
|
720
|
-
getWidget()?.update();
|
|
721
|
-
|
|
722
|
-
return; // Return to main menu
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Handle option changes
|
|
726
|
-
if (choice.startsWith("Description")) {
|
|
727
|
-
const input = await ctx.ui.input("Description", description);
|
|
728
|
-
if (input !== undefined && input.trim()) {
|
|
729
|
-
description = input.trim();
|
|
730
|
-
}
|
|
731
|
-
} else if (choice.startsWith("Model")) {
|
|
732
|
-
const chosen = await promptModelSelection(
|
|
733
|
-
ctx, modelOptions, currentModelStr || "(inherits parent)",
|
|
734
|
-
);
|
|
735
|
-
if (chosen !== null) {
|
|
736
|
-
currentModelStr = chosen === "(inherits parent)" ? "" : chosen;
|
|
737
|
-
}
|
|
738
|
-
} else if (choice.startsWith("Thinking")) {
|
|
739
|
-
const allLevels = [...THINKING_LEVELS, "inherit"];
|
|
740
|
-
const chosen = await ctx.ui.select("Thinking level", allLevels);
|
|
741
|
-
if (chosen !== undefined) {
|
|
742
|
-
currentThinking = chosen === "inherit" ? undefined : (chosen as ThinkingLevel);
|
|
743
|
-
}
|
|
744
|
-
} else if (choice.startsWith("Max turns")) {
|
|
745
|
-
const initial = currentMaxTurns != null ? String(currentMaxTurns) : "unlimited";
|
|
746
|
-
const input = await ctx.ui.input("Max turns (number or 'unlimited')", initial);
|
|
747
|
-
if (input !== undefined) {
|
|
748
|
-
const trimmed = input.trim().toLowerCase();
|
|
749
|
-
if (trimmed === "unlimited" || trimmed === "") {
|
|
750
|
-
currentMaxTurns = undefined;
|
|
751
|
-
} else {
|
|
752
|
-
const parsed = parseInt(trimmed, 10);
|
|
753
|
-
if (isNaN(parsed) || parsed < 1) {
|
|
754
|
-
ctx.ui.notify("Invalid value — must be a number ≥ 1 or 'unlimited'", "error");
|
|
755
|
-
} else {
|
|
756
|
-
currentMaxTurns = parsed;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
} else if (choice.startsWith("Grace turns")) {
|
|
761
|
-
const parsed = await parseNumericInput(ctx, "Grace turns (≥ 0)", String(currentGraceTurns ?? 6), 0, "≥ 0");
|
|
762
|
-
if (parsed !== undefined) currentGraceTurns = parsed;
|
|
763
|
-
} else if (choice.startsWith("Background")) {
|
|
764
|
-
currentBackground = !currentBackground;
|
|
765
|
-
} else if (choice.startsWith("Worktree") && inGitRepo) {
|
|
766
|
-
// Open worktree picker
|
|
767
|
-
const worktrees = await listWorktrees(parentCwd);
|
|
768
|
-
if (!worktrees || worktrees.length === 0) {
|
|
769
|
-
ctx.ui.notify(
|
|
770
|
-
"No worktrees found or git worktree list unavailable",
|
|
771
|
-
"error",
|
|
772
|
-
);
|
|
773
|
-
continue; // Return to options sub-menu
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
const pickerItems = [
|
|
777
|
-
"Inherits parent cwd",
|
|
778
|
-
...worktrees.map(wt => {
|
|
779
|
-
const branchLabel = wt.isDetached ? "detached" : (wt.branch ?? "detached");
|
|
780
|
-
const truncPath = truncatePath(wt.path);
|
|
781
|
-
return `${branchLabel} · ${truncPath}`;
|
|
782
|
-
}),
|
|
783
|
-
];
|
|
784
|
-
|
|
785
|
-
const picked = await ctx.ui.select("Select worktree", pickerItems);
|
|
786
|
-
if (picked === undefined) continue; // Escape → return to options sub-menu
|
|
787
|
-
|
|
788
|
-
if (picked === "Inherits parent cwd") {
|
|
789
|
-
currentWorktreePath = undefined;
|
|
790
|
-
currentWorktreeLabel = "Inherits parent cwd";
|
|
791
|
-
} else {
|
|
792
|
-
// Find the matching worktree by index (offset by "Inherits parent cwd")
|
|
793
|
-
const idx = pickerItems.indexOf(picked) - 1;
|
|
794
|
-
if (idx >= 0 && idx < worktrees.length) {
|
|
795
|
-
const wt = worktrees[idx];
|
|
796
|
-
currentWorktreePath = wt.path;
|
|
797
|
-
currentWorktreeLabel = wt.branch ?? "detached";
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
export async function showSettingsMenu(
|
|
805
|
-
ctx: ExtensionCommandContext,
|
|
806
|
-
modelOptions: string[],
|
|
807
|
-
): Promise<void> {
|
|
808
|
-
const menuItems = [
|
|
809
|
-
"1. Model settings — Set global default and per-type model overrides",
|
|
810
|
-
"2. Concurrency settings — Set per-model slot limits",
|
|
811
|
-
"3. Widget settings — Configure widget display options",
|
|
812
|
-
"",
|
|
813
|
-
"Back",
|
|
814
|
-
];
|
|
815
|
-
|
|
816
|
-
const handlers: Record<string, () => Promise<void>> = {
|
|
817
|
-
"1": () => showModelSettingsMenu(ctx, modelOptions),
|
|
818
|
-
"2": () => showConcurrencySettingsMenu(ctx, modelOptions),
|
|
819
|
-
"3": () => showWidgetSettingsMenu(ctx),
|
|
820
|
-
};
|
|
821
|
-
|
|
822
|
-
while (true) {
|
|
823
|
-
const choice = await ctx.ui.select("Settings", menuItems);
|
|
824
|
-
if (choice === undefined || choice === "Back") return;
|
|
825
|
-
|
|
826
|
-
const action = matchMenuChoice(choice, handlers);
|
|
827
|
-
if (action) await action();
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
export async function showAgentsMainMenu(
|
|
832
|
-
ctx: ExtensionCommandContext,
|
|
833
|
-
modelOptions: string[],
|
|
834
|
-
): Promise<void> {
|
|
835
|
-
const menuItems = [
|
|
836
|
-
"1. Running agents — List running/queued agents",
|
|
837
|
-
"2. Spawn agent — Manually spawn a new agent",
|
|
838
|
-
"3. Settings — Model, concurrency, and widget settings",
|
|
839
|
-
"4. Debug — Agent types, briefing, diagnostics",
|
|
840
|
-
"",
|
|
841
|
-
"Press Escape to close",
|
|
842
|
-
];
|
|
843
|
-
|
|
844
|
-
const handlers: Record<string, () => Promise<void>> = {
|
|
845
|
-
"1": () => showRunningAgentsMenu(ctx),
|
|
846
|
-
"2": () => showSpawnAgentMenu(ctx, modelOptions),
|
|
847
|
-
"3": () => showSettingsMenu(ctx, modelOptions),
|
|
848
|
-
"4": () => showDebugMenu(ctx),
|
|
849
|
-
};
|
|
850
|
-
|
|
851
|
-
// Loop so sub-menus navigate back to root; only Escape at root closes
|
|
852
|
-
while (true) {
|
|
853
|
-
const choice = await ctx.ui.select("Subagents Management", menuItems);
|
|
854
|
-
if (choice === undefined || choice === "Press Escape to close") return;
|
|
855
|
-
|
|
856
|
-
const action = matchMenuChoice(choice, handlers);
|
|
857
|
-
if (action) await action();
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
async function showDebugMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
862
|
-
const menuItems = [
|
|
863
|
-
"1. Agent types — List available agent types and their configs",
|
|
864
|
-
"2. Agent briefing — Send agent types/capabilities info to LLM (Optional, if having issues)",
|
|
865
|
-
];
|
|
866
|
-
|
|
867
|
-
const handlers: Record<string, () => Promise<void>> = {
|
|
868
|
-
"1": () => showAgentTypes(ctx),
|
|
869
|
-
"2": () => handleAgentBriefing(ctx),
|
|
870
|
-
};
|
|
871
|
-
|
|
872
|
-
while (true) {
|
|
873
|
-
const choice = await ctx.ui.select("Debug", menuItems);
|
|
874
|
-
if (choice === undefined) return;
|
|
875
|
-
|
|
876
|
-
const action = matchMenuChoice(choice, handlers);
|
|
877
|
-
if (action) await action();
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
export async function showWidgetSettingsMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
882
|
-
return runMenuLoop(ctx, "Widget Settings", () => {
|
|
883
|
-
const items: string[] = [];
|
|
884
|
-
const actions: Array<() => Promise<void>> = [];
|
|
885
|
-
|
|
886
|
-
// Force compact mode toggle
|
|
887
|
-
const isForceCompact = __config.agent.widgetCompact === true;
|
|
888
|
-
items.push(`Force compact mode · ${isForceCompact ? "ON" : "OFF"}`);
|
|
889
|
-
actions.push(async () => {
|
|
890
|
-
setWidgetCompact(!isForceCompact);
|
|
891
|
-
ctx.ui.notify(`Force compact mode ${__config.agent.widgetCompact ? "ON" : "OFF"}`, "info");
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
// Max lines (full mode)
|
|
895
|
-
const maxLines = __config.agent.widgetMaxLines ?? 12;
|
|
896
|
-
items.push(`Max lines (full) · ${maxLines}`);
|
|
897
|
-
actions.push(async () => {
|
|
898
|
-
const parsed = await parseNumericInput(ctx, "Max lines (full mode, ≥ 2)", String(maxLines), 2, "≥ 2");
|
|
899
|
-
if (parsed === undefined) return;
|
|
900
|
-
setWidgetMaxLines(parsed);
|
|
901
|
-
ctx.ui.notify(`Max lines (full) set to ${parsed}`, "info");
|
|
902
|
-
});
|
|
903
|
-
|
|
904
|
-
// Max lines (compact mode)
|
|
905
|
-
const maxLinesCompact = __config.agent.widgetMaxLinesCompact ?? Math.floor(maxLines / 2);
|
|
906
|
-
items.push(`Max lines (compact) · ${maxLinesCompact}`);
|
|
907
|
-
actions.push(async () => {
|
|
908
|
-
const parsed = await parseNumericInput(ctx, "Max lines (compact mode, ≥ 1)", String(maxLinesCompact), 1, "≥ 1");
|
|
909
|
-
if (parsed === undefined) return;
|
|
910
|
-
setWidgetMaxLinesCompact(parsed);
|
|
911
|
-
ctx.ui.notify(`Max lines (compact) set to ${parsed}`, "info");
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
// Ctrl+o shortcut toggle
|
|
915
|
-
const shortcutEnabled = __config.agent.widgetShortcut === true;
|
|
916
|
-
items.push(`Ctrl+o shortcut · ${shortcutEnabled ? "ON" : "OFF"}`);
|
|
917
|
-
actions.push(async () => {
|
|
918
|
-
setWidgetShortcut(!shortcutEnabled);
|
|
919
|
-
ctx.ui.notify(`Ctrl+o shortcut ${__config.agent.widgetShortcut ? "ON" : "OFF"}`, "info");
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
return { items, actions };
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void> {
|
|
927
|
-
const types = getAvailableTypes();
|
|
928
|
-
const agents = types.map((t) => ({ name: t, config: getAgentConfig(t) }));
|
|
929
|
-
|
|
930
|
-
const lines: string[] = [
|
|
931
|
-
"# Agent Types and Capabilities\n",
|
|
932
|
-
"The following agent types are available. Use the `agent` parameter to select one.\n",
|
|
933
|
-
];
|
|
934
|
-
|
|
935
|
-
for (const { name, config } of agents) {
|
|
936
|
-
if (!config) continue;
|
|
937
|
-
lines.push(`## ${config.displayName ?? name}`);
|
|
938
|
-
lines.push(config.description);
|
|
939
|
-
lines.push("");
|
|
940
|
-
|
|
941
|
-
if (config.registeredTools) {
|
|
942
|
-
lines.push(`**Tools:** ${config.registeredTools.join(", ")}`);
|
|
943
|
-
}
|
|
944
|
-
if (config.model) {
|
|
945
|
-
lines.push(`**Default model:** ${config.model}`);
|
|
946
|
-
}
|
|
947
|
-
if (config.maxTurns) {
|
|
948
|
-
lines.push(`**Max turns:** ${config.maxTurns}`);
|
|
949
|
-
}
|
|
950
|
-
lines.push("");
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
// Parameter descriptions
|
|
954
|
-
lines.push("## Agent Tool Parameters\n");
|
|
955
|
-
lines.push("| Parameter | Description |");
|
|
956
|
-
lines.push("|-----------|-------------|");
|
|
957
|
-
lines.push("| `prompt` | The task for the agent (required) |");
|
|
958
|
-
lines.push("| `description` | One-line summary of what the agent should do (required) |");
|
|
959
|
-
lines.push("| `agent` | Which agent type to use (default: general-purpose) |");
|
|
960
|
-
lines.push("| `thinking` | Optional thinking mode override (e.g., `off`, `minimal`, `low`, `medium`, `high`, `xhigh`) |");
|
|
961
|
-
lines.push("| `run_in_background` | When `true`, result is auto-delivered — do NOT poll. Continue working while waiting. |");
|
|
962
|
-
lines.push("| `worktree_path` | Optional path to a git worktree of the parent's repo. See below for details. |");
|
|
963
|
-
lines.push("");
|
|
964
|
-
|
|
965
|
-
// Usage guidelines
|
|
966
|
-
lines.push("## Usage Guidelines\n");
|
|
967
|
-
lines.push("- Agents start fresh with their config — they do NOT inherit the parent conversation");
|
|
968
|
-
lines.push("- For parallel tasks, spawn multiple `run_in_background: true` agents in one turn");
|
|
969
|
-
lines.push(" → Results are auto-delivered — do NOT poll, the result will arrive when ready");
|
|
970
|
-
lines.push("");
|
|
971
|
-
lines.push("## `worktree_path` Parameter\n");
|
|
972
|
-
lines.push("Use `worktree_path` to run a subagent in a different git worktree of the parent's repository.");
|
|
973
|
-
lines.push("");
|
|
974
|
-
lines.push("- **Optional.** Omit to run the subagent in the parent's working directory (default behavior).");
|
|
975
|
-
lines.push("- **Must be a path** inside a git worktree of the parent's repo, including the main checkout. Not a different repo, not a non-git directory.");
|
|
976
|
-
lines.push("- **Relative paths** are resolved against the parent's working directory.");
|
|
977
|
-
lines.push("- **On failure** the validator returns a specific reason (e.g., 'not a worktree of the parent's repository', 'path does not exist') — use this to self-correct.");
|
|
978
|
-
lines.push("- **Agent type discovery:** The worktree's `.pi/agents/` directory is scanned for agent types when this param is set, so worktree-local types become available to that spawn.");
|
|
979
|
-
piInstance.sendUserMessage(lines.join("\n"));
|
|
980
|
-
ctx.ui.notify("Agent briefing sent to LLM", "info");
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Build a sub-menu for a single per-provider or per-model entry:
|
|
985
|
-
* "Edit limit" to change the value, or "Remove limit" to delete it.
|
|
986
|
-
* Callers pass setter callbacks that handle save + sync internally.
|
|
987
|
-
*/
|
|
988
|
-
async function editOrRemoveConcurrencyEntry(
|
|
989
|
-
ctx: ExtensionCommandContext,
|
|
990
|
-
label: string,
|
|
991
|
-
entityType: "provider" | "model",
|
|
992
|
-
entityKey: string,
|
|
993
|
-
currentValue: number,
|
|
994
|
-
setEntry: (key: string, value: number) => void,
|
|
995
|
-
removeEntry: () => void,
|
|
996
|
-
): Promise<void> {
|
|
997
|
-
await runMenu(ctx, `${entityKey} concurrency`, [
|
|
998
|
-
"Edit limit",
|
|
999
|
-
"Remove limit",
|
|
1000
|
-
], [
|
|
1001
|
-
async () => {
|
|
1002
|
-
await promptConcurrencyInput(
|
|
1003
|
-
ctx, entityKey, currentValue,
|
|
1004
|
-
(value) => setEntry(entityKey, value),
|
|
1005
|
-
);
|
|
1006
|
-
},
|
|
1007
|
-
async () => {
|
|
1008
|
-
removeEntry();
|
|
1009
|
-
ctx.ui.notify(
|
|
1010
|
-
`Removed per-${entityType} limit for ${entityKey}`,
|
|
1011
|
-
"info",
|
|
1012
|
-
);
|
|
1013
|
-
},
|
|
1014
|
-
]);
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
export async function showConcurrencySettingsMenu(
|
|
1018
|
-
ctx: ExtensionCommandContext,
|
|
1019
|
-
modelOptions: string[],
|
|
1020
|
-
): Promise<void> {
|
|
1021
|
-
const providers = [...new Set(modelOptions.map((m) => m.split("/")[0]))].sort();
|
|
1022
|
-
|
|
1023
|
-
return runMenuLoop(ctx, "Concurrency Settings", () => {
|
|
1024
|
-
const items: string[] = [];
|
|
1025
|
-
const actions: Array<() => Promise<void>> = [];
|
|
1026
|
-
|
|
1027
|
-
// Global default
|
|
1028
|
-
items.push(`Default concurrency limit · ${__config.concurrency.default}`);
|
|
1029
|
-
actions.push(async () => {
|
|
1030
|
-
await promptConcurrencyInput(
|
|
1031
|
-
ctx, "Default limit", __config.concurrency.default,
|
|
1032
|
-
(value) => setConcurrencyDefault(value),
|
|
1033
|
-
);
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
// Reset all to defaults
|
|
1037
|
-
items.push("Reset all to defaults");
|
|
1038
|
-
actions.push(async () => {
|
|
1039
|
-
resetConcurrency();
|
|
1040
|
-
ctx.ui.notify("Concurrency reset to defaults", "info");
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
// ── Per-provider limits ──
|
|
1044
|
-
const providerLimits = __config.concurrency.providers ?? {};
|
|
1045
|
-
const configuredProviders = Object.keys(providerLimits);
|
|
1046
|
-
if (configuredProviders.length > 0) {
|
|
1047
|
-
items.push("");
|
|
1048
|
-
actions.push(async () => {});
|
|
1049
|
-
items.push("─── per-provider limits ───");
|
|
1050
|
-
actions.push(async () => {}); // separator
|
|
1051
|
-
|
|
1052
|
-
for (const provider of configuredProviders) {
|
|
1053
|
-
const limit = providerLimits[provider];
|
|
1054
|
-
items.push(`${provider} · ${limit} slots`);
|
|
1055
|
-
actions.push(async () => {
|
|
1056
|
-
await editOrRemoveConcurrencyEntry(
|
|
1057
|
-
ctx,
|
|
1058
|
-
`Concurrency slots for ${provider}`,
|
|
1059
|
-
"provider",
|
|
1060
|
-
provider,
|
|
1061
|
-
limit,
|
|
1062
|
-
(key, value) => setConcurrencyProvider(key, value),
|
|
1063
|
-
() => removeConcurrencyProvider(provider),
|
|
1064
|
-
);
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// Add per-provider limit
|
|
1070
|
-
items.push("Add per-provider limit...");
|
|
1071
|
-
actions.push(async () => {
|
|
1072
|
-
const provider = await ctx.ui.select("Select provider", providers);
|
|
1073
|
-
if (provider === undefined) return;
|
|
1074
|
-
await promptAddConcurrencyLimit(
|
|
1075
|
-
ctx, provider,
|
|
1076
|
-
(key, value) => setConcurrencyProvider(key, value),
|
|
1077
|
-
);
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
// ── Per-model limits ──
|
|
1081
|
-
const models = __config.concurrency.models ?? {};
|
|
1082
|
-
const modelKeys = Object.keys(models);
|
|
1083
|
-
if (modelKeys.length > 0) {
|
|
1084
|
-
items.push("");
|
|
1085
|
-
actions.push(async () => {});
|
|
1086
|
-
items.push("─── per-model limits ───");
|
|
1087
|
-
actions.push(async () => {}); // separator
|
|
1088
|
-
|
|
1089
|
-
for (const modelKey of modelKeys) {
|
|
1090
|
-
const limit = models[modelKey];
|
|
1091
|
-
items.push(`${modelKey} · ${limit} slots`);
|
|
1092
|
-
actions.push(async () => {
|
|
1093
|
-
await editOrRemoveConcurrencyEntry(
|
|
1094
|
-
ctx,
|
|
1095
|
-
`Concurrency slots for ${modelKey}`,
|
|
1096
|
-
"model",
|
|
1097
|
-
modelKey,
|
|
1098
|
-
limit,
|
|
1099
|
-
(key, value) => setConcurrencyModel(key, value),
|
|
1100
|
-
() => removeConcurrencyModel(modelKey),
|
|
1101
|
-
);
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
// Add per-model limit
|
|
1107
|
-
items.push("Add per-model limit...");
|
|
1108
|
-
actions.push(async () => {
|
|
1109
|
-
const modelKey = await promptModelSelection(
|
|
1110
|
-
ctx, modelOptions, __config.agent.default ?? "(inherits parent)",
|
|
1111
|
-
);
|
|
1112
|
-
if (modelKey === null) return;
|
|
1113
|
-
await promptAddConcurrencyLimit(
|
|
1114
|
-
ctx, modelKey.trim(),
|
|
1115
|
-
(key, value) => setConcurrencyModel(key, value),
|
|
1116
|
-
);
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
return { items, actions };
|
|
1120
|
-
});
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
async function showRunningAgentsMenu(
|
|
1124
|
-
ctx: ExtensionCommandContext,
|
|
1125
|
-
): Promise<void> {
|
|
1126
|
-
const records = getManager()?.listAgents() ?? [];
|
|
1127
|
-
if (records.length === 0) {
|
|
1128
|
-
ctx.ui.notify("No agents have been spawned this session", "info");
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
return runMenuLoop(ctx, "Running Agents", () => {
|
|
1133
|
-
const records = getManager()?.listAgents() ?? [];
|
|
1134
|
-
const running = records.filter((r) => r.lifecycle.status === "running" || r.lifecycle.status === "queued");
|
|
1135
|
-
|
|
1136
|
-
const items: string[] = [];
|
|
1137
|
-
const actions: Array<() => Promise<void>> = [];
|
|
1138
|
-
|
|
1139
|
-
for (const record of records) {
|
|
1140
|
-
const elapsed = Math.round((Date.now() - record.lifecycle.startedAt) / 1000);
|
|
1141
|
-
const statusIcon = record.lifecycle.status === "running" ? "▶" :
|
|
1142
|
-
record.lifecycle.status === "completed" ? "✓" :
|
|
1143
|
-
record.lifecycle.status === "queued" ? "⏳" :
|
|
1144
|
-
record.lifecycle.status === "error" ? "✗" : "•";
|
|
1145
|
-
const headline = record.display.description
|
|
1146
|
-
? (record.display.description.length > 50 ? record.display.description.slice(0, 47) + "..." : record.display.description)
|
|
1147
|
-
: "";
|
|
1148
|
-
const suffix = headline ? ` — ${headline}` : "";
|
|
1149
|
-
items.push(
|
|
1150
|
-
`${statusIcon} ${record.id.slice(0, SHORT_ID_LENGTH)} ${record.display.type} ${record.lifecycle.status} ${elapsed}s${suffix}`,
|
|
1151
|
-
);
|
|
1152
|
-
|
|
1153
|
-
actions.push(async () => {
|
|
1154
|
-
await showAgentActions(ctx, record);
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
if (running.length > 0) {
|
|
1159
|
-
items.push("");
|
|
1160
|
-
actions.push(async () => {});
|
|
1161
|
-
items.push("─── actions ───");
|
|
1162
|
-
actions.push(async () => {}); // separator
|
|
1163
|
-
|
|
1164
|
-
items.push(`Stop ${running.length} running agent(s)`);
|
|
1165
|
-
actions.push(async () => {
|
|
1166
|
-
for (const record of running) {
|
|
1167
|
-
getManager()?.abort(record.id);
|
|
1168
|
-
}
|
|
1169
|
-
ctx.ui.notify(`Stopped ${running.length} agent(s)`, "info");
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
return { items, actions };
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
/**
|
|
1178
|
-
* Show a ResultViewer for an agent's result, error, or snapshot.
|
|
1179
|
-
* @param kind — "result", "error", or "snapshot" — used for the title suffix
|
|
1180
|
-
*/
|
|
1181
|
-
async function showResultViewer(
|
|
1182
|
-
ctx: ExtensionCommandContext,
|
|
1183
|
-
record: AgentRecord,
|
|
1184
|
-
kind: "result" | "error" | "snapshot",
|
|
1185
|
-
text: string,
|
|
1186
|
-
): Promise<void> {
|
|
1187
|
-
const titleSuffix = kind === "result"
|
|
1188
|
-
? record.id.slice(0, SHORT_ID_LENGTH)
|
|
1189
|
-
: kind === "snapshot"
|
|
1190
|
-
? `snapshot \u00b7 ${record.id.slice(0, SHORT_ID_LENGTH)}`
|
|
1191
|
-
: "Error";
|
|
1192
|
-
const stats: ResultViewerStats = {
|
|
1193
|
-
lifetimeUsage: record.stats.lifetimeUsage,
|
|
1194
|
-
turnCount: record.stats.turnCount,
|
|
1195
|
-
durationMs: (record.lifecycle.completedAt ?? Date.now()) - record.lifecycle.startedAt,
|
|
1196
|
-
};
|
|
1197
|
-
const refreshCallback =
|
|
1198
|
-
kind === "snapshot" && record.execution.session
|
|
1199
|
-
? () => buildSnapshotMarkdown(record.execution.session!.messages)
|
|
1200
|
-
: undefined;
|
|
1201
|
-
|
|
1202
|
-
await ctx.ui.custom<void>(
|
|
1203
|
-
(tui, theme, _kb, done) =>
|
|
1204
|
-
new ResultViewer(
|
|
1205
|
-
`${getDisplayName(record.display.type)} · ${titleSuffix}`,
|
|
1206
|
-
text,
|
|
1207
|
-
{ onClose: () => done(), onRefresh: refreshCallback },
|
|
1208
|
-
theme,
|
|
1209
|
-
tui.terminal.rows,
|
|
1210
|
-
stats,
|
|
1211
|
-
),
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
/**
|
|
1216
|
-
* Send a steer message to a specific agent. Used by the per-agent action menu.
|
|
1217
|
-
*/
|
|
1218
|
-
async function steerAgentById(
|
|
1219
|
-
agentId: string,
|
|
1220
|
-
ctx: ExtensionCommandContext,
|
|
1221
|
-
): Promise<void> {
|
|
1222
|
-
const record = getManager()?.getRecord(agentId);
|
|
1223
|
-
if (!record) {
|
|
1224
|
-
ctx.ui.notify("Agent not found", "error");
|
|
1225
|
-
return;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
const message = await ctx.ui.input(`Steer ${record.display.type}`);
|
|
1229
|
-
if (!message?.trim()) return;
|
|
1230
|
-
|
|
1231
|
-
const sent = await getManager().steer(agentId, message.trim());
|
|
1232
|
-
if (sent) {
|
|
1233
|
-
ctx.ui.notify(`Steer sent to ${record.id.slice(0, SHORT_ID_LENGTH)}…`, "info");
|
|
1234
|
-
} else {
|
|
1235
|
-
ctx.ui.notify(`Steer failed for ${record.id.slice(0, SHORT_ID_LENGTH)}`, "error");
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
/**
|
|
1240
|
-
* Sub-menu with actions for a single agent. Replaces the old showAgentDetail
|
|
1241
|
-
* notify popup — clicking an agent in the running agents menu opens actions.
|
|
1242
|
-
*/
|
|
1243
|
-
export async function showAgentActions(
|
|
1244
|
-
ctx: ExtensionCommandContext,
|
|
1245
|
-
record: AgentRecord,
|
|
1246
|
-
): Promise<void> {
|
|
1247
|
-
const items: string[] = [];
|
|
1248
|
-
const actions: Array<() => Promise<void>> = [];
|
|
1249
|
-
|
|
1250
|
-
const isRunning = record.lifecycle.status === "running" || record.lifecycle.status === "queued";
|
|
1251
|
-
const hasSession = !!record.execution.session;
|
|
1252
|
-
const hasResult = !!record.result && record.result.length > 0;
|
|
1253
|
-
const hasError = !!record.error && record.error.length > 0;
|
|
1254
|
-
|
|
1255
|
-
// View actions first
|
|
1256
|
-
if (record.lifecycle.status === "running" && hasSession) {
|
|
1257
|
-
items.push("View snapshot");
|
|
1258
|
-
actions.push(async () => {
|
|
1259
|
-
const messages = record.execution.session!.messages;
|
|
1260
|
-
const markdown = buildSnapshotMarkdown(messages);
|
|
1261
|
-
await showResultViewer(ctx, record, "snapshot", markdown);
|
|
1262
|
-
});
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
if (hasResult) {
|
|
1266
|
-
items.push("View result");
|
|
1267
|
-
actions.push(async () => {
|
|
1268
|
-
await showResultViewer(ctx, record, "result", record.result!);
|
|
1269
|
-
});
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
if (hasError) {
|
|
1273
|
-
items.push("View error");
|
|
1274
|
-
actions.push(async () => {
|
|
1275
|
-
await showResultViewer(ctx, record, "error", record.error!);
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
// Then control actions
|
|
1280
|
-
if (isRunning) {
|
|
1281
|
-
items.push("Steer");
|
|
1282
|
-
actions.push(async () => {
|
|
1283
|
-
await steerAgentById(record.id, ctx);
|
|
1284
|
-
});
|
|
1285
|
-
|
|
1286
|
-
items.push("Stop");
|
|
1287
|
-
actions.push(async () => {
|
|
1288
|
-
getManager()?.abort(record.id);
|
|
1289
|
-
ctx.ui.notify(`Stopped ${record.id.slice(0, SHORT_ID_LENGTH)}`, "info");
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
if (items.length === 0) {
|
|
1294
|
-
ctx.ui.notify(`Agent ${record.id.slice(0, SHORT_ID_LENGTH)} — no actions available`, "info");
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
// Append blank spacer + "Back" as the last items
|
|
1299
|
-
items.push("");
|
|
1300
|
-
actions.push(async () => {});
|
|
1301
|
-
items.push("Back");
|
|
1302
|
-
actions.push(async () => {});
|
|
1303
|
-
|
|
1304
|
-
await runMenu(ctx, `Agent ${record.id.slice(0, SHORT_ID_LENGTH)}`, items, actions);
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
async function showAgentTypes(ctx: ExtensionCommandContext): Promise<void> {
|
|
1308
|
-
const types = getAllTypes();
|
|
1309
|
-
if (types.length === 0) {
|
|
1310
|
-
ctx.ui.notify("No agent types available", "info");
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
const lines: string[] = ["Available agent types:\n"];
|
|
1315
|
-
for (const name of types) {
|
|
1316
|
-
const cfg = getAgentConfig(name);
|
|
1317
|
-
if (!cfg) continue;
|
|
1318
|
-
const hidden = cfg.hidden === true ? " [HIDDEN]" : "";
|
|
1319
|
-
const model = cfg.model ? ` Model: ${cfg.model}` : "";
|
|
1320
|
-
const tools = cfg.registeredTools
|
|
1321
|
-
? ` Tools: ${cfg.registeredTools.join(", ")}`
|
|
1322
|
-
: " Tools: all built-in tools";
|
|
1323
|
-
const source = cfg.source ? ` Source: ${cfg.source}` : "";
|
|
1324
|
-
lines.push(` ${name}${hidden}`);
|
|
1325
|
-
lines.push(` ${cfg.description}`);
|
|
1326
|
-
if (model) lines.push(model);
|
|
1327
|
-
lines.push(tools);
|
|
1328
|
-
if (source) lines.push(source);
|
|
1329
|
-
lines.push("");
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
1333
|
-
}
|