@bastani/atomic 0.8.21-0 → 0.8.22-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.
Files changed (235) hide show
  1. package/CHANGELOG.md +46 -9
  2. package/dist/builtin/intercom/broker/broker.ts +3 -3
  3. package/dist/builtin/intercom/config.ts +3 -3
  4. package/dist/builtin/intercom/index.ts +1 -1
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/intercom/ui/compose.ts +2 -2
  7. package/dist/builtin/mcp/host-html-template.ts +0 -3
  8. package/dist/builtin/mcp/package.json +1 -1
  9. package/dist/builtin/subagents/CHANGELOG.md +13 -4
  10. package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
  11. package/dist/builtin/subagents/agents/debugger.md +6 -6
  12. package/dist/builtin/subagents/package.json +1 -1
  13. package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
  14. package/dist/builtin/subagents/skills/browser-use/SKILL.md +234 -0
  15. package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +76 -0
  16. package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +92 -0
  17. package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
  18. package/dist/builtin/subagents/src/agents/skills.ts +19 -1
  19. package/dist/builtin/subagents/src/extension/index.ts +24 -22
  20. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +7 -1
  21. package/dist/builtin/subagents/src/runs/background/async-execution.ts +23 -7
  22. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +98 -3
  23. package/dist/builtin/subagents/src/runs/background/async-status.ts +3 -1
  24. package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -1
  25. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +3 -0
  26. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +37 -12
  27. package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +15 -15
  28. package/dist/builtin/subagents/src/runs/foreground/execution.ts +26 -2
  29. package/dist/builtin/subagents/src/runs/shared/nested-render.ts +1 -1
  30. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +7 -0
  31. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +28 -1
  32. package/dist/builtin/subagents/src/shared/fast-mode.ts +80 -0
  33. package/dist/builtin/subagents/src/shared/formatters.ts +4 -2
  34. package/dist/builtin/subagents/src/shared/types.ts +4 -2
  35. package/dist/builtin/subagents/src/shared/utils.ts +3 -61
  36. package/dist/builtin/subagents/src/tui/render.ts +303 -157
  37. package/dist/builtin/web-access/package.json +1 -1
  38. package/dist/builtin/workflows/CHANGELOG.md +101 -35
  39. package/dist/builtin/workflows/README.md +228 -41
  40. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +535 -541
  41. package/dist/builtin/workflows/builtin/goal.ts +39 -25
  42. package/dist/builtin/workflows/builtin/open-claude-design.ts +66 -69
  43. package/dist/builtin/workflows/builtin/ralph.ts +21 -21
  44. package/dist/builtin/workflows/package.json +6 -5
  45. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
  46. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +2 -2
  47. package/dist/builtin/workflows/src/extension/discovery.ts +25 -146
  48. package/dist/builtin/workflows/src/extension/dispatcher.ts +72 -24
  49. package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +363 -0
  50. package/dist/builtin/workflows/src/extension/index.ts +690 -352
  51. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +99 -62
  52. package/dist/builtin/workflows/src/extension/render-call.ts +2 -1
  53. package/dist/builtin/workflows/src/extension/render-result.ts +9 -3
  54. package/dist/builtin/workflows/src/extension/renderers.ts +5 -3
  55. package/dist/builtin/workflows/src/extension/runtime.ts +68 -33
  56. package/dist/builtin/workflows/src/extension/status-writer.ts +1 -1
  57. package/dist/builtin/workflows/src/extension/wiring.ts +34 -13
  58. package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +142 -0
  59. package/dist/builtin/workflows/src/extension/workflow-schema.ts +4 -4
  60. package/dist/builtin/workflows/src/index.ts +2 -0
  61. package/dist/builtin/workflows/src/intercom/result-intercom.ts +1 -1
  62. package/dist/builtin/workflows/src/runs/background/runner.ts +6 -4
  63. package/dist/builtin/workflows/src/runs/background/status.ts +45 -21
  64. package/dist/builtin/workflows/src/runs/foreground/executor.ts +624 -52
  65. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +1 -1
  66. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +80 -24
  67. package/dist/builtin/workflows/src/runs/shared/validate-inputs.ts +61 -24
  68. package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +32 -10
  69. package/dist/builtin/workflows/src/sdk-surface.ts +6 -0
  70. package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +178 -0
  71. package/dist/builtin/workflows/src/shared/persistence-restore.ts +92 -12
  72. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +21 -3
  73. package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +1 -2
  74. package/dist/builtin/workflows/src/shared/run-visibility.ts +9 -0
  75. package/dist/builtin/workflows/src/shared/schema-introspection.ts +121 -0
  76. package/dist/builtin/workflows/src/shared/serializable.ts +132 -0
  77. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +91 -9
  78. package/dist/builtin/workflows/src/shared/store-types.ts +31 -3
  79. package/dist/builtin/workflows/src/shared/store.ts +58 -14
  80. package/dist/builtin/workflows/src/shared/types.ts +105 -40
  81. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +129 -13
  82. package/dist/builtin/workflows/src/tui/chat-surface.ts +6 -1
  83. package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +3 -2
  84. package/dist/builtin/workflows/src/tui/graph-canvas.ts +1 -1
  85. package/dist/builtin/workflows/src/tui/graph-view.ts +91 -65
  86. package/dist/builtin/workflows/src/tui/inline-form-card.ts +1 -1
  87. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +3 -2
  88. package/dist/builtin/workflows/src/tui/inputs-overlay.ts +3 -2
  89. package/dist/builtin/workflows/src/tui/inputs-picker.ts +8 -7
  90. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
  91. package/dist/builtin/workflows/src/tui/node-card.ts +34 -8
  92. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -11
  93. package/dist/builtin/workflows/src/tui/prompt-card.ts +98 -50
  94. package/dist/builtin/workflows/src/tui/session-list.ts +7 -2
  95. package/dist/builtin/workflows/src/tui/session-picker.ts +2 -0
  96. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +226 -55
  97. package/dist/builtin/workflows/src/tui/status-helpers.ts +2 -0
  98. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +37 -158
  99. package/dist/builtin/workflows/src/tui/toast.ts +2 -2
  100. package/dist/builtin/workflows/src/tui/widget.ts +53 -12
  101. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +270 -19
  102. package/dist/builtin/workflows/src/tui/workflow-notice-card.ts +184 -0
  103. package/dist/builtin/workflows/src/workflows/define-workflow.ts +138 -43
  104. package/dist/config.d.ts +9 -0
  105. package/dist/config.d.ts.map +1 -1
  106. package/dist/config.js +45 -0
  107. package/dist/config.js.map +1 -1
  108. package/dist/core/agent-session.d.ts +27 -9
  109. package/dist/core/agent-session.d.ts.map +1 -1
  110. package/dist/core/agent-session.js +196 -17
  111. package/dist/core/agent-session.js.map +1 -1
  112. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  113. package/dist/core/atomic-guide-command.js +2 -2
  114. package/dist/core/atomic-guide-command.js.map +1 -1
  115. package/dist/core/codex-fast-mode.d.ts +36 -0
  116. package/dist/core/codex-fast-mode.d.ts.map +1 -0
  117. package/dist/core/codex-fast-mode.js +117 -0
  118. package/dist/core/codex-fast-mode.js.map +1 -0
  119. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  120. package/dist/core/compaction/branch-summarization.js +1 -1
  121. package/dist/core/compaction/branch-summarization.js.map +1 -1
  122. package/dist/core/compaction/compaction.d.ts.map +1 -1
  123. package/dist/core/compaction/compaction.js +1 -1
  124. package/dist/core/compaction/compaction.js.map +1 -1
  125. package/dist/core/extensions/index.d.ts +4 -1
  126. package/dist/core/extensions/index.d.ts.map +1 -1
  127. package/dist/core/extensions/index.js +1 -0
  128. package/dist/core/extensions/index.js.map +1 -1
  129. package/dist/core/extensions/loader.d.ts +7 -2
  130. package/dist/core/extensions/loader.d.ts.map +1 -1
  131. package/dist/core/extensions/loader.js +23 -8
  132. package/dist/core/extensions/loader.js.map +1 -1
  133. package/dist/core/extensions/reactive-widget.d.ts +58 -0
  134. package/dist/core/extensions/reactive-widget.d.ts.map +1 -0
  135. package/dist/core/extensions/reactive-widget.js +182 -0
  136. package/dist/core/extensions/reactive-widget.js.map +1 -0
  137. package/dist/core/extensions/runner.d.ts.map +1 -1
  138. package/dist/core/extensions/runner.js +1 -0
  139. package/dist/core/extensions/runner.js.map +1 -1
  140. package/dist/core/extensions/types.d.ts +26 -12
  141. package/dist/core/extensions/types.d.ts.map +1 -1
  142. package/dist/core/extensions/types.js.map +1 -1
  143. package/dist/core/messages.d.ts +1 -1
  144. package/dist/core/messages.d.ts.map +1 -1
  145. package/dist/core/messages.js +8 -2
  146. package/dist/core/messages.js.map +1 -1
  147. package/dist/core/model-registry.d.ts +4 -0
  148. package/dist/core/model-registry.d.ts.map +1 -1
  149. package/dist/core/model-registry.js +11 -0
  150. package/dist/core/model-registry.js.map +1 -1
  151. package/dist/core/resource-loader.d.ts +9 -1
  152. package/dist/core/resource-loader.d.ts.map +1 -1
  153. package/dist/core/resource-loader.js +49 -21
  154. package/dist/core/resource-loader.js.map +1 -1
  155. package/dist/core/sdk.d.ts.map +1 -1
  156. package/dist/core/sdk.js +22 -13
  157. package/dist/core/sdk.js.map +1 -1
  158. package/dist/core/session-manager.d.ts +7 -5
  159. package/dist/core/session-manager.d.ts.map +1 -1
  160. package/dist/core/session-manager.js +5 -3
  161. package/dist/core/session-manager.js.map +1 -1
  162. package/dist/core/settings-manager.d.ts +16 -0
  163. package/dist/core/settings-manager.d.ts.map +1 -1
  164. package/dist/core/settings-manager.js +64 -5
  165. package/dist/core/settings-manager.js.map +1 -1
  166. package/dist/core/slash-commands.d.ts.map +1 -1
  167. package/dist/core/slash-commands.js +1 -0
  168. package/dist/core/slash-commands.js.map +1 -1
  169. package/dist/core/system-prompt.d.ts.map +1 -1
  170. package/dist/core/system-prompt.js +7 -4
  171. package/dist/core/system-prompt.js.map +1 -1
  172. package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
  173. package/dist/core/tools/ask-user-question/ask-user-question.js +2 -2
  174. package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
  175. package/dist/index.d.ts +4 -3
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +3 -2
  178. package/dist/index.js.map +1 -1
  179. package/dist/main.d.ts +3 -0
  180. package/dist/main.d.ts.map +1 -1
  181. package/dist/main.js +12 -0
  182. package/dist/main.js.map +1 -1
  183. package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -1
  184. package/dist/modes/interactive/chat-input-actions.js.map +1 -1
  185. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  186. package/dist/modes/interactive/components/diff.js +0 -1
  187. package/dist/modes/interactive/components/diff.js.map +1 -1
  188. package/dist/modes/interactive/components/fast-mode-selector.d.ts +27 -0
  189. package/dist/modes/interactive/components/fast-mode-selector.d.ts.map +1 -0
  190. package/dist/modes/interactive/components/fast-mode-selector.js +105 -0
  191. package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -0
  192. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  193. package/dist/modes/interactive/components/footer.js +7 -12
  194. package/dist/modes/interactive/components/footer.js.map +1 -1
  195. package/dist/modes/interactive/components/index.d.ts +1 -0
  196. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  197. package/dist/modes/interactive/components/index.js +1 -0
  198. package/dist/modes/interactive/components/index.js.map +1 -1
  199. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  200. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  201. package/dist/modes/interactive/interactive-mode.js +132 -30
  202. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  203. package/dist/modes/print-mode.d.ts.map +1 -1
  204. package/dist/modes/print-mode.js +53 -6
  205. package/dist/modes/print-mode.js.map +1 -1
  206. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  207. package/dist/modes/rpc/rpc-mode.js +3 -0
  208. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  209. package/docs/compaction.md +1 -1
  210. package/docs/custom-provider.md +2 -2
  211. package/docs/development.md +2 -2
  212. package/docs/docs.json +2 -2
  213. package/docs/extensions.md +18 -13
  214. package/docs/providers.md +5 -1
  215. package/docs/quickstart.md +5 -3
  216. package/docs/rpc.md +5 -5
  217. package/docs/sdk.md +12 -12
  218. package/docs/settings.md +18 -0
  219. package/docs/themes.md +6 -6
  220. package/docs/tui.md +20 -18
  221. package/docs/usage.md +2 -0
  222. package/docs/workflows.md +403 -39
  223. package/examples/extensions/qna.ts +2 -2
  224. package/package.json +4 -4
  225. package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +0 -392
  226. package/dist/builtin/subagents/skills/playwright-cli/references/element-attributes.md +0 -23
  227. package/dist/builtin/subagents/skills/playwright-cli/references/playwright-tests.md +0 -39
  228. package/dist/builtin/subagents/skills/playwright-cli/references/request-mocking.md +0 -87
  229. package/dist/builtin/subagents/skills/playwright-cli/references/running-code.md +0 -241
  230. package/dist/builtin/subagents/skills/playwright-cli/references/session-management.md +0 -225
  231. package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
  232. package/dist/builtin/subagents/skills/playwright-cli/references/storage-state.md +0 -275
  233. package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +0 -134
  234. package/dist/builtin/subagents/skills/playwright-cli/references/tracing.md +0 -139
  235. package/dist/builtin/subagents/skills/playwright-cli/references/video-recording.md +0 -143
