pi-subagents 0.24.2 → 0.24.4

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.
@@ -6,6 +6,7 @@ import { execSync } from "node:child_process";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
+ import { getAgentDir } from "../shared/utils.ts";
9
10
 
10
11
  export type SkillSource =
11
12
  | "project"
@@ -46,11 +47,10 @@ interface SkillSearchPath {
46
47
  const skillCache = new Map<string, SkillCacheEntry>();
47
48
  const MAX_CACHE_SIZE = 50;
48
49
 
49
- let loadSkillsCache: { cwd: string; skills: CachedSkillEntry[]; timestamp: number } | null = null;
50
+ let loadSkillsCache: { cwd: string; agentDir: string; skills: CachedSkillEntry[]; timestamp: number } | null = null;
50
51
  const LOAD_SKILLS_CACHE_TTL_MS = 5000;
51
52
 
52
53
  const CONFIG_DIR = ".pi";
53
- const AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
54
54
  const SUBAGENT_ORCHESTRATION_SKILL = "pi-subagents";
55
55
 
56
56
  const SOURCE_PRIORITY: Record<SkillSource, number> = {
@@ -133,10 +133,10 @@ function getGlobalNpmRoot(): string | null {
133
133
  }
134
134
  }
135
135
 
136
- function collectInstalledPackageSkillPaths(cwd: string): SkillSearchPath[] {
136
+ function collectInstalledPackageSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
137
137
  const dirs: SkillSearchPath[] = [
138
138
  { path: path.join(cwd, CONFIG_DIR, "npm", "node_modules"), source: "project-package" },
139
- { path: path.join(AGENT_DIR, "npm", "node_modules"), source: "user-package" },
139
+ { path: path.join(agentDir, "npm", "node_modules"), source: "user-package" },
140
140
  ];
141
141
 
142
142
  const globalRoot = getGlobalNpmRoot();
@@ -184,11 +184,11 @@ function collectInstalledPackageSkillPaths(cwd: string): SkillSearchPath[] {
184
184
  return results;
185
185
  }
186
186
 
187
- function collectSettingsSkillPaths(cwd: string): SkillSearchPath[] {
187
+ function collectSettingsSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
188
188
  const results: SkillSearchPath[] = [];
189
189
  const settingsFiles = [
190
190
  { file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR), source: "project-settings" as const },
191
- { file: path.join(AGENT_DIR, "settings.json"), base: AGENT_DIR, source: "user-settings" as const },
191
+ { file: path.join(agentDir, "settings.json"), base: agentDir, source: "user-settings" as const },
192
192
  ];
193
193
 
194
194
  for (const { file, base, source } of settingsFiles) {
@@ -285,10 +285,10 @@ function resolveSettingsPackageRoot(source: string, baseDir: string): string | u
285
285
  return undefined;
286
286
  }
287
287
 
288
- function collectSettingsPackageSkillPaths(cwd: string): SkillSearchPath[] {
288
+ function collectSettingsPackageSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
289
289
  const settingsFiles = [
290
290
  { file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR), source: "project-package" as const },
291
- { file: path.join(AGENT_DIR, "settings.json"), base: AGENT_DIR, source: "user-package" as const },
291
+ { file: path.join(agentDir, "settings.json"), base: agentDir, source: "user-package" as const },
292
292
  ];
293
293
  const results: SkillSearchPath[] = [];
294
294
 
@@ -315,16 +315,16 @@ function collectSettingsPackageSkillPaths(cwd: string): SkillSearchPath[] {
315
315
  return results;
316
316
  }
317
317
 
318
- function buildSkillPaths(cwd: string): SkillSearchPath[] {
318
+ function buildSkillPaths(cwd: string, agentDir: string): SkillSearchPath[] {
319
319
  const skillPaths: SkillSearchPath[] = [
320
320
  { path: path.join(cwd, CONFIG_DIR, "skills"), source: "project" },
321
321
  { path: path.join(cwd, ".agents", "skills"), source: "project" },
322
- { path: path.join(AGENT_DIR, "skills"), source: "user" },
322
+ { path: path.join(agentDir, "skills"), source: "user" },
323
323
  { path: path.join(os.homedir(), ".agents", "skills"), source: "user" },
324
- ...collectInstalledPackageSkillPaths(cwd),
325
- ...collectSettingsPackageSkillPaths(cwd),
324
+ ...collectInstalledPackageSkillPaths(cwd, agentDir),
325
+ ...collectSettingsPackageSkillPaths(cwd, agentDir),
326
326
  ...extractSkillPathsFromPackageRoot(cwd, "project-package"),
327
- ...collectSettingsSkillPaths(cwd),
327
+ ...collectSettingsSkillPaths(cwd, agentDir),
328
328
  ];
329
329
 
330
330
  const deduped = new Map<string, SkillSearchPath>();
@@ -337,15 +337,16 @@ function buildSkillPaths(cwd: string): SkillSearchPath[] {
337
337
  return [...deduped.values()];
338
338
  }
339
339
 
340
- function inferSkillSource(filePath: string, cwd: string, sourceHint?: SkillSource): SkillSource {
340
+ function inferSkillSource(filePath: string, cwd: string, agentDir: string, sourceHint?: SkillSource): SkillSource {
341
341
  if (sourceHint) return sourceHint;
342
342
 
343
343
  const projectConfigRoot = path.resolve(cwd, CONFIG_DIR);
344
344
  const projectSkillsRoot = path.resolve(cwd, CONFIG_DIR, "skills");
345
345
  const projectPackagesRoot = path.resolve(cwd, CONFIG_DIR, "npm", "node_modules");
346
346
  const projectAgentsRoot = path.resolve(cwd, ".agents");
347
- const userSkillsRoot = path.resolve(AGENT_DIR, "skills");
348
- const userPackagesRoot = path.resolve(AGENT_DIR, "npm", "node_modules");
347
+ const userSkillsRoot = path.resolve(agentDir, "skills");
348
+ const userPackagesRoot = path.resolve(agentDir, "npm", "node_modules");
349
+ const userAgentRoot = path.resolve(agentDir);
349
350
  const userAgentsRoot = path.resolve(os.homedir(), ".agents");
350
351
 
351
352
  if (isWithinPath(filePath, projectPackagesRoot)) return "project-package";
@@ -354,7 +355,7 @@ function inferSkillSource(filePath: string, cwd: string, sourceHint?: SkillSourc
354
355
 
355
356
  if (isWithinPath(filePath, userPackagesRoot)) return "user-package";
356
357
  if (isWithinPath(filePath, userSkillsRoot) || isWithinPath(filePath, userAgentsRoot)) return "user";
357
- if (isWithinPath(filePath, AGENT_DIR)) return "user-settings";
358
+ if (isWithinPath(filePath, userAgentRoot)) return "user-settings";
358
359
 
359
360
  const globalRoot = getGlobalNpmRoot();
360
361
  if (globalRoot && isWithinPath(filePath, globalRoot)) return "user-package";
@@ -390,7 +391,7 @@ function maybeReadSkillDescription(filePath: string): string | undefined {
390
391
  }
391
392
  }
392
393
 
393
- function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): CachedSkillEntry[] {
394
+ function collectFilesystemSkills(cwd: string, agentDir: string, skillPaths: SkillSearchPath[]): CachedSkillEntry[] {
394
395
  const entries: CachedSkillEntry[] = [];
395
396
  const seen = new Set<string>();
396
397
  let order = 0;
@@ -403,7 +404,7 @@ function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): Ca
403
404
  entries.push({
404
405
  name,
405
406
  filePath: resolvedFile,
406
- source: inferSkillSource(resolvedFile, cwd, sourceHint),
407
+ source: inferSkillSource(resolvedFile, cwd, agentDir, sourceHint),
407
408
  description: maybeReadSkillDescription(resolvedFile),
408
409
  order: order++,
409
410
  });
@@ -464,12 +465,13 @@ function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): Ca
464
465
 
465
466
  function getCachedSkills(cwd: string): CachedSkillEntry[] {
466
467
  const now = Date.now();
467
- if (loadSkillsCache && loadSkillsCache.cwd === cwd && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
468
+ const agentDir = getAgentDir();
469
+ if (loadSkillsCache && loadSkillsCache.cwd === cwd && loadSkillsCache.agentDir === agentDir && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
468
470
  return loadSkillsCache.skills;
469
471
  }
470
472
 
471
- const skillPaths = buildSkillPaths(cwd);
472
- const loaded = collectFilesystemSkills(cwd, skillPaths);
473
+ const skillPaths = buildSkillPaths(cwd, agentDir);
474
+ const loaded = collectFilesystemSkills(cwd, agentDir, skillPaths);
473
475
  const dedupedByName = new Map<string, CachedSkillEntry>();
474
476
 
475
477
  for (const entry of loaded) {
@@ -478,7 +480,7 @@ function getCachedSkills(cwd: string): CachedSkillEntry[] {
478
480
  }
479
481
 
480
482
  const skills = [...dedupedByName.values()].sort((a, b) => a.order - b.order);
481
- loadSkillsCache = { cwd, skills, timestamp: now };
483
+ loadSkillsCache = { cwd, agentDir, skills, timestamp: now };
482
484
  return skills;
483
485
  }
484
486
 
@@ -0,0 +1,16 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { ExtensionConfig } from "../shared/types.ts";
4
+ import { getAgentDir } from "../shared/utils.ts";
5
+
6
+ export function loadConfig(): ExtensionConfig {
7
+ const configPath = path.join(getAgentDir(), "extensions", "subagent", "config.json");
8
+ try {
9
+ if (fs.existsSync(configPath)) {
10
+ return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
11
+ }
12
+ } catch (error) {
13
+ console.error(`Failed to load subagent config from '${configPath}':`, error);
14
+ }
15
+ return {};
16
+ }
@@ -22,7 +22,7 @@ import { discoverAgents } from "../agents/agents.ts";
22
22
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
23
23
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
24
24
  import { cleanupOldChainDirs } from "../shared/settings.ts";
25
- import { renderWidget, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, syncResultAnimation } from "../tui/render.ts";
25
+ import { clearLegacyResultAnimationTimer, renderWidget, renderSubagentResult } from "../tui/render.ts";
26
26
  import { SubagentParams } from "./schemas.ts";
27
27
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
28
28
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
@@ -35,9 +35,9 @@ import { inspectSubagentStatus } from "../runs/background/run-status.ts";
35
35
  import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
36
36
  import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
37
37
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
38
+ import { loadConfig } from "./config.ts";
38
39
  import {
39
40
  type Details,
40
- type ExtensionConfig,
41
41
  type SubagentState,
42
42
  ASYNC_DIR,
43
43
  DEFAULT_ARTIFACT_CONFIG,
@@ -56,6 +56,8 @@ import {
56
56
  type SubagentControlMessageDetails,
57
57
  } from "./control-notices.ts";
58
58
 
59
+ export { loadConfig } from "./config.ts";
60
+
59
61
  /**
60
62
  * Derive subagent session base directory from parent session file.
61
63
  * If parent session is ~/.pi/agent/sessions/abc123.jsonl,
@@ -72,18 +74,6 @@ function getSubagentSessionRoot(parentSessionFile: string | null): string {
72
74
  return fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-session-"));
73
75
  }
74
76
 
75
- function loadConfig(): ExtensionConfig {
76
- const configPath = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "config.json");
77
- try {
78
- if (fs.existsSync(configPath)) {
79
- return JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
80
- }
81
- } catch (error) {
82
- console.error(`Failed to load subagent config from '${configPath}':`, error);
83
- }
84
- return {};
85
- }
86
-
87
77
  function expandTilde(p: string): string {
88
78
  return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
89
79
  }
@@ -142,14 +132,11 @@ function createSlashResultComponent(
142
132
  details: SlashMessageDetails,
143
133
  options: { expanded: boolean },
144
134
  theme: ExtensionContext["ui"]["theme"],
145
- requestRender: () => void,
146
135
  ): Container {
147
136
  const container = new Container();
148
- const animationState: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> } = {};
149
137
  let lastVersion = -1;
150
138
  container.render = (width: number): string[] => {
151
139
  const snapshot = getSlashRenderableSnapshot(details);
152
- syncResultAnimation(snapshot.result, { state: animationState, invalidate: requestRender });
153
140
  if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
154
141
  lastVersion = snapshot.version;
155
142
  rebuildSlashResultContainer(container, snapshot.result, options, theme);
@@ -271,8 +258,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
271
258
  primeExistingResults();
272
259
 
273
260
  const runtimeCleanup = () => {
274
- stopWidgetAnimation();
275
- stopResultAnimations();
276
261
  stopResultWatcher();
277
262
  clearPendingForegroundControlNotices(state);
278
263
  if (state.poller) {
@@ -297,7 +282,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
297
282
  pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
298
283
  const details = resolveSlashMessageDetails(message.details);
299
284
  if (!details) return undefined;
300
- return createSlashResultComponent(details, options, theme, () => state.lastUiContext?.ui.requestRender?.());
285
+ return createSlashResultComponent(details, options, theme);
301
286
  });
302
287
 
303
288
  pi.registerMessageRenderer<SubagentNotifyDetails>("subagent-notify", (message, options, theme) => {
@@ -445,7 +430,7 @@ DIAGNOSTICS:
445
430
  }
446
431
  const isParallel = (args.tasks?.length ?? 0) > 0;
447
432
  const parallelCount = effectiveParallelTaskCount(args.tasks as Array<{ count?: unknown }> | undefined);
448
- const asyncLabel = args.async === true && !isParallel ? theme.fg("warning", " [async]") : "";
433
+ const asyncLabel = args.async === true && args.clarify !== true && !isParallel ? theme.fg("warning", " [async]") : "";
449
434
  if (args.chain?.length)
450
435
  return new Text(
451
436
  `${theme.fg("toolTitle", theme.bold("subagent "))}chain (${args.chain.length})${asyncLabel}`,
@@ -466,7 +451,7 @@ DIAGNOSTICS:
466
451
  },
467
452
 
468
453
  renderResult(result, options, theme, context) {
469
- syncResultAnimation(result, context);
454
+ clearLegacyResultAnimationTimer(context);
470
455
  return renderSubagentResult(result, options, theme);
471
456
  },
472
457
 
@@ -514,6 +499,7 @@ DIAGNOSTICS:
514
499
  state.lastUiContext = ctx;
515
500
  if (state.asyncJobs.size > 0) {
516
501
  renderWidget(ctx, Array.from(state.asyncJobs.values()));
502
+ ctx.ui.requestRender?.();
517
503
  ensurePoller();
518
504
  }
519
505
  });
@@ -569,8 +555,6 @@ DIAGNOSTICS:
569
555
  slashBridge.dispose();
570
556
  promptTemplateBridge.cancelAll();
571
557
  promptTemplateBridge.dispose();
572
- stopWidgetAnimation();
573
- stopResultAnimations();
574
558
  if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
575
559
  delete globalStore[runtimeCleanupStoreKey];
576
560
  }
@@ -152,7 +152,7 @@ export const SubagentParams = Type.Object({
152
152
  Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
153
153
  ),
154
154
  // Clarification TUI
155
- clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution (default: true for chains, false for single/parallel). Implies sync mode." })),
155
+ clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution. Explicit clarify: true keeps the run foreground for the clarify UI; omitted clarify can still run in the background when async: true is set." })),
156
156
  control: Type.Optional(ControlOverrides),
157
157
  // Solo agent overrides
158
158
  output: Type.Optional(Type.Unsafe({
@@ -4,12 +4,13 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import type { AgentConfig } from "../agents/agents.ts";
6
6
  import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
7
+ import { getAgentDir } from "../shared/utils.ts";
7
8
 
8
9
  const PI_INTERCOM_PACKAGE_NAME = "pi-intercom";
9
10
  const CONFIG_DIR = ".pi";
10
11
 
11
12
  function defaultAgentDir(): string {
12
- return path.join(os.homedir(), ".pi", "agent");
13
+ return getAgentDir();
13
14
  }
14
15
 
15
16
  function defaultIntercomExtensionDir(agentDir = defaultAgentDir()): string {
@@ -11,13 +11,14 @@ import { createRequire } from "node:module";
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import type { AgentConfig } from "../../agents/agents.ts";
13
13
  import { applyThinkingSuffix } from "../shared/pi-args.ts";
14
- import { injectSingleOutputInstruction, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
14
+ import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
15
15
  import { buildChainInstructions, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
16
16
  import type { RunnerStep } from "../shared/parallel-utils.ts";
17
17
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
18
18
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
19
19
  import { resolveChildCwd } from "../../shared/utils.ts";
20
20
  import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "../shared/model-fallback.ts";
21
+ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
21
22
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
22
23
  import {
23
24
  type ArtifactConfig,
@@ -125,7 +126,7 @@ interface AsyncSingleParams {
125
126
  sessionRoot?: string;
126
127
  sessionFile?: string;
127
128
  skills?: string[];
128
- output?: string | false;
129
+ output?: string | boolean;
129
130
  outputMode?: "inline" | "file-only";
130
131
  modelOverride?: string;
131
132
  availableModels?: AvailableModelInfo[];
@@ -309,17 +310,20 @@ export function executeAsyncChain(
309
310
  const task = injectSingleOutputInstruction(`${readInstructions.prefix}${s.task ?? "{previous}"}${progressInstructions.suffix}`, outputPath);
310
311
 
311
312
  const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
313
+ const model = applyThinkingSuffix(primaryModel, a.thinking);
312
314
  return {
313
315
  agent: s.agent,
314
316
  task,
315
317
  cwd: stepCwd,
316
- model: applyThinkingSuffix(primaryModel, a.thinking),
318
+ model,
319
+ thinking: resolveEffectiveThinking(model, a.thinking),
317
320
  modelCandidates: buildModelCandidates(behavior.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
318
321
  applyThinkingSuffix(candidate, a.thinking),
319
322
  ),
320
323
  tools: a.tools,
321
324
  extensions: a.extensions,
322
325
  mcpDirectTools: a.mcpDirectTools,
326
+ completionGuard: a.completionGuard,
323
327
  systemPrompt,
324
328
  systemPromptMode: a.systemPromptMode,
325
329
  inheritProjectContext: a.inheritProjectContext,
@@ -520,11 +524,16 @@ export function executeAsyncSingle(
520
524
  };
521
525
  }
522
526
 
523
- const outputPath = resolveSingleOutputPath(params.output, ctx.cwd, runnerCwd);
527
+ const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
528
+ const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd);
524
529
  const outputMode = params.outputMode ?? "inline";
525
530
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
526
531
  if (validationError) return formatAsyncStartError("single", validationError);
527
532
  const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath);
533
+ const model = applyThinkingSuffix(
534
+ resolveModelCandidate(params.modelOverride ?? agentConfig.model, availableModels, ctx.currentModelProvider),
535
+ agentConfig.thinking,
536
+ );
528
537
  let spawnResult: { pid?: number; error?: string } = {};
529
538
  try {
530
539
  spawnResult = spawnRunner(
@@ -535,13 +544,15 @@ export function executeAsyncSingle(
535
544
  agent,
536
545
  task: taskWithOutputInstruction,
537
546
  cwd: runnerCwd,
538
- model: applyThinkingSuffix(resolveModelCandidate(params.modelOverride ?? agentConfig.model, availableModels, ctx.currentModelProvider), agentConfig.thinking),
547
+ model,
548
+ thinking: resolveEffectiveThinking(model, agentConfig.thinking),
539
549
  modelCandidates: buildModelCandidates(params.modelOverride ?? agentConfig.model, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
540
550
  applyThinkingSuffix(candidate, agentConfig.thinking),
541
551
  ),
542
552
  tools: agentConfig.tools,
543
553
  extensions: agentConfig.extensions,
544
554
  mcpDirectTools: agentConfig.mcpDirectTools,
555
+ completionGuard: agentConfig.completionGuard,
545
556
  systemPrompt,
546
557
  systemPromptMode: agentConfig.systemPromptMode,
547
558
  inheritProjectContext: agentConfig.inheritProjectContext,
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { renderWidget } from "../../tui/render.ts";
4
+ import { renderWidget, widgetRenderKey } from "../../tui/render.ts";
5
5
  import { formatControlNoticeMessage } from "../shared/subagent-control.ts";
6
6
  import {
7
7
  type AsyncJobState,
@@ -118,7 +118,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
118
118
  return;
119
119
  }
120
120
 
121
+ let widgetChanged = false;
121
122
  for (const job of state.asyncJobs.values()) {
123
+ const widgetStateBefore = widgetRenderKey(job);
122
124
  try {
123
125
  emitNewControlEvents(job);
124
126
  const reconciliation = reconcileAsyncRun(job.asyncDir, {
@@ -153,7 +155,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
153
155
  job.currentStep = status.currentStep ?? job.currentStep;
154
156
  job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
155
157
  job.startedAt = status.startedAt ?? job.startedAt;
156
- job.updatedAt = status.lastUpdate ?? Date.now();
158
+ if (status.lastUpdate !== undefined) job.updatedAt = status.lastUpdate;
157
159
  if (status.steps?.length) {
158
160
  const groups = normalizeParallelGroups(status.parallelGroups, status.steps.length, status.chainStepCount ?? status.steps.length);
159
161
  job.parallelGroups = groups.length ? groups : job.parallelGroups;
@@ -179,21 +181,27 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
179
181
  if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
180
182
  scheduleCleanup(job.asyncId);
181
183
  }
184
+ if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
182
185
  continue;
183
186
  }
184
- job.status = job.status === "queued" ? "running" : job.status;
185
- job.updatedAt = Date.now();
187
+ if (job.status === "queued") {
188
+ job.status = "running";
189
+ job.updatedAt = Date.now();
190
+ }
186
191
  } catch (error) {
187
- console.error(`Failed to read async status for '${job.asyncDir}':`, error);
188
- job.status = "failed";
189
- job.updatedAt = Date.now();
192
+ if (job.status !== "failed") {
193
+ console.error(`Failed to read async status for '${job.asyncDir}':`, error);
194
+ job.status = "failed";
195
+ job.updatedAt = Date.now();
196
+ }
190
197
  if (!state.cleanupTimers.has(job.asyncId)) {
191
198
  scheduleCleanup(job.asyncId);
192
199
  }
193
200
  }
201
+ if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
194
202
  }
195
203
 
196
- if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
204
+ if (widgetChanged && state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
197
205
  }, pollIntervalMs);
198
206
  state.poller.unref?.();
199
207
  };
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { formatDuration, formatTokens, shortenPath } from "../../shared/formatters.ts";
3
+ import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
4
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
5
  import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
6
6
  import { readStatus } from "../../shared/utils.ts";
@@ -25,6 +25,7 @@ interface AsyncRunStepSummary {
25
25
  tokens?: TokenUsage;
26
26
  skills?: string[];
27
27
  model?: string;
28
+ thinking?: string;
28
29
  attemptedModels?: string[];
29
30
  error?: string;
30
31
  }
@@ -160,6 +161,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
160
161
  ...(step.tokens ? { tokens: step.tokens } : {}),
161
162
  ...(step.skills ? { skills: step.skills } : {}),
162
163
  ...(step.model ? { model: step.model } : {}),
164
+ ...(step.thinking ? { thinking: step.thinking } : {}),
163
165
  ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
164
166
  ...(step.error ? { error: step.error } : {}),
165
167
  };
@@ -235,7 +237,8 @@ function formatStepLine(step: AsyncRunStepSummary): string {
235
237
  const parts = [`${step.index + 1}. ${step.agent}`, step.status];
236
238
  const activity = formatActivityFacts(step);
237
239
  if (activity) parts.push(activity);
238
- if (step.model) parts.push(step.model);
240
+ const modelThinking = formatModelThinking(step.model, step.thinking);
241
+ if (modelThinking) parts.push(modelThinking);
239
242
  if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
240
243
  if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
241
244
  return parts.join(" | ");
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
5
+ import { formatModelThinking } from "../../shared/formatters.ts";
5
6
  import { formatActivityLabel } from "../../shared/status-format.ts";
6
7
  import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details } from "../../shared/types.ts";
7
8
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
@@ -142,8 +143,10 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
142
143
  ].filter((line): line is string => Boolean(line));
143
144
  for (const [index, step] of (status.steps ?? []).entries()) {
144
145
  const stepActivityText = step.status === "running" ? formatActivityLabel(step.lastActivityAt, step.activityState) : undefined;
146
+ const modelThinking = formatModelThinking(step.model, step.thinking);
147
+ const modelText = modelThinking ? ` (${modelThinking})` : "";
145
148
  const errorText = step.error ? `, error: ${step.error}` : "";
146
- lines.push(`${stepLineLabel(status, index)}: ${step.agent} ${step.status}${stepActivityText ? `, ${stepActivityText}` : ""}${errorText}`);
149
+ lines.push(`${stepLineLabel(status, index)}: ${step.agent} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${errorText}`);
147
150
  const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
148
151
  if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);
149
152
  if (step.status === "running") {