@bastani/atomic 0.9.11-alpha.9 → 0.9.11

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 (285) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +1 -1
  3. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  4. package/dist/builtin/intercom/package.json +1 -1
  5. package/dist/builtin/intercom/subagent-relay.ts +8 -5
  6. package/dist/builtin/mcp/CHANGELOG.md +10 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +20 -0
  9. package/dist/builtin/subagents/agents/code-simplifier.md +2 -2
  10. package/dist/builtin/subagents/agents/codebase-analyzer.md +2 -2
  11. package/dist/builtin/subagents/agents/codebase-locator.md +2 -2
  12. package/dist/builtin/subagents/agents/codebase-online-researcher.md +2 -2
  13. package/dist/builtin/subagents/agents/codebase-pattern-finder.md +2 -2
  14. package/dist/builtin/subagents/agents/codebase-research-analyzer.md +2 -2
  15. package/dist/builtin/subagents/agents/codebase-research-locator.md +2 -2
  16. package/dist/builtin/subagents/agents/worker.md +2 -2
  17. package/dist/builtin/subagents/package.json +1 -1
  18. package/dist/builtin/subagents/src/extension/notification-content.ts +14 -9
  19. package/dist/builtin/subagents/src/runs/background/notify.ts +20 -4
  20. package/dist/builtin/web-access/CHANGELOG.md +20 -0
  21. package/dist/builtin/web-access/content-tools.ts +1 -0
  22. package/dist/builtin/web-access/flat-string.ts +37 -0
  23. package/dist/builtin/web-access/github-api.ts +2 -1
  24. package/dist/builtin/web-access/github-extract.ts +3 -2
  25. package/dist/builtin/web-access/package.json +2 -2
  26. package/dist/builtin/workflows/CHANGELOG.md +57 -0
  27. package/dist/builtin/workflows/builtin/goal-ledger.ts +6 -4
  28. package/dist/builtin/workflows/builtin/goal-runner.ts +2 -1
  29. package/dist/builtin/workflows/builtin/goal.ts +1 -1
  30. package/dist/builtin/workflows/builtin/ralph-core.ts +8 -9
  31. package/dist/builtin/workflows/builtin/ralph-forked-prompts.ts +2 -4
  32. package/dist/builtin/workflows/builtin/ralph-models.ts +4 -4
  33. package/dist/builtin/workflows/builtin/ralph-runner.ts +5 -8
  34. package/dist/builtin/workflows/builtin/ralph.ts +2 -1
  35. package/dist/builtin/workflows/package.json +1 -1
  36. package/dist/builtin/workflows/src/durable/completed-catalog.ts +14 -8
  37. package/dist/builtin/workflows/src/durable/completed-inspection.ts +10 -11
  38. package/dist/builtin/workflows/src/durable/resume-catalog.ts +1 -1
  39. package/dist/builtin/workflows/src/durable/resume-eligibility.ts +42 -0
  40. package/dist/builtin/workflows/src/durable/resume-runtime.ts +36 -37
  41. package/dist/builtin/workflows/src/durable/tool-primitive.ts +10 -2
  42. package/dist/builtin/workflows/src/engine/run.ts +1 -0
  43. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +12 -8
  44. package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +5 -2
  45. package/dist/builtin/workflows/src/extension/index.bundle.mjs +1966 -880
  46. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +6 -3
  47. package/dist/builtin/workflows/src/extension/postmortem-deps.ts +3 -2
  48. package/dist/builtin/workflows/src/extension/render-result.ts +4 -11
  49. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +17 -20
  50. package/dist/builtin/workflows/src/extension/runtime.ts +3 -3
  51. package/dist/builtin/workflows/src/extension/workflow-command-completions.ts +1 -1
  52. package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +3 -7
  53. package/dist/builtin/workflows/src/extension/workflow-durable-resume-command.ts +21 -24
  54. package/dist/builtin/workflows/src/extension/workflow-resume-picker-rows.ts +38 -26
  55. package/dist/builtin/workflows/src/extension/workflow-resume-shadow.ts +2 -1
  56. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +56 -64
  57. package/dist/builtin/workflows/src/extension/workflow-schema.ts +2 -2
  58. package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +0 -5
  59. package/dist/builtin/workflows/src/extension/workflow-targets.ts +17 -20
  60. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +1 -1
  61. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +24 -51
  62. package/dist/builtin/workflows/src/extension/workflow-tool-inspection.ts +7 -29
  63. package/dist/builtin/workflows/src/extension/workflow-tool-send.ts +5 -13
  64. package/dist/builtin/workflows/src/extension/workflow-tool.ts +4 -13
  65. package/dist/builtin/workflows/src/runs/background/quit.ts +4 -3
  66. package/dist/builtin/workflows/src/runs/background/run-inspect.ts +8 -8
  67. package/dist/builtin/workflows/src/runs/background/status.ts +2 -1
  68. package/dist/builtin/workflows/src/runs/background/workflow-lifecycle-aggregate.ts +2 -1
  69. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
  70. package/dist/builtin/workflows/src/runs/foreground/executor-lifecycle.ts +4 -0
  71. package/dist/builtin/workflows/src/runs/foreground/executor-scheduler.ts +2 -0
  72. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +23 -8
  73. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +6 -2
  74. package/dist/builtin/workflows/src/runs/foreground/executor-task-prompts.ts +5 -0
  75. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +56 -10
  76. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +6 -0
  77. package/dist/builtin/workflows/src/runs/foreground/stage-runner-output.ts +137 -6
  78. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +2 -0
  79. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +2 -0
  80. package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +159 -10
  81. package/dist/builtin/workflows/src/shared/flat-string.ts +33 -0
  82. package/dist/builtin/workflows/src/shared/graph-store-snapshot.ts +144 -0
  83. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +2 -3
  84. package/dist/builtin/workflows/src/shared/run-id.ts +36 -0
  85. package/dist/builtin/workflows/src/shared/store-internal.ts +32 -3
  86. package/dist/builtin/workflows/src/shared/store-observation.ts +10 -0
  87. package/dist/builtin/workflows/src/shared/store-public-types.ts +8 -0
  88. package/dist/builtin/workflows/src/shared/store-run-methods.ts +14 -0
  89. package/dist/builtin/workflows/src/shared/store-types.ts +4 -0
  90. package/dist/builtin/workflows/src/shared/types.ts +2 -0
  91. package/dist/builtin/workflows/src/shared/workflow-artifacts.ts +261 -0
  92. package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +39 -23
  93. package/dist/builtin/workflows/src/tui/graph-canvas.ts +26 -9
  94. package/dist/builtin/workflows/src/tui/graph-view-constants.ts +3 -0
  95. package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +95 -98
  96. package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -20
  97. package/dist/builtin/workflows/src/tui/graph-view-state.ts +160 -13
  98. package/dist/builtin/workflows/src/tui/layout.ts +8 -2
  99. package/dist/builtin/workflows/src/tui/node-card.ts +57 -45
  100. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -2
  101. package/dist/builtin/workflows/src/tui/prompt-card-render.ts +291 -21
  102. package/dist/builtin/workflows/src/tui/prompt-card.ts +1 -1
  103. package/dist/builtin/workflows/src/tui/renderers.ts +0 -1
  104. package/dist/builtin/workflows/src/tui/run-detail.ts +52 -35
  105. package/dist/builtin/workflows/src/tui/run-identity-rows.ts +97 -0
  106. package/dist/builtin/workflows/src/tui/session-overlays.ts +24 -3
  107. package/dist/builtin/workflows/src/tui/session-picker.ts +94 -41
  108. package/dist/builtin/workflows/src/tui/stage-chat-view-archive-history.ts +190 -26
  109. package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +52 -18
  110. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +9 -5
  111. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +44 -4
  112. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +3 -1
  113. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -2
  114. package/dist/builtin/workflows/src/tui/status-list.ts +67 -45
  115. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +7 -6
  116. package/dist/builtin/workflows/src/tui/widget.ts +49 -40
  117. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +15 -10
  118. package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +21 -8
  119. package/dist/core/agent-session-accessors.d.ts.map +1 -1
  120. package/dist/core/agent-session-accessors.js +5 -0
  121. package/dist/core/agent-session-accessors.js.map +1 -1
  122. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  123. package/dist/core/agent-session-auto-compaction.js +5 -0
  124. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  125. package/dist/core/agent-session-compaction.d.ts.map +1 -1
  126. package/dist/core/agent-session-compaction.js +3 -0
  127. package/dist/core/agent-session-compaction.js.map +1 -1
  128. package/dist/core/agent-session-events.d.ts.map +1 -1
  129. package/dist/core/agent-session-events.js +2 -1
  130. package/dist/core/agent-session-events.js.map +1 -1
  131. package/dist/core/agent-session-message-queue.d.ts +3 -7
  132. package/dist/core/agent-session-message-queue.d.ts.map +1 -1
  133. package/dist/core/agent-session-message-queue.js +27 -13
  134. package/dist/core/agent-session-message-queue.js.map +1 -1
  135. package/dist/core/agent-session-methods.d.ts +5 -3
  136. package/dist/core/agent-session-methods.d.ts.map +1 -1
  137. package/dist/core/agent-session-methods.js.map +1 -1
  138. package/dist/core/agent-session-persistent-custom-messages.d.ts.map +1 -1
  139. package/dist/core/agent-session-persistent-custom-messages.js +6 -3
  140. package/dist/core/agent-session-persistent-custom-messages.js.map +1 -1
  141. package/dist/core/agent-session-post-tool-compaction.d.ts.map +1 -1
  142. package/dist/core/agent-session-post-tool-compaction.js +11 -2
  143. package/dist/core/agent-session-post-tool-compaction.js.map +1 -1
  144. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  145. package/dist/core/agent-session-runtime.js +5 -3
  146. package/dist/core/agent-session-runtime.js.map +1 -1
  147. package/dist/core/agent-session-tree.d.ts.map +1 -1
  148. package/dist/core/agent-session-tree.js +4 -0
  149. package/dist/core/agent-session-tree.js.map +1 -1
  150. package/dist/core/agent-session-types.d.ts +4 -0
  151. package/dist/core/agent-session-types.d.ts.map +1 -1
  152. package/dist/core/agent-session-types.js.map +1 -1
  153. package/dist/core/agent-session.d.ts +2 -1
  154. package/dist/core/agent-session.d.ts.map +1 -1
  155. package/dist/core/agent-session.js +1 -0
  156. package/dist/core/agent-session.js.map +1 -1
  157. package/dist/core/auth-storage-backends.d.ts.map +1 -1
  158. package/dist/core/auth-storage-backends.js +12 -1
  159. package/dist/core/auth-storage-backends.js.map +1 -1
  160. package/dist/core/bounded-model-refresh.d.ts +14 -0
  161. package/dist/core/bounded-model-refresh.d.ts.map +1 -0
  162. package/dist/core/bounded-model-refresh.js +38 -0
  163. package/dist/core/bounded-model-refresh.js.map +1 -0
  164. package/dist/core/context-tool-pairing.d.ts +4 -4
  165. package/dist/core/context-tool-pairing.js +4 -4
  166. package/dist/core/context-tool-pairing.js.map +1 -1
  167. package/dist/core/http-dispatcher.d.ts.map +1 -1
  168. package/dist/core/http-dispatcher.js +26 -1
  169. package/dist/core/http-dispatcher.js.map +1 -1
  170. package/dist/core/messages.d.ts +18 -6
  171. package/dist/core/messages.d.ts.map +1 -1
  172. package/dist/core/messages.js +46 -18
  173. package/dist/core/messages.js.map +1 -1
  174. package/dist/core/model-refresh-timeout.d.ts +9 -0
  175. package/dist/core/model-refresh-timeout.d.ts.map +1 -0
  176. package/dist/core/model-refresh-timeout.js +9 -0
  177. package/dist/core/model-refresh-timeout.js.map +1 -0
  178. package/dist/core/model-runtime-snapshot.d.ts +3 -0
  179. package/dist/core/model-runtime-snapshot.d.ts.map +1 -1
  180. package/dist/core/model-runtime-snapshot.js +59 -0
  181. package/dist/core/model-runtime-snapshot.js.map +1 -1
  182. package/dist/core/model-runtime.d.ts +7 -1
  183. package/dist/core/model-runtime.d.ts.map +1 -1
  184. package/dist/core/model-runtime.js +72 -17
  185. package/dist/core/model-runtime.js.map +1 -1
  186. package/dist/core/sdk.d.ts.map +1 -1
  187. package/dist/core/sdk.js +4 -3
  188. package/dist/core/sdk.js.map +1 -1
  189. package/dist/core/session-manager-core.d.ts +1 -1
  190. package/dist/core/session-manager-core.d.ts.map +1 -1
  191. package/dist/core/session-manager-core.js +2 -2
  192. package/dist/core/session-manager-core.js.map +1 -1
  193. package/dist/core/session-manager-entries.d.ts +1 -1
  194. package/dist/core/session-manager-entries.d.ts.map +1 -1
  195. package/dist/core/session-manager-entries.js +2 -1
  196. package/dist/core/session-manager-entries.js.map +1 -1
  197. package/dist/core/session-manager-history.d.ts.map +1 -1
  198. package/dist/core/session-manager-history.js +2 -2
  199. package/dist/core/session-manager-history.js.map +1 -1
  200. package/dist/core/session-manager-types.d.ts +3 -0
  201. package/dist/core/session-manager-types.d.ts.map +1 -1
  202. package/dist/core/session-manager-types.js.map +1 -1
  203. package/dist/index.d.ts +1 -1
  204. package/dist/index.d.ts.map +1 -1
  205. package/dist/index.js.map +1 -1
  206. package/dist/main-app-mode.d.ts +1 -0
  207. package/dist/main-app-mode.d.ts.map +1 -1
  208. package/dist/main-app-mode.js +3 -0
  209. package/dist/main-app-mode.js.map +1 -1
  210. package/dist/main.d.ts.map +1 -1
  211. package/dist/main.js +8 -3
  212. package/dist/main.js.map +1 -1
  213. package/dist/modes/interactive/components/chat-session-host-actions.d.ts +7 -2
  214. package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
  215. package/dist/modes/interactive/components/chat-session-host-actions.js +13 -4
  216. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  217. package/dist/modes/interactive/components/chat-session-host-editor.d.ts +3 -1
  218. package/dist/modes/interactive/components/chat-session-host-editor.d.ts.map +1 -1
  219. package/dist/modes/interactive/components/chat-session-host-editor.js +2 -2
  220. package/dist/modes/interactive/components/chat-session-host-editor.js.map +1 -1
  221. package/dist/modes/interactive/components/chat-session-host-events.d.ts +3 -0
  222. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  223. package/dist/modes/interactive/components/chat-session-host-events.js +5 -4
  224. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  225. package/dist/modes/interactive/components/chat-session-host.d.ts +16 -2
  226. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  227. package/dist/modes/interactive/components/chat-session-host.js +35 -5
  228. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  229. package/dist/modes/interactive/components/index.d.ts +1 -0
  230. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  231. package/dist/modes/interactive/components/index.js.map +1 -1
  232. package/dist/modes/interactive/components/model-selector.d.ts +0 -1
  233. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  234. package/dist/modes/interactive/components/model-selector.js +25 -35
  235. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  236. package/dist/modes/interactive/interactive-editor-actions.js +2 -1
  237. package/dist/modes/interactive/interactive-editor-actions.js.map +1 -1
  238. package/dist/modes/interactive/interactive-input-handling.js +1 -1
  239. package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
  240. package/dist/modes/interactive/interactive-mode-surface.d.ts +4 -1
  241. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  242. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  243. package/dist/modes/interactive/interactive-model-routing.js +4 -1
  244. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  245. package/dist/modes/interactive/interactive-pause.d.ts +3 -1
  246. package/dist/modes/interactive/interactive-pause.d.ts.map +1 -1
  247. package/dist/modes/interactive/interactive-pause.js +5 -1
  248. package/dist/modes/interactive/interactive-pause.js.map +1 -1
  249. package/dist/modes/interactive/interactive-queueing.js +3 -3
  250. package/dist/modes/interactive/interactive-queueing.js.map +1 -1
  251. package/dist/modes/interactive-engine/engine-health.d.ts +6 -0
  252. package/dist/modes/interactive-engine/engine-health.d.ts.map +1 -1
  253. package/dist/modes/interactive-engine/engine-health.js +15 -1
  254. package/dist/modes/interactive-engine/engine-health.js.map +1 -1
  255. package/dist/modes/interactive-engine/isolated-auth.js +1 -1
  256. package/dist/modes/interactive-engine/isolated-auth.js.map +1 -1
  257. package/dist/modes/interactive-engine/isolated-runtime.d.ts +1 -0
  258. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  259. package/dist/modes/interactive-engine/isolated-runtime.js +7 -1
  260. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  261. package/dist/modes/interactive-engine/remote-model-catalog.d.ts.map +1 -1
  262. package/dist/modes/interactive-engine/remote-model-catalog.js +5 -1
  263. package/dist/modes/interactive-engine/remote-model-catalog.js.map +1 -1
  264. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  265. package/dist/modes/rpc/rpc-command-handler.js +23 -7
  266. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  267. package/dist/modes/rpc/rpc-input-scheduler.js +1 -1
  268. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  269. package/dist/modes/rpc/rpc-types.d.ts +2 -1
  270. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  271. package/dist/modes/rpc/rpc-types.js.map +1 -1
  272. package/docs/custom-provider.md +3 -3
  273. package/docs/extensions.md +3 -1
  274. package/docs/index.md +10 -0
  275. package/docs/keybindings.md +1 -1
  276. package/docs/models.md +2 -0
  277. package/docs/providers.md +7 -3
  278. package/docs/quickstart.md +7 -1
  279. package/docs/session-format.md +2 -0
  280. package/docs/termux.md +3 -1
  281. package/docs/usage.md +3 -2
  282. package/docs/workflows.md +82 -46
  283. package/npm-shrinkwrap.json +59 -27
  284. package/package.json +3 -3
  285. package/dist/builtin/workflows/src/tui/toast.ts +0 -107