@@ -119,11 +119,11 @@ function runningSeed(...values: Array<number | undefined>): number | undefined {
119
119
  return seed;
120
120
  }
121
121
 
122
- function runningGlyph(seed?: number): string {
122
+ function runningGlyph(seed?: number, now?: number): string {
123
123
  // Fold the wall-clock frame into the (optional) progress seed so the glyph
124
- // advances over time. The frame is always finite, so a running entity always
125
- // animates; the seed only offsets its starting phase between concurrent agents.
126
- const animatedSeed = runningSeed(seed, currentRunningFrame()) ?? 0;
124
+ // advances over time. Callers that render into chat scrollback can pass a
125
+ // captured `now` so host re-renders do not mutate already-emitted lines.
126
+ const animatedSeed = runningSeed(seed, currentRunningFrame(now)) ?? 0;
127
127
  return RUNNING_FRAMES[Math.abs(animatedSeed) % RUNNING_FRAMES.length]!;
128
128
  }
129
129
 
@@ -140,83 +140,59 @@ function progressRunningSeed(progress: ProgressSeedSource | undefined): number |
140
140
  );
141
141
  }
142
142
 
143
- type ResultAnimationState = { subagentResultAnimationTimer?: ReturnType<typeof setInterval> };
143
+ type ResultAnimationTimer = ReturnType<typeof setInterval>;
144
144
 
