pi-subagents-lite 1.0.2 → 1.2.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 +47 -5
- package/package.json +1 -1
- package/src/agent-manager.ts +88 -62
- package/src/agent-runner.ts +194 -167
- package/src/agent-types.ts +21 -1
- package/src/config-io.ts +9 -1
- package/src/config-mutator.ts +183 -0
- package/src/context.ts +1 -1
- package/src/format.ts +173 -0
- package/src/index.ts +127 -177
- package/src/menus.ts +586 -137
- package/src/model-precedence.ts +5 -0
- package/src/output-file.ts +1 -68
- package/src/renderer.ts +163 -0
- package/src/result-viewer.ts +2 -1
- package/src/state.ts +83 -0
- package/src/tool-execution.ts +179 -56
- package/src/types.ts +104 -31
- package/src/ui/agent-widget.ts +159 -146
- package/src/usage.ts +5 -0
- package/src/worktree-validator.ts +199 -0
- package/src/stop-agent-tool.ts +0 -77
package/src/menus.ts
CHANGED
|
@@ -2,27 +2,51 @@
|
|
|
2
2
|
* menus.ts — /agents command menu system.
|
|
3
3
|
*
|
|
4
4
|
* All menu-related functions extracted from index.ts.
|
|
5
|
-
* Imports shared state (config, manager, piInstance) from
|
|
5
|
+
* Imports shared state (config, manager, piInstance) from state.ts.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { getAgentConfig, getAvailableTypes, getAllTypes } from "./agent-types.js";
|
|
10
|
-
import type { AgentRecord } from "./types.js";
|
|
11
|
-
import { SHORT_ID_LENGTH } from "./types.js";
|
|
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";
|
|
12
13
|
import { ModelSelectorDialog, type ModelOption } from "./model-selector.js";
|
|
13
14
|
import { ResultViewer, type ResultViewerStats } from "./result-viewer.js";
|
|
14
|
-
import { getDisplayName } from "./
|
|
15
|
+
import { getDisplayName } from "./format.js";
|
|
15
16
|
import { buildSnapshotMarkdown } from "./context.js";
|
|
16
17
|
|
|
17
|
-
import { parseModelKey } from "./utils.js";
|
|
18
|
+
import { parseModelKey, findModelInRegistry } from "./utils.js";
|
|
18
19
|
import {
|
|
19
20
|
__config,
|
|
20
21
|
sessionOverrides,
|
|
21
|
-
manager,
|
|
22
22
|
piInstance,
|
|
23
|
-
|
|
23
|
+
sessionCtx,
|
|
24
|
+
agentActivity,
|
|
25
|
+
getManager,
|
|
26
|
+
getWidget,
|
|
27
|
+
} from "./state.js";
|
|
24
28
|
import { resolveModel } from "./model-precedence.js";
|
|
25
|
-
import {
|
|
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";
|
|
26
50
|
|
|
27
51
|
// ============================================================================
|
|
28
52
|
// Helpers
|
|
@@ -113,45 +137,50 @@ async function applyModelOverride(
|
|
|
113
137
|
}
|
|
114
138
|
|
|
115
139
|
/**
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
function applyConcurrencyConfig(): void {
|
|
119
|
-
saveConfigAtomic(__config);
|
|
120
|
-
manager?.setConcurrency(__config.concurrency);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Parse a concurrency input: prompt, validate (integer ≥ 1), return parsed value or undefined.
|
|
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.
|
|
125
142
|
*/
|
|
126
|
-
async function
|
|
143
|
+
async function parseNumericInput(
|
|
127
144
|
ctx: ExtensionCommandContext,
|
|
128
145
|
label: string,
|
|
129
146
|
initialValue: string,
|
|
147
|
+
min: number,
|
|
148
|
+
minLabel: string,
|
|
130
149
|
): Promise<number | undefined> {
|
|
131
150
|
const input = await ctx.ui.input(label, initialValue);
|
|
132
151
|
if (input === undefined) return undefined;
|
|
133
152
|
const parsed = parseInt(input.trim(), 10);
|
|
134
|
-
if (isNaN(parsed) || parsed <
|
|
135
|
-
ctx.ui.notify(
|
|
153
|
+
if (isNaN(parsed) || parsed < min) {
|
|
154
|
+
ctx.ui.notify(`Invalid value — must be a number ${minLabel}`, "error");
|
|
136
155
|
return undefined;
|
|
137
156
|
}
|
|
138
157
|
return parsed;
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
/**
|
|
142
|
-
*
|
|
143
|
-
|
|
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.
|
|
144
174
|
*/
|
|
145
175
|
async function promptConcurrencyInput(
|
|
146
176
|
ctx: ExtensionCommandContext,
|
|
147
177
|
label: string,
|
|
148
178
|
currentValue: number,
|
|
149
|
-
|
|
179
|
+
setter: (value: number) => void,
|
|
150
180
|
): Promise<void> {
|
|
151
181
|
const parsed = await parseConcurrencyInput(ctx, label, String(currentValue));
|
|
152
182
|
if (parsed === undefined) return;
|
|
153
|
-
|
|
154
|
-
applyConcurrencyConfig();
|
|
183
|
+
setter(parsed);
|
|
155
184
|
ctx.ui.notify(
|
|
156
185
|
`${label.replace("Concurrency slots for ", "")} concurrency set to ${parsed}`,
|
|
157
186
|
"info",
|
|
@@ -160,16 +189,16 @@ async function promptConcurrencyInput(
|
|
|
160
189
|
|
|
161
190
|
/**
|
|
162
191
|
* Prompt to add a new concurrency limit for a named entity.
|
|
192
|
+
* Calls the setter which handles save + sync internally.
|
|
163
193
|
*/
|
|
164
194
|
async function promptAddConcurrencyLimit(
|
|
165
195
|
ctx: ExtensionCommandContext,
|
|
166
196
|
label: string,
|
|
167
|
-
|
|
197
|
+
setter: (key: string, value: number) => void,
|
|
168
198
|
): Promise<void> {
|
|
169
199
|
const parsed = await parseConcurrencyInput(ctx, "Concurrency slots", "1");
|
|
170
200
|
if (parsed === undefined) return;
|
|
171
|
-
|
|
172
|
-
applyConcurrencyConfig();
|
|
201
|
+
setter(label, parsed);
|
|
173
202
|
ctx.ui.notify(`${label} concurrency set to ${parsed}`, "info");
|
|
174
203
|
}
|
|
175
204
|
|
|
@@ -218,6 +247,96 @@ async function runMenuLoop(
|
|
|
218
247
|
}
|
|
219
248
|
}
|
|
220
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
|
+
|
|
221
340
|
// ============================================================================
|
|
222
341
|
// /agents command handler
|
|
223
342
|
// ============================================================================
|
|
@@ -246,13 +365,12 @@ export async function showModelSettingsMenu(
|
|
|
246
365
|
|
|
247
366
|
// Handle "clear" — remove all overrides (session + config) and save
|
|
248
367
|
if (mode === "clear") {
|
|
249
|
-
|
|
368
|
+
clearModelOverride(targetKey);
|
|
250
369
|
if (targetKey !== "default") {
|
|
251
370
|
delete sessionOverrides[targetKey];
|
|
252
371
|
} else {
|
|
253
372
|
sessionOverrides.default = null;
|
|
254
373
|
}
|
|
255
|
-
saveConfigAtomic(__config);
|
|
256
374
|
ctx.ui.notify(`${label} overrides cleared`, "info");
|
|
257
375
|
return;
|
|
258
376
|
}
|
|
@@ -264,12 +382,9 @@ export async function showModelSettingsMenu(
|
|
|
264
382
|
isSession
|
|
265
383
|
? (chosen) => { sessionOverrides[targetKey] = chosen; }
|
|
266
384
|
: (chosen) => {
|
|
267
|
-
|
|
385
|
+
setModelOverride(targetKey, chosen);
|
|
268
386
|
},
|
|
269
387
|
);
|
|
270
|
-
if (!isSession) {
|
|
271
|
-
saveConfigAtomic(__config);
|
|
272
|
-
}
|
|
273
388
|
};
|
|
274
389
|
|
|
275
390
|
// Global default — show session value if present
|
|
@@ -293,31 +408,28 @@ export async function showModelSettingsMenu(
|
|
|
293
408
|
: "Force background · OFF";
|
|
294
409
|
items.push(forceBgLabel);
|
|
295
410
|
actions.push(async () => {
|
|
296
|
-
|
|
297
|
-
saveConfigAtomic(__config);
|
|
411
|
+
setForceBackground(!__config.agent.forceBackground);
|
|
298
412
|
ctx.ui.notify(
|
|
299
413
|
`Force background ${__config.agent.forceBackground ? "ON" : "OFF"}`,
|
|
300
414
|
"info",
|
|
301
415
|
);
|
|
302
416
|
});
|
|
303
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
|
+
|
|
304
426
|
// Grace turns setting
|
|
305
427
|
const graceTurns = __config.agent.graceTurns ?? 6;
|
|
306
428
|
items.push(`Grace turns · ${graceTurns}`);
|
|
307
429
|
actions.push(async () => {
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
if (isNaN(parsed)) {
|
|
312
|
-
ctx.ui.notify("Invalid value — must be a number", "error");
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if (parsed < 0) {
|
|
316
|
-
ctx.ui.notify("Invalid value — must be ≥ 0", "error");
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
__config.agent.graceTurns = parsed;
|
|
320
|
-
saveConfigAtomic(__config);
|
|
430
|
+
const parsed = await parseNumericInput(ctx, "Grace turns (≥ 0)", String(graceTurns), 0, "≥ 0");
|
|
431
|
+
if (parsed === undefined) return;
|
|
432
|
+
setGraceTurns(parsed);
|
|
321
433
|
ctx.ui.notify(`Grace turns set to ${parsed}`, "info");
|
|
322
434
|
});
|
|
323
435
|
|
|
@@ -395,21 +507,13 @@ export async function showModelSettingsMenu(
|
|
|
395
507
|
items.push("Clear all overrides");
|
|
396
508
|
actions.push(async () => {
|
|
397
509
|
const hasOverrides = Object.entries(__config.agent).some(
|
|
398
|
-
([k, v]) => k
|
|
510
|
+
([k, v]) => !CONFIG_AGENT_NON_MODEL_KEYS.includes(k) && v != null,
|
|
399
511
|
);
|
|
400
512
|
if (!hasOverrides && __config.agent.default === null) {
|
|
401
513
|
ctx.ui.notify("No overrides to clear", "info");
|
|
402
514
|
return;
|
|
403
515
|
}
|
|
404
|
-
|
|
405
|
-
default: __config.agent.default,
|
|
406
|
-
forceBackground: __config.agent.forceBackground,
|
|
407
|
-
};
|
|
408
|
-
if (__config.agent.graceTurns != null) {
|
|
409
|
-
preserved.graceTurns = __config.agent.graceTurns;
|
|
410
|
-
}
|
|
411
|
-
__config.agent = preserved as typeof __config.agent;
|
|
412
|
-
saveConfigAtomic(__config);
|
|
516
|
+
clearAllModelOverrides();
|
|
413
517
|
ctx.ui.notify("All model overrides cleared", "info");
|
|
414
518
|
});
|
|
415
519
|
|
|
@@ -417,33 +521,340 @@ export async function showModelSettingsMenu(
|
|
|
417
521
|
});
|
|
418
522
|
}
|
|
419
523
|
|
|
420
|
-
|
|
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(
|
|
421
805
|
ctx: ExtensionCommandContext,
|
|
422
806
|
modelOptions: string[],
|
|
423
807
|
): Promise<void> {
|
|
424
808
|
const menuItems = [
|
|
425
809
|
"1. Model settings — Set global default and per-type model overrides",
|
|
426
810
|
"2. Concurrency settings — Set per-model slot limits",
|
|
427
|
-
"3.
|
|
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",
|
|
428
839
|
"4. Debug — Agent types, briefing, diagnostics",
|
|
429
840
|
"",
|
|
430
841
|
"Press Escape to close",
|
|
431
842
|
];
|
|
432
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
|
+
|
|
433
851
|
// Loop so sub-menus navigate back to root; only Escape at root closes
|
|
434
852
|
while (true) {
|
|
435
853
|
const choice = await ctx.ui.select("Subagents Management", menuItems);
|
|
436
854
|
if (choice === undefined || choice === "Press Escape to close") return;
|
|
437
855
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
} else if (choice.startsWith("2.")) {
|
|
441
|
-
await showConcurrencySettingsMenu(ctx, modelOptions);
|
|
442
|
-
} else if (choice.startsWith("3.")) {
|
|
443
|
-
await showRunningAgentsMenu(ctx);
|
|
444
|
-
} else if (choice.startsWith("4.")) {
|
|
445
|
-
await showDebugMenu(ctx);
|
|
446
|
-
}
|
|
856
|
+
const action = matchMenuChoice(choice, handlers);
|
|
857
|
+
if (action) await action();
|
|
447
858
|
}
|
|
448
859
|
}
|
|
449
860
|
|
|
@@ -453,18 +864,65 @@ async function showDebugMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
453
864
|
"2. Agent briefing — Send agent types/capabilities info to LLM (Optional, if having issues)",
|
|
454
865
|
];
|
|
455
866
|
|
|
867
|
+
const handlers: Record<string, () => Promise<void>> = {
|
|
868
|
+
"1": () => showAgentTypes(ctx),
|
|
869
|
+
"2": () => handleAgentBriefing(ctx),
|
|
870
|
+
};
|
|
871
|
+
|
|
456
872
|
while (true) {
|
|
457
873
|
const choice = await ctx.ui.select("Debug", menuItems);
|
|
458
874
|
if (choice === undefined) return;
|
|
459
875
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
} else if (choice.startsWith("2.")) {
|
|
463
|
-
await handleAgentBriefing(ctx);
|
|
464
|
-
}
|
|
876
|
+
const action = matchMenuChoice(choice, handlers);
|
|
877
|
+
if (action) await action();
|
|
465
878
|
}
|
|
466
879
|
}
|
|
467
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
|
+
|
|
468
926
|
async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void> {
|
|
469
927
|
const types = getAvailableTypes();
|
|
470
928
|
const agents = types.map((t) => ({ name: t, config: getAgentConfig(t) }));
|
|
@@ -501,6 +959,7 @@ async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void>
|
|
|
501
959
|
lines.push("| `agent` | Which agent type to use (default: general-purpose) |");
|
|
502
960
|
lines.push("| `thinking` | Optional thinking mode override (e.g., `off`, `minimal`, `low`, `medium`, `high`, `xhigh`) |");
|
|
503
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. |");
|
|
504
963
|
lines.push("");
|
|
505
964
|
|
|
506
965
|
// Usage guidelines
|
|
@@ -508,6 +967,15 @@ async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void>
|
|
|
508
967
|
lines.push("- Agents start fresh with their config — they do NOT inherit the parent conversation");
|
|
509
968
|
lines.push("- For parallel tasks, spawn multiple `run_in_background: true` agents in one turn");
|
|
510
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.");
|
|
511
979
|
piInstance.sendUserMessage(lines.join("\n"));
|
|
512
980
|
ctx.ui.notify("Agent briefing sent to LLM", "info");
|
|
513
981
|
}
|
|
@@ -515,6 +983,7 @@ async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void>
|
|
|
515
983
|
/**
|
|
516
984
|
* Build a sub-menu for a single per-provider or per-model entry:
|
|
517
985
|
* "Edit limit" to change the value, or "Remove limit" to delete it.
|
|
986
|
+
* Callers pass setter callbacks that handle save + sync internally.
|
|
518
987
|
*/
|
|
519
988
|
async function editOrRemoveConcurrencyEntry(
|
|
520
989
|
ctx: ExtensionCommandContext,
|
|
@@ -522,8 +991,8 @@ async function editOrRemoveConcurrencyEntry(
|
|
|
522
991
|
entityType: "provider" | "model",
|
|
523
992
|
entityKey: string,
|
|
524
993
|
currentValue: number,
|
|
525
|
-
|
|
526
|
-
|
|
994
|
+
setEntry: (key: string, value: number) => void,
|
|
995
|
+
removeEntry: () => void,
|
|
527
996
|
): Promise<void> {
|
|
528
997
|
await runMenu(ctx, `${entityKey} concurrency`, [
|
|
529
998
|
"Edit limit",
|
|
@@ -531,13 +1000,12 @@ async function editOrRemoveConcurrencyEntry(
|
|
|
531
1000
|
], [
|
|
532
1001
|
async () => {
|
|
533
1002
|
await promptConcurrencyInput(
|
|
534
|
-
ctx,
|
|
535
|
-
|
|
1003
|
+
ctx, entityKey, currentValue,
|
|
1004
|
+
(value) => setEntry(entityKey, value),
|
|
536
1005
|
);
|
|
537
1006
|
},
|
|
538
1007
|
async () => {
|
|
539
|
-
|
|
540
|
-
applyConcurrencyConfig();
|
|
1008
|
+
removeEntry();
|
|
541
1009
|
ctx.ui.notify(
|
|
542
1010
|
`Removed per-${entityType} limit for ${entityKey}`,
|
|
543
1011
|
"info",
|
|
@@ -561,15 +1029,14 @@ export async function showConcurrencySettingsMenu(
|
|
|
561
1029
|
actions.push(async () => {
|
|
562
1030
|
await promptConcurrencyInput(
|
|
563
1031
|
ctx, "Default limit", __config.concurrency.default,
|
|
564
|
-
(value) =>
|
|
1032
|
+
(value) => setConcurrencyDefault(value),
|
|
565
1033
|
);
|
|
566
1034
|
});
|
|
567
1035
|
|
|
568
1036
|
// Reset all to defaults
|
|
569
1037
|
items.push("Reset all to defaults");
|
|
570
1038
|
actions.push(async () => {
|
|
571
|
-
|
|
572
|
-
applyConcurrencyConfig();
|
|
1039
|
+
resetConcurrency();
|
|
573
1040
|
ctx.ui.notify("Concurrency reset to defaults", "info");
|
|
574
1041
|
});
|
|
575
1042
|
|
|
@@ -592,16 +1059,8 @@ export async function showConcurrencySettingsMenu(
|
|
|
592
1059
|
"provider",
|
|
593
1060
|
provider,
|
|
594
1061
|
limit,
|
|
595
|
-
(value) =>
|
|
596
|
-
|
|
597
|
-
__config.concurrency.providers = { ...current, [provider]: value };
|
|
598
|
-
},
|
|
599
|
-
() => {
|
|
600
|
-
const providers = __config.concurrency.providers;
|
|
601
|
-
if (providers) {
|
|
602
|
-
delete providers[provider];
|
|
603
|
-
}
|
|
604
|
-
},
|
|
1062
|
+
(key, value) => setConcurrencyProvider(key, value),
|
|
1063
|
+
() => removeConcurrencyProvider(provider),
|
|
605
1064
|
);
|
|
606
1065
|
});
|
|
607
1066
|
}
|
|
@@ -614,10 +1073,7 @@ export async function showConcurrencySettingsMenu(
|
|
|
614
1073
|
if (provider === undefined) return;
|
|
615
1074
|
await promptAddConcurrencyLimit(
|
|
616
1075
|
ctx, provider,
|
|
617
|
-
(key, value) =>
|
|
618
|
-
const current = __config.concurrency.providers ?? {};
|
|
619
|
-
__config.concurrency.providers = { ...current, [key]: value };
|
|
620
|
-
},
|
|
1076
|
+
(key, value) => setConcurrencyProvider(key, value),
|
|
621
1077
|
);
|
|
622
1078
|
});
|
|
623
1079
|
|
|
@@ -640,16 +1096,8 @@ export async function showConcurrencySettingsMenu(
|
|
|
640
1096
|
"model",
|
|
641
1097
|
modelKey,
|
|
642
1098
|
limit,
|
|
643
|
-
(value) =>
|
|
644
|
-
|
|
645
|
-
__config.concurrency.models = { ...current, [modelKey]: value };
|
|
646
|
-
},
|
|
647
|
-
() => {
|
|
648
|
-
const models = __config.concurrency.models;
|
|
649
|
-
if (models) {
|
|
650
|
-
delete models[modelKey];
|
|
651
|
-
}
|
|
652
|
-
},
|
|
1099
|
+
(key, value) => setConcurrencyModel(key, value),
|
|
1100
|
+
() => removeConcurrencyModel(modelKey),
|
|
653
1101
|
);
|
|
654
1102
|
});
|
|
655
1103
|
}
|
|
@@ -664,10 +1112,7 @@ export async function showConcurrencySettingsMenu(
|
|
|
664
1112
|
if (modelKey === null) return;
|
|
665
1113
|
await promptAddConcurrencyLimit(
|
|
666
1114
|
ctx, modelKey.trim(),
|
|
667
|
-
(key, value) =>
|
|
668
|
-
const current = __config.concurrency.models ?? {};
|
|
669
|
-
__config.concurrency.models = { ...current, [key]: value };
|
|
670
|
-
},
|
|
1115
|
+
(key, value) => setConcurrencyModel(key, value),
|
|
671
1116
|
);
|
|
672
1117
|
});
|
|
673
1118
|
|
|
@@ -678,27 +1123,31 @@ export async function showConcurrencySettingsMenu(
|
|
|
678
1123
|
async function showRunningAgentsMenu(
|
|
679
1124
|
ctx: ExtensionCommandContext,
|
|
680
1125
|
): Promise<void> {
|
|
681
|
-
const records =
|
|
1126
|
+
const records = getManager()?.listAgents() ?? [];
|
|
682
1127
|
if (records.length === 0) {
|
|
683
1128
|
ctx.ui.notify("No agents have been spawned this session", "info");
|
|
684
1129
|
return;
|
|
685
1130
|
}
|
|
686
1131
|
|
|
687
1132
|
return runMenuLoop(ctx, "Running Agents", () => {
|
|
688
|
-
const records =
|
|
689
|
-
const running = records.filter((r) => r.status === "running" || r.status === "queued");
|
|
1133
|
+
const records = getManager()?.listAgents() ?? [];
|
|
1134
|
+
const running = records.filter((r) => r.lifecycle.status === "running" || r.lifecycle.status === "queued");
|
|
690
1135
|
|
|
691
1136
|
const items: string[] = [];
|
|
692
1137
|
const actions: Array<() => Promise<void>> = [];
|
|
693
1138
|
|
|
694
1139
|
for (const record of records) {
|
|
695
|
-
const elapsed = Math.round((Date.now() - record.startedAt) / 1000);
|
|
696
|
-
const statusIcon = record.status === "running" ? "▶" :
|
|
697
|
-
record.status === "completed" ? "✓" :
|
|
698
|
-
record.status === "queued" ? "⏳" :
|
|
699
|
-
record.status === "error" ? "✗" : "•";
|
|
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}` : "";
|
|
700
1149
|
items.push(
|
|
701
|
-
`${statusIcon} ${record.id.slice(0, SHORT_ID_LENGTH)} ${record.type} ${record.status} ${elapsed}s`,
|
|
1150
|
+
`${statusIcon} ${record.id.slice(0, SHORT_ID_LENGTH)} ${record.display.type} ${record.lifecycle.status} ${elapsed}s${suffix}`,
|
|
702
1151
|
);
|
|
703
1152
|
|
|
704
1153
|
actions.push(async () => {
|
|
@@ -715,7 +1164,7 @@ async function showRunningAgentsMenu(
|
|
|
715
1164
|
items.push(`Stop ${running.length} running agent(s)`);
|
|
716
1165
|
actions.push(async () => {
|
|
717
1166
|
for (const record of running) {
|
|
718
|
-
|
|
1167
|
+
getManager()?.abort(record.id);
|
|
719
1168
|
}
|
|
720
1169
|
ctx.ui.notify(`Stopped ${running.length} agent(s)`, "info");
|
|
721
1170
|
});
|
|
@@ -741,19 +1190,19 @@ async function showResultViewer(
|
|
|
741
1190
|
? `snapshot \u00b7 ${record.id.slice(0, SHORT_ID_LENGTH)}`
|
|
742
1191
|
: "Error";
|
|
743
1192
|
const stats: ResultViewerStats = {
|
|
744
|
-
lifetimeUsage: record.lifetimeUsage,
|
|
745
|
-
turnCount: record.turnCount,
|
|
746
|
-
durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
|
|
1193
|
+
lifetimeUsage: record.stats.lifetimeUsage,
|
|
1194
|
+
turnCount: record.stats.turnCount,
|
|
1195
|
+
durationMs: (record.lifecycle.completedAt ?? Date.now()) - record.lifecycle.startedAt,
|
|
747
1196
|
};
|
|
748
1197
|
const refreshCallback =
|
|
749
|
-
kind === "snapshot" && record.session
|
|
750
|
-
? () => buildSnapshotMarkdown(record.session!.messages)
|
|
1198
|
+
kind === "snapshot" && record.execution.session
|
|
1199
|
+
? () => buildSnapshotMarkdown(record.execution.session!.messages)
|
|
751
1200
|
: undefined;
|
|
752
1201
|
|
|
753
1202
|
await ctx.ui.custom<void>(
|
|
754
1203
|
(tui, theme, _kb, done) =>
|
|
755
1204
|
new ResultViewer(
|
|
756
|
-
`${getDisplayName(record.type)} · ${titleSuffix}`,
|
|
1205
|
+
`${getDisplayName(record.display.type)} · ${titleSuffix}`,
|
|
757
1206
|
text,
|
|
758
1207
|
{ onClose: () => done(), onRefresh: refreshCallback },
|
|
759
1208
|
theme,
|
|
@@ -770,16 +1219,16 @@ async function steerAgentById(
|
|
|
770
1219
|
agentId: string,
|
|
771
1220
|
ctx: ExtensionCommandContext,
|
|
772
1221
|
): Promise<void> {
|
|
773
|
-
const record =
|
|
1222
|
+
const record = getManager()?.getRecord(agentId);
|
|
774
1223
|
if (!record) {
|
|
775
1224
|
ctx.ui.notify("Agent not found", "error");
|
|
776
1225
|
return;
|
|
777
1226
|
}
|
|
778
1227
|
|
|
779
|
-
const message = await ctx.ui.input(`Steer ${record.type}`);
|
|
1228
|
+
const message = await ctx.ui.input(`Steer ${record.display.type}`);
|
|
780
1229
|
if (!message?.trim()) return;
|
|
781
1230
|
|
|
782
|
-
const sent = await
|
|
1231
|
+
const sent = await getManager().steer(agentId, message.trim());
|
|
783
1232
|
if (sent) {
|
|
784
1233
|
ctx.ui.notify(`Steer sent to ${record.id.slice(0, SHORT_ID_LENGTH)}…`, "info");
|
|
785
1234
|
} else {
|
|
@@ -798,16 +1247,16 @@ export async function showAgentActions(
|
|
|
798
1247
|
const items: string[] = [];
|
|
799
1248
|
const actions: Array<() => Promise<void>> = [];
|
|
800
1249
|
|
|
801
|
-
const isRunning = record.status === "running" || record.status === "queued";
|
|
802
|
-
const hasSession = !!record.session;
|
|
1250
|
+
const isRunning = record.lifecycle.status === "running" || record.lifecycle.status === "queued";
|
|
1251
|
+
const hasSession = !!record.execution.session;
|
|
803
1252
|
const hasResult = !!record.result && record.result.length > 0;
|
|
804
1253
|
const hasError = !!record.error && record.error.length > 0;
|
|
805
1254
|
|
|
806
1255
|
// View actions first
|
|
807
|
-
if (record.status === "running" && hasSession) {
|
|
1256
|
+
if (record.lifecycle.status === "running" && hasSession) {
|
|
808
1257
|
items.push("View snapshot");
|
|
809
1258
|
actions.push(async () => {
|
|
810
|
-
const messages = record.session!.messages;
|
|
1259
|
+
const messages = record.execution.session!.messages;
|
|
811
1260
|
const markdown = buildSnapshotMarkdown(messages);
|
|
812
1261
|
await showResultViewer(ctx, record, "snapshot", markdown);
|
|
813
1262
|
});
|
|
@@ -836,7 +1285,7 @@ export async function showAgentActions(
|
|
|
836
1285
|
|
|
837
1286
|
items.push("Stop");
|
|
838
1287
|
actions.push(async () => {
|
|
839
|
-
|
|
1288
|
+
getManager()?.abort(record.id);
|
|
840
1289
|
ctx.ui.notify(`Stopped ${record.id.slice(0, SHORT_ID_LENGTH)}`, "info");
|
|
841
1290
|
});
|
|
842
1291
|
}
|