@bastani/atomic 0.9.3-alpha.1 → 0.9.3-alpha.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.
Files changed (175) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +21 -0
  3. package/dist/builtin/cursor/README.md +2 -1
  4. package/dist/builtin/cursor/package.json +2 -2
  5. package/dist/builtin/cursor/src/cursor-models-raw.json +2 -9
  6. package/dist/builtin/cursor/src/model-mapper.ts +14 -3
  7. package/dist/builtin/cursor/src/proto/protobuf-codec-base64.ts +22 -0
  8. package/dist/builtin/cursor/src/proto/protobuf-codec-request.ts +53 -13
  9. package/dist/builtin/cursor/src/proto/protobuf-codec-wire.ts +24 -7
  10. package/dist/builtin/cursor/src/proto/protobuf-codec.ts +3 -2
  11. package/dist/builtin/cursor/src/stream.ts +5 -11
  12. package/dist/builtin/cursor/src/transport-types.ts +3 -0
  13. package/dist/builtin/cursor/src/transport.ts +1 -0
  14. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  15. package/dist/builtin/intercom/package.json +1 -1
  16. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  17. package/dist/builtin/mcp/package.json +1 -1
  18. package/dist/builtin/subagents/CHANGELOG.md +15 -0
  19. package/dist/builtin/subagents/package.json +1 -1
  20. package/dist/builtin/subagents/src/extension/fanout-child.ts +1 -0
  21. package/dist/builtin/subagents/src/extension/index.ts +6 -3
  22. package/dist/builtin/subagents/src/extension/schemas.ts +0 -5
  23. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +1 -4
  24. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +15 -1
  25. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +35 -1
  26. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  27. package/dist/builtin/subagents/src/shared/types-async.ts +1 -0
  28. package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +27 -5
  29. package/dist/builtin/subagents/src/tui/render-layout.ts +27 -4
  30. package/dist/builtin/subagents/src/tui/render-result-animation.ts +22 -31
  31. package/dist/builtin/subagents/src/tui/render-result-compact.ts +6 -6
  32. package/dist/builtin/subagents/src/tui/render-result.ts +20 -19
  33. package/dist/builtin/subagents/src/tui/render-status-progress.ts +3 -3
  34. package/dist/builtin/subagents/src/tui/render-widget.ts +46 -7
  35. package/dist/builtin/subagents/src/tui/render.ts +2 -2
  36. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  37. package/dist/builtin/web-access/package.json +1 -1
  38. package/dist/builtin/workflows/CHANGELOG.md +49 -0
  39. package/dist/builtin/workflows/README.md +1 -1
  40. package/dist/builtin/workflows/package.json +1 -1
  41. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  42. package/dist/builtin/workflows/src/durable/backend.ts +343 -0
  43. package/dist/builtin/workflows/src/durable/child-primitive.ts +79 -0
  44. package/dist/builtin/workflows/src/durable/dbos-backend.ts +421 -0
  45. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +171 -0
  46. package/dist/builtin/workflows/src/durable/factory.ts +96 -0
  47. package/dist/builtin/workflows/src/durable/file-backend.ts +433 -0
  48. package/dist/builtin/workflows/src/durable/index.ts +73 -0
  49. package/dist/builtin/workflows/src/durable/resume-catalog.ts +217 -0
  50. package/dist/builtin/workflows/src/durable/resume-runtime.ts +299 -0
  51. package/dist/builtin/workflows/src/durable/scoped-backend.ts +171 -0
  52. package/dist/builtin/workflows/src/durable/stage-primitive.ts +284 -0
  53. package/dist/builtin/workflows/src/durable/tool-primitive.ts +180 -0
  54. package/dist/builtin/workflows/src/durable/types.ts +168 -0
  55. package/dist/builtin/workflows/src/durable/ui-primitive.ts +96 -0
  56. package/dist/builtin/workflows/src/engine/options.ts +3 -0
  57. package/dist/builtin/workflows/src/engine/primitives/parallel.ts +2 -2
  58. package/dist/builtin/workflows/src/engine/primitives/task.ts +4 -4
  59. package/dist/builtin/workflows/src/engine/primitives/ui.ts +22 -8
  60. package/dist/builtin/workflows/src/engine/primitives/workflow.ts +8 -0
  61. package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +69 -0
  62. package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +31 -0
  63. package/dist/builtin/workflows/src/engine/run.ts +148 -6
  64. package/dist/builtin/workflows/src/engine/runtime.ts +8 -2
  65. package/dist/builtin/workflows/src/extension/extension-factory.ts +6 -12
  66. package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +5 -1
  67. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +3 -0
  68. package/dist/builtin/workflows/src/extension/runtime.ts +48 -9
  69. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +143 -4
  70. package/dist/builtin/workflows/src/runs/background/quit.ts +61 -0
  71. package/dist/builtin/workflows/src/runs/background/status.ts +1 -0
  72. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
  73. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +74 -33
  74. package/dist/builtin/workflows/src/runs/foreground/executor-stage-context.ts +20 -1
  75. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +8 -7
  76. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  77. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +1 -1
  78. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +19 -2
  79. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +4 -0
  80. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -10
  81. package/dist/builtin/workflows/src/runs/foreground/stage-runner-options.ts +5 -1
  82. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +25 -0
  83. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -0
  84. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +16 -0
  85. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +20 -0
  86. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +23 -1
  87. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +30 -1
  88. package/dist/builtin/workflows/src/shared/store-public-types.ts +6 -2
  89. package/dist/builtin/workflows/src/shared/store-run-methods.ts +12 -6
  90. package/dist/builtin/workflows/src/shared/types.ts +55 -0
  91. package/dist/builtin/workflows/src/tui/graph-view-constants.ts +1 -1
  92. package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +41 -0
  93. package/dist/builtin/workflows/src/tui/graph-view-input.ts +82 -24
  94. package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -0
  95. package/dist/builtin/workflows/src/tui/graph-view-state.ts +22 -2
  96. package/dist/builtin/workflows/src/tui/graph-view-types.ts +4 -5
  97. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -11
  98. package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +9 -3
  99. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +11 -2
  100. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +35 -0
  101. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +51 -17
  102. package/dist/builtin/workflows/src/tui/stage-chat-view-status.ts +36 -0
  103. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +5 -1
  104. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -1
  105. package/dist/builtin/workflows/src/tui/status-list.ts +14 -2
  106. package/dist/builtin/workflows/src/tui/widget.ts +23 -8
  107. package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +5 -4
  108. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
  109. package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +151 -0
  110. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  111. package/dist/core/extensions/loader-virtual-modules.js +47 -30
  112. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  113. package/dist/core/messages.d.ts +1 -0
  114. package/dist/core/messages.d.ts.map +1 -1
  115. package/dist/core/messages.js +46 -1
  116. package/dist/core/messages.js.map +1 -1
  117. package/dist/core/sdk.d.ts.map +1 -1
  118. package/dist/core/sdk.js +12 -0
  119. package/dist/core/sdk.js.map +1 -1
  120. package/dist/core/session-manager-core.d.ts +15 -7
  121. package/dist/core/session-manager-core.d.ts.map +1 -1
  122. package/dist/core/session-manager-core.js +20 -9
  123. package/dist/core/session-manager-core.js.map +1 -1
  124. package/dist/core/session-manager-entries.d.ts +2 -2
  125. package/dist/core/session-manager-entries.d.ts.map +1 -1
  126. package/dist/core/session-manager-entries.js +9 -3
  127. package/dist/core/session-manager-entries.js.map +1 -1
  128. package/dist/core/session-manager-history.d.ts.map +1 -1
  129. package/dist/core/session-manager-history.js +2 -1
  130. package/dist/core/session-manager-history.js.map +1 -1
  131. package/dist/core/session-manager-list.d.ts +3 -3
  132. package/dist/core/session-manager-list.d.ts.map +1 -1
  133. package/dist/core/session-manager-list.js +27 -8
  134. package/dist/core/session-manager-list.js.map +1 -1
  135. package/dist/core/session-manager-storage.d.ts +3 -1
  136. package/dist/core/session-manager-storage.d.ts.map +1 -1
  137. package/dist/core/session-manager-storage.js +55 -12
  138. package/dist/core/session-manager-storage.js.map +1 -1
  139. package/dist/core/session-manager-tool-dependencies.d.ts +10 -0
  140. package/dist/core/session-manager-tool-dependencies.d.ts.map +1 -0
  141. package/dist/core/session-manager-tool-dependencies.js +133 -0
  142. package/dist/core/session-manager-tool-dependencies.js.map +1 -0
  143. package/dist/core/session-manager-types.d.ts +22 -0
  144. package/dist/core/session-manager-types.d.ts.map +1 -1
  145. package/dist/core/session-manager-types.js.map +1 -1
  146. package/dist/core/session-manager.d.ts +2 -2
  147. package/dist/core/session-manager.d.ts.map +1 -1
  148. package/dist/core/session-manager.js +1 -1
  149. package/dist/core/session-manager.js.map +1 -1
  150. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +1 -0
  151. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/chat-session-host-runtime.js +12 -0
  153. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  154. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts +4 -0
  155. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts.map +1 -0
  156. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js +131 -0
  157. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js.map +1 -0
  158. package/dist/modes/interactive/components/chat-session-host.d.ts +2 -0
  159. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/chat-session-host.js +7 -1
  161. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  162. package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/chat-transcript.js +15 -4
  164. package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
  165. package/dist/modes/interactive/components/tool-execution.d.ts +3 -0
  166. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  167. package/dist/modes/interactive/components/tool-execution.js +26 -0
  168. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  169. package/docs/compaction.md +2 -0
  170. package/docs/models.md +1 -1
  171. package/docs/providers.md +2 -1
  172. package/docs/session-format.md +6 -0
  173. package/docs/sessions.md +6 -0
  174. package/docs/workflows.md +105 -3
  175. package/package.json +4 -3