145
- interface ResultAnimationContext {
146
- state: ResultAnimationState;
147
- invalidate: () => void;
145
+ export interface SubagentResultRenderState {
146
+ subagentResultAnimationTimer?: ResultAnimationTimer;
147
+ subagentResultSnapshotKey?: string;
148
+ /** Stable semantic/content timestamp used for durations and activity text. */
149
+ subagentResultSnapshotNow?: number;
150
+ /** Timer-driven timestamp used only for spinner glyph frames. */
151
+ subagentResultSpinnerFrameNow?: number;
148
152
  }
149
153
 
150
- type LegacyResultAnimationContext = { state: ResultAnimationState };
151
- type ResultAnimationEntry = ResultAnimationContext;
152
-
153
- // Registry of every live result-animation timer so they can be torn down in one
154
- // shot on reload/shutdown even if their owning render context never re-renders.
155
- // Each tick reads the latest `invalidate` from here so a re-sync can refresh the
156
- // callback if the host ever swaps render contexts for the same renderable.
157
- const resultAnimationTimers = new Map<ReturnType<typeof setInterval>, ResultAnimationEntry>();
154
+ type ResultAnimationContext = {
155
+ state: SubagentResultRenderState;
156
+ invalidate: () => void;
157
+ };
158
+ type LegacyResultAnimationContext = { state: { subagentResultAnimationTimer?: ResultAnimationTimer } };
158
159
 
159
- function resultIsRunning(result: AgentToolResult<Details>): boolean {
160
- return Boolean(
161
- result.details?.progress?.some((entry) => entry.status === "running")
162
- || result.details?.results.some((entry) => entry.progress?.status === "running"),
163
- );
164
- }
160
+ const activeResultAnimationTimers = new Map<ResultAnimationTimer, SubagentResultRenderState>();
165
161
 
166
- function stopResultAnimation(context: LegacyResultAnimationContext): void {
162
+ export function clearResultAnimationTimer(context: LegacyResultAnimationContext): void {
167
163
  const timer = context.state.subagentResultAnimationTimer;
168
- if (!timer) return;
169
- clearInterval(timer);
170
- resultAnimationTimers.delete(timer);
164
+ if (timer) {
165
+ clearInterval(timer);
166
+ activeResultAnimationTimers.delete(timer);
167
+ }
171
168
  context.state.subagentResultAnimationTimer = undefined;
172
169
  }
173
170
 
174
171
  export function clearLegacyResultAnimationTimer(context: LegacyResultAnimationContext): void {
175
- stopResultAnimation(context);
172
+ clearResultAnimationTimer(context);
176
173
  }
177
174
 
178
- /**
179
- * Keep a running subagent result's spinner animating by scheduling a steady
180
- * re-render while it is active, and tearing the timer down once it settles.
181
- * The timer only calls `context.invalidate()`; the glyph value itself comes
182
- * from {@link currentRunningFrame}, so each tick produces a single-glyph diff.
183
- */
184
- export function syncResultAnimation(result: AgentToolResult<Details>, context: ResultAnimationContext): void {
185
- if (!resultIsRunning(result)) {
186
- stopResultAnimation(context);
187
- return;
188
- }
189
- const existing = context.state.subagentResultAnimationTimer;
190
- if (existing) {
191
- // Keep using the most recent invalidate in case the host handed us a fresh
192
- // render context object on this re-sync.
193
- const entry = resultAnimationTimers.get(existing);
194
- if (entry) entry.invalidate = context.invalidate;
195
- return;
196
- }
175
+ export function ensureResultAnimation(context: ResultAnimationContext): void {
176
+ if (context.state.subagentResultAnimationTimer) return;
197
177
  const timer = setInterval(() => {
198
- const entry = resultAnimationTimers.get(timer);
199
- if (!entry) return;
178
+ context.state.subagentResultSpinnerFrameNow = Date.now();
200
179
  try {
201
- entry.invalidate();
180
+ context.invalidate();
202
181
  } catch {
203
- // A cosmetic spinner tick must never crash the host (e.g. a stale extension
204
- // context after reload/session swap, or any other render glitch). Stop this
205
- // timer; the next real render re-syncs and restarts it while still running.
206
- stopResultAnimation(context);
182
+ clearResultAnimationTimer(context);
207
183
  }
208
184
  }, RUNNING_ANIMATION_MS);
209
185
  timer.unref?.();
210
186
  context.state.subagentResultAnimationTimer = timer;
211
- resultAnimationTimers.set(timer, { state: context.state, invalidate: context.invalidate });
187
+ activeResultAnimationTimers.set(timer, context.state);
212
188
  }
213
189
 
214
190
  export function stopResultAnimations(): void {
215
- for (const [timer, entry] of resultAnimationTimers) {
191
+ for (const [timer, state] of activeResultAnimationTimers) {
216
192
  clearInterval(timer);
217
- entry.state.subagentResultAnimationTimer = undefined;
193
+ if (state.subagentResultAnimationTimer === timer) state.subagentResultAnimationTimer = undefined;
218
194
  }
219
- resultAnimationTimers.clear();
195
+ activeResultAnimationTimers.clear();
220
196
  }
221
197
 
222
198
  function extractOutputTarget(task: string): string | undefined {
@@ -247,9 +223,11 @@ function getToolCallLines(
247
223
  }
248
224
 
249
225
 
250
- function snapshotNowForProgress(progress: Pick<AgentProgress, "currentToolStartedAt" | "durationMs" | "lastActivityAt">): number | undefined {
226
+ function snapshotNowForProgress(progress: Pick<AgentProgress, "currentToolStartedAt" | "durationMs" | "lastActivityAt">, now?: number): number | undefined {
227
+ if (now !== undefined) return now;
228
+ if (progress.lastActivityAt !== undefined) return progress.lastActivityAt;
251
229
  if (progress.currentToolStartedAt !== undefined && progress.durationMs !== undefined) return progress.currentToolStartedAt + progress.durationMs;
252
- return progress.lastActivityAt;
230
+ return undefined;
253
231
  }
254
232
 
255
233
  function formatCurrentToolLine(
@@ -297,12 +275,25 @@ function formatToolUseStat(count: number): string {
297
275
  return `${count} tool use${count === 1 ? "" : "s"}`;
298
276
  }
299
277
 
300
- function formatProgressStats(theme: Theme, progress: Pick<AgentProgress, "toolCount" | "tokens" | "durationMs"> | undefined, includeDuration = true): string {
278
+ function displayProgressDurationMs(progress: Pick<AgentProgress, "durationMs"> & Partial<Pick<AgentProgress, "lastActivityAt" | "status">>, now?: number): number {
279
+ if (progress.status === "running" && progress.lastActivityAt !== undefined && now !== undefined) {
280
+ return progress.durationMs + Math.max(0, now - progress.lastActivityAt);
281
+ }
282
+ return progress.durationMs;
283
+ }
284
+
285
+ function formatProgressStats(
286
+ theme: Theme,
287
+ progress: (Pick<AgentProgress, "toolCount" | "tokens" | "durationMs"> & Partial<Pick<AgentProgress, "lastActivityAt" | "status">>) | undefined,
288
+ includeDuration = true,
289
+ now?: number,
290
+ ): string {
301
291
  if (!progress) return "";
302
292
  const parts: string[] = [];
303
293
  if (progress.toolCount > 0) parts.push(formatToolUseStat(progress.toolCount));
304
294
  if (progress.tokens > 0) parts.push(formatTokenStat(progress.tokens));
305
- if (includeDuration && progress.durationMs > 0) parts.push(formatDuration(progress.durationMs));
295
+ const durationMs = displayProgressDurationMs(progress, now);
296
+ if (includeDuration && durationMs > 0) parts.push(formatDuration(durationMs));
306
297
  return statJoin(theme, parts);
307
298
  }
308
299
 
@@ -318,8 +309,8 @@ function resultStatusLine(result: Details["results"][number], output: string): s
318
309
  return "Done";
319
310
  }
320
311
 
321
- function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary)): string {
322
- if (running) return theme.fg("accent", runningGlyph(seed));
312
+ function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary), now?: number): string {
313
+ if (running) return theme.fg("accent", runningGlyph(seed, now));
323
314
  if (result.detached) return theme.fg("warning", "■");
324
315
  if (result.interrupted) return theme.fg("warning", "■");
325
316
  if (result.exitCode !== 0) return theme.fg("error", "✗");
@@ -327,8 +318,8 @@ function resultGlyph(result: Details["results"][number], output: string, theme:
327
318
  return theme.fg("success", "✓");
328
319
  }
329
320
 
330
- function compactCurrentActivity(progress: AgentProgress): string {
331
- const snapshotNow = snapshotNowForProgress(progress);
321
+ function compactCurrentActivity(progress: AgentProgress, now?: number): string {
322
+ const snapshotNow = snapshotNowForProgress(progress, now);
332
323
  return formatCurrentToolLine(progress, getTermWidth() - 4, false, snapshotNow) ?? buildLiveStatusLine(progress, snapshotNow) ?? "thinking…";
333
324
  }
334
325
 
@@ -432,16 +423,16 @@ function widgetJobsRunningSeed(jobs: AsyncJobState[]): number | undefined {
432
423
  return seed;
433
424
  }
434
425
 
435
- function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
436
- if (job.status === "running") return theme.fg("accent", runningGlyph(widgetJobRunningSeed(job)));
426
+ function widgetStatusGlyph(job: AsyncJobState, theme: Theme, now?: number): string {
427
+ if (job.status === "running") return theme.fg("accent", runningGlyph(widgetJobRunningSeed(job), now));
437
428
  if (job.status === "queued") return theme.fg("muted", "◦");
438
429
  if (job.status === "complete") return theme.fg("success", "✓");
439
430
  if (job.status === "paused") return theme.fg("warning", "■");
440
431
  return theme.fg("error", "✗");
441
432
  }
442
433
 
443
- function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number): string {
444
- if (status === "running") return theme.fg("accent", runningGlyph(seed));
434
+ function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number, now?: number): string {
435
+ if (status === "running") return theme.fg("accent", runningGlyph(seed, now));
445
436
  if (status === "complete" || status === "completed") return theme.fg("success", "✓");
446
437
  if (status === "failed") return theme.fg("error", "✗");
447
438
  if (status === "paused") return theme.fg("warning", "■");
@@ -471,7 +462,7 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], s
471
462
  }
472
463
 
473
464
 
474
- function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
465
+ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), now?: number): string[] {
475
466
  if (!job.steps?.length) return [];
476
467
  const total = job.chainStepCount ?? job.steps.length;
477
468
  const lines: string[] = [];
@@ -479,7 +470,7 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
479
470
  const steps = job.steps.slice(span.start, span.start + span.count);
480
471
  if (span.isParallel) {
481
472
  const status = aggregateStepStatus(steps);
482
- lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps))} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
473
+ lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps), now)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
483
474
  continue;