@@ -5,17 +5,18 @@ import {
5
5
  type ExpandedWorkflowStageTarget,
6
6
  expandedStageTarget,
7
7
  expandWorkflowGraph,
8
+ refreshExpandedWorkflowGraph,
9
+ sameExpandedWorkflowTopology,
8
10
  } from "../shared/expanded-workflow-graph.js";
9
11
  import type { Store } from "../shared/store.js";
12
+ import { readGraphStoreSnapshot, subscribeStoreInvalidation } from "../shared/store-observation.js";
10
13
  import type { PendingPrompt, RunSnapshot, StageSnapshot, StoreSnapshot } from "../shared/store-types.js";
11
14
  import type { GraphTheme } from "./graph-theme.js";
12
15
  import { ANIMATION_TICK_MS } from "./graph-view-constants.js";
13
16
  import type { GraphViewMode, GraphViewOpts } from "./graph-view-types.js";
14
- import type { LayoutNode } from "./layout.js";
15
- import { computeLayout } from "./layout.js";
17
+ import { computeLayout, type LayoutNode, NODE_H, NODE_W } from "./layout.js";
16
18
  import { createPromptCardState, type PromptCardState } from "./prompt-card.js";
17
19
  import type { SwitcherState } from "./switcher.js";
18
- import { createToastManager } from "./toast.js";
19
20
 