@@ -10,7 +10,7 @@ import { discoverAgents } from "../agents/agents.ts";
10
10
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
11
11
  import { resolveCurrentSessionId } from "../shared/session-identity.ts";
12
12
  import { cleanupOldChainDirs } from "../shared/settings.ts";
13
- import { renderLiveSubagentResult, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, type SubagentResultRenderState } from "../tui/render.ts";
13
+ import { advanceResultPulseFrame, renderLiveSubagentResult, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, type SubagentResultRenderState } from "../tui/render.ts";
14
14
  import { SubagentParams } from "./schemas.ts";
15
15
  import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
16
16
  import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
@@ -69,7 +69,7 @@ type SubagentToolRenderState = SubagentResultRenderState;
69
69
  function rebuildSlashResultContainer(
70
70
  container: Container,
71
71
  result: AgentToolResult<Details>,
72
- options: { expanded: boolean; now?: number },
72
+ options: { expanded: boolean; now?: number; pulseFrame?: number },
73
73
  theme: ExtensionContext["ui"]["theme"],
74
74
  ): void {
75
75
  container.clear();
@@ -87,12 +87,14 @@ function createSlashResultComponent(
87
87
  const container = new Container();
88
88
  let lastVersion = -1;
89
89
  let lastSnapshotNow = 0;
90
+ let pulseFrame = 0;
90
91
  container.render = (width: number): string[] => {
91
92
  const snapshot = getSlashRenderableSnapshot(details);
92
93
  if (snapshot.version !== lastVersion) {
93
94
  lastVersion = snapshot.version;
94
95
  lastSnapshotNow = Date.now();
95
- rebuildSlashResultContainer(container, snapshot.result, { ...options, now: lastSnapshotNow }, theme);
96
+ pulseFrame = advanceResultPulseFrame(pulseFrame);
97
+ rebuildSlashResultContainer(container, snapshot.result, { ...options, now: lastSnapshotNow, pulseFrame }, theme);
96
98
  }
97
99
  return Container.prototype.render.call(container, width);
98
100
  };
@@ -179,6 +181,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
179
181
  baseCwd: "",
180
182
  currentSessionId: null,
181
183
  asyncJobs: new Map(),
184
+ subagentInProgress: false,
182
185
  foregroundRuns: new Map(),
183
186
  foregroundControls: new Map(),
184
187
  lastForegroundControlId: null,
@@ -137,11 +137,6 @@ const ChainItem = Type.Object({
137
137
  }, {
138
138
  description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
139
139
  additionalProperties: false,
140
- allOf: [
141
- { if: { required: ["expand"] }, then: { required: ["parallel", "collect"], properties: { parallel: { type: "object" } } } },
142
- { if: { required: ["collect"] }, then: { required: ["expand", "parallel"], properties: { parallel: { type: "object" } } } },
143
- { not: { required: ["expand"], properties: { parallel: { type: "array", items: {} } } } },
144
- ],
145
140
  });
146
141
 
147
142
  const ControlOverrides = Type.Object({
@@ -395,10 +395,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
395
395
  state.foregroundControls?.clear();
396
396
  state.lastForegroundControlId = null;
397
397
  state.resultFileCoalescer.clear();
398
- if (ctx?.hasUI) {
399
- state.lastUiContext = ctx;
400
- rerenderWidget(ctx, []);
401
- }
398
+ if (ctx?.hasUI) state.lastUiContext = ctx;
402
399
  };
403
400
 
404
401
  return { ensurePoller, handleStarted, handleComplete, resetJobs, hydrateActiveJobs };
@@ -25,11 +25,25 @@ import {
25
25
  wrapForkTask,
26
26
  type AgentProgress,
27
27
  type ArtifactPaths,
28
+ type SingleResult,
28
29
  type SubagentToolResult,
29
30
  } from "../../shared/types.ts";
30
31
  import type { ExecutionContextData, ResolvedExecutorDeps } from "./subagent-executor-types.ts";
31
32
  import { createForegroundControlNotifier, maybeBuildForegroundIntercomReceipt, rememberForegroundRun } from "./subagent-executor-status.ts";
32
33
 
34
+ function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string): string {
35
+ const error = result.error || "Failed";
36
+ const output = displayOutput.trim();
37
+ const lines = [error];
38
+ if (output && output !== error.trim()) {
39
+ lines.push("", "Output:", output);
40
+ }
41
+ if (result.artifactPaths?.outputPath) {
42
+ lines.push("", `Output artifact: ${result.artifactPaths.outputPath}`);
43
+ }
44
+ return lines.join("\n");
45
+ }
46
+
33
47
  export async function runSinglePath(data: ExecutionContextData, deps: ResolvedExecutorDeps): Promise<SubagentToolResult> {
34
48
  const {
35
49
  params,
@@ -309,7 +323,7 @@ export async function runSinglePath(data: ExecutionContextData, deps: ResolvedEx
309
323
 
310
324
  if (r.exitCode !== 0)
311
325
  return {
312
- content: [{ type: "text", text: r.error || "Failed" }],
326
+ content: [{ type: "text", text: formatFailedSingleRunOutput(r, finalizedOutput.displayOutput) }],
313
327
  details,
314
328
  isError: true,
315
329
  };
@@ -172,6 +172,23 @@ async function handleInterruptRequest(input: {
172
172
  };
173
173
  }
174
174
 
175
+ function inferExecutionMode(params: SubagentParamsLike): "single" | "parallel" | "chain" {
176
+ if ((params.chain?.length ?? 0) > 0) return "chain";
177
+ if ((params.tasks?.length ?? 0) > 0) return "parallel";
178
+ return "single";
179
+ }
180
+
181
+ function duplicateSubagentCallResult(params: SubagentParamsLike): SubagentToolResult {
182
+ return {
183
+ content: [{
184
+ type: "text",
185
+ text: "Rejected: a subagent call is already in progress. Issue exactly ONE subagent call per turn.",
186
+ }],
187
+ isError: true,
188
+ details: { mode: inferExecutionMode(params), results: [] },
189
+ };
190
+ }
191
+
175
192
  export function createSubagentExecutor(rawDeps: ExecutorDeps): {
176
193
  execute: (
177
194
  id: string,
@@ -249,5 +266,22 @@ export function createSubagentExecutor(rawDeps: ExecutorDeps): {
249
266
  }, prepared.effectiveParams.context);
250
267
  };
251
268
 
252
- return { execute };
269
+ const executeWithSingleDispatchGuard = async (
270
+ id: string,
271
+ params: SubagentParamsLike,
272
+ signal: AbortSignal,
273
+ onUpdate: ((r: SubagentToolResult) => void) | undefined,
274
+ ctx: ExtensionContext,
275
+ ): Promise<SubagentToolResult> => {
276
+ if (params.action) return execute(id, params, signal, onUpdate, ctx);
277
+ if (deps.state.subagentInProgress === true) return duplicateSubagentCallResult(params);
278
+ deps.state.subagentInProgress = true;
279
+ try {
280
+ return await execute(id, params, signal, onUpdate, ctx);
281
+ } finally {
282
+ deps.state.subagentInProgress = false;
283
+ }
284
+ };
285
+
286
+ return { execute: executeWithSingleDispatchGuard };
253
287
  }
@@ -31,6 +31,7 @@ export const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [
31
31
  const PARENT_ONLY_CUSTOM_MESSAGE_TYPES = new Set([
32
32
  "subagent-orchestration-instructions",
33
33
  "subagent-slash-result",
34
+ "subagent-slash-text-result",
34
35
  "subagent-notify",
35
36
  "subagent_control_notice",
36
37
  "subagent-control",
@@ -130,14 +131,15 @@ function stripAssistantSubagentToolCallBlocks(message: unknown): unknown | undef
130
131
  }
131
132
 
132
133
  export function stripParentOnlySubagentMessages(messages: unknown[]): unknown[] {
134
+ const preserveCurrentFanoutToolHistory = process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1";
133
135
  let changed = false;
134
136
  const filtered: unknown[] = [];
135
137
  for (const message of messages) {
136
- if (isParentOnlySubagentMessage(message) || isSubagentToolResultMessage(message)) {
138
+ if (isParentOnlySubagentMessage(message) || (!preserveCurrentFanoutToolHistory && isSubagentToolResultMessage(message))) {
137
139
  changed = true;
138
140
  continue;
139
141
  }
140
- const stripped = stripAssistantSubagentToolCallBlocks(message);
142
+ const stripped = preserveCurrentFanoutToolHistory ? message : stripAssistantSubagentToolCallBlocks(message);
141
143
  if (stripped === undefined) {
142
144
  changed = true;
143
145
  continue;
@@ -230,6 +230,7 @@ export interface SubagentState {
230
230
  baseCwd: string;
231
231
  currentSessionId: string | null;
232
232
  asyncJobs: Map<string, AsyncJobState>;
233
+ subagentInProgress?: boolean;
233
234
  foregroundRuns?: Map<string, ForegroundResumeRun>;
234
235
  foregroundControls: Map<string, {
235
236
  runId: string;
@@ -82,6 +82,7 @@ interface PromptTemplateBridgeResult {
82
82
  exitCode?: number;
83
83
  error?: string;
84
84
  model?: string;
85
+ toolCalls?: Array<{ text?: string; expandedText?: string }>;
85
86
  }>;
86
87
  progress?: Array<{
87
88
  index?: number;
@@ -147,10 +148,13 @@ function parsePromptTemplateRequest(data: unknown): PromptTemplateDelegationRequ
147
148
  if (!hasSingle && tasks.length === 0) return undefined;
148
149
 
149
150
  const fallbackTask = tasks[0];
151
+ const agent = hasSingle ? value.agent : fallbackTask?.agent;
152
+ const task = hasSingle ? value.task : fallbackTask?.task;
153
+ if (!agent || !task) return undefined;
150
154
  return {
151
155
  requestId: value.requestId,
152
- agent: hasSingle ? value.agent : fallbackTask!.agent,
153
- task: hasSingle ? value.task : fallbackTask!.task,
156
+ agent,
157
+ task,
154
158
  ...(tasks.length > 0 ? { tasks } : {}),
155
159
  context: value.context,
156
160
  model: value.model,
@@ -209,13 +213,31 @@ function resolveProgressModel(
209
213
  return firstWithModel?.model;
210
214
  }
211
215
 
212
- function buildDelegationMessages(result: { messages?: unknown[]; finalOutput?: string }, fallbackText?: string): unknown[] {
216
+ function toolCallSummaryText(summary: { text?: string; expandedText?: string }): string | undefined {
217
+ const text = typeof summary.expandedText === "string" && summary.expandedText.trim().length > 0
218
+ ? summary.expandedText.trim()
219
+ : typeof summary.text === "string"
220
+ ? summary.text.trim()
221
+ : "";
222
+ return text || undefined;
223
+ }
224
+
225
+ function buildDelegationMessages(
226
+ result: { messages?: unknown[]; finalOutput?: string; toolCalls?: Array<{ text?: string; expandedText?: string }> },
227
+ fallbackText?: string,
228
+ ): unknown[] {
213
229
  if (Array.isArray(result.messages) && result.messages.length > 0) return result.messages;
230
+ const toolCallSummaries = (result.toolCalls ?? []).flatMap((summary) => {
231
+ const text = toolCallSummaryText(summary);
232
+ return text ? [`- ${text}`] : [];
233
+ });
234
+ const toolCallText = toolCallSummaries.length > 0 ? `Tool calls:\n${toolCallSummaries.join("\n")}` : undefined;
214
235
  const text = typeof result.finalOutput === "string" && result.finalOutput.trim().length > 0
215
236
  ? result.finalOutput.trim()
216
237
  : fallbackText;
217
- if (!text) return [];
218
- return [{ role: "assistant", content: [{ type: "text", text }] }];
238
+ const contentText = [toolCallText, text].filter((part): part is string => Boolean(part)).join("\n\n");
239
+ if (!contentText) return [];
240
+ return [{ role: "assistant", content: [{ type: "text", text: contentText }] }];
219
241
  }
220
242
 
221
243
  function toDelegationUpdate(requestId: string, update: PromptTemplateBridgeResult): PromptTemplateDelegationUpdate | undefined {
@@ -66,16 +66,22 @@ export function truncLine(text: string, maxWidth: number): string {
66
66
  return result + activeStyles.join("") + "…";
67
67
  }
68
68
 
69
- const RUNNING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
69
+ export const RUNNING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
70
70
 
71
71
  /**
72
72
  * Spinner cadence (ms per frame). The running glyph is derived from wall-clock
73
73
  * time so every active spinner advances smoothly and in lockstep, independent
74
74
  * of how often (or how irregularly) progress data updates arrive. The animation
75
75
  * timers below only schedule re-renders; the displayed frame always comes from
76
- * the clock. This fixes the frozen/stuttering spinner from issue #1084 while
77
- * keeping per-frame diffs to a single glyph cell so the differential renderer
78
- * never needs a full-clear (no flicker).
76
+ * the clock. This fixes the frozen/stuttering spinner from issue #1084.
77
+ *
78
+ * IMPORTANT: a wall-clock spinner only stays flicker-free for widgets pinned to
79
+ * the bottom of the buffer (e.g. the below-editor async widget), where every
80
+ * tick stays inside the viewport. Content rendered into chat scrollback (live
81
+ * foreground subagent results) can scroll above the viewport fold; there, even
82
+ * a single-cell spinner diff forces pi-tui into a destructive full-screen +
83
+ * scrollback clear on every tick. Such surfaces must NOT animate on a timer —
84
+ * see pulseGlyph(), which is advanced once per real progress update instead.
79
85
  */
80
86
  export const RUNNING_ANIMATION_MS = 80;
81
87
 
@@ -106,6 +112,23 @@ export function runningGlyph(seed?: number, now?: number): string {
106
112
  return RUNNING_FRAMES[Math.abs(animatedSeed) % RUNNING_FRAMES.length]!;
107
113
  }
108
114
 
115
+ export const PULSE_FRAMES = ["·", "•", "●", "•"];
116
+
117
+ /**
118
+ * Activity "heartbeat" glyph for live foreground subagent results. Unlike
119
+ * runningGlyph(), the frame is NOT derived from wall-clock time: the caller
120
+ * advances `frame` exactly once per real progress update (see
121
+ * renderLiveSubagentResult). With no animation timer, the only line diffs this
122
+ * produces coincide with progress data that genuinely changed, so the pulse can
123
+ * live in chat scrollback (above or below the fold) without ever triggering
124
+ * pi-tui's full-screen/scrollback clear. Returns a steady breathing dot that
125
+ * grows and settles as the subagent reports activity.
126
+ */
127
+ export function pulseGlyph(frame?: number): string {
128
+ const index = Number.isFinite(frame) ? Math.abs(Math.trunc(frame as number)) : 0;
129
+ return PULSE_FRAMES[index % PULSE_FRAMES.length]!;
130
+ }
131
+
109
132
  export function progressRunningSeed(progress: ProgressSeedSource | undefined): number | undefined {
110
133
  if (!progress) return undefined;
111
134
  return runningSeed(
@@ -1,56 +1,47 @@
1
- import { RUNNING_ANIMATION_MS } from "./render-layout.ts";
2
-
3
1
  type ResultAnimationTimer = ReturnType<typeof setInterval>;
4
2
 
5
3
  export interface SubagentResultRenderState {
6
4
  subagentResultAnimationTimer?: ResultAnimationTimer;
5
+ subagentResultAnimationCleanup?: () => void;
7
6
  subagentResultSnapshotKey?: string;
8
7
  /** Stable semantic/content timestamp used for durations and activity text. */
9
8
  subagentResultSnapshotNow?: number;
10
- /** Timer-driven timestamp used only for spinner glyph frames. */
11
- subagentResultSpinnerFrameNow?: number;
9
+ /** Monotonic pulse frame, advanced once per progress update (no timer). */
10
+ subagentResultPulseFrame?: number;
12
11
  }
13
12
 
14
13
  export type ResultAnimationContext = {
15
14
  state: SubagentResultRenderState;
16
15
  invalidate: () => void;
17
16
  };
18
- type LegacyResultAnimationContext = { state: { subagentResultAnimationTimer?: ResultAnimationTimer } };
19
-
20
- const activeResultAnimationTimers = new Map<ResultAnimationTimer, SubagentResultRenderState>();
17
+ type LegacyResultAnimationContext = {
18
+ state: {
19
+ subagentResultAnimationTimer?: ResultAnimationTimer;
20
+ subagentResultAnimationCleanup?: () => void;
21
+ };
22
+ };
21
23
 
24
+ /**
25
+ * Legacy safety net for render state objects created by earlier timer-driven
26
+ * foreground result rendering. New code never schedules result timers, but
27
+ * clearing the field prevents a stale interval from surviving across upgrades.
28
+ */
22
29
  export function clearResultAnimationTimer(context: LegacyResultAnimationContext): void {
23
30
  const timer = context.state.subagentResultAnimationTimer;
24
- if (timer) {
25
- clearInterval(timer);
26
- activeResultAnimationTimers.delete(timer);
27
- }
31
+ if (timer) clearInterval(timer);
28
32
  context.state.subagentResultAnimationTimer = undefined;
33
+ context.state.subagentResultAnimationCleanup = undefined;
29
34
  }
30
35
 
31
- export function clearLegacyResultAnimationTimer(context: LegacyResultAnimationContext): void {
32
- clearResultAnimationTimer(context);
36
+ export function advanceResultPulseFrame(frame: number | undefined): number {
37
+ return (frame ?? 0) + 1;
33
38
  }
34
39
 
35
- export function ensureResultAnimation(context: ResultAnimationContext): void {
36
- if (context.state.subagentResultAnimationTimer) return;
37
- const timer = setInterval(() => {
38
- context.state.subagentResultSpinnerFrameNow = Date.now();
39
- try {
40
- context.invalidate();
41
- } catch {
42
- clearResultAnimationTimer(context);
43
- }
44
- }, RUNNING_ANIMATION_MS);
45
- timer.unref?.();
46
- context.state.subagentResultAnimationTimer = timer;
47
- activeResultAnimationTimers.set(timer, context.state);
40
+ export function clearLegacyResultAnimationTimer(context: LegacyResultAnimationContext): void {
41
+ clearResultAnimationTimer(context);
48
42
  }
49
43
 
50
44
  export function stopResultAnimations(): void {
51
- for (const [timer, state] of activeResultAnimationTimers) {
52
- clearInterval(timer);
53
- if (state.subagentResultAnimationTimer === timer) state.subagentResultAnimationTimer = undefined;
54
- }
55
- activeResultAnimationTimers.clear();
45
+ // Retained for extension teardown compatibility; result rendering no longer
46
+ // registers global animation timers.
56
47
  }
@@ -2,7 +2,7 @@ import { Container, Text, type Component } from "@earendil-works/pi-tui";
2
2
  import type { AgentProgress, AsyncJobStep, Details } from "../shared/types.ts";
3
3
  import { shortenPath } from "../shared/formatters.ts";
4
4
  import { getSingleResultOutput } from "../shared/utils.ts";
5
- import { getTermWidth, progressRunningSeed, runningGlyph, runningSeed, truncLine, type Theme } from "./render-layout.ts";
5
+ import { getTermWidth, pulseGlyph, truncLine, type Theme } from "./render-layout.ts";
6
6
  import {
7
7
  buildLiveStatusLine,
8
8
  compactCurrentActivity,
@@ -25,7 +25,7 @@ import {
25
25
  } from "./render-chain-graph.ts";
26
26
  import { modelThinkingBadge, widgetStepGlyph, widgetStepStatus } from "./render-event-formatting.ts";
27
27
 
28
- export function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, now?: number, spinnerNow?: number): Component {
28
+ export function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, now?: number, pulseFrame?: number): Component {
29
29
  const output = r.truncation?.text || getSingleResultOutput(r);
30
30
  const progress = r.progress || r.progressSummary;
31
31
  const isRunning = r.progress?.status === "running";
@@ -37,7 +37,7 @@ export function renderSingleCompact(d: Details, r: Details["results"][number], t
37
37
  const c = new Container();
38
38
  const width = getTermWidth() - 4;
39
39
  const modelDisplay = modelThinkingBadge(theme, r.model, undefined, r.fastMode);
40
- 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));
40
+ c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, pulseFrame)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
41
41
 
42
42
  if (isRunning && r.progress) {
43
43
  const progressSnapshotNow = snapshotNowForProgress(r.progress, now);
@@ -61,7 +61,7 @@ export function renderSingleCompact(d: Details, r: Details["results"][number], t
61
61
  return c;
62
62
  }
63
63
 
64
- export function renderMultiCompact(d: Details, theme: Theme, now?: number, spinnerNow?: number): Component {
64
+ export function renderMultiCompact(d: Details, theme: Theme, now?: number, pulseFrame?: number): Component {
65
65
  const hasRunning = d.progress?.some((p) => p.status === "running")
66
66
  || d.results.some((r) => r.progress?.status === "running")
67
67
  || workflowGraphHasStatus(d, ["running"]);
@@ -87,7 +87,7 @@ export function renderMultiCompact(d: Details, theme: Theme, now?: number, spinn
87
87
  const itemTitle = multiLabel.itemTitle;
88
88
  const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary, true, now)]);
89
89
  const glyph = hasRunning
90
- ? theme.fg("accent", runningGlyph(runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex), spinnerNow ?? now))
90
+ ? theme.fg("accent", pulseGlyph(pulseFrame))
91
91
  : failed
92
92
  ? theme.fg("error", "✗")
93
93
  : paused
@@ -133,7 +133,7 @@ export function renderMultiCompact(d: Details, theme: Theme, now?: number, spinn
133
133
  const rPending = rProg && "status" in rProg && rProg.status === "pending";
134
134
  const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
135
135
  const stepStats = formatProgressStats(theme, rProg, true, now);
136
- const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg), spinnerNow ?? now);
136
+ const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, pulseFrame);
137
137
  const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
138
138
  const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
139
139
  const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
@@ -5,10 +5,10 @@ import type { AgentProgress, AsyncJobStep, Details } from "../shared/types.ts";
5
5
  import { formatDuration, formatTokens, formatUsage, shortenPath } from "../shared/formatters.ts";
6
6
  import { getSingleResultOutput } from "../shared/utils.ts";
7
7
  import { getTermWidth, truncLine, type Theme } from "./render-layout.ts";
8
- import { clearResultAnimationTimer, ensureResultAnimation, type ResultAnimationContext } from "./render-result-animation.ts";
8
+ import { advanceResultPulseFrame, clearResultAnimationTimer, type ResultAnimationContext } from "./render-result-animation.ts";
9
9
  import { renderMultiCompact, renderSingleCompact } from "./render-result-compact.ts";
10
10
  import { buildChainRenderEntries, buildMultiProgressLabel, resultRowLabel, workflowGraphHasStatus, type ChainRenderEntry } from "./render-chain-graph.ts";
11
- import { isRunningSubagentResult, subagentResultRenderKey } from "./render-stable-output.ts";
11
+ import { subagentResultRenderKey } from "./render-stable-output.ts";
12
12
  import { modelThinkingBadge, widgetStepStatus } from "./render-event-formatting.ts";
13
13
  import {
14
14
  buildLiveStatusLine,
@@ -28,26 +28,27 @@ export function renderLiveSubagentResult(
28
28
  ): Component {
29
29
  const nextKey = subagentResultRenderKey(result, options);
30
30
  if (context.state.subagentResultSnapshotKey !== nextKey) {
31
- const frameNow = Date.now();
32
31
  context.state.subagentResultSnapshotKey = nextKey;
33
- context.state.subagentResultSnapshotNow = frameNow;
34
- context.state.subagentResultSpinnerFrameNow = frameNow;
32
+ context.state.subagentResultSnapshotNow = Date.now();
33
+ // Advance the activity pulse exactly once per real progress update.
34
+ // Foreground subagent results render into chat scrollback, which can sit
35
+ // above the viewport fold. Animating on a timer there forces pi-tui into a
36
+ // destructive full-screen/scrollback clear on every tick (the flicker that
37
+ // scaled with widget height). Driving the pulse off genuine updates keeps
38
+ // the only line diffs tied to content that actually changed, so the
39
+ // differential renderer repaints exactly as it would for any progress
40
+ // update — no extra above-fold churn between updates.
41
+ context.state.subagentResultPulseFrame = advanceResultPulseFrame(context.state.subagentResultPulseFrame);
35
42
  }
36
43
  context.state.subagentResultSnapshotNow ??= Date.now();
37
- context.state.subagentResultSpinnerFrameNow ??= context.state.subagentResultSnapshotNow;
38
- // Foreground subagent results render inside chat scrollback. Keep semantic
39
- // content time stable between tool/progress updates, but let the spinner tick
40
- // independently. That limits timer-driven diffs to spinner glyph cells instead
41
- // of updating elapsed/tool/activity text and causing broad chatbox churn.
42
- if (options.isPartial && isRunningSubagentResult(result)) {
43
- ensureResultAnimation(context);
44
- } else {
45
- clearResultAnimationTimer(context);
46
- }
44
+ context.state.subagentResultPulseFrame ??= 0;
45
+ // Never schedule timer-driven re-renders for scrollback content; clear any
46
+ // stale timer a previous version may have installed for this render slot.
47
+ clearResultAnimationTimer(context);
47
48
  return renderSubagentResult(result, {
48
49
  ...options,
49
50
  now: context.state.subagentResultSnapshotNow,
50
- spinnerNow: context.state.subagentResultSpinnerFrameNow,
51
+ pulseFrame: context.state.subagentResultPulseFrame,
51
52
  }, theme);
52
53
  }
53
54
 
@@ -56,7 +57,7 @@ export function renderLiveSubagentResult(
56
57
  */
57
58
  export function renderSubagentResult(
58
59
  result: AgentToolResult<Details>,
59
- options: { expanded: boolean; now?: number; spinnerNow?: number },
60
+ options: { expanded: boolean; now?: number; pulseFrame?: number },
60
61
  theme: Theme,
61
62
  ): Component {
62
63
  const d = result.details;
@@ -72,7 +73,7 @@ export function renderSubagentResult(
72
73
 
73
74
  if (d.mode === "single" && d.results.length === 1) {
74
75
  const r = d.results[0];
75
- if (!expanded) return renderSingleCompact(d, r, theme, options.now, options.spinnerNow);
76
+ if (!expanded) return renderSingleCompact(d, r, theme, options.now, options.pulseFrame);
76
77
  const isRunning = r.progress?.status === "running";
77
78
  const icon = isRunning
78
79
  ? theme.fg("warning", "running")
@@ -166,7 +167,7 @@ export function renderSubagentResult(
166
167
  return c;
167
168
  }
168
169
 
169
- if (!expanded) return renderMultiCompact(d, theme, options.now, options.spinnerNow);
170
+ if (!expanded) return renderMultiCompact(d, theme, options.now, options.pulseFrame);
170
171
 
171
172
  const hasRunning = d.progress?.some((p) => p.status === "running")
172
173
  || d.results.some((r) => r.progress?.status === "running")
@@ -2,7 +2,7 @@ import type { AgentProgress, Details } from "../shared/types.ts";
2
2
  import { formatDuration, formatTokens, formatToolCall } from "../shared/formatters.ts";
3
3
  import { getDisplayItems } from "../shared/utils.ts";
4
4
  import { formatActivityLabel } from "../shared/status-format.ts";
5
- import { getTermWidth, progressRunningSeed, runningGlyph, type Theme } from "./render-layout.ts";
5
+ import { getTermWidth, pulseGlyph, type Theme } from "./render-layout.ts";
6
6
 
7
7
  export function extractOutputTarget(task: string): string | undefined {
8
8
  const writeToMatch = task.match(/\[Write to:\s*([^\]\n]+)\]/i);
@@ -118,8 +118,8 @@ export function resultStatusLine(result: Details["results"][number], output: str
118
118
  return "Done";
119
119
  }
120
120
 
121
- export function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary), now?: number): string {
122
- if (running) return theme.fg("accent", runningGlyph(seed, now));
121
+ export function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", pulseFrame?: number): string {
122
+ if (running) return theme.fg("accent", pulseGlyph(pulseFrame));
123
123
  if (result.detached) return theme.fg("warning", "■");
124
124
  if (result.interrupted) return theme.fg("warning", "■");
125
125
  if (result.exitCode !== 0) return theme.fg("error", "✗");
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionContext } from "@bastani/atomic";
2
2
  import { Container, Text, type Component } from "@earendil-works/pi-tui";
3
+ import * as path from "node:path";
3
4
  import { MAX_WIDGET_JOBS, WIDGET_KEY, type AsyncJobState } from "../shared/types.ts";
4
5
  import { getTermWidth, RUNNING_ANIMATION_MS, runningGlyph, truncLine, type Theme } from "./render-layout.ts";
5
6
  import { themeBold } from "./render-status-progress.ts";
@@ -64,6 +65,7 @@ let latestWidgetJobs: AsyncJobState[] = [];
64
65
  let latestWidgetFrameNow = 0;
65
66
  let widgetTimer: ReturnType<typeof setInterval> | undefined;
66
67
  let mountedWidgetCtx: ExtensionContext | undefined;
68
+ let mountedWidgetOwnerKey: string | undefined;
67
69
  let widgetMounted = false;
68
70
 
69
71
  function getLatestWidgetJobs(): AsyncJobState[] {
@@ -87,9 +89,38 @@ function clearLatestWidgetState(): void {
87
89
  latestWidgetJobs = [];
88
90
  latestWidgetFrameNow = 0;
89
91
  mountedWidgetCtx = undefined;
92
+ mountedWidgetOwnerKey = undefined;
90
93
  widgetMounted = false;
91
94
  }
92
95
 
96
+ function getWidgetOwnerKey(ctx: ExtensionContext): string {
97
+ const resolvedCwd = ctx.cwd ? path.resolve(ctx.cwd) : undefined;
98
+ const cwdOwner = resolvedCwd ?? "cwd:unknown";
99
+ let sessionOwner = "session:unknown";
100
+ try {
101
+ const sessionFile = ctx.sessionManager.getSessionFile?.();
102
+ if (sessionFile) {
103
+ const resolvedSessionFile = resolvedCwd
104
+ ? path.resolve(resolvedCwd, sessionFile)
105
+ : path.resolve(sessionFile);
106
+ sessionOwner = `sessionFile:${resolvedSessionFile}`;
107
+ }
108
+ } catch {
109
+ // Fall through to the session id fallback below.
110
+ }
111
+ if (sessionOwner === "session:unknown") {
112
+ try {
113
+ const sessionId = ctx.sessionManager.getSessionId?.();
114
+ if (sessionId) sessionOwner = `sessionId:${sessionId}`;
115
+ } catch {
116
+ // Keep the unknown marker; cwd still scopes ownership.
117
+ }
118
+ }
119
+ // If no session identifier is available, cwd is the best available owner
120
+ // boundary and may intentionally coalesce concurrent sessions in one folder.
121
+ return `${sessionOwner}|cwd:${cwdOwner}`;
122
+ }
123
+
93
124
  function requestWidgetRender(ctx: ExtensionContext): void {
94
125
  (ctx as RenderRequestingContext).ui.requestRender?.();
95
126
  }
@@ -226,10 +257,12 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
226
257
  * Render the async jobs widget
227
258
  */
228
259
  export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
260
+ const ownerKey = getWidgetOwnerKey(ctx);
229
261
  if (jobs.length === 0) {
230
- if (widgetMounted && mountedWidgetCtx !== ctx) {
231
- // Empty updates from stale contexts must not clear the active context's
232
- // widget. The mounted context owns the eventual teardown.
262
+ if (widgetMounted && mountedWidgetOwnerKey !== ownerKey) {
263
+ // With no visible job frame, stale-owner empty updates and newly-active
264
+ // empty owners are indistinguishable here. Preserve active-widget safety;
265
+ // host session disposal owns cross-owner empty-session teardown.
233
266
  return;
234
267
  }
235
268
  stopWidgetAnimation();
@@ -242,11 +275,12 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
242
275
  latestWidgetCtx = ctx;
243
276
  latestWidgetJobs = [...jobs];
244
277
  latestWidgetFrameNow = Date.now();
245
- if (widgetMounted && mountedWidgetCtx !== ctx) {
246
- // Context rebinding can leave the previous host UI alive briefly; clear the
247
- // old mount before installing the singleton widget on the new context.
278
+ if (widgetMounted && mountedWidgetOwnerKey !== ownerKey) {
279
+ // Session rebinding can leave the previous host UI alive briefly; clear the
280
+ // old mount before installing the singleton widget on the new owner/context.
248
281
  unmountWidgetBestEffort(mountedWidgetCtx);
249
282
  mountedWidgetCtx = undefined;
283
+ mountedWidgetOwnerKey = undefined;
250
284
  widgetMounted = false;
251
285
  }
252
286
  if (!widgetMounted) {
@@ -260,10 +294,15 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
260
294
  placement: "belowEditor",
261
295
  });
262
296
  mountedWidgetCtx = ctx;
297
+ mountedWidgetOwnerKey = ownerKey;
263
298
  widgetMounted = true;
264
299
  } else {
265
300
  // The mounted widget reads latestWidgetJobs via getLatestWidgetJobs(), so a
266
- // visible->visible update only needs to ask the host to render in place.
301
+ // visible->visible update only needs to ask the host to render in place. Keep
302
+ // teardown pointed at the freshest same-owner wrapper because older wrappers
303
+ // can go stale after host session/context rebinding.
304
+ mountedWidgetCtx = ctx;
305
+ mountedWidgetOwnerKey = ownerKey;
267
306
  requestWidgetRender(ctx);
268
307
  }
269
308
  // Keep the just-rendered ctx/jobs as the last-rendered state; only the ticker
@@ -5,11 +5,11 @@
5
5
  * rendering responsibility across sibling modules.
6
6
  */
7
7
 
8
- export { RUNNING_ANIMATION_MS, currentRunningFrame } from "./render-layout.ts";
8
+ export { PULSE_FRAMES, RUNNING_ANIMATION_MS, RUNNING_FRAMES, currentRunningFrame, pulseGlyph } from "./render-layout.ts";
9
9
  export {
10
+ advanceResultPulseFrame,
10
11
  clearLegacyResultAnimationTimer,
11
12
  clearResultAnimationTimer,
12
- ensureResultAnimation,
13
13
  stopResultAnimations,
14
14
  } from "./render-result-animation.ts";
15
15
  export type { SubagentResultRenderState } from "./render-result-animation.ts";