484
475
  }
485
476
  const step = steps[0];
@@ -487,24 +478,24 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
487
478
  lines.push(` ${theme.fg("dim", `◦ Step ${span.stepIndex + 1}/${total}: pending`)}`);
488
479
  continue;
489
480
  }
490
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width));
481
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width, now));
491
482
  }
492
483
  return lines;
493
484
  }
494
485
 
495
- function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
486
+ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), now?: number): string[] {
496
487
  if (!job.steps?.length) return [];
497
488
  if (job.mode !== "parallel" && job.mode !== "chain") return [];
498
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
489
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, now);
499
490
  const total = job.stepsTotal ?? job.steps.length;
500
491
  const lines: string[] = [];
501
492
  for (const [index, step] of job.steps.entries()) {
502
493
  const marker = index === job.steps.length - 1 ? "└" : "├";
503
494
  const activity = widgetStepActivity(step, job.updatedAt);
504
495
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
505
- const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
506
- lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
507
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1)) lines.push(` ${nestedLine}`);
496
+ const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
497
+ lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), now)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
498
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1, now)) lines.push(` ${nestedLine}`);
508
499
  }
509
500
  return lines;
510
501
  }
@@ -723,8 +714,8 @@ function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>
723
714
  ]);
724
715
  }
725
716
 
726
- function modelThinkingBadge(theme: Theme, model?: string, thinking?: string): string {
727
- const label = formatModelThinking(model, thinking);
717
+ function modelThinkingBadge(theme: Theme, model?: string, thinking?: string, fastMode?: boolean): string {
718
+ const label = formatModelThinking(model, thinking, fastMode);
728
719
  return label ? theme.fg("dim", ` (${label})`) : "";
729
720
  }
730
721
 
@@ -748,8 +739,8 @@ function nestedRunName(run: NestedRunSummary): string {
748
739
  return run.id;
749
740
  }
750
741
 
751
- function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary["status"], theme: Theme, seed?: number): string {
752
- if (state === "running") return theme.fg("accent", runningGlyph(seed));
742
+ function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary["status"], theme: Theme, seed?: number, now?: number): string {
743
+ if (state === "running") return theme.fg("accent", runningGlyph(seed, now));
753
744
  if (state === "complete" || state === "completed") return theme.fg("success", "✓");
754
745
  if (state === "failed") return theme.fg("error", "✗");
755
746
  if (state === "paused") return theme.fg("warning", "■");
@@ -778,7 +769,7 @@ function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activ
778
769
  return "Done";
779
770
  }
