pi-subagents-lite 1.0.2 → 1.1.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/src/menus.ts CHANGED
@@ -2,27 +2,46 @@
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 index.ts.
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
9
  import { getAgentConfig, getAvailableTypes, getAllTypes } from "./agent-types.js";
10
10
  import type { AgentRecord } from "./types.js";
11
- import { SHORT_ID_LENGTH } from "./types.js";
11
+ import { SHORT_ID_LENGTH, CONFIG_AGENT_NON_MODEL_KEYS } from "./types.js";
12
12
  import { ModelSelectorDialog, type ModelOption } from "./model-selector.js";
13
13
  import { ResultViewer, type ResultViewerStats } from "./result-viewer.js";
14
- import { getDisplayName } from "./ui/agent-widget.js";
14
+ import { getDisplayName } from "./format.js";
15
15
  import { buildSnapshotMarkdown } from "./context.js";
16
16
 
17
17
  import { parseModelKey } from "./utils.js";
18
18
  import {
19
19
  __config,
20
20
  sessionOverrides,
21
- manager,
22
21
  piInstance,
23
- } from "./index.js";
22
+ getManager,
23
+ } from "./state.js";
24
24
  import { resolveModel } from "./model-precedence.js";
25
- import { saveConfigAtomic, DEFAULT_CONFIG } from "./config-io.js";
25
+ import {
26
+ setModelOverride,
27
+ setDefaultModel,
28
+ clearModelOverride,
29
+ clearAllModelOverrides,
30
+ setForceBackground,
31
+ setShowCost,
32
+ setGraceTurns,
33
+ setWidgetCompact,
34
+ setWidgetMaxLines,
35
+ setWidgetMaxLinesCompact,
36
+ setWidgetShortcut,
37
+ setAgent,
38
+ setConcurrencyDefault,
39
+ setConcurrencyProvider,
40
+ setConcurrencyModel,
41
+ removeConcurrencyProvider,
42
+ removeConcurrencyModel,
43
+ resetConcurrency,
44
+ } from "./config-mutator.js";
26
45
 
27
46
  // ============================================================================
28
47
  // Helpers
@@ -113,45 +132,50 @@ async function applyModelOverride(
113
132
  }
114
133
 
115
134
  /**
116
- * Persist concurrency config to disk and apply to the running manager.
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.
135
+ * Prompt for numeric input, validate (integer min), return parsed value or undefined.
136
+ * Returns undefined if the user cancels or the value is invalid.
125
137
  */