20
21
  export interface GraphStageCounts {
21
22
  pending: number;
@@ -41,6 +42,30 @@ interface GraphViewportGeometry {
41
42
  viewportWidth: number;
42
43
  }
43
44
 
45
+ interface GraphLayoutBand {
46
+ top: number;
47
+ bottom: number;
48
+ nodeIndices: number[];
49
+ }
50
+
51
+ interface GraphRenderEdge {
52
+ parentX: number;
53
+ parentY: number;
54
+ childX: number;
55
+ childY: number;
56
+ top: number;
57
+ bottom: number;
58
+ left: number;
59
+ right: number;
60
+ }
61
+
62
+ interface GraphRenderGeometry {
63
+ canvasWidth: number;
64
+ totalRows: number;
65
+ bands: GraphLayoutBand[];
66
+ edges: GraphRenderEdge[];
67
+ }
68
+
44
69
  /** Expansion, focus, prompt, and store-backed layout state for GraphView. */
45
70
  export abstract class GraphViewState {
46
71
  protected mode: GraphViewMode;
@@ -66,9 +91,10 @@ export abstract class GraphViewState {
66
91
  protected focusedIndex = 0;
67
92
  protected switcherOpen = false;
68
93
  protected switcherState: SwitcherState = { query: "", selectedIndex: 0 };
69
- protected toastManager = createToastManager();
70
94
  protected detailsExpanded = true;
71
95
  protected cachedLayout: LayoutNode[] = [];
96
+ /** Stages mirrored from `cachedLayout` — shared across card renders. */
97
+ protected cachedDisplayStages: StageSnapshot[] = [];
72
98
  protected expandedGraph: ExpandedWorkflowGraph = {
73
99
  stages: [],
74
100
  renderStages: [],
@@ -81,9 +107,14 @@ export abstract class GraphViewState {
81
107
  protected graphScrollColOffset = 0;
82
108
  protected graphNodeHitRects: GraphNodeHitRect[] = [];
83
109
  protected lastGraphViewport: GraphViewportGeometry | null = null;
110
+ protected lastGraphTotalRows = 0;
84
111
  protected lastOverlayFrameWidth = 80;
85
112
  protected pendingEnsureFocusedVisible = true;
86
113
  protected lastAutoFocusedAwaitingInputKey: string | null = null;
114
+ protected lastBuiltSnapshotVersion: number | null = null;
115
+ protected topologySnapshot: StoreSnapshot | null = null;
116
+ protected hasAnimatingStages = false;
117
+ protected cachedRenderGeometry: GraphRenderGeometry = { canvasWidth: 0, totalRows: 0, bands: [], edges: [] };
87
118
 
88
119
  protected _intervalId: ReturnType<typeof setInterval> | null = null;
89
120
  protected _lastGTime: number | null = null;
@@ -107,11 +138,11 @@ export abstract class GraphViewState {
107
138
  this.footerData = opts.footerData;
108
139
  this.getStageQueuedMessageCount = opts.getStageQueuedMessageCount;
109
140
 
110
- this._unsubscribe = this.store.subscribe((snap) => {
111
- this.currentSnapshot = snap;
141
+ this._unsubscribe = subscribeStoreInvalidation(this.store, () => {
142
+ this.currentSnapshot = readGraphStoreSnapshot(this.store);
112
143
  this._rebuildLayout();
113
144
  });
114
- this.currentSnapshot = this.store.snapshot();
145
+ this.currentSnapshot = readGraphStoreSnapshot(this.store);
115
146
  this._rebuildLayout();
116
147
 
117
148
  // Animation tick: while the overlay is mounted, fire a render
@@ -125,18 +156,35 @@ export abstract class GraphViewState {
125
156
  // gate in place the previous tmux scrollback "ghost overlay"
126
157
  // failure mode does not apply: pi-tui owns the screen buffer
127
158
  // and diff-blits frames in place.
159
+ //
160
+ // Skip ticks when nothing is animating (#2100): a large completed
161
+ // or pending-only graph must not burn ~10 full paints/sec.
128
162
  if (this.mode === "overlay" && this.requestRender) {
129
163
  this._intervalId = setInterval(() => {
164
+ if (!this._needsAnimationTick()) return;
130
165
  this.requestRender?.();
131
166
  }, ANIMATION_TICK_MS);
132
167
  (this._intervalId as { unref?: () => void }).unref?.();
133
168
  }
134
169
  }
135
170
 
171
+ protected _needsAnimationTick(): boolean {
172
+ return this.promptState !== null || this.hasAnimatingStages;
173
+ }
174
+
136
175
  protected _rebuildLayout(): void {
176
+ const version = this.currentSnapshot?.version ?? null;
177
+ // Overlay adapter calls `invalidate()` after GraphView's own store
178
+ // subscriber already rebuilt for this snapshot — skip the duplicate.
179
+ if (version !== null && version === this.lastBuiltSnapshotVersion) {
180
+ return;
181
+ }
182
+
137
183
  const run = this._getCurrentRun();
138
184
  if (!run) {
139
185
  this.cachedLayout = [];
186
+ this.cachedDisplayStages = [];
187
+ this.cachedRenderGeometry = { canvasWidth: 0, totalRows: 0, bands: [], edges: [] };
140
188
  this.expandedGraph = { stages: [], renderStages: [], tools: [], nodes: [], targets: new Map() };
141
189
  this.focusedIndex = 0;
142
190
  this.graphScrollOffset = 0;
@@ -145,16 +193,109 @@ export abstract class GraphViewState {
145
193
  this.lastGraphViewport = null;
146
194
  this.pendingEnsureFocusedVisible = true;
147
195
  this.promptState = null;
196
+ this.topologySnapshot = this.currentSnapshot;
197
+ this.hasAnimatingStages = false;
198
+ this.lastBuiltSnapshotVersion = version;
148
199
  return;
149
200
  }
150
201
 
151
202
  const previousFocusedStageId = this.cachedLayout[this.focusedIndex]?.stage.id;
152
203
  const graphStages = this._graphStages(run);
204
+ const sameTopology =
205
+ this.cachedLayout.length === graphStages.length &&
206
+ this.cachedLayout.length > 0 &&
207
+ this.cachedLayout.every((node, index) => {
208
+ const next = graphStages[index];
209
+ if (!next || node.stage.id !== next.id || node.stage.nodeKind !== next.nodeKind) return false;
210
+ if (node.stage.parentIds.length !== next.parentIds.length) return false;
211
+ return node.stage.parentIds.every((parentId, parentIndex) => parentId === next.parentIds[parentIndex]);
212
+ });
213
+
214
+ if (sameTopology) {
215
+ for (let index = 0; index < this.cachedLayout.length; index++) {
216
+ this.cachedLayout[index]!.stage = graphStages[index]!;
217
+ }
218
+ this.cachedDisplayStages = [...graphStages];
219
+ this.hasAnimatingStages = graphStages.some(
220
+ (stage) => stage.status === "running" || stage.status === "awaiting_input",
221
+ );
222
+ this._finalizeFocusAndPrompt(run, previousFocusedStageId, true);
223
+ this.lastBuiltSnapshotVersion = version;
224
+ return;
225
+ }
226
+
153
227
  const nextLayout = computeLayout(graphStages, { orientation: "vertical" });
154
228
  this.cachedLayout = nextLayout;
229
+ this.cachedDisplayStages = nextLayout.map((node) => node.stage);
230
+ this.cachedRenderGeometry = this._buildRenderGeometry(nextLayout);
231
+ this.hasAnimatingStages = graphStages.some(
232
+ (stage) => stage.status === "running" || stage.status === "awaiting_input",
233
+ );
155
234
  this.graphNodeHitRects = [];
156
235
  this.lastGraphViewport = null;
236
+ this._finalizeFocusAndPrompt(run, previousFocusedStageId, false);
237
+ this.lastBuiltSnapshotVersion = version;
238
+ }
239
+
240
+ private _buildRenderGeometry(layout: readonly LayoutNode[]): GraphRenderGeometry {
241
+ let canvasWidth = 0;
242
+ let totalRows = 0;
243
+ const nodeByStageId = new Map<string, LayoutNode>();
244
+ const bandsByTop = new Map<number, GraphLayoutBand>();
245
+ for (let index = 0; index < layout.length; index++) {
246
+ const node = layout[index]!;
247
+ canvasWidth = Math.max(canvasWidth, node.x + NODE_W);
248
+ totalRows = Math.max(totalRows, node.y + NODE_H);
249
+ nodeByStageId.set(node.stage.id, node);
250
+ const band = bandsByTop.get(node.y);
251
+ if (band) band.nodeIndices.push(index);
252
+ else bandsByTop.set(node.y, { top: node.y, bottom: node.y + NODE_H, nodeIndices: [index] });
253
+ }
157
254
 
255
+ const edges: GraphRenderEdge[] = [];
256
+ for (const node of layout) {
257
+ for (const parentId of node.stage.parentIds) {
258
+ const parent = nodeByStageId.get(parentId);
259
+ if (!parent) continue;
260
+ const parentCol = parent.x + Math.floor(NODE_W / 2);
261
+ const childCol = node.x + Math.floor(NODE_W / 2);
262
+ edges.push({
263
+ parentX: parent.x,
264
+ parentY: parent.y,
265
+ childX: node.x,
266
+ childY: node.y,
267
+ top: parent.y + NODE_H,
268
+ bottom: node.y,
269
+ left: Math.min(parentCol, childCol),
270
+ right: Math.max(parentCol, childCol) + 1,
271
+ });
272
+ }
273
+ }
274
+ return {
275
+ canvasWidth,
276
+ totalRows,
277
+ bands: [...bandsByTop.values()].sort((left, right) => left.top - right.top),
278
+ edges,
279
+ };
280
+ }
281
+
282
+ protected _firstVisibleLayoutBand(viewportTop: number): number {
283
+ const bands = this.cachedRenderGeometry.bands;
284
+ let low = 0;
285
+ let high = bands.length;
286
+ while (low < high) {
287
+ const middle = Math.floor((low + high) / 2);
288
+ if (bands[middle]!.bottom <= viewportTop) low = middle + 1;
289
+ else high = middle;
290
+ }
291
+ return low;
292
+ }
293
+
294
+ protected _finalizeFocusAndPrompt(
295
+ run: RunSnapshot,
296
+ previousFocusedStageId: string | undefined,
297
+ preserveHitRects: boolean,
298
+ ): void {
158
299
  let focusNeedsReveal = this.pendingEnsureFocusedVisible;
159
300
  // One-shot: if the host passed `initialFocusedStageId`, snap the
160
301
  // cursor to that stage now that the layout exists. The attach shell
@@ -206,6 +347,10 @@ export abstract class GraphViewState {
206
347
  focusNeedsReveal = true;
207
348
  }
208
349
  this.pendingEnsureFocusedVisible = focusNeedsReveal;
350
+ if (!preserveHitRects && focusNeedsReveal) {
351
+ this.graphNodeHitRects = [];
352
+ this.lastGraphViewport = null;
353
+ }
209
354
  this._syncPromptState(run.pendingPrompt);
210
355
  }
211
356
 
@@ -247,9 +392,13 @@ export abstract class GraphViewState {
247
392
  }
248
393
 
249
394
  protected _graphStages(run: RunSnapshot): ExpandedWorkflowStage[] {
250
- this.expandedGraph = this.currentSnapshot
251
- ? expandWorkflowGraph(this.currentSnapshot, run.id)
252
- : { stages: [], renderStages: [], tools: [], nodes: [], targets: new Map() };
395
+ const snapshot = this.currentSnapshot;
396
+ if (!snapshot) return [];
397
+ const canRefresh =
398
+ this.topologySnapshot !== null && sameExpandedWorkflowTopology(this.topologySnapshot, snapshot);
399
+ const refreshed = canRefresh ? refreshExpandedWorkflowGraph(this.expandedGraph, snapshot) : undefined;
400
+ this.expandedGraph = refreshed ?? expandWorkflowGraph(snapshot, run.id);
401
+ this.topologySnapshot = snapshot;
253
402
  const stages = [...this.expandedGraph.renderStages];
254
403
  const hasStagePrompt = stages.some(
255
404
  (stage) =>
@@ -258,8 +407,6 @@ export abstract class GraphViewState {
258
407
  );
259
408
  if (!hasStagePrompt) return stages;
260
409
  return stages.filter((stage) => {
261
- // Prompt-node injection can leave unstarted author stages in the store
262
- // while the prompt node owns focus; hide only these inert placeholders.
263
410
  const isUnstartedPlaceholder =
264
411
  stage.status === "pending" &&
265
412
  stage.startedAt === undefined &&
@@ -324,7 +471,7 @@ export abstract class GraphViewState {
324
471
  }
325
472
 
326
473
  protected _displayStages(run: RunSnapshot): StageSnapshot[] {
327
- return this.cachedLayout.length > 0 ? this.cachedLayout.map((layoutNode) => layoutNode.stage) : [...run.stages];
474
+ return this.cachedDisplayStages.length > 0 ? this.cachedDisplayStages : [...run.stages];
328
475
  }
329
476
 
330
477
  protected _counts(stages: readonly StageSnapshot[]): GraphStageCounts {
@@ -129,11 +129,17 @@ export function computeLayout(stages: readonly StageSnapshot[], opts: LayoutOpts
129
129
  centreShift.set(depth, Math.round(widestCentre - centre));
130
130
  }
131
131
 
132
+ const rowIndex = new Map<string, number>();
133
+ for (const ids of colGroups.values()) {
134
+ for (let i = 0; i < ids.length; i++) {
135
+ rowIndex.set(ids[i]!, i);
136
+ }
137
+ }
138
+
132
139
  const nodes: LayoutNode[] = [];
133
140
  for (const s of stages) {
134
141
  const col = colMap.get(s.id) ?? 0;
135
- const group = colGroups.get(col) ?? [];
136
- const row = group.indexOf(s.id);
142
+ const row = rowIndex.get(s.id) ?? 0;
137
143
  const x =
138
144
  orientation === "vertical" ? row * (NODE_W + colGap) + (centreShift.get(col) ?? 0) : col * (NODE_W + colGap);
139
145
  const y = orientation === "vertical" ? col * (NODE_H + rowGap) : row * (NODE_H + rowGap);
@@ -30,6 +30,7 @@ import { elapsedStageMs } from "../shared/timing.js";
30
30
  import { BOLD, hexBg, hexToAnsi, lerpColor, paint, RESET } from "./color-utils.js";
31
31
  import type { GraphTheme } from "./graph-theme.js";
32
32
  import { NODE_H, NODE_W } from "./layout.js";
33
+ import { wrapIdentifierLines } from "./run-identity-rows.js";
33
34
  import { fmtDuration, statusIcon } from "./status-helpers.js";
34
35
  import { truncateToWidth, visibleWidth } from "./text-helpers.js";
35
36
 
@@ -131,28 +132,29 @@ function metaText(stage: StageSnapshot): string {
131
132
  return stage.fastMode === true ? `${dependencyText} · fast` : dependencyText;
132
133
  }
133
134
 
134
- function shortRunId(runId: string): string {
135
- return runId.length <= 8 ? runId : runId.slice(0, 8);
136
- }
137
-
138
- function workflowChildSummaryText(stage: StageSnapshot): string {
135
+ function workflowChildRunRows(stage: StageSnapshot, width: number): string[] {
139
136
  const child = stage.workflowChild ?? stage.workflowChildRun;
140
- if (child === undefined) return durationText(stage);
141
- return `↳ ${child.workflow}`;
137
+ if (child === undefined) return [];
138
+ return wrapIdentifierLines(child.runId, Math.max(1, width), "run ", "").map((row) => `${row.prefix}${row.chunk}`);
142
139
  }
143
140
 
144
- function workflowChildMetaText(stage: StageSnapshot): string {
141
+ function workflowChildMetaText(stage: StageSnapshot): string | undefined {
145
142
  const completed = stage.workflowChild;
146
143
  if (completed !== undefined) {
147
- const outputCount = Object.keys(completed.outputs).length;
148
- const outputs = outputCount === 1 ? "1 out" : `${outputCount} outs`;
149
- return `run ${shortRunId(completed.runId)} · ${outputs}`;
144
+ // #2140 moved the child run id onto its own wrapped row, so the meta
145
+ // line carries only the output count. Prefer the payload-free
146
+ // `outputCount` the compact graph projection supplies; fall back to
147
+ // counting a full `outputs` map on the legacy snapshot path.
148
+ const outputCount = completed.outputCount ?? Object.keys(completed.outputs).length;
149
+ return outputCount === 1 ? "1 out" : `${outputCount} outs`;
150
150
  }
151
+ if (stage.workflowChildRun !== undefined) return "live";
152
+ return undefined;
153
+ }
151
154
 
152
- const live = stage.workflowChildRun;
153
- if (live !== undefined) return `run ${shortRunId(live.runId)} · live`;
154
-
155
- return metaText(stage);
155
+ function joinCompactStatusMeta(status: string, meta: string, width: number): string {
156
+ const candidates = [`${status} · ${meta}`, `${status} ·${meta}`, `${status}· ${meta}`, `${status}·${meta}`];
157
+ return candidates.find((candidate) => visibleWidth(candidate) <= width) ?? meta;
156
158
  }
157
159
 
158
160
  function statusLabel(status: StageStatus): string {
@@ -210,22 +212,23 @@ function buildTitleSlot(
210
212
  focused: boolean,
211
213
  theme: GraphTheme,
212
214
  cardBg: string,
215
+ compact = false,
213
216
  ): { slot: string; visibleWidth: number } {
214
- const maxName = Math.max(2, innerWidth - 4);
217
+ const maxName = Math.max(2, compact ? innerWidth - 1 : innerWidth - 4);
215
218
  const safeName = truncate(name, maxName);
216
219
  if (focused) {
217
220
  // Flanking spaces sit on the accent tab so the pill reads as a
218
221
  // single coloured run. Use `paint` to combine bg + fg + bold +
219
222
  // RESET in one ANSI sequence, then re-prime the card stratum so
220
223
  // the dashes outside the slot stay on the body bg.
221
- const tabText = ` ${safeName} `;
224
+ const tabText = compact ? safeName : ` ${safeName} `;
222
225
  const styled = `${paint(tabText, theme.surface, {
223
226
  bg: theme.accent,
224
227
  bold: true,
225
228
  })}${cardBg}`;
226
229
  return { slot: styled, visibleWidth: visibleWidth(tabText) };
227
230
  }
228
- const titleRaw = ` ${safeName} `;
231
+ const titleRaw = compact ? safeName : ` ${safeName} `;
229
232
  const styled = `${BOLD}${titleRaw}${RESET}${cardBg}`;
230
233
  return { slot: styled, visibleWidth: visibleWidth(titleRaw) };
231
234
  }
@@ -251,14 +254,16 @@ export function renderNodeCard(stage: StageSnapshot, opts: NodeCardOpts): string
251
254
  const bg = hexBg(theme.bg);
252
255
  const innerWidth = Math.max(2, width - 2);
253
256
 
254
- // Title sits inside the top border. Focus adds an accent-coloured
255
- // pill to the name slot without adding an extra glyph.
257
+ // Child workflow boundaries use the compact title path so their workflow
258
+ // identity remains visible without changing the fixed card geometry.
259
+ const child = stage.workflowChild ?? stage.workflowChildRun;
256
260
  const { slot: titleSlot, visibleWidth: titleVisibleWidth } = buildTitleSlot(
257
- stage.name,
261
+ child === undefined ? stage.name : `↳ ${child.workflow}`,
258
262
  innerWidth,
259
263
  focused,
260
264
  theme,
261
265
  bg,
266
+ child !== undefined,
262
267
  );
263
268
  const titleStart = Math.max(1, Math.floor((innerWidth - titleVisibleWidth) / 2));
264
269
  const titleEnd = titleStart + titleVisibleWidth;
@@ -267,17 +272,12 @@ export function renderNodeCard(stage: StageSnapshot, opts: NodeCardOpts): string
267
272
  const top = `${bg}${bc}╭${topMiddle}╮${RESET}`;
268
273
  const bottom = `${bg}${bc}╰${"─".repeat(innerWidth)}╯${RESET}`;
269
274
 
270
- // Interior — compact status + duration. Child workflow boundary
271
- // stages otherwise look like empty completed nodes, so use the first
272
- // body row for the child workflow identity and the final row for a
273
- // terse child-run summary. This keeps the graph dense while making
274
- // the boundary explain what actually ran.
275
275
  const bodyText =
276
276
  stage.nodeKind === "tool"
277
277
  ? (stage.error ?? stage.result ?? "durable tool")
278
278
  : stage.status === "blocked"
279
279
  ? blockedBadgeText(stage, opts.stages, innerWidth)
280
- : workflowChildSummaryText(stage);
280
+ : durationText(stage);
281
281
  const bodyHex = durationColor(stage.status, theme);
282
282
  const statusText = `${statusIcon(stage.status)} ${stage.toolStatus ?? statusLabel(stage.status)}`;
283
283
  const statusLine =
@@ -292,10 +292,26 @@ export function renderNodeCard(stage: StageSnapshot, opts: NodeCardOpts): string
292
292
  bold: stage.status === "blocked",
293
293
  }) +
294
294
  `${bg}${bc}│${RESET}`;
295
- const metaLine =
296
- `${bg}${bc}│${RESET}` +
297
- centreColored(workflowChildMetaText(stage), innerWidth, theme.dim, bg) +
298
- `${bg}${bc}│${RESET}`;
295
+
296
+ const contentRows = Math.max(0, height - 2);
297
+ const metaLine = `${bg}${bc}│${RESET}${centreColored(metaText(stage), innerWidth, theme.dim, bg)}${bg}${bc}│${RESET}`;
298
+ const childRunLines = workflowChildRunRows(stage, innerWidth).map(
299
+ (row) => `${bg}${bc}│${RESET}${centreColored(row, innerWidth, theme.dim, bg)}${bg}${bc}│${RESET}`,
300
+ );
301
+ const queuedCount = queuedBadgeCount(opts.queuedMessageCount);
302
+ const childMeta = workflowChildMetaText(stage);
303
+ const childSummary =
304
+ childMeta === undefined
305
+ ? undefined
306
+ : joinCompactStatusMeta(statusText, queuedCount > 0 ? queuedBadgeText(queuedCount) : childMeta, innerWidth);
307
+ const childSummaryLine =
308
+ childSummary === undefined
309
+ ? undefined
310
+ : `${bg}${bc}│${RESET}` +
311
+ centreColored(childSummary, innerWidth, bodyHex, bg, {
312
+ bold: stage.status === "running" || stage.status === "awaiting_input",
313
+ }) +
314
+ `${bg}${bc}│${RESET}`;
299
315
 
300
316
  const interior: string[] =
301
317
  stage.status === "awaiting_input"
@@ -308,29 +324,25 @@ export function renderNodeCard(stage: StageSnapshot, opts: NodeCardOpts): string
308
324
  centreColored("↵ enter to respond", innerWidth, theme.dim, bg) +
309
325
  `${bg}${bc}│${RESET}`,
310
326
  ]
311
- : [durLine, statusLine, metaLine];
327
+ : childSummaryLine === undefined
328
+ ? [durLine, statusLine, metaLine]
329
+ : [...childRunLines, childSummaryLine];
312
330
 
313
331
  // A queued steer/follow-up is invisible once the user leaves the stage chat,
314
332
  // so it claims one existing body row rather than competing for space inside a
315
- // line that would truncate. Card geometry is unchanged: the row is replaced,
316
- // not added. Pick the least useful row: the dim metadata row on an ordinary
317
- // card, and the redundant "waiting for response" row on an awaiting-input
318
- // card, which keeps its status row and its `↵ enter to respond` action hint.
319
- const queuedCount = queuedBadgeCount(opts.queuedMessageCount);
320
- const preferredBadgeRow = stage.status === "awaiting_input" ? 1 : interior.length - 1;
333
+ // line that would truncate. Child boundaries pack it beside status except when
334
+ // the awaiting-input interior leaves its redundant response row available.
335
+ const preferredBadgeRow =
336
+ stage.status === "awaiting_input" ? 1 : childSummaryLine === undefined ? interior.length - 1 : -1;
321
337
 
322
338
  // Pad / clip to exactly `height` lines.
323
- const contentRows = Math.max(0, height - 2);
324
339
  while (interior.length < contentRows) {
325
340
  interior.push(`${bg}${bc}│${RESET}${bg}${" ".repeat(innerWidth)}${bg}${bc}│${RESET}`);
326
341
  }
327
- if (interior.length > contentRows) {
328
- interior.length = contentRows;
329
- }
342
+ if (interior.length > contentRows) interior.length = contentRows;
330
343
 
331
- if (queuedCount > 0 && interior.length > 0) {
332
- const badgeRow =
333
- preferredBadgeRow >= 0 && preferredBadgeRow < interior.length ? preferredBadgeRow : interior.length - 1;
344
+ if (queuedCount > 0 && interior.length > 0 && preferredBadgeRow >= 0) {
345
+ const badgeRow = preferredBadgeRow < interior.length ? preferredBadgeRow : interior.length - 1;
334
346
  interior[badgeRow] =
335
347
  `${bg}${bc}│${RESET}` +
336
348
  centreColored(queuedBadgeText(queuedCount), innerWidth, theme.info, bg, { bold: true }) +
@@ -34,6 +34,7 @@ import type { StageControlRegistry } from "../runs/foreground/stage-control-regi
34
34
  import { stageControlRegistry as defaultStageControlRegistry } from "../runs/foreground/stage-control-registry.js";
35
35
  import type { StageUiBroker } from "../shared/stage-ui-broker.js";
36
36
  import type { Store } from "../shared/store.js";
37
+ import { readGraphStoreSnapshot, subscribeStoreInvalidation } from "../shared/store-observation.js";
37
38
  import type { StoreSnapshot } from "../shared/store-types.js";
38
39
  import { deriveGraphThemeFromPiTheme } from "./graph-theme.js";
39
40
  import type { OverlayTerminalOutput } from "./overlay-terminal-modes.js";
@@ -274,7 +275,8 @@ export function buildGraphOverlayAdapter(
274
275
 
275
276
  function makeComponent(view: WorkflowAttachPane, tui: PiCustomOverlayFactoryTui): PiCustomComponent {
276
277
  requestMountedRender = () => tui.requestRender?.();
277
- const onStoreUpdate = (snapshot: StoreSnapshot): void => {
278
+ const onStoreUpdate = (): void => {
279
+ const snapshot = readGraphStoreSnapshot(store);
278
280
  // Always invalidate retained view state so a later reopen renders the
279
281
  // current snapshot — but while the overlay is hidden, never ask the
280
282
  // host to render (#1856): each hidden-overlay render request became
@@ -285,7 +287,7 @@ export function buildGraphOverlayAdapter(
285
287
  refocusVisibleOverlayForAwaitingInput(snapshot);
286
288
  tui.requestRender?.();
287
289
  };
288
- const unsubscribe = store.subscribe(onStoreUpdate);
290
+ const unsubscribe = subscribeStoreInvalidation(store, onStoreUpdate);
289
291
  return {
290
292
  render: (width: number) => view.render(width),
291
293
  handleInput: (data: string) => {