@ferris1225/pi-subagents 4.1.2 → 4.1.3

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.
@@ -11,7 +11,12 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
11
11
  import { existsSync } from "node:fs";
12
12
  import { rm } from "node:fs/promises";
13
13
  import { resolve } from "node:path";
14
- import { discoverAgents, isWriteCapableAgent, type AgentConfig } from "./agents.ts";
14
+ import {
15
+ discoverAgents,
16
+ isWriteCapableAgent,
17
+ resolveAgentTools,
18
+ type AgentConfig,
19
+ } from "./agents.ts";
15
20
  import { completionTriggersTurn, type CompletionMessageItem } from "./completion.ts";
16
21
  import {
17
22
  DEFAULT_THINKING_LEVEL,
@@ -43,7 +48,7 @@ import {
43
48
  resolveAgentModelRoute,
44
49
  resolveThinkingLevel,
45
50
  } from "./models.ts";
46
- import { monitor, sumUsage } from "./monitor.ts";
51
+ import { monitor, sumUsage, type ContinuationKind } from "./monitor.ts";
47
52
  import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
48
53
  import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
49
54
  import { forkRetainedSession } from "./session-fork.ts";
@@ -185,6 +190,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
185
190
  prompt?: string;
186
191
  worktree?: WorktreeIsolation;
187
192
  forkedFromRunId?: number;
193
+ continuationKind?: ContinuationKind;
188
194
  }
189
195
 