780
771
 
781
- function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme: Theme, width: number, expanded: boolean, snapshotNow?: number, lineBudget = expanded ? 12 : 1): string[] {
772
+ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme: Theme, width: number, expanded: boolean, snapshotNow?: number, lineBudget = expanded ? 12 : 1, now?: number): string[] {
782
773
  if (!children?.length || lineBudget <= 0) return [];
783
774
  if (!expanded) {
784
775
  const aggregate = formatNestedAggregate(children);
@@ -802,7 +793,7 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
802
793
  }
803
794
  const activity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
804
795
  const error = child.error ? ` · ${child.error}` : "";
805
- lines.push(theme.fg("dim", `${prefix}↳ ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child))} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
796
+ lines.push(theme.fg("dim", `${prefix}↳ ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child), now)} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
806
797
  if (depth === maxDepth) {
807
798
  const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
808
799
  if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix} ↳ ${aggregate}`));
@@ -810,7 +801,7 @@ function formatNestedWidgetLines(children: NestedRunSummary[] | undefined, theme
810
801
  }
811
802
  for (const step of child.steps ?? []) {
812
803
  if (lines.length >= lineBudget) return;
813
- lines.push(theme.fg("dim", `${prefix} ↳ ${nestedStatusGlyph(step.status, theme)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
804
+ lines.push(theme.fg("dim", `${prefix} ↳ ${nestedStatusGlyph(step.status, theme, undefined, now)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
814
805
  append(step.children, depth + 1, `${prefix} `);
815
806
  }
816
807
  append(child.children, depth + 1, `${prefix} `);
@@ -829,14 +820,15 @@ function foregroundStyleWidgetStepLines(
829
820
  total: number,
830
821
  expanded: boolean,
831
822
  width: number,
823
+ now?: number,
832
824
  ): string[] {
833
825
  const status = widgetStepStatus(step.status, theme);
834
826
  const stats = widgetStepStats(theme, step);
835
- const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
836
- const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1))} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
827
+ const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
828
+ const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1), now)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
837
829
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
838
830
  if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
839
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt)) {
831
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, undefined, now)) {
840
832
  lines.push(` ${nestedLine}`);
841
833
  }
842
834
  if (step.status === "running") {
@@ -859,40 +851,40 @@ function foregroundStyleWidgetStepLines(
859
851
  return lines;
860
852
  }
861
853
 
862
- function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number): string[] {
854
+ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, now?: number): string[] {
863
855
  if (!job.steps?.length) return [
864
856
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
865
- ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt).map((line) => ` ${line}`),
857
+ ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, undefined, now).map((line) => ` ${line}`),
866
858
  ];
867
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
859
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, now);
868
860
  const total = job.stepsTotal ?? job.steps.length;
869
861
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
870
862
  const lines: string[] = [];
871
863
  for (const [index, step] of job.steps.entries()) {
872
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width));
864
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, now));
873
865
  }
874
866
  const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
875
867
  const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
876
- for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt)) {
868
+ for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, undefined, now)) {
877
869
  lines.push(` ${nestedLine}`);
878
870
  }
879
871
  return lines;
880
872
  }
881
873
 
882
- function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean): string[] {
874
+ function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, now?: number): string[] {
883
875
  const stats = widgetStats(job, theme);
884
876
  const count = job.mode === "chain" ? job.chainStepCount : job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
885
877
  const mode = widgetJobName(job);
886
878
  const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
887
879
  return [
888
880
  `${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
889
- `${widgetStatusGlyph(job, theme)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
890
- ...foregroundStyleWidgetDetails(job, theme, expanded, width),
881
+ `${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
882
+ ...foregroundStyleWidgetDetails(job, theme, expanded, width, now),
891
883
  ].map((line) => truncLine(line, width));
892
884
  }
893
885
 
894
- function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number): string[] {
895
- const fullLines = buildSingleWidgetLines(job, theme, width, false);
886
+ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, now?: number): string[] {
887
+ const fullLines = buildSingleWidgetLines(job, theme, width, false, now);
896
888
  if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
897
889
 
898
890
  const total = job.stepsTotal ?? job.steps.length;
@@ -903,9 +895,9 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
903
895
  const activity = widgetStepActivityLine(step, width, false, job.updatedAt);
904
896
  const stepStats = widgetStepStats(theme, step);
905
897
  const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
906
- const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
907
- lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
908
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt)) lines.push(` ${nestedLine}`);
898
+ const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
899
+ lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), now)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
900
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, undefined, now)) lines.push(` ${nestedLine}`);
909
901
  }
910
902
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press ctrl+o for live detail"));
911
903
  return lines.map((line) => truncLine(line, width));
@@ -926,38 +918,44 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
926
918
  }
927
919
 
928
920
  /**
929
- * Live async-agents widget. Recomputes its lines on every render so the
930
- * wall-clock-driven running glyph (and elapsed-time labels) stay current; the
931
- * widget animation ticker below schedules those re-renders while jobs run.
921
+ * Live async-agents widget. Recomputes its lines on every render from the latest
922
+ * job snapshot, but reads animation time from the widget ticker/status-update
923
+ * snapshot rather than Date.now(). That keeps arbitrary host re-renders from
924
+ * mutating widget bytes unless the widget itself intentionally advanced.
932
925
  */
933
926
  class LiveWidgetComponent implements Component {
934
927
  private readonly container = new Container();
935
928
 
936
929
  constructor(
937
- private readonly jobs: AsyncJobState[],
930
+ private readonly getJobs: () => AsyncJobState[],
938
931
  private readonly theme: Theme,
939
932
  private readonly getExpanded: () => boolean,
933
+ private readonly getNow: () => number,
940
934
  ) {}
941
935
 
942
936
  render(width: number): string[] {
937
+ const jobs = this.getJobs();
943
938
  const expanded = this.getExpanded();
944
- const lines = expanded
945
- ? buildWidgetLines(this.jobs, this.theme, width, true)
946
- : this.jobs.length === 1
947
- ? compactSingleWidgetLines(this.jobs[0]!, this.theme, width)
948
- : buildWidgetLines(this.jobs, this.theme, width, false);
939
+ const now = this.getNow();
940
+ const lines = this.buildLines(jobs, width, expanded, now);
949
941
  this.container.clear();
950
942
  for (const line of fitWidgetLineBudget(lines, this.theme, width, expanded)) this.container.addChild(new Text(line, 1, 0));
951
943
  return this.container.render(width);
952
944
  }
953
945
 
946
+ private buildLines(jobs: AsyncJobState[], width: number, expanded: boolean, now: number): string[] {
947
+ if (expanded) return buildWidgetLines(jobs, this.theme, width, true, now);
948
+ if (jobs.length === 1) return compactSingleWidgetLines(jobs[0]!, this.theme, width, now);
949
+ return buildWidgetLines(jobs, this.theme, width, false, now);
950
+ }
951
+
954
952
  invalidate(): void {
955
953
  this.container.invalidate();
956
954
  }
957
955
  }