126
- async function parseConcurrencyInput(
138
+ async function parseNumericInput(
127
139
  ctx: ExtensionCommandContext,
128
140
  label: string,
129
141
  initialValue: string,
142
+ min: number,
143
+ minLabel: string,
130
144
  ): Promise<number | undefined> {
131
145
  const input = await ctx.ui.input(label, initialValue);
132
146
  if (input === undefined) return undefined;
133
147
  const parsed = parseInt(input.trim(), 10);
134
- if (isNaN(parsed) || parsed < 1) {
135
- ctx.ui.notify("Invalid value — must be a number 1", "error");
148
+ if (isNaN(parsed) || parsed < min) {
149
+ ctx.ui.notify(`Invalid value — must be a number ${minLabel}`, "error");
136
150
  return undefined;
137
151
  }
138
152
  return parsed;
139
153
  }
140
154
 
141
155
  /**
142
- * Prompt for a concurrency value, validate, save and apply.
143
- * Used for editing an existing concurrency limit.
156
+ * Parse a concurrency input: prompt, validate (integer ≥ 1), return parsed value or undefined.
157
+ */
158
+ async function parseConcurrencyInput(
159
+ ctx: ExtensionCommandContext,
160
+ label: string,
161
+ initialValue: string,
162
+ ): Promise<number | undefined> {
163
+ return parseNumericInput(ctx, label, initialValue, 1, "≥ 1");
164
+ }
165
+
166
+ /**
167
+ * Prompt for a concurrency value, validate, and apply via setter.
168
+ * The setter handles save + sync internally.
144
169
  */
145
170
  async function promptConcurrencyInput(
146
171
  ctx: ExtensionCommandContext,
147
172
  label: string,
148
173
  currentValue: number,
149
- apply: (value: number) => void,
174
+ setter: (value: number) => void,
150
175
  ): Promise<void> {
151
176
  const parsed = await parseConcurrencyInput(ctx, label, String(currentValue));
152
177
  if (parsed === undefined) return;
153
- apply(parsed);
154
- applyConcurrencyConfig();
178
+ setter(parsed);
155
179
  ctx.ui.notify(
156
180
  `${label.replace("Concurrency slots for ", "")} concurrency set to ${parsed}`,
157
181
  "info",
@@ -160,16 +184,16 @@ async function promptConcurrencyInput(
160
184
 
161
185
  /**
162
186
  * Prompt to add a new concurrency limit for a named entity.
187
+ * Calls the setter which handles save + sync internally.
163
188
  */
164
189
  async function promptAddConcurrencyLimit(
165
190
  ctx: ExtensionCommandContext,
166
191
  label: string,
167
- apply: (key: string, value: number) => void,
192
+ setter: (key: string, value: number) => void,
168
193
  ): Promise<void> {
169
194
  const parsed = await parseConcurrencyInput(ctx, "Concurrency slots", "1");
170
195
  if (parsed === undefined) return;
171
- apply(label, parsed);
172
- applyConcurrencyConfig();
196
+ setter(label, parsed);
173
197
  ctx.ui.notify(`${label} concurrency set to ${parsed}`, "info");
174
198
  }
175
199
 
@@ -246,13 +270,12 @@ export async function showModelSettingsMenu(
246
270
 
247
271
  // Handle "clear" — remove all overrides (session + config) and save
248
272
  if (mode === "clear") {
249
- delete __config.agent[targetKey];
273
+ clearModelOverride(targetKey);
250
274
  if (targetKey !== "default") {
251
275
  delete sessionOverrides[targetKey];
252
276
  } else {
253
277
  sessionOverrides.default = null;
254
278
  }
255
- saveConfigAtomic(__config);
256
279
  ctx.ui.notify(`${label} overrides cleared`, "info");
257
280
  return;
258
281
  }
@@ -264,12 +287,9 @@ export async function showModelSettingsMenu(
264
287
  isSession
265
288
  ? (chosen) => { sessionOverrides[targetKey] = chosen; }
266
289
  : (chosen) => {
267
- __config.agent[targetKey] = chosen;
290
+ setModelOverride(targetKey, chosen);
268
291
  },
269
292
  );
270
- if (!isSession) {
271
- saveConfigAtomic(__config);
272
- }
273
293
  };
274
294
 
275
295
  // Global default — show session value if present
@@ -293,31 +313,28 @@ export async function showModelSettingsMenu(
293
313
  : "Force background · OFF";
294
314
  items.push(forceBgLabel);
295
315
  actions.push(async () => {
296
- __config.agent.forceBackground = !__config.agent.forceBackground;
297
- saveConfigAtomic(__config);
316
+ setForceBackground(!__config.agent.forceBackground);
298
317
  ctx.ui.notify(
299
318
  `Force background ${__config.agent.forceBackground ? "ON" : "OFF"}`,
300
319
  "info",
301
320
  );
302
321
  });
303
322
 
323
+ // Cost display toggle
324
+ const showCost = __config.agent.showCost === true; // default false
325
+ items.push(`Cost display · ${showCost ? "ON" : "OFF"}`);
326
+ actions.push(async () => {
327
+ setShowCost(!showCost);
328
+ ctx.ui.notify(`Cost display ${showCost ? "OFF" : "ON"}`, "info");
329
+ });
330
+
304
331
  // Grace turns setting
305
332
  const graceTurns = __config.agent.graceTurns ?? 6;
306
333
  items.push(`Grace turns · ${graceTurns}`);
307
334
  actions.push(async () => {
308
- const input = await ctx.ui.input("Grace turns (≥ 0)", String(graceTurns));
309
- if (input === undefined) return;
310
- const parsed = parseInt(input.trim(), 10);
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);
335
+ const parsed = await parseNumericInput(ctx, "Grace turns (≥ 0)", String(graceTurns), 0, "≥ 0");
336
+ if (parsed === undefined) return;
337
+ setGraceTurns(parsed);
321
338
  ctx.ui.notify(`Grace turns set to ${parsed}`, "info");
322
339
  });
323
340
 
@@ -395,21 +412,13 @@ export async function showModelSettingsMenu(
395
412
  items.push("Clear all overrides");
396
413
  actions.push(async () => {
397
414
  const hasOverrides = Object.entries(__config.agent).some(
398
- ([k, v]) => k !== "default" && k !== "forceBackground" && k !== "graceTurns" && v != null,
415
+ ([k, v]) => !CONFIG_AGENT_NON_MODEL_KEYS.includes(k) && v != null,
399
416
  );
400
417
  if (!hasOverrides && __config.agent.default === null) {
401
418
  ctx.ui.notify("No overrides to clear", "info");
402
419
  return;
403
420
  }
404
- const preserved: Record<string, unknown> = {
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);
421
+ clearAllModelOverrides();
413
422
  ctx.ui.notify("All model overrides cleared", "info");
414
423
  });
415
424
 
@@ -417,33 +426,48 @@ export async function showModelSettingsMenu(
417
426
  });
418
427
  }
419
428
 
429
+ /** Map menu choice to handler. Matches by number prefix or first word. */
430
+ function matchMenuChoice(
431
+ choice: string,
432
+ handlers: Record<string, () => Promise<void>>,
433
+ ): (() => Promise<void>) | undefined {
434
+ // Try number prefix first (e.g., "1." from "1. Running agents")
435
+ const numMatch = choice.match(/^(\d+)/);
436
+ if (numMatch) return handlers[numMatch[1]];
437
+ // Fall back to first word
438
+ const key = choice.split(" ")[0].toLowerCase();
439
+ return handlers[key];
440
+ }
441
+
420
442
  export async function showAgentsMainMenu(
421
443
  ctx: ExtensionCommandContext,
422
444
  modelOptions: string[],
423
445
  ): Promise<void> {
424
446
  const menuItems = [
425
- "1. Model settingsSet global default and per-type model overrides",
426
- "2. Concurrency settings — Set per-model slot limits",
427
- "3. Running agentsList running/queued agents",
428
- "4. DebugAgent types, briefing, diagnostics",
447
+ "1. Running agentsList running/queued agents",
448
+ "2. Model settings — Set global default and per-type model overrides",
449
+ "3. Concurrency settingsSet per-model slot limits",
450
+ "4. Widget settings Configure widget display options",
451
+ "5. Debug — Agent types, briefing, diagnostics",
429
452
  "",
430
453
  "Press Escape to close",
431
454
  ];
432
455
 
456
+ const handlers: Record<string, () => Promise<void>> = {
457
+ "1": () => showRunningAgentsMenu(ctx),
458
+ "2": () => showModelSettingsMenu(ctx, modelOptions),
459
+ "3": () => showConcurrencySettingsMenu(ctx, modelOptions),
460
+ "4": () => showWidgetSettingsMenu(ctx),
461
+ "5": () => showDebugMenu(ctx),
462
+ };
463
+
433
464
  // Loop so sub-menus navigate back to root; only Escape at root closes
434
465
  while (true) {
435
466
  const choice = await ctx.ui.select("Subagents Management", menuItems);
436
467
  if (choice === undefined || choice === "Press Escape to close") return;
437
468
 
438
- if (choice.startsWith("1.")) {
439
- await showModelSettingsMenu(ctx, modelOptions);
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
- }
469
+ const action = matchMenuChoice(choice, handlers);
470
+ if (action) await action();
447
471
  }
448
472
  }
449
473
 
@@ -453,18 +477,65 @@ async function showDebugMenu(ctx: ExtensionCommandContext): Promise<void> {
453
477
  "2. Agent briefing — Send agent types/capabilities info to LLM (Optional, if having issues)",
454
478
  ];
455
479
 
480
+ const handlers: Record<string, () => Promise<void>> = {
481
+ "1": () => showAgentTypes(ctx),
482
+ "2": () => handleAgentBriefing(ctx),
483
+ };
484
+
456
485
  while (true) {
457
486
  const choice = await ctx.ui.select("Debug", menuItems);
458
487
  if (choice === undefined) return;
459
488
 
460
- if (choice.startsWith("1.")) {
461
- await showAgentTypes(ctx);
462
- } else if (choice.startsWith("2.")) {
463
- await handleAgentBriefing(ctx);
464
- }
489
+ const action = matchMenuChoice(choice, handlers);
490
+ if (action) await action();
465
491
  }
466
492
  }
467
493
 
494
+ export async function showWidgetSettingsMenu(ctx: ExtensionCommandContext): Promise<void> {
495
+ return runMenuLoop(ctx, "Widget Settings", () => {
496
+ const items: string[] = [];
497
+ const actions: Array<() => Promise<void>> = [];
498
+
499
+ // Force compact mode toggle
500
+ const isForceCompact = __config.agent.widgetCompact === true;
501
+ items.push(`Force compact mode · ${isForceCompact ? "ON" : "OFF"}`);
502
+ actions.push(async () => {
503
+ setWidgetCompact(!isForceCompact);
504
+ ctx.ui.notify(`Force compact mode ${__config.agent.widgetCompact ? "ON" : "OFF"}`, "info");
505
+ });
506
+
507
+ // Max lines (full mode)
508
+ const maxLines = __config.agent.widgetMaxLines ?? 12;
509
+ items.push(`Max lines (full) · ${maxLines}`);
510
+ actions.push(async () => {
511
+ const parsed = await parseNumericInput(ctx, "Max lines (full mode, ≥ 2)", String(maxLines), 2, "≥ 2");
512
+ if (parsed === undefined) return;
513
+ setWidgetMaxLines(parsed);
514
+ ctx.ui.notify(`Max lines (full) set to ${parsed}`, "info");
515
+ });
516
+
517
+ // Max lines (compact mode)
518
+ const maxLinesCompact = __config.agent.widgetMaxLinesCompact ?? Math.floor(maxLines / 2);
519
+ items.push(`Max lines (compact) · ${maxLinesCompact}`);
520
+ actions.push(async () => {
521
+ const parsed = await parseNumericInput(ctx, "Max lines (compact mode, ≥ 1)", String(maxLinesCompact), 1, "≥ 1");
522
+ if (parsed === undefined) return;
523
+ setWidgetMaxLinesCompact(parsed);
524
+ ctx.ui.notify(`Max lines (compact) set to ${parsed}`, "info");
525
+ });
526
+
527
+ // Ctrl+o shortcut toggle
528
+ const shortcutEnabled = __config.agent.widgetShortcut === true;
529
+ items.push(`Ctrl+o shortcut · ${shortcutEnabled ? "ON" : "OFF"}`);
530
+ actions.push(async () => {
531
+ setWidgetShortcut(!shortcutEnabled);
532
+ ctx.ui.notify(`Ctrl+o shortcut ${__config.agent.widgetShortcut ? "ON" : "OFF"}`, "info");
533
+ });
534
+
535
+ return { items, actions };
536
+ });
537
+ }
538
+
468
539
  async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void> {
469
540
  const types = getAvailableTypes();
470
541
  const agents = types.map((t) => ({ name: t, config: getAgentConfig(t) }));
@@ -515,6 +586,7 @@ async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void>
515
586
  /**
516
587
  * Build a sub-menu for a single per-provider or per-model entry:
517
588
  * "Edit limit" to change the value, or "Remove limit" to delete it.
589
+ * Callers pass setter callbacks that handle save + sync internally.
518
590
  */
519
591
  async function editOrRemoveConcurrencyEntry(
520
592
  ctx: ExtensionCommandContext,
@@ -522,8 +594,8 @@ async function editOrRemoveConcurrencyEntry(
522
594
  entityType: "provider" | "model",
523
595
  entityKey: string,
524
596
  currentValue: number,
525
- applyUpdate: (value: number) => void,
526
- applyRemove: () => void,
597
+ setEntry: (key: string, value: number) => void,
598
+ removeEntry: () => void,
527
599
  ): Promise<void> {
528
600
  await runMenu(ctx, `${entityKey} concurrency`, [
529
601
  "Edit limit",
@@ -531,13 +603,12 @@ async function editOrRemoveConcurrencyEntry(
531
603
  ], [
532
604
  async () => {
533
605
  await promptConcurrencyInput(
534
- ctx, label, currentValue,
535
- applyUpdate,
606
+ ctx, entityKey, currentValue,
607
+ (value) => setEntry(entityKey, value),
536
608
  );
537
609
  },
538
610
  async () => {
539
- applyRemove();
540
- applyConcurrencyConfig();
611
+ removeEntry();
541
612
  ctx.ui.notify(
542
613
  `Removed per-${entityType} limit for ${entityKey}`,
543
614
  "info",
@@ -561,15 +632,14 @@ export async function showConcurrencySettingsMenu(
561
632
  actions.push(async () => {
562
633
  await promptConcurrencyInput(
563
634
  ctx, "Default limit", __config.concurrency.default,
564
- (value) => { __config.concurrency.default = value; },
635
+ (value) => setConcurrencyDefault(value),
565
636
  );
566
637
  });
567
638
 
568
639
  // Reset all to defaults
569
640
  items.push("Reset all to defaults");
570
641
  actions.push(async () => {
571
- __config.concurrency = { ...DEFAULT_CONFIG.concurrency };
572
- applyConcurrencyConfig();
642
+ resetConcurrency();
573
643
  ctx.ui.notify("Concurrency reset to defaults", "info");
574
644
  });
575
645
 
@@ -592,16 +662,8 @@ export async function showConcurrencySettingsMenu(
592
662
  "provider",
593
663
  provider,
594
664
  limit,
595
- (value) => {
596
- const current = __config.concurrency.providers ?? {};
597
- __config.concurrency.providers = { ...current, [provider]: value };
598
- },
599
- () => {
600
- const providers = __config.concurrency.providers;
601
- if (providers) {
602
- delete providers[provider];
603
- }
604
- },
665
+ (key, value) => setConcurrencyProvider(key, value),
666
+ () => removeConcurrencyProvider(provider),
605
667
  );
606
668
  });
607
669
  }
@@ -614,10 +676,7 @@ export async function showConcurrencySettingsMenu(
614
676
  if (provider === undefined) return;
615
677
  await promptAddConcurrencyLimit(
616
678
  ctx, provider,
617
- (key, value) => {
618
- const current = __config.concurrency.providers ?? {};
619
- __config.concurrency.providers = { ...current, [key]: value };
620
- },
679
+ (key, value) => setConcurrencyProvider(key, value),
621
680
  );
622
681
  });
623
682
 
@@ -640,16 +699,8 @@ export async function showConcurrencySettingsMenu(
640
699
  "model",
641
700
  modelKey,
642
701
  limit,
643
- (value) => {
644
- const current = __config.concurrency.models ?? {};
645
- __config.concurrency.models = { ...current, [modelKey]: value };
646
- },
647
- () => {
648
- const models = __config.concurrency.models;
649
- if (models) {
650
- delete models[modelKey];
651
- }
652
- },
702
+ (key, value) => setConcurrencyModel(key, value),
703
+ () => removeConcurrencyModel(modelKey),
653
704
  );
654
705
  });
655
706
  }
@@ -664,10 +715,7 @@ export async function showConcurrencySettingsMenu(
664
715
  if (modelKey === null) return;
665
716
  await promptAddConcurrencyLimit(
666
717
  ctx, modelKey.trim(),
667
- (key, value) => {
668
- const current = __config.concurrency.models ?? {};
669
- __config.concurrency.models = { ...current, [key]: value };
670
- },
718
+ (key, value) => setConcurrencyModel(key, value),
671
719
  );
672
720
  });
673
721
 
@@ -678,27 +726,31 @@ export async function showConcurrencySettingsMenu(
678
726
  async function showRunningAgentsMenu(
679
727
  ctx: ExtensionCommandContext,
680
728
  ): Promise<void> {
681
- const records = manager?.listAgents() ?? [];
729
+ const records = getManager()?.listAgents() ?? [];
682
730
  if (records.length === 0) {
683
731
  ctx.ui.notify("No agents have been spawned this session", "info");
684
732
  return;
685
733
  }
686
734
 
687
735
  return runMenuLoop(ctx, "Running Agents", () => {
688
- const records = manager?.listAgents() ?? [];
689
- const running = records.filter((r) => r.status === "running" || r.status === "queued");
736
+ const records = getManager()?.listAgents() ?? [];
737
+ const running = records.filter((r) => r.lifecycle.status === "running" || r.lifecycle.status === "queued");
690
738
 
691
739
  const items: string[] = [];
692
740
  const actions: Array<() => Promise<void>> = [];
693
741
 
694
742
  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" ? "✗" : "•";
743
+ const elapsed = Math.round((Date.now() - record.lifecycle.startedAt) / 1000);
744
+ const statusIcon = record.lifecycle.status === "running" ? "▶" :
745
+ record.lifecycle.status === "completed" ? "✓" :
746
+ record.lifecycle.status === "queued" ? "⏳" :
747
+ record.lifecycle.status === "error" ? "✗" : "•";
748
+ const headline = record.display.description
749
+ ? (record.display.description.length > 50 ? record.display.description.slice(0, 47) + "..." : record.display.description)
750
+ : "";
751
+ const suffix = headline ? ` — ${headline}` : "";
700
752
  items.push(
701
- `${statusIcon} ${record.id.slice(0, SHORT_ID_LENGTH)} ${record.type} ${record.status} ${elapsed}s`,
753
+ `${statusIcon} ${record.id.slice(0, SHORT_ID_LENGTH)} ${record.display.type} ${record.lifecycle.status} ${elapsed}s${suffix}`,
702
754
  );
703
755
 
704
756
  actions.push(async () => {
@@ -715,7 +767,7 @@ async function showRunningAgentsMenu(
715
767
  items.push(`Stop ${running.length} running agent(s)`);
716
768
  actions.push(async () => {
717
769
  for (const record of running) {
718
- manager?.abort(record.id);
770
+ getManager()?.abort(record.id);
719
771
  }
720
772
  ctx.ui.notify(`Stopped ${running.length} agent(s)`, "info");
721
773
  });
@@ -741,19 +793,19 @@ async function showResultViewer(
741
793
  ? `snapshot \u00b7 ${record.id.slice(0, SHORT_ID_LENGTH)}`
742
794
  : "Error";
743
795
  const stats: ResultViewerStats = {
744
- lifetimeUsage: record.lifetimeUsage,
745
- turnCount: record.turnCount,
746
- durationMs: (record.completedAt ?? Date.now()) - record.startedAt,
796
+ lifetimeUsage: record.stats.lifetimeUsage,
797
+ turnCount: record.stats.turnCount,
798
+ durationMs: (record.lifecycle.completedAt ?? Date.now()) - record.lifecycle.startedAt,
747
799
  };
748
800
  const refreshCallback =
749
- kind === "snapshot" && record.session
750
- ? () => buildSnapshotMarkdown(record.session!.messages)
801
+ kind === "snapshot" && record.execution.session
802
+ ? () => buildSnapshotMarkdown(record.execution.session!.messages)
751
803
  : undefined;
752
804
 
753
805
  await ctx.ui.custom<void>(
754
806
  (tui, theme, _kb, done) =>
755
807
  new ResultViewer(
756
- `${getDisplayName(record.type)} · ${titleSuffix}`,
808
+ `${getDisplayName(record.display.type)} · ${titleSuffix}`,
757
809
  text,
758
810
  { onClose: () => done(), onRefresh: refreshCallback },
759
811
  theme,
@@ -770,16 +822,16 @@ async function steerAgentById(
770
822
  agentId: string,
771
823
  ctx: ExtensionCommandContext,
772
824
  ): Promise<void> {
773
- const record = manager?.getRecord(agentId);
825
+ const record = getManager()?.getRecord(agentId);
774
826
  if (!record) {
775
827
  ctx.ui.notify("Agent not found", "error");
776
828
  return;
777
829
  }
778
830
 
779
- const message = await ctx.ui.input(`Steer ${record.type}`);
831
+ const message = await ctx.ui.input(`Steer ${record.display.type}`);
780
832
  if (!message?.trim()) return;
781
833
 
782
- const sent = await manager.steer(agentId, message.trim());
834
+ const sent = await getManager().steer(agentId, message.trim());
783
835
  if (sent) {
784
836
  ctx.ui.notify(`Steer sent to ${record.id.slice(0, SHORT_ID_LENGTH)}…`, "info");
785
837
  } else {
@@ -798,16 +850,16 @@ export async function showAgentActions(
798
850
  const items: string[] = [];
799
851
  const actions: Array<() => Promise<void>> = [];
800
852
 
801
- const isRunning = record.status === "running" || record.status === "queued";
802
- const hasSession = !!record.session;
853
+ const isRunning = record.lifecycle.status === "running" || record.lifecycle.status === "queued";
854
+ const hasSession = !!record.execution.session;
803
855
  const hasResult = !!record.result && record.result.length > 0;
804
856
  const hasError = !!record.error && record.error.length > 0;
805
857
 
806
858
  // View actions first
807
- if (record.status === "running" && hasSession) {
859
+ if (record.lifecycle.status === "running" && hasSession) {
808
860
  items.push("View snapshot");
809
861
  actions.push(async () => {
810
- const messages = record.session!.messages;
862
+ const messages = record.execution.session!.messages;
811
863
  const markdown = buildSnapshotMarkdown(messages);
812
864
  await showResultViewer(ctx, record, "snapshot", markdown);
813
865
  });
@@ -836,7 +888,7 @@ export async function showAgentActions(
836
888
 
837
889
  items.push("Stop");
838
890
  actions.push(async () => {
839
- manager?.abort(record.id);
891
+ getManager()?.abort(record.id);
840
892
  ctx.ui.notify(`Stopped ${record.id.slice(0, SHORT_ID_LENGTH)}`, "info");
841
893
  });
842
894
  }
@@ -18,6 +18,11 @@ export interface SubagentsConfig {
18
18
  default: string | null;
19
19
  forceBackground: boolean;
20
20
  graceTurns?: number;
21
+ showCost?: boolean;
22
+ widgetMaxLines?: number;
23
+ widgetMaxLinesCompact?: number;
24
+ widgetCompact?: boolean;
25
+ widgetShortcut?: boolean;
21
26
  [agentType: string]: string | null | undefined | boolean | number;
22
27
  };
23
28
  concurrency: {