190
196
  interface ResumeReservation {
@@ -225,7 +231,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
225
231
  cwd: string | undefined,
226
232
  isolation: IsolationMode = "shared",
227
233
  existingThread?: SubagentThread,
228
- newObjectiveOnResume = false,
234
+ appendedObjectiveOnResume = false,
229
235
  environment?: DispatchEnvironment,
230
236
  seed?: SessionSeed,
231
237
  resumeReservation?: ResumeReservation,
@@ -239,8 +245,11 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
239
245
  const runCtx = environment?.ctx ?? ctx;
240
246
  const runConfig = environment?.config ?? config;
241
247
  const runAgents = environment?.agents ?? agents;
242
- const agent = runAgents.find((candidate) => candidate.name === agentName);
243
- if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
248
+ const discoveredAgent = runAgents.find((candidate) => candidate.name === agentName);
249
+ if (!discoveredAgent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
250
+ const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
251
+ resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
252
+ const agent = resolveLiveAgentTools(discoveredAgent);
244
253
  if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
245
254
  return {
246
255
  ...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as worker, cleaner, or documenter.`),
@@ -288,6 +297,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
288
297
  const runId = existingThread?.id ?? monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
289
298
  isolation,
290
299
  ...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
300
+ ...(seed?.continuationKind ? { continuationKind: seed.continuationKind } : {}),
291
301
  });
292
302
  const generation = (existingThread?.generation ?? 0) + 1;
293
303
  const pending: SingleResult = {
@@ -302,7 +312,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
302
312
  ...(seed?.forkedFromRunId !== undefined ? { forkedFromRunId: seed.forkedFromRunId } : {}),
303
313
  };
304
314
  if (existingThread) {
305
- monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation);
315
+ monitor.restartRun(runId, agent.name, task, route.agent.model, thinkingLevel, isolation, {
316
+ elapsedMs: existingThread.elapsedMs,
317
+ continuationKind: appendedObjectiveOnResume ? "resume-appended" : "resume-retained",
318
+ });
306
319
  runtime.settledRuns.delete(runId);
307
320
  }
308
321
 
@@ -367,6 +380,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
367
380
  control,
368
381
  generationCompletion: Promise.resolve(),
369
382
  lifecycleVersion: 0,
383
+ elapsedMs: 0,
370
384
  sessionId: seed?.sessionId,
371
385
  sessionDir: seed?.sessionDir,
372
386
  forkedFromRunId: seed?.forkedFromRunId,
@@ -504,6 +518,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
504
518
  });
505
519
  };
506
520
 
521
+ const persistElapsedTime = (): void => {
522
+ thread.elapsedMs = monitor.getElapsedMs(runId) ?? thread.elapsedMs;
523
+ };
524
+
507
525
  thread.park = async (): Promise<"queued" | "active"> => {
508
526
  if (thread.retired) throw new Error(`Run #${runId} was retired by subagent_stop.`);
509
527
  if (thread.lifecycleOperation) throw new Error(`Run #${runId} is already handling ${thread.lifecycleOperation}.`);
@@ -544,7 +562,14 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
544
562
  thread.state = "parked";
545
563
  thread.queueController = undefined;
546
564
  runtime.runControllers.delete(runId);
565
+ const parkedRun = monitor.findRun(runId);
566
+ if (parkedRun?.managedWorkflow && parkedRun.task !== thread.task) {
567
+ // The active child row previously showed this stage objective. Once it
568
+ // disappears, keep the parked parent aligned with what resume retains.
569
+ monitor.setTask(runId, thread.task);
570
+ }
547
571
  monitor.setStatus(runId, "parked");
572
+ persistElapsedTime();
548
573
  return queued ? "queued" : "active";
549
574
  } finally {
550
575
  if (thread.lifecycleVersion === version && thread.lifecycleOperation === "park") {
@@ -822,6 +847,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
822
847
  prompt: forkObjective ?? FORK_CONTINUATION_PROMPT,
823
848
  worktree: childWorktree,
824
849
  forkedFromRunId: runId,
850
+ continuationKind: forkObjective ? "fork-appended" : "fork-retained",
825
851
  },
826
852
  );
827
853
  if (child.exitCode !== -1 || child.runId === undefined) {
@@ -877,6 +903,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
877
903
  {
878
904
  defaultCwd: executionCwd,
879
905
  agent: route.agent,
906
+ resolveAgentForAttempt: resolveLiveAgentTools,
880
907
  agentName,
881
908
  task,
882
909
  cwd: executionCwd,
@@ -891,7 +918,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
891
918
  ? {
892
919
  sessionId: priorSessionId,
893
920
  sessionDir: priorSessionDir,
894
- stdinText: seed?.prompt ?? (newObjectiveOnResume
921
+ stdinText: seed?.prompt ?? (appendedObjectiveOnResume
895
922
  ? task
896
923
  : buildResumePrompt(priorTask ?? task, "the retained thread was resumed")),
897
924
  }
@@ -955,9 +982,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
955
982
  const workflowPlan = getManagedWorkflowPlan(result, runConfig, workflowAvailability);
956
983
  if (workflowPlan && runtime.sessionActive) {
957
984
  thread.state = "running";
958
- // The stable parent row remains active until every internal writer and
959
- // reviewer settles. Internal rows are independently queryable but never
960
- // enter this top-level lifecycle or publish completions.
985
+ // The stable parent row now represents workflow ownership, not whichever
986
+ // model stage ran most recently. Internal rows own their exact role/model/
987
+ // thinking/timing telemetry and remain independently queryable.
988
+ monitor.setManagedWorkflow(runId, true);
961
989
  monitor.setStatus(runId, "running");
962
990
  monitor.setActivity(
963
991
  runId,
@@ -978,8 +1006,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
978
1006
  rememberLatest: (latest) => {
979
1007
  if (runtime.threads.get(runId) !== thread || thread.generation !== generation) return;
980
1008
  thread.lastResult = latest;
1009
+ // Retained control follows the newest child session, but the live parent
1010
+ // row keeps the original top-level role/model/usage. The active internal
1011
+ // row already owns the current stage's role and telemetry.
981
1012
  thread.agentName = latest.agent;
982
- monitor.setAgent(runId, latest.agent);
983
1013
  thread.task = latest.task;
984
1014
  thread.sessionId = latest.sessionId;
985
1015
  thread.sessionDir = latest.sessionDir;
@@ -1041,6 +1071,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
1041
1071
  // Stamp the terminal monitor state before projecting it. This gives every
1042
1072
  // path a fixed endedAt even when the row is removed immediately.
1043
1073
  monitor.setStatus(runId, failed ? "failed" : "done");
1074
+ persistElapsedTime();
1044
1075
  if (!runtime.sessionActive || !ownsSettlement()) return;
1045
1076
 
1046
1077
  const modelLevel = failed && isModelLevelFailure(result);
@@ -1175,6 +1206,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
1175
1206
  if (!ownsSettlement()) return;
1176
1207
  thread.state = "failed";
1177
1208
  monitor.setStatus(runId, "failed");
1209
+ persistElapsedTime();
1178
1210
  finishRun(runId, "failed", { silent: true });
1179
1211
  runtime.registerRunResult(runId, crashed);
1180
1212
  runtime.runControllers.delete(runId);
package/src/tools.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Thread controls and lookup tools around the subagent runtime:
3
- * subagent_control (steer/retarget/park/resume), subagent_wait (in-turn result
4
- * lookup), subagent_status, and destructive subagent_stop.
3
+ * subagent_control (steer/retarget/park/resume/fork), subagent_wait (in-turn
4
+ * result lookup), subagent_status, and destructive subagent_stop.
5
5
  */
6
6
 
7
7
  import { StringEnum } from "@earendil-works/pi-ai";
@@ -12,8 +12,7 @@ import { DEFAULT_MAX_RESULT_LINES, loadConfig } from "./config.ts";
12
12
  import { formatCompletionBlock, formatUsage, matchRunIds } from "./format.ts";
13
13
  import { emptyUsage } from "./rpc-run.ts";
14
14
  import {
15
- formatElapsed,
16
- formatUsageCompact,
15
+ formatTaskSummary,
17
16
  isRunActiveStatus,
18
17
  monitor,
19
18
  runLabel,
@@ -51,7 +50,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
51
50
  Type.String({ description: "Instruction queued by steer after the current child tool batch." }),
52
51
  ),
53
52
  objective: Type.Optional(
54
- Type.String({ description: "Replacement objective for retarget, or optional objective for resume/fork." }),
53
+ Type.String({ description: "Replacement objective for retarget; optional appended objective for resume/fork. Omit on resume/fork to continue the current retained objective." }),
55
54
  ),
56
55
  });
57
56
 
@@ -64,14 +63,14 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
64
63
  "retarget replaces the objective in that same active top-level child.",
65
64
  "Managed downstream documenter/reviewer/fix stages are controlled by the parent queue rather than its settled RPC control: use park or stop there, then resume with an objective to redirect retained context.",
66
65
  "park aborts to a stable checkpoint, terminates the child, preserves context, and releases its concurrency slot.",
67
- "resume restarts a parked, completed, or failed retained thread with the same run id; objective is optional.",
68
- "fork copies a parked/completed/failed retained session branch into a new logical thread and run id; an isolated checkpoint must be settled and integrated first; objective is optional.",
66
+ "resume restarts a parked, completed, or failed retained thread with the same run id and cumulative active time; omit objective to continue the current goal, or provide one to append it to retained context and make it the displayed current goal.",
67
+ "fork copies a parked/completed/failed retained session branch into a new logical thread and run id; an isolated checkpoint must be settled and integrated first; omit objective to continue the current goal, or provide one to append a new branch goal.",
69
68
  ].join(" "),
70
69
  promptSnippet: "Control a subagent thread: steer/retarget an active top-level child; park/stop a managed downstream stage; resume or fork retained context.",
71
70
  promptGuidelines: [
72
71
  "Use subagent_control steer to refine an active top-level RPC child without restarting it; the instruction is delivered after its current tool batch.",
73
72
  "Use subagent_control retarget only while that top-level child is active. During a managed downstream stage, park it and resume with a replacement objective instead.",
74
- "Use subagent_control park to checkpoint useful context while releasing the process/concurrency slot, and resume to continue the same run id later.",
73
+ "Use subagent_control park to checkpoint useful context while releasing the process/concurrency slot, and resume to continue the same run id later. Resume without objective keeps the current goal; resume with objective appends that goal to retained context.",
75
74
  "Use subagent_control fork only on a parked or settled retained thread; isolated work must settle and integrate before it can fork. Fork creates a new run id while leaving the source untouched.",
76
75
  "Use subagent_stop only for destructive cancellation; it retires that thread's retained session without retiring independent forks.",
77
76
  ],
@@ -110,13 +109,15 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
110
109
  thread.task = objective;
111
110
  thread.control.retargetPending(objective);
112
111
  monitor.setTask(thread.id, objective);
113
- return { content: [{ type: "text", text: `Updated queued run #${thread.id} to the new objective; no child was spawned by this control action.` }], details: {} };
112
+ monitor.setContinuationKind(thread.id, "retarget");
113
+ return { content: [{ type: "text", text: `Updated queued run #${thread.id} to the replacement objective; no child was spawned by this control action.` }], details: {} };
114
114
  }
115
115
  if (!(["starting", "running", "steering", "interrupting", "retrying"] as const).includes(phase as any)) {
116
116
  return { content: [{ type: "text", text: `Run #${thread.id} is ${thread.state}; use resume with objective to restart retained context.` }], details: {} };
117
117
  }
118
118
  thread.task = objective;
119
119
  monitor.setTask(thread.id, objective);
120
+ monitor.setContinuationKind(thread.id, "retarget");
120
121
  await thread.control.retarget(objective);
121
122
  return { content: [{ type: "text", text: `Retargeted run #${thread.id} in the same session; the aborted objective will not be delivered as a completion.` }], details: {} };
122
123
  }
@@ -146,11 +147,19 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
146
147
  if (params.objective !== undefined && !objective) {
147
148
  return { content: [{ type: "text", text: "resume objective must be non-blank when provided." }], details: {} };
148
149
  }
150
+ const hadRetainedSession = Boolean(thread.sessionId && thread.sessionDir);
149
151
  const pending = await thread.resume(objective, ctx);
150
152
  if (pending.exitCode !== -1) {
151
153
  return { content: [{ type: "text", text: getResultOutput(pending) }], details: {} };
152
154
  }
153
- return { content: [{ type: "text", text: `Resumed run #${thread.id}${objective ? " with a new objective" : " from retained context"}; completion will arrive automatically.` }], details: {}, terminate: true };
155
+ const currentObjective = formatTaskSummary(objective ?? thread.task, 80, false);
156
+ const mode = objective
157
+ ? `appended objective: ${currentObjective}`
158
+ : `continuing current objective: ${currentObjective}`;
159
+ const context = hadRetainedSession
160
+ ? "the same retained session and prior context are preserved"
161
+ : "no prior child session existed, so only the logical run and objective are continued";
162
+ return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. Completion will arrive automatically.` }], details: {}, terminate: true };
154
163
  }
155
164
  case "fork": {
156
165
  const objective = params.objective === undefined ? undefined : nonBlank(params.objective);
@@ -164,7 +173,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
164
173
  return {
165
174
  content: [{
166
175
  type: "text",
167
- text: `Forked run #${thread.id} into new run #${pending.runId}${objective ? " with a new objective" : " from retained context"}; the source is unchanged and child completion will arrive automatically.`,
176
+ text: `Forked run #${thread.id} into new run #${pending.runId}; ${objective ? `appended branch objective: ${formatTaskSummary(objective, 80, false)}` : `continuing current objective: ${formatTaskSummary(thread.task, 80, false)}`}. Retained context is copied, the source is unchanged, and child completion will arrive automatically.`,
168
177
  }],
169
178
  details: { sourceRunId: thread.id, childRunId: pending.runId, result: pending },
170
179
  terminate: true,
@@ -352,10 +361,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
352
361
  });