958
956
 
959
- function buildWidgetComponent(jobs: AsyncJobState[], getExpanded: () => boolean): (_tui: unknown, theme: Theme) => Component {
960
- return (_tui, theme) => new LiveWidgetComponent(jobs, theme, getExpanded);
957
+ function buildWidgetComponent(getJobs: () => AsyncJobState[], getExpanded: () => boolean, getNow: () => number): (_tui: unknown, theme: Theme) => Component {
958
+ return (_tui, theme) => new LiveWidgetComponent(getJobs, theme, getExpanded, getNow);
961
959
  }
962
960
 
963
961
  interface RenderRequestingContext {
@@ -965,10 +963,53 @@ interface RenderRequestingContext {
965
963
  }
966
964
 
967
965
  // There is only ever one async-agents widget per host process, so the widget
968
- // ticker keeps its driving context/jobs in module-level singletons.
966
+ // ticker and mounted component read their driving context/jobs from module-level
967
+ // singletons instead of remounting the widget for every visible update.
969
968
  let latestWidgetCtx: ExtensionContext | undefined;
970
969
  let latestWidgetJobs: AsyncJobState[] = [];
970
+ let latestWidgetFrameNow = 0;
971
971
  let widgetTimer: ReturnType<typeof setInterval> | undefined;
972
+ let mountedWidgetCtx: ExtensionContext | undefined;
973
+ let widgetMounted = false;
974
+
975
+ function getLatestWidgetJobs(): AsyncJobState[] {
976
+ return latestWidgetJobs;
977
+ }
978
+
979
+ function getLatestWidgetFrameNow(): number {
980
+ return latestWidgetFrameNow;
981
+ }
982
+
983
+ function getLatestWidgetExpanded(): boolean {
984
+ // LiveWidgetComponent re-renders outside a specific renderWidget() call, so
985
+ // read expansion from the latest live singleton context. If that context was
986
+ // cleared or went stale, collapse safely instead of consulting a stale caller.
987
+ if (!latestWidgetCtx?.hasUI) return false;
988
+ return latestWidgetCtx.ui.getToolsExpanded?.() ?? false;
989
+ }
990
+
991
+ function clearLatestWidgetState(): void {
992
+ latestWidgetCtx = undefined;
993
+ latestWidgetJobs = [];
994
+ latestWidgetFrameNow = 0;
995
+ mountedWidgetCtx = undefined;
996
+ widgetMounted = false;
997
+ }
998
+
999
+ function requestWidgetRender(ctx: ExtensionContext): void {
1000
+ (ctx as RenderRequestingContext).ui.requestRender?.();
1001
+ }
1002
+
1003
+ function unmountWidgetBestEffort(ctx: ExtensionContext | undefined): void {
1004
+ if (!ctx?.hasUI) return;
1005
+ try {
1006
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
1007
+ } catch {
1008
+ // Best-effort teardown only: stale host contexts can reject cleanup during
1009
+ // reload/session rebinding, but local state still needs to move on so the
1010
+ // next status update can mount cleanly on the active UI context.
1011
+ }
1012
+ }
972
1013
 
973
1014
  function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
974
1015
  // Animate while any job — or any of its nested steps — is still running so the
@@ -979,12 +1020,12 @@ function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
979
1020
  function refreshAnimatedWidget(): void {
980
1021
  if (!latestWidgetCtx?.hasUI) return;
981
1022
  try {
982
- // The cast is required because narrowing on `hasUI` above collapses `ui` to
983
- // the base ExtensionUIContext, which does not declare the optional
984
- // requestRender that the running interactive host actually provides.
985
- (latestWidgetCtx as RenderRequestingContext).ui.requestRender?.();
1023
+ latestWidgetFrameNow = Date.now();
1024
+ requestWidgetRender(latestWidgetCtx);
986
1025
  } catch {
987
- // Never let a cosmetic widget tick crash the host; stop on any error.
1026
+ // A stale render context means the cosmetic ticker can no longer update the
1027
+ // mounted widget safely; tear it down best-effort and let the next status
1028
+ // update remount on the active host context.
988
1029
  stopWidgetAnimation();
989
1030
  }
990
1031
  }
@@ -1009,23 +1050,24 @@ function stopWidgetTicker(): void {
1009
1050
  }
1010
1051
  }
1011
1052
 
1012
- // Full teardown: stop the ticker and forget the driving context/jobs entirely.
1053
+ // Full teardown: stop the ticker, clear the mounted widget if possible, and
1054
+ // forget the driving context/jobs entirely.
1013
1055
  export function stopWidgetAnimation(): void {
1014
1056
  stopWidgetTicker();
1015
- latestWidgetCtx = undefined;
1016
- latestWidgetJobs = [];
1057
+ if (widgetMounted) unmountWidgetBestEffort(mountedWidgetCtx);
1058
+ clearLatestWidgetState();
1017
1059
  }
1018
1060
 
1019
- export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false): string[] {
1061
+ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, now: number = Date.now()): string[] {
1020
1062
  if (jobs.length === 0) return [];
1021
- if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded);
1063
+ if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, now);
1022
1064
  const running = jobs.filter((job) => job.status === "running");
1023
1065
  const queued = jobs.filter((job) => job.status === "queued");
1024
1066
  const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
1025
1067
 
1026
1068
  const lines: string[] = [];
1027
1069
  const hasActive = running.length > 0 || queued.length > 0;
1028
- const headerGlyph = running.length > 0 ? runningGlyph(widgetJobsRunningSeed(running)) : hasActive ? "●" : "○";
1070
+ const headerGlyph = running.length > 0 ? runningGlyph(widgetJobsRunningSeed(running), now) : hasActive ? "●" : "○";
1029
1071
  lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", headerGlyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`, width));
1030
1072
 
1031
1073
  const items: string[][] = [];
@@ -1038,9 +1080,9 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1038
1080
  if (slots <= 0) { hiddenRunning++; continue; }
1039
1081
  const stats = widgetStats(job, theme);
1040
1082
  items.push([
1041
- `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1083
+ `${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1042
1084
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1043
- ...widgetParallelAgentDetails(job, theme, expanded, width),
1085
+ ...widgetParallelAgentDetails(job, theme, expanded, width, now),
1044
1086
  ]);
1045
1087
  slots--;
1046
1088
  }
@@ -1055,9 +1097,9 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1055
1097
  if (slots <= 0) { hiddenFinished++; continue; }
1056
1098
  const stats = widgetStats(job, theme);
1057
1099
  items.push([
1058
- `${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1100
+ `${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
1059
1101
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
1060
- ...widgetParallelAgentDetails(job, theme, expanded, width),
1102
+ ...widgetParallelAgentDetails(job, theme, expanded, width, now),
1061
1103
  ]);
1062
1104
  slots--;
1063
1105
  }
@@ -1091,8 +1133,12 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1091
1133
  */
1092
1134
  export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