353
362
 
354
363
  // Status overview: what is running right now and what finished this session,
355
- // with per-run details (id, agent, model, usage, elapsed, activity) so the
356
- // main agent can decide whether to wait, stop, or re-dispatch. Learned from
357
- // nicobailon/pi-subagents ({action:"status"} + status files): inspect before
358
- // you act, and report run ids when handing off.
364
+ // with per-run details (id, role, model, usage, elapsed, activity).
359
365
  const SubagentStatusParams = Type.Object({
360
366
  id: Type.Optional(
361
367
  Type.String({
@@ -368,7 +374,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
368
374
  name: "subagent_status",
369
375
  label: "Subagent Status",
370
376
  description: [
371
- "List active background sub-agent runs (id, agent, model, usage, elapsed, current activity) and recently finished results.",
377
+ "List active background sub-agent runs (id, role, model, thinking, usage, elapsed, current activity) and recently finished results.",
372
378
  "Pass id to read the full result of a finished run; pass no id for the overview.",
373
379
  "Use it to decide whether to subagent_wait, subagent_stop, or re-dispatch — never to poll: results arrive by themselves.",
374
380
  ].join(" "),
@@ -412,20 +418,30 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
412
418
  const activeThread = runtime.threads.get(active.id);
413
419
  const managedDownstream =
414
420
  activeThread?.state === "running" && activeThread.control.getPhase() === "settled";
421
+ const activeChild = runs.find((run) =>
422
+ run.parentRunId === active.id && isRunActiveStatus(run.status)
423
+ );
424
+ const owner = active.managedWorkflow ? `${active.agent} workflow` : active.agent;
425
+ const retainedStage = active.managedWorkflow && activeThread?.agentName !== active.agent
426
+ ? activeThread?.agentName
427
+ : undefined;
415
428
  const metadata = [
416
429
  activeThread?.isolation === "worktree" ? `worktree ${active.integrationStatus ?? activeThread.worktree?.state ?? "active"}` : undefined,
417
430
  activeThread?.forkedFromRunId !== undefined ? `forked from #${activeThread.forkedFromRunId}` : undefined,
418
431
  (activeThread?.forkChildRunIds.length ?? 0) > 0 ? `forks ${activeThread!.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
419
432
  ].filter(Boolean).join(" · ");
433
+ const stageStatus = activeChild
434
+ ? monitor.summarize(activeChild)
435
+ : active.activity ?? statusLabel(active.status);
420
436
  return {
421
437
  content: [
422
438
  {
423
439
  type: "text",
424
440
  text: parked
425
- ? `Run #${active.id} ${active.agent} is parked with retained context${metadata ? ` (${metadata})` : ""}. Use subagent_control resume to restart it, or subagent_stop to retire it.`
441
+ ? `Run #${active.id} ${owner} is parked with retained${retainedStage ? ` ${retainedStage} stage` : ""} context${metadata ? ` (${metadata})` : ""}. Use subagent_control resume to restart it, or subagent_stop to retire it.`
426
442
  : managedDownstream
427
- ? `Run #${active.id} ${active.agent} is in a managed downstream stage (${active.activity ?? statusLabel(active.status)}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait for its result, subagent_control park to checkpoint it, or subagent_stop to cancel it. Steer/retarget are unavailable until you park and resume the retained stage.`
428
- : `Run #${active.id} ${active.agent} is still active (${active.activity ?? statusLabel(active.status)}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait to block for its result, subagent_control to steer/park it, or subagent_stop to cancel it.`,
443
+ ? `Run #${active.id} ${owner} is in a managed downstream stage (${stageStatus}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait for its result, subagent_control park to checkpoint it, or subagent_stop to cancel it. Steer/retarget are unavailable until you park and resume the retained stage.`
444
+ : `Run #${active.id} ${owner} is still active (${active.activity ?? statusLabel(active.status)}${metadata ? ` · ${metadata}` : ""}). Use subagent_wait to block for its result, subagent_control to steer/park it, or subagent_stop to cancel it.`,
429
445
  },
430
446
  ],
431
447
  details: {},
@@ -434,36 +450,34 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
434
450
  return { content: [{ type: "text", text: `No subagent run matches "${requested}".` }], details: {} };
435
451
  }
436
452
 
437
- const now = Date.now();
438
453
  const activeRuns = monitor.getRuns().filter(
439
454
  (run) => isRunActiveStatus(run.status),
440
455
  );
441
456
  const activeLines = activeRuns.map((run) => {
442
457
  const thread = runtime.threads.get(run.id);
443
- const model = run.modelFallbackFrom
444
- ? `${run.model ?? "?"} (main after ${run.modelFallbackFrom} failed)`
445
- : (run.model ?? "?");
446
458
  const parts = [
447
- `#${run.id} ${run.agent}`,
459
+ `#${run.id} ${monitor.summarize(run)}`,
448
460
  run.label,
449
- model,
450
- formatUsageCompact(run.usage),
451
- formatElapsed(run, now),
452
- thread?.isolation === "worktree" ? `worktree ${run.integrationStatus ?? thread.worktree?.state ?? "active"}` : undefined,
453
461
  thread?.forkedFromRunId !== undefined ? `forked from #${thread.forkedFromRunId}` : undefined,
454
462
  (thread?.forkChildRunIds.length ?? 0) > 0 ? `forks ${thread!.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
463
+ run.activity ?? statusLabel(run.status),
455
464
  ].filter(Boolean);
456
- return `- ${parts.join(" · ")} · ${run.activity ?? statusLabel(run.status)}`;
465
+ return `- ${parts.join(" · ")}`;
457
466
  });
458
467
  const parkedThreads = [...runtime.threads.values()].filter((thread) => thread.state === "parked");
459
468
  const parkedLines = parkedThreads.map((thread) => {
469
+ const run = monitor.findRun(thread.id);
470
+ const owner = run?.managedWorkflow ? `${run.agent} workflow` : run?.agent ?? thread.agentName;
471
+ const retainedStage = run?.managedWorkflow && thread.agentName !== run.agent
472
+ ? ` · retained stage ${thread.agentName}`
473
+ : "";
460
474
  const relations = [
461
475
  thread.forkedFromRunId !== undefined ? `forked from #${thread.forkedFromRunId}` : undefined,
462
476
  thread.forkChildRunIds.length > 0 ? `forks ${thread.forkChildRunIds.map((id) => `#${id}`).join(",")}` : undefined,
463
477
  ].filter(Boolean);
464
478
  const relation = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
465
479
  const isolation = thread.isolation === "worktree" ? ` · worktree ${thread.worktree?.state ?? "active"}` : "";
466
- return `- #${thread.id} ${thread.agentName} · ${runLabel(thread.task)} · parked${thread.sessionDir ? " · context retained" : " · not started"}${isolation}${relation}`;
480
+ return `- #${thread.id} ${owner} · ${run?.label ?? runLabel(thread.task)} · parked${thread.sessionDir ? " · context retained" : " · not started"}${retainedStage}${isolation}${relation}`;
467
481
  });
468
482
  const completed = [...runtime.settledRuns.entries()].slice(-5);
469
483
  const completedLines = completed.map(([id, result]) => {
package/src/widget.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
4
4
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
5
  import {
6
+ continuationLabel,
6
7
  formatElapsed,
7
8
  formatTaskSummary,
8
9
  isRunActiveStatus,
@@ -25,10 +26,22 @@ function compactLine(left: string, right: string, width: number): string {
25
26
  return `${truncateToWidth(left, leftWidth, "…")}${separator}${right}`;
26
27
  }
27
28
 
29
+ /** Keep continuation semantics intact while independently compacting the task. */
30
+ function formatContinuationTask(label: string, task: string, width: number): string {
31
+ if (width <= 0) return "";
32
+ const labelWidth = visibleWidth(label);
33
+ if (labelWidth >= width) return truncateToWidth(label, width, "");
34
+ const separator = " · ";
35
+ const taskWidth = width - labelWidth - visibleWidth(separator);
36
+ if (taskWidth <= 0) return label;
37
+ const summary = formatTaskSummary(task, taskWidth);
38
+ return summary ? `${label}${separator}${summary}` : label;
39
+ }
40
+
28
41
  /** One compact primary line per genuinely active run, plus an optional indented
29
- * activity line. The primary line reserves effective model/thinking and elapsed
30
- * width before truncating the task. Settled and parked threads never appear, so
31
- * elapsed time cannot keep ticking beside a terminal status. */
42
+ * activity line. The primary line reserves stage model/thinking when present and
43
+ * elapsed width before truncating the task. Settled and parked threads never
44
+ * appear, so elapsed time cannot keep ticking beside a terminal status. */
32
45
  function runPrimaryLine(
33
46
  run: RunView,
34
47
  theme: Theme,
@@ -38,22 +51,31 @@ function runPrimaryLine(
38
51
  ): string {
39
52
  const dim = (text: string): string => theme.fg("dim", text);
40
53
  const icon = statusIcon(run.status, theme);
41
- const name = theme.fg("accent", theme.bold(run.agent));
54
+ const displayName = run.managedWorkflow ? `${run.agent} workflow` : run.agent;
55
+ const name = theme.fg("accent", theme.bold(displayName));
42
56
  const identity = `${prefix}${icon} ${name}`;
43
57
  const elapsed = formatElapsed(run, now);
44
58
  // Render only the resolved model id plus thinking level. Provider auth and
45
59
  // other configuration never enter monitor state or this line.
46
60
  const modelId = run.model?.split("/").at(-1);
47
- const modelSource = formatTaskSummary(
48
- modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
49
- 64,
50
- false,
51
- );
61
+ const modelSource = run.managedWorkflow
62
+ ? ""
63
+ : formatTaskSummary(
64
+ modelId ? `${modelId}${run.thinking ? `/${run.thinking}` : ""}` : run.thinking ? `thinking:${run.thinking}` : "",
65
+ 64,
66
+ false,
67
+ );
52
68
  // A chain child shows its role in the chain plus a task-derived label; the
53
69
  // templated fix brief itself would only repeat the parent review's content.
70
+ const continuation = run.parentRunId === undefined
71
+ ? continuationLabel(run.continuationKind, run.forkedFromRunId)
72
+ : undefined;
54
73
  const taskSource = run.parentRunId !== undefined
55
74
  ? [run.relationLabel, run.label].filter((part): part is string => Boolean(part)).join(" · ")
56
75
  : formatTaskSummary(run.task, 64);
76
+ const taskDesiredSource = [continuation, taskSource]
77
+ .filter((part): part is string => Boolean(part))
78
+ .join(" · ");
57
79
  const primaryPartCount = 2 + (modelSource ? 1 : 0) + (elapsed ? 1 : 0);
58
80
  const contentWidth = Math.max(
59
81
  0,
@@ -64,15 +86,40 @@ function runPrimaryLine(
64
86
  );
65
87
  const modelDesired = visibleWidth(modelSource);
66
88
  const modelFloor = Math.min(modelDesired, Math.min(12, contentWidth));
67
- let modelWidth = modelSource
68
- ? Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8))
69
- : 0;
89
+ let modelWidth = 0;
90
+ if (modelSource) {
91
+ if (continuation) {
92
+ const continuationWidth = visibleWidth(continuation);
93
+ // Preserve the full semantic label and the usual eight-column task tail
94
+ // before giving the remaining space to model/thinking.
95
+ const taskFloor = Math.min(
96
+ visibleWidth(taskDesiredSource),
97
+ continuationWidth +
98
+ (taskSource ? visibleWidth(" · ") + Math.min(8, visibleWidth(taskSource)) : 0),
99
+ contentWidth,
100
+ );
101
+ const effectiveModelFloor = Math.min(
102
+ modelFloor,
103
+ Math.max(0, contentWidth - continuationWidth),
104
+ );
105
+ modelWidth = Math.min(
106
+ modelDesired,
107
+ Math.max(effectiveModelFloor, contentWidth - taskFloor),
108
+ );
109
+ } else {
110
+ modelWidth = Math.min(modelDesired, Math.max(modelFloor, contentWidth - 8));
111
+ }
112
+ }
70
113
  let taskWidth = contentWidth - modelWidth;
71
- if (visibleWidth(taskSource) < taskWidth) {
72
- modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskSource));
114
+ if (visibleWidth(taskDesiredSource) < taskWidth) {
115
+ modelWidth = Math.min(modelDesired, modelWidth + taskWidth - visibleWidth(taskDesiredSource));
73
116
  taskWidth = contentWidth - modelWidth;
74
117
  }
75
- const task = taskWidth > 0 ? formatTaskSummary(taskSource, taskWidth, run.parentRunId === undefined) : "";
118
+ const task = taskWidth <= 0
119
+ ? ""
120
+ : continuation
121
+ ? formatContinuationTask(continuation, taskSource, taskWidth)
122
+ : formatTaskSummary(taskSource, taskWidth, run.parentRunId === undefined);
76
123
  const modelThinking = modelWidth > 0 ? formatTaskSummary(modelSource, modelWidth, false) : "";
77
124
  const primaryLeft = [
78
125
  identity,
@@ -94,9 +141,8 @@ function runActivityLine(run: RunView, theme: Theme, width: number, indent: stri
94
141
  }
95
142
 
96
143
  /** Render active runs as a tree: main-agent dispatches are roots and managed
97
- * documenter/reviewer/fix steps nest under the stable parent row. No run ids
98
- * appear here the tree and the task label say what each row is, and ids stay
99
- * available through subagent_status when a thread must be controlled. */
144
+ * documenter/reviewer/fix steps nest under the stable parent row. Fork labels
145
+ * include their source id; other control ids remain available through status. */
100
146
  export function formatActiveRunLines(
101
147
  runs: readonly RunView[],
102
148
  theme: Theme,
@@ -134,7 +180,7 @@ export function formatActiveRunLines(
134
180
 
135
181
  function hasTickingRun(): boolean {
136
182
  return monitor.getRuns().some(
137
- (run) => isRunActiveStatus(run.status) && run.startedAt !== undefined,
183
+ (run) => isRunActiveStatus(run.status) && run.activeSince !== undefined,
138
184
  );
139
185
  }
140
186