1093
1135
  if (jobs.length === 0) {
1136
+ if (widgetMounted && mountedWidgetCtx !== ctx) {
1137
+ // Empty updates from stale contexts must not clear the active context's
1138
+ // widget. The mounted context owns the eventual teardown.
1139
+ return;
1140
+ }
1094
1141
  stopWidgetAnimation();
1095
- if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
1096
1142
  return;
1097
1143
  }
1098
1144
  if (!ctx.hasUI) {
@@ -1101,38 +1147,54 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
1101
1147
  }
1102
1148
  latestWidgetCtx = ctx;
1103
1149
  latestWidgetJobs = [...jobs];
1104
- // belowEditor: the widget animates a running glyph / elapsed labels on a
1105
- // timer. pi-tui full-clears the screen+scrollback whenever a changed line
1106
- // sits above the viewport fold, so an aboveEditor widget flickers once the
1107
- // bottom region grows tall and pushes it above the fold. Rendering below the
1108
- // editor keeps the live line within the bottom viewport (flicker-free), and
1109
- // matches the workflow companion widget's placement (#1109).
1110
- ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, () => ctx.ui.getToolsExpanded?.() ?? false), {
1111
- placement: "belowEditor",
1112
- });
1150
+ latestWidgetFrameNow = Date.now();
1151
+ if (widgetMounted && mountedWidgetCtx !== ctx) {
1152
+ // Context rebinding can leave the previous host UI alive briefly; clear the
1153
+ // old mount before installing the singleton widget on the new context.
1154
+ unmountWidgetBestEffort(mountedWidgetCtx);
1155
+ mountedWidgetCtx = undefined;
1156
+ widgetMounted = false;
1157
+ }
1158
+ if (!widgetMounted) {
1159
+ // belowEditor: the widget animates a running glyph / elapsed labels on a
1160
+ // timer. pi-tui full-clears the screen+scrollback whenever a changed line
1161
+ // sits above the viewport fold, so an aboveEditor widget flickers once the
1162
+ // bottom region grows tall and pushes it above the fold. Rendering below the
1163
+ // editor keeps the live line within the bottom viewport (flicker-free), and
1164
+ // matches the workflow companion widget's placement (#1109).
1165
+ ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(getLatestWidgetJobs, getLatestWidgetExpanded, getLatestWidgetFrameNow), {
1166
+ placement: "belowEditor",
1167
+ });
1168
+ mountedWidgetCtx = ctx;
1169
+ widgetMounted = true;
1170
+ } else {
1171
+ // The mounted widget reads latestWidgetJobs via getLatestWidgetJobs(), so a
1172
+ // visible->visible update only needs to ask the host to render in place.
1173
+ requestWidgetRender(ctx);
1174
+ }
1113
1175
  // Keep the just-rendered ctx/jobs as the last-rendered state; only the ticker
1114
1176
  // is conditional on whether anything is still animating.
1115
1177
  if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
1116
1178
  else stopWidgetTicker();
1117
1179
  }
1118
1180
 
1119
- function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme): Component {
1181
+ function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, now?: number, spinnerNow?: number): Component {
1120
1182
  const output = r.truncation?.text || getSingleResultOutput(r);
1121
1183
  const progress = r.progress || r.progressSummary;
1122
1184
  const isRunning = r.progress?.status === "running";
1123
1185
  const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
1124
1186
  const stats = statJoin(theme, [
1125
1187
  r.usage?.turns ? `⟳ ${r.usage.turns}` : "",
1126
- formatProgressStats(theme, progress),
1188
+ formatProgressStats(theme, progress, true, now),
1127
1189
  ]);
1128
1190
  const c = new Container();
1129
1191
  const width = getTermWidth() - 4;
1130
- const modelDisplay = modelThinkingBadge(theme, r.model);
1131
- c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1192
+ const modelDisplay = modelThinkingBadge(theme, r.model, undefined, r.fastMode);
1193
+ c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, progressRunningSeed(r.progress ?? r.progressSummary), spinnerNow ?? now)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1132
1194
 
1133
1195
  if (isRunning && r.progress) {
1134
- const progressSnapshotNow = snapshotNowForProgress(r.progress);
1135
- const activity = compactCurrentActivity(r.progress);
1196
+ const progressSnapshotNow = snapshotNowForProgress(r.progress, now);
1197
+ const activity = compactCurrentActivity(r.progress, now);
1136
1198
  c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
1137
1199
  const liveStatus = buildLiveStatusLine(r.progress, progressSnapshotNow);
1138
1200
  if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
@@ -1152,7 +1214,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1152
1214
  return c;
1153
1215
  }
1154
1216
 
1155
- function renderMultiCompact(d: Details, theme: Theme): Component {
1217
+ function renderMultiCompact(d: Details, theme: Theme, now?: number, spinnerNow?: number): Component {
1156
1218
  const hasRunning = d.progress?.some((p) => p.status === "running")
1157
1219
  || d.results.some((r) => r.progress?.status === "running");
1158
1220
  const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running");
@@ -1173,9 +1235,9 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
1173
1235
  }
1174
1236
  const multiLabel = buildMultiProgressLabel(d, hasRunning);
1175
1237
  const itemTitle = multiLabel.itemTitle;
1176
- const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary)]);
1238
+ const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary, true, now)]);
1177
1239
  const glyph = hasRunning
1178
- ? theme.fg("accent", runningGlyph(runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex)))
1240
+ ? theme.fg("accent", runningGlyph(runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex), spinnerNow ?? now))
1179
1241
  : failed
1180
1242
  ? theme.fg("error", "✗")
1181
1243
  : paused
@@ -1204,14 +1266,14 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
1204
1266
  const rRunning = rProg && "status" in rProg && rProg.status === "running";
1205
1267
  const rPending = rProg && "status" in rProg && rProg.status === "pending";
1206
1268
  const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
1207
- const stepStats = formatProgressStats(theme, rProg);
1208
- const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg));
1269
+ const stepStats = formatProgressStats(theme, rProg, true, now);
1270
+ const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg), spinnerNow ?? now);
1209
1271
  const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
1210
1272
  const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
1211
1273
  const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1212
1274
  c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
1213
1275
  if (rRunning && rProg && "status" in rProg) {
1214
- const activity = compactCurrentActivity(rProg);
1276
+ const activity = compactCurrentActivity(rProg, now);
1215
1277
  c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
1216
1278
  c.addChild(new Text(truncLine(theme.fg("accent", " Press ctrl+o for live detail"), width), 0, 0));
1217
1279
  } else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
@@ -1225,12 +1287,96 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
1225
1287
  return c;
1226
1288
  }
1227
1289
 
1290
+ function progressRenderKey(progress: Partial<AgentProgress> | undefined): string {
1291
+ if (!progress) return "";
1292
+ return [
1293
+ progress.index,
1294
+ progress.agent,
1295
+ progress.status,
1296
+ progress.durationMs,
1297
+ progress.toolCount,
1298
+ progress.tokens,
1299
+ progress.turnCount ?? "",
1300
+ progress.lastActivityAt ?? "",
1301
+ progress.currentTool ?? "",
1302
+ progress.currentToolStartedAt ?? "",
1303
+ progress.currentPath ?? "",
1304
+ ].join(":");
1305
+ }
1306
+
1307
+ function isRunningSubagentResult(result: AgentToolResult<Details>): boolean {
1308
+ return result.details?.progress?.some((entry) => entry.status === "running")
1309
+ || result.details?.results.some((entry) => entry.progress?.status === "running")
1310
+ || false;
1311
+ }
1312
+
1313
+ function subagentResultRenderKey(
1314
+ result: AgentToolResult<Details>,
1315
+ options: { expanded: boolean; isPartial: boolean },
1316
+ ): string {
1317
+ const details = result.details;
1318
+ if (!details) return `${options.isPartial ? "partial" : "final"}:${result.content.length}`;
1319
+ const progressKeys = [
1320
+ ...(details.progress ?? []).map(progressRenderKey),
1321
+ ...details.results.map((entry) => [
1322
+ entry.agent,
1323
+ entry.exitCode,
1324
+ entry.interrupted === true ? "interrupted" : "",
1325
+ entry.detached === true ? "detached" : "",
1326
+ progressRenderKey(entry.progress),
1327
+ progressRenderKey(entry.progressSummary),
1328
+ entry.finalOutput?.length ?? "",
1329
+ entry.error?.length ?? "",
1330
+ ].join(":")),
1331
+ ];
1332
+ return [
1333
+ options.isPartial ? "partial" : "final",
1334
+ options.expanded ? "expanded" : "compact",
1335
+ details.mode,
1336
+ details.currentStepIndex ?? "",
1337
+ details.totalSteps ?? "",
1338
+ progressRenderKey(details.progressSummary),
1339
+ progressKeys.join("|"),
1340
+ ].join("|");
1341
+ }
1342
+
1343
+ export function renderLiveSubagentResult(
1344
+ result: AgentToolResult<Details>,
1345
+ options: { expanded: boolean; isPartial: boolean },
1346
+ theme: Theme,
1347
+ context: ResultAnimationContext,
1348
+ ): Component {
1349
+ const nextKey = subagentResultRenderKey(result, options);
1350
+ if (context.state.subagentResultSnapshotKey !== nextKey) {
1351
+ const frameNow = Date.now();
1352
+ context.state.subagentResultSnapshotKey = nextKey;
1353
+ context.state.subagentResultSnapshotNow = frameNow;
1354
+ context.state.subagentResultSpinnerFrameNow = frameNow;
1355
+ }
1356
+ context.state.subagentResultSnapshotNow ??= Date.now();
1357
+ context.state.subagentResultSpinnerFrameNow ??= context.state.subagentResultSnapshotNow;
1358
+ // Foreground subagent results render inside chat scrollback. Keep semantic
1359
+ // content time stable between tool/progress updates, but let the spinner tick
1360
+ // independently. That limits timer-driven diffs to spinner glyph cells instead
1361
+ // of updating elapsed/tool/activity text and causing broad chatbox churn.
1362
+ if (options.isPartial && isRunningSubagentResult(result)) {
1363
+ ensureResultAnimation(context);
1364
+ } else {
1365
+ clearResultAnimationTimer(context);
1366
+ }
1367
+ return renderSubagentResult(result, {
1368
+ ...options,
1369
+ now: context.state.subagentResultSnapshotNow,
1370
+ spinnerNow: context.state.subagentResultSpinnerFrameNow,
1371
+ }, theme);
1372
+ }
1373
+
1228
1374
  /**
1229
1375
  * Render a subagent result
1230
1376
  */
1231
1377
  export function renderSubagentResult(
1232
1378
  result: AgentToolResult<Details>,
1233
- options: { expanded: boolean },
1379
+ options: { expanded: boolean; now?: number; spinnerNow?: number },
1234
1380
  theme: Theme,
1235
1381
  ): Component {
1236
1382
  const d = result.details;
@@ -1246,7 +1392,7 @@ export function renderSubagentResult(
1246
1392
 
1247
1393
  if (d.mode === "single" && d.results.length === 1) {
1248
1394
  const r = d.results[0];
1249
- if (!expanded) return renderSingleCompact(d, r, theme);
1395
+ if (!expanded) return renderSingleCompact(d, r, theme, options.now, options.spinnerNow);
1250
1396
  const isRunning = r.progress?.status === "running";
1251
1397
  const icon = isRunning
1252
1398
  ? theme.fg("warning", "running")
@@ -1259,7 +1405,7 @@ export function renderSubagentResult(
1259
1405
  const output = r.truncation?.text || getSingleResultOutput(r);
1260
1406
 
1261
1407
  const progressInfo = isRunning && r.progress
1262
- ? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
1408
+ ? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(displayProgressDurationMs(r.progress, options.now))}`
1263
1409
  : r.progressSummary
1264
1410
  ? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tok, ${formatDuration(r.progressSummary.durationMs)}`
1265
1411
  : "";
@@ -1280,7 +1426,7 @@ export function renderSubagentResult(
1280
1426
  c.addChild(new Spacer(1));
1281
1427
 
1282
1428
  if (isRunning && r.progress) {
1283
- const progressSnapshotNow = snapshotNowForProgress(r.progress);
1429
+ const progressSnapshotNow = snapshotNowForProgress(r.progress, options.now);
1284
1430
  const toolLine = formatCurrentToolLine(r.progress, w, expanded, progressSnapshotNow);
1285
1431
  if (toolLine) {
1286
1432
  c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
@@ -1340,7 +1486,7 @@ export function renderSubagentResult(
1340
1486
  return c;
1341
1487
  }
1342
1488
 
1343
- if (!expanded) return renderMultiCompact(d, theme);
1489
+ if (!expanded) return renderMultiCompact(d, theme, options.now, options.spinnerNow);
1344
1490
 
1345
1491
  const hasRunning = d.progress?.some((p) => p.status === "running")
1346
1492
  || d.results.some((r) => r.progress?.status === "running");
@@ -1458,8 +1604,8 @@ export function renderSubagentResult(
1458
1604
  : hasEmptyTextOutputWithoutOutputTarget(r.task, resultOutput)
1459
1605
  ? theme.fg("warning", "warning")
1460
1606
  : theme.fg("success", "done");
1461
- const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
1462
- const modelDisplay = modelThinkingBadge(theme, r.model);
1607
+ const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(displayProgressDurationMs(rProg, options.now))}` : "";
1608
+ const modelDisplay = modelThinkingBadge(theme, r.model, undefined, r.fastMode);
1463
1609
  const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
1464
1610
  const stepHeader = rRunning
1465
1611
  ? `${statusIcon} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
@@ -1492,7 +1638,7 @@ export function renderSubagentResult(
1492
1638
  if (rProg.skills?.length) {
1493
1639
  c.addChild(new Text(fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)), 0, 0));
1494
1640
  }
1495
- const progressSnapshotNow = snapshotNowForProgress(rProg);
1641
+ const progressSnapshotNow = snapshotNowForProgress(rProg, options.now);
1496
1642
  const toolLine = formatCurrentToolLine(rProg, w, expanded, progressSnapshotNow);
1497
1643
  if (toolLine) {
1498
1644
  c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));