@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
@@ -1,6 +1,7 @@
1
1
  import { type AgentSessionEvent, ChatSessionHost } from "@bastani/atomic";
2
2
  import { Editor, type EditorComponent } from "@earendil-works/pi-tui";
3
3
  import { stageUiBroker } from "../shared/stage-ui-broker.js";
4
+ import { readGraphStoreSnapshot, subscribeStoreInvalidation } from "../shared/store-observation.js";
4
5
  import type { PendingPrompt, RunSnapshot, StageSnapshot } from "../shared/store-types.js";
5
6
  import { hexToAnsi, RESET } from "./color-utils.js";
6
7
  import { createPromptCardState } from "./prompt-card.js";
@@ -66,6 +67,7 @@ export function initializeStageChatView(ctx: StageChatViewContext, opts: StageCh
66
67
  ctx.promptEditorSubmitFromEnter = false;
67
68
  ctx.promptScrollOffset = 0;
68
69
  ctx.promptMaxScroll = 0;
70
+ ctx.promptVisibleRows = 0;
69
71
  ctx.localPaused = false;
70
72
  ctx.mouseScrollCaptureEnabled = true;
71
73
  ctx.lastObservedStageStatus = undefined;
@@ -101,9 +103,10 @@ export function initializeStageChatView(ctx: StageChatViewContext, opts: StageCh
101
103
  snapshotMessagesFromSessionFile(ctx, initialStage);
102
104
  absorbStageNotices(ctx, initialStage);
103
105
  syncPromptState(ctx, initialStage?.pendingPrompt);
104
- if (isTerminalStageChatState(initialRun?.status) || isTerminalStageChatState(initialStage?.status))
105
- ctx.chatHost.clearBusyForTerminalWorkflowStage();
106
- ctx._unsubscribeStore = ctx.store.subscribe(() => handleStoreUpdate(ctx));
106
+ const initialChatIsTerminal =
107
+ isTerminalStageChatState(initialRun?.status) || isTerminalStageChatState(initialStage?.status);
108
+ if (initialChatIsTerminal) ctx.chatHost.clearBusyForTerminalWorkflowStage();
109
+ ctx._unsubscribeStore = subscribeStoreInvalidation(ctx.store, () => handleStoreUpdate(ctx));
107
110
  ctx._unsubscribeFooterData = ctx.footerData?.onBranchChange(() => ctx.requestRender?.()) ?? null;
108
111
 
109
112
  if (ctx.handle) {
@@ -112,11 +115,47 @@ export function initializeStageChatView(ctx: StageChatViewContext, opts: StageCh
112
115
  // stage can see that a live delivery authorizes it.
113
116
  ctx._unsubscribeDeliveryActivity = subscribeStageChatDeliveryActivity(ctx);
114
117
  ctx._unsubscribeHandle = ctx.handle.subscribe((event) => applyStageChatLiveHandleEvent(ctx, event));
118
+ if (!initialChatIsTerminal) rehydrateCompactionStatusAfterAttach(ctx);
115
119
  rehydrateQueuedMessages(ctx);
116
120
  }
117
121
  ctx.chatHost.syncAnimationTick();
118
122
  }
119
123
 
124
+ /**
125
+ * Restore the factual compaction indicator into a freshly mounted host.
126
+ *
127
+ * Compaction start is an event, but the active reason lives on the session so
128
+ * a host rebuilt after detach can render the same label even when it missed
129
+ * that event. An absent reason explicitly clears the fresh host's status.
130
+ */
131
+ function rehydrateCompactionStatus(ctx: StageChatViewContext): void {
132
+ const session = liveHandle(ctx)?.agentSession;
133
+ ctx.chatHost.hydrateCompactionStatus(session?.isCompacting === true ? session.compactionReason : undefined);
134
+ }
135
+
136
+ /**
137
+ * The live stage handle may create its session lazily. Re-read compaction state
138
+ * after that async attach so a host mounted while the session is already
139
+ * compacting does not stay on the generic Working row forever.
140
+ */
141
+ function rehydrateCompactionStatusAfterAttach(ctx: StageChatViewContext): void {
142
+ const handle = liveHandle(ctx);
143
+ if (!handle) return;
144
+ rehydrateCompactionStatus(ctx);
145
+ void handle.ensureAttached().then(
146
+ () => {
147
+ if (ctx._unsubscribeHandle === null || liveHandle(ctx) === undefined) return;
148
+ if (isTerminalStageChatState(currentRun(ctx)?.status) || isTerminalStageChatState(currentStage(ctx)?.status)) {
149
+ return;
150
+ }
151
+ rehydrateCompactionStatus(ctx);
152
+ ctx.chatHost.syncAnimationTick();
153
+ ctx.requestRender?.();
154
+ },
155
+ () => {},
156
+ );
157
+ }
158
+
120
159
  /**
121
160
  * Replay the stage's pending queue into a freshly mounted chat host.
122
161
  *
@@ -308,7 +347,7 @@ function absorbStageNotices(ctx: StageChatViewContext, stage: StageSnapshot | un
308
347
  }
309
348
 
310
349
  export function currentRun(ctx: StageChatViewContext): RunSnapshot | undefined {
311
- return ctx.store.snapshot().runs.find((r) => r.id === ctx.runId);
350
+ return readGraphStoreSnapshot(ctx.store).runs.find((r) => r.id === ctx.runId);
312
351
  }
313
352
 
314
353
  export function currentStage(ctx: StageChatViewContext): StageSnapshot | undefined {
@@ -337,6 +376,7 @@ export function syncPromptState(ctx: StageChatViewContext, prompt: PendingPrompt
337
376
  function resetPromptScroll(ctx: StageChatViewContext): void {
338
377
  ctx.promptScrollOffset = 0;
339
378
  ctx.promptMaxScroll = 0;
379
+ ctx.promptVisibleRows = 0;
340
380
  }
341
381
 
342
382
  function promptSeedText(ctx: StageChatViewContext, prompt: PendingPrompt): string {
@@ -15,7 +15,8 @@ import type { PromptCardState } from "./prompt-card.js";
15
15
 
16
16
  export const VIEW_LINE_COUNT = 32;
17
17
  export const PROMPT_SCROLL_STEP_ROWS = 4;
18
- export const HEADER_ROWS = 1;
18
+ /** Header rows reserved by prompt paging for the normal UUID continuation shape. */
19
+ export const HEADER_ROWS = 2;
19
20
  export const SEP_ROWS = 1;
20
21
  export const STAGE_CHAT_MOUSE_SCROLL_TOGGLE_LABEL = "ctrl+t";
21
22
 
@@ -148,6 +149,7 @@ export interface StageChatViewContext {
148
149
  promptEditorSubmitFromEnter: boolean;
149
150
  promptScrollOffset: number;
150
151
  promptMaxScroll: number;
152
+ promptVisibleRows: number;
151
153
  localPaused: boolean;
152
154
  mouseScrollCaptureEnabled: boolean;
153
155
  lastObservedStageStatus: StageStatus | undefined;
@@ -53,7 +53,6 @@ import {
53
53
  } from "./stage-chat-view-state.js";
54
54
  import { transcriptDebugEntries } from "./stage-chat-view-transcript.js";
55
55
  import {
56
- HEADER_ROWS,
57
56
  SEP_ROWS,
58
57
  type StageChatViewContext,
59
58
  type StageChatViewOpts,
@@ -99,6 +98,7 @@ export class StageChatView implements Component, Focusable {
99
98
  private promptEditorSubmitFromEnter!: StageChatViewContext["promptEditorSubmitFromEnter"];
100
99
  private promptScrollOffset!: StageChatViewContext["promptScrollOffset"];
101
100
  private promptMaxScroll!: StageChatViewContext["promptMaxScroll"];
101
+ private promptVisibleRows!: StageChatViewContext["promptVisibleRows"];
102
102
  private localPaused!: StageChatViewContext["localPaused"];
103
103
  private mouseScrollCaptureEnabled!: StageChatViewContext["mouseScrollCaptureEnabled"];
104
104
  private seenNoticeIds!: StageChatViewContext["seenNoticeIds"];
@@ -141,7 +141,7 @@ export class StageChatView implements Component, Focusable {
141
141
  const totalRows = viewLineCount(ctx);
142
142
  const plan = planStageChatFrame({
143
143
  viewportRows: totalRows,
144
- headerRows: HEADER_ROWS,
144
+ headerRows: headerLines.length,
145
145
  separatorRows: SEP_ROWS,
146
146
  pendingRows: pendingLines.length,
147
147
  workingRows: workingLines.length,
@@ -234,6 +234,7 @@ export class StageChatView implements Component, Focusable {
234
234
  void this.promptEditorSubmitFromEnter;
235
235
  void this.promptScrollOffset;
236
236
  void this.promptMaxScroll;
237
+ void this.promptVisibleRows;
237
238
  void this.mouseScrollCaptureEnabled;
238
239
  void this.seenNoticeIds;
239
240
  void this._unsubscribeStore;
@@ -4,13 +4,8 @@
4
4
  * Visual contract (DESIGN.md §5):
5
5
  * - One rounded `BACKGROUND` panel with subtitle and count badges.
6
6
  * - One rounded card per run (replaces the indented per-stage rows):
7
- * title: runId · workflow · state badge
8
- * row 1: mode · progress strip · meta
9
- * - Status colour is carried by the card border and state badge semantics,
10
- * never by decorative body text.
11
- * - One trailing hint row pointing at `/workflow status <id>` for the
12
- * most-recently-active run; full per-stage detail moves into
13
- * `/workflow status <id>` ({@link renderRunDetail}).
7
+ * title: full runId · workflow · state badge
8
+ * rows: status glyph + full runId, then workflow identity and meta
14
9
  *
15
10
  * Plain mode (theme omitted) preserves the rounded panel/card shape without
16
11
  * ANSI escapes, with ASCII bracket cells `[✓][●][○][✗]`.
@@ -30,14 +25,13 @@ import { effectiveRunStatus } from "../shared/returned-run-status.js";
30
25
  import type { RunSnapshot, StageSnapshot, StageStatus } from "../shared/store-types.js";
31
26
  import { elapsedRunMs, elapsedStageMs } from "../shared/timing.js";
32
27
  import type { FlatBandBadge } from "./chat-surface.js";
33
- import { chatWidth, ELLIPSIS, progressStrip, renderHintRows, renderRoundedBox } from "./chat-surface.js";
28
+ import { chatWidth, ELLIPSIS, progressStrip, renderRoundedBox } from "./chat-surface.js";
34
29
  import { BOLD, hexToAnsi, RESET } from "./color-utils.js";
35
30
  import type { GraphTheme } from "./graph-theme.js";
31
+ import { wrapIdentifierLines } from "./run-identity-rows.js";
36
32
  import { fmtDuration } from "./status-helpers.js";
37
33
  import { truncateToWidth, visibleWidth } from "./text-helpers.js";
38
34
 
39
- const SHORT_ID_LEN = 6;
40
- const MIN_TITLE_BUDGET = 12;
41
35
  const STAGE_LABEL_BUDGET = 24;
42
36
 
43
37
  export interface RenderStatusListOpts {
@@ -83,17 +77,10 @@ export function renderStatusList(runs: readonly RunSnapshot[], opts: RenderStatu
83
77
  body.push(...renderRunEntry(sorted[i]!, now, cardWidth, opts.theme));
84
78
  }
85
79
  }
86
-
87
80
  if (opts.showDetailHint !== false && sorted.length > 0) {
88
- const sid = shortId(sorted[0]!.id);
89
81
  body.push("");
90
- body.push(
91
- ...renderHintRows([{ command: `/workflow status ${sid}`, hint: "drill into a run" }], opts.theme)
92
- .split("\n")
93
- .map((line) => ` ${line} `),
94
- );
82
+ body.push(...renderStatusHintRows(sorted[0]!.id, opts.theme, width).map((line) => ` ${line} `));
95
83
  }
96
-
97
84
  const badgeText = badges && badges.length > 0 ? ` ${badges.map((b) => b.text).join(" ")}` : "";
98
85
  return renderRoundedBox({
99
86
  title: `BACKGROUND ${subtitle}${badgeText}`,
@@ -108,43 +95,50 @@ export function renderStatusList(runs: readonly RunSnapshot[], opts: RenderStatu
108
95
  // ---------------------------------------------------------------------------
109
96
 
110
97
  function renderRunEntry(run: RunSnapshot, now: number, width: number, theme?: GraphTheme): string[] {
111
- const sid = shortId(run.id);
112
- const trailing = runTrailing(run, theme);
113
- const mode = run.stages.length > 1 ? "chain " : "single";
114
98
  const bodyWidth = effectiveWidth(width);
115
99
  const interior = Math.max(8, bodyWidth - 4);
100
+ const glyph = statusIconForRun(run);
101
+ const glyphFg = theme ? hexToAnsi(runAccent(run, theme)) : "";
102
+ const accent = theme ? hexToAnsi(theme.accent) : "";
103
+ const text = theme ? hexToAnsi(theme.text) : "";
104
+ const muted = theme ? hexToAnsi(theme.textMuted) : "";
105
+ const dim = theme ? hexToAnsi(theme.dim) : "";
106
+ const reset = theme ? RESET : "";
107
+
108
+ // The identifier owns its row and is never sent through truncateToWidth.
109
+ // At narrow widths, continuation rows preserve every identifier character
110
+ // while keeping each row inside the rounded panel's interior.
111
+ const idRows = wrapIdentifierLines(run.id, interior, ` ${glyph} `, " ");
112
+ const identityRows = idRows.map(({ prefix, chunk }, index) => {
113
+ if (!theme) return `${prefix}${chunk}`;
114
+ if (index === 0) return ` ${glyphFg}${glyph}${RESET} ${accent}${chunk}${RESET}`;
115
+ return ` ${accent}${chunk}${RESET}`;
116
+ });
117
+
118
+ const trailing = runTrailing(run, theme);
119
+ const trailingText = truncateToWidth(trailing?.text ?? "", Math.max(0, interior - 1), ELLIPSIS);
120
+ const nameBudget = Math.max(1, interior - 3 - visibleWidth(trailingText) - (trailingText ? 2 : 0));
121
+ const name = truncateToWidth(run.name, nameBudget, ELLIPSIS);
122
+ const nameSeg = theme ? `${text}${BOLD}${name}${RESET}` : name;
123
+ const trailingSeg =
124
+ theme && trailingText ? `${hexToAnsi(trailing?.fg ?? theme.dim)}${trailingText}${RESET}` : trailingText;
125
+ const identity = ` ${nameSeg}${trailingSeg ? ` ${trailingSeg}` : ""}`;
126
+
127
+ const mode = run.stages.length > 1 ? "chain " : "single";
116
128
  const rawMeta = runCardMeta(run, now);
117
129
  const modeW = mode.length + 4;
118
130
  const maxMetaW = Math.max(0, interior - modeW - 3);
119
131
  const meta = truncateToWidth(rawMeta, maxMetaW, ELLIPSIS);
120
132
  const metaW = visibleWidth(meta);
121
133
  const stripBudget = Math.max(0, interior - modeW - metaW - 2);
122
-
123
134
  const strip = progressStrip(stageCells(run), stripBudget, theme);
124
135
  const usedLeftW = modeW + visibleWidth(strip);
125
136
  const gap = Math.max(metaW > 0 ? 1 : 0, interior - usedLeftW - metaW);
126
-
127
- const glyph = statusIconForRun(run);
128
- const glyphFg = theme ? hexToAnsi(runAccent(run, theme)) : "";
129
- const accent = theme ? hexToAnsi(theme.accent) : "";
130
- const text = theme ? hexToAnsi(theme.text) : "";
131
- const muted = theme ? hexToAnsi(theme.textMuted) : "";
132
- const dim = theme ? hexToAnsi(theme.dim) : "";
133
- const reset = theme ? RESET : "";
134
-
135
- const name = truncateToWidth(
136
- run.name,
137
- Math.max(MIN_TITLE_BUDGET, interior - visibleWidth(sid) - visibleWidth(trailing?.text ?? "") - 8),
138
- ELLIPSIS,
139
- );
140
- const line1 = theme
141
- ? ` ${glyphFg}${glyph}${RESET} ${accent}${sid}${RESET} ${text}${BOLD}${name}${RESET} ${glyphFg}${trailing?.text ?? ""}${RESET} `
142
- : ` ${glyph} ${sid} ${name} ${trailing?.text ?? ""} `;
143
137
  const modeSeg = theme ? `${muted}${mode}${reset}` : mode;
144
138
  const metaSeg = theme ? `${dim}${meta}${reset}` : meta;
145
- const line2 = ` ${modeSeg} ${strip}${" ".repeat(gap)}${metaSeg} `;
139
+ const metaLine = ` ${modeSeg} ${strip}${" ".repeat(gap)}${metaSeg} `;
146
140
 
147
- return [line1, line2];
141
+ return [...identityRows, identity, metaLine];
148
142
  }
149
143
 
150
144
  function runAccent(run: RunSnapshot, theme?: GraphTheme): string {
@@ -403,8 +397,38 @@ function sortRuns(runs: readonly RunSnapshot[]): RunSnapshot[] {
403
397
  return [...[...active].sort(byStart), ...[...ended].sort(byStart)];
404
398
  }
405
399
 
406
- function shortId(id: string): string {
407
- return id.length > SHORT_ID_LEN ? id.slice(0, SHORT_ID_LEN) : id;
400
+ function renderStatusHintRows(id: string, theme: GraphTheme | undefined, width: number): string[] {
401
+ const budget = Math.max(1, width - 4);
402
+ const prefix = "▸ /workflow status ";
403
+ const continuation = " ";
404
+ const rows = wrapIdentifierLines(id, budget, prefix, continuation);
405
+ const identifierRowCount = rows.length;
406
+ const suffix = " drill into a run";
407
+ const last = rows[rows.length - 1]!;
408
+ if (visibleWidth(`${last.prefix}${last.chunk}${suffix}`) <= budget) {
409
+ last.chunk += suffix;
410
+ } else {
411
+ rows.push({
412
+ prefix: continuation,
413
+ chunk: truncateToWidth(suffix.trimStart(), Math.max(1, budget - visibleWidth(continuation)), ELLIPSIS),
414
+ });
415
+ }
416
+ if (!theme) return rows.map((row) => `${row.prefix}${row.chunk}`);
417
+ const dim = hexToAnsi(theme.dim);
418
+ const accent = hexToAnsi(theme.accent);
419
+ return rows.map((row, index) => {
420
+ const carriesIdentifier = index < identifierRowCount;
421
+ const carriesSuffix = carriesIdentifier && index === identifierRowCount - 1 && row.chunk.endsWith(suffix);
422
+ const idChunk = carriesSuffix ? row.chunk.slice(0, -suffix.length) : row.chunk;
423
+ const suffixText = carriesSuffix ? suffix : "";
424
+ if (index === 0) {
425
+ return `${dim}▸${RESET} ${accent}/workflow status ${idChunk}${RESET}${suffixText ? `${dim}${suffixText}${RESET}` : ""}`;
426
+ }
427
+ if (carriesIdentifier) {
428
+ return `${row.prefix}${accent}${idChunk}${RESET}${suffixText ? `${dim}${suffixText}${RESET}` : ""}`;
429
+ }
430
+ return `${row.prefix}${dim}${row.chunk}${RESET}`;
431
+ });
408
432
  }
409
433
 
410
434
  function emptyStateLine(theme?: GraphTheme): string {
@@ -418,7 +442,6 @@ function statusIconForRun(run: RunSnapshot): string {
418
442
  case "completed":
419
443
  return "✓";
420
444
  case "skipped":
421
- return "⊘";
422
445
  case "cancelled":
423
446
  return "⊘";
424
447
  case "blocked":
@@ -437,4 +460,3 @@ function statusIconForRun(run: RunSnapshot): string {
437
460
  }
438
461
 
439
462
  // Re-export for callers that need to inspect width budgeting.
440
- export { MIN_TITLE_BUDGET };
@@ -45,6 +45,7 @@ import {
45
45
  type ReactiveWidgetTimerHandle,
46
46
  } from "@bastani/atomic";
47
47
  import type { Store } from "../shared/store.js";
48
+ import { readGraphStoreSnapshot, subscribeStoreInvalidation } from "../shared/store-observation.js";
48
49
  import type { StoreSnapshot } from "../shared/store-types.js";
49
50
  import { buildThemedWidgetLines, nextWidgetRefreshDelayMs } from "./widget.js";
50
51
 
@@ -106,8 +107,8 @@ export function installStoreWidget(
106
107
  key: WIDGET_KEY,
107
108
  placement: "belowEditor",
108
109
  timers,
109
- getSnapshot: () => storeInstance.snapshot(),
110
- subscribe: (listener) => storeInstance.subscribe(() => listener()),
110
+ getSnapshot: () => readGraphStoreSnapshot(storeInstance),
111
+ subscribe: (listener) => subscribeStoreInvalidation(storeInstance, listener),
111
112
  getPreviewLines: (snap, now) => buildThemedWidgetLines(snap, undefined, 120, now),
112
113
  render: (snap, { theme, width, now }) => buildThemedWidgetLines(snap, theme as PiTheme | undefined, width, now),
113
114
  getNextRefreshDelayMs: (snap, now) => nextWidgetRefreshDelayMs(snap, now),
@@ -206,12 +207,12 @@ export function installToolExecutionHooks(pi: LiveWidgetAPI, storeInstance: Stor
206
207
  return call ? { key, call } : null;
207
208
  }
208
209
 
209
- storeInstance.subscribe(pruneActiveToolCalls);
210
+ subscribeStoreInvalidation(storeInstance, () => pruneActiveToolCalls(readGraphStoreSnapshot(storeInstance)));
210
211
 
211
212
  function recordToolStart(payload: unknown): void {
212
213
  if (!isToolExecutionPayload(payload)) return;
213
214
 
214
- const snap = storeInstance.snapshot();
215
+ const snap = readGraphStoreSnapshot(storeInstance);
215
216
  pruneActiveToolCalls(snap);
216
217
 
217
218
  const scope = resolveExplicitStageScope(payload);
@@ -234,7 +235,7 @@ export function installToolExecutionHooks(pi: LiveWidgetAPI, storeInstance: Stor
234
235
  function recordToolUpdate(payload: unknown): void {
235
236
  if (!isToolExecutionPayload(payload)) return;
236
237
 
237
- pruneActiveToolCalls(storeInstance.snapshot());
238
+ pruneActiveToolCalls(readGraphStoreSnapshot(storeInstance));
238
239
 
239
240
  if (!activeToolCallForPayload(payload)) return;
240
241
  // Updates are attach-only until the store has an explicit update API.
@@ -243,7 +244,7 @@ export function installToolExecutionHooks(pi: LiveWidgetAPI, storeInstance: Stor
243
244
  function recordToolEnd(payload: unknown): void {
244
245
  if (!isToolExecutionPayload(payload)) return;
245
246
 
246
- pruneActiveToolCalls(storeInstance.snapshot());
247
+ pruneActiveToolCalls(readGraphStoreSnapshot(storeInstance));
247
248
 
248
249
  const active = activeToolCallForPayload(payload);
249
250
  if (!active) return;
@@ -5,9 +5,8 @@
5
5
  * - One transparent rounded `BACKGROUND` panel with `N runs` and status
6
6
  * badges (`✓ n ● n ○ n ✗ n`) in the title.
7
7
  * - One compact rounded card per run:
8
- * title: `<status glyph> <short id> <name>`
9
- * row 1: `<dim mode · progress · duration>`
10
- * - Blank line between cards; trailing blank trimmed.
8
+ * title: `<status glyph> <full id>`
9
+ * row 1: `<name> · <dim mode · progress · duration>`
11
10
  * - Collapsed single-line form below 80 cells:
12
11
  * `▾ N background · X ●` in dim+warning.
13
12
  *
@@ -28,9 +27,10 @@ import type { RunSnapshot, StoreSnapshot } from "../shared/store-types.js";
28
27
  import { elapsedRunMs } from "../shared/timing.js";
29
28
  import type { FlatBandBadge } from "./chat-surface.js";
30
29
  import { renderRoundedBoxLines } from "./chat-surface.js";
31
- import { BOLD, hexToAnsi, RESET } from "./color-utils.js";
30
+ import { hexToAnsi, RESET } from "./color-utils.js";
32
31
  import type { GraphTheme } from "./graph-theme.js";
33
32
  import { deriveGraphTheme } from "./graph-theme.js";
33
+ import { renderRunIdentityRows } from "./run-identity-rows.js";
34
34
  import { statusIcon } from "./status-helpers.js";
35
35
  import type { PiTheme } from "./store-widget-installer.js";
36
36
 
@@ -38,8 +38,6 @@ import type { PiTheme } from "./store-widget-installer.js";
38
38
  // Tunables
39
39
  // ---------------------------------------------------------------------------
40
40
 
41
- const SHORT_ID_LEN = 6;
42
- const MAX_VISIBLE_RUNS = 4;
43
41
  export const RECENT_ENDED_WINDOW_MS = 30_000;
44
42
  const COLLAPSED_BREAKPOINT_COLS = 80;
45
43
 
@@ -62,16 +60,28 @@ export function formatDuration(ms: number): string {
62
60
  // Run classification + selection
63
61
  // ---------------------------------------------------------------------------
64
62
 
63
+ function isQuitRun(run: RunSnapshot): boolean {
64
+ return run.endedAt === undefined && run.status === "paused" && run.exitReason === "quit";
65
+ }
66
+
65
67
  function isActive(run: RunSnapshot): boolean {
66
- return run.endedAt === undefined;
68
+ return run.endedAt === undefined && !isQuitRun(run);
67
69
  }
68
70
 
69
71
  function recentlyEnded(run: RunSnapshot, now: number): boolean {
70
72
  return run.endedAt !== undefined && now - run.endedAt <= RECENT_ENDED_WINDOW_MS;
71
73
  }
72
74
 
73
- function isQuitRun(run: RunSnapshot): boolean {
74
- return run.endedAt === undefined && run.status === "paused" && run.exitReason === "quit";
75
+ /**
76
+ * Returns the timestamp from which a quit card's display-only expiry is measured.
77
+ * Older snapshots do not have `quitAt`, so retain the bounded legacy fallbacks.
78
+ */
79
+ function quitExpiryTimestamp(run: RunSnapshot): number {
80
+ return run.quitAt ?? run.pausedAt ?? run.startedAt;
81
+ }
82
+
83
+ function recentlyQuit(run: RunSnapshot, now: number): boolean {
84
+ return isQuitRun(run) && now - quitExpiryTimestamp(run) <= RECENT_ENDED_WINDOW_MS;
75
85
  }
76
86
 
77
87
  interface RunCounts {
@@ -123,7 +133,7 @@ function countRuns(runs: readonly RunSnapshot[], allRuns: readonly RunSnapshot[]
123
133
  /**
124
134
  * Returns the next wall-clock boundary that can change the visible widget.
125
135
  * Running elapsed labels tick on exact one-second boundaries; paused runs stay
126
- * frozen. Recently ended cards retain their independent one-shot expiry.
136
+ * frozen. Recently ended and quit cards retain their independent one-shot expiry.
127
137
  * Reactive-widget updates the existing mounted component in place, so these
128
138
  * ticks repaint the visible panel without disposing or remounting it.
129
139
  */
@@ -131,9 +141,14 @@ export function nextWidgetRefreshDelayMs(snap: StoreSnapshot, now = Date.now()):
131
141
  const display = selectDisplayRuns(snap, now);
132
142
  if (display.length === 0) return undefined;
133
143
 
134
- const delays: number[] = display
135
- .filter((run) => run.endedAt !== undefined)
136
- .map((run) => Math.max(1, run.endedAt! + RECENT_ENDED_WINDOW_MS - now + 1));
144
+ const delays: number[] = [];
145
+ for (const run of display) {
146
+ if (run.endedAt !== undefined) {
147
+ delays.push(Math.max(1, run.endedAt + RECENT_ENDED_WINDOW_MS - now + 1));
148
+ } else if (isQuitRun(run)) {
149
+ delays.push(Math.max(1, quitExpiryTimestamp(run) + RECENT_ENDED_WINDOW_MS - now + 1));
150
+ }
151
+ }
137
152
  for (const run of display) {
138
153
  if (run.endedAt !== undefined || effectiveRunStatus(run) !== "running" || run.pausedAt !== undefined) continue;
139
154
  const remainder = elapsedRunMs(run, now) % 1_000;
@@ -150,21 +165,16 @@ function selectDisplayRuns(snap: StoreSnapshot, now: number): RunSnapshot[] {
150
165
  // rule `statusRuns`/the `status` action already apply.
151
166
  const all = topLevelWorkflowRuns(snap.runs);
152
167
  const active = all.filter((r) => isActive(r));
153
- const recent = all.filter((r) => recentlyEnded(r, now));
168
+ const recent = all.filter((r) => recentlyEnded(r, now) || recentlyQuit(r, now));
154
169
  // Most recently started first within each bucket; active runs precede recent.
155
170
  const sort = (xs: RunSnapshot[]) => [...xs].sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
156
- const ordered = [...sort(active), ...sort(recent)];
157
- return ordered.slice(0, MAX_VISIBLE_RUNS);
171
+ return [...sort(active), ...sort(recent)];
158
172
  }
159
173
 
160
174
  // ---------------------------------------------------------------------------
161
175
  // Per-run derived strings
162
176
  // ---------------------------------------------------------------------------
163
177
 
164
- function shortId(run: RunSnapshot): string {
165
- return run.id.length > SHORT_ID_LEN ? run.id.slice(0, SHORT_ID_LEN) : run.id;
166
- }
167
-
168
178
  function statusGlyph(run: RunSnapshot): string {
169
179
  if (isQuitRun(run)) return "○";
170
180
  switch (effectiveRunStatus(run)) {
@@ -298,29 +308,28 @@ function formatTitleBadges(badges: readonly FlatBandBadge[], theme: GraphTheme,
298
308
  // ---------------------------------------------------------------------------
299
309
 
300
310
  function themedRunLines(run: RunSnapshot, now: number, theme: GraphTheme): string[] {
301
- const dim = hexToAnsi(theme.dim);
302
- const text = hexToAnsi(theme.text);
303
- const accent = hexToAnsi(theme.accent);
304
- const muted = hexToAnsi(theme.textMuted);
305
- const glyphFg = hexToAnsi(statusFg(run, theme));
306
-
307
- const glyph = statusGlyph(run);
308
- const sid = shortId(run);
309
- const name = run.name;
310
-
311
- const line1 = ` ${glyphFg}${glyph}${RESET} ${accent}${sid}${RESET} ${text}${BOLD}${name}${RESET}`;
312
311
  const meta = metaLine(run, now);
313
312
  // Render the meta line in muted while running so the elapsed-time
314
313
  // gradient stays readable; dim it once the run has terminated.
315
- const metaFg = effectiveRunStatus(run) === "running" ? muted : dim;
316
- const line2 = ` ${metaFg}${meta}${RESET}`;
317
- return [line1, line2];
314
+ const metaColor = effectiveRunStatus(run) === "running" ? theme.textMuted : theme.dim;
315
+ return renderRunIdentityRows({
316
+ runId: run.id,
317
+ name: run.name,
318
+ meta,
319
+ glyph: statusGlyph(run),
320
+ glyphColor: statusFg(run, theme),
321
+ metaColor,
322
+ theme,
323
+ });
318
324
  }
319
325
 
320
326
  function plainRunLines(run: RunSnapshot, now: number): string[] {
321
- const line1 = ` ${statusGlyph(run)} ${shortId(run)} ${run.name}`;
322
- const line2 = ` ${metaLine(run, now)}`;
323
- return [line1, line2];
327
+ return renderRunIdentityRows({
328
+ runId: run.id,
329
+ name: run.name,
330
+ meta: metaLine(run, now),
331
+ glyph: statusGlyph(run),
332
+ });
324
333
  }
325
334
 
326
335
  // ---------------------------------------------------------------------------
@@ -371,7 +380,7 @@ export function buildThemedWidgetLines(
371
380
  const display = selectDisplayRuns(snap, now);
372
381
  if (display.length === 0) return [];
373
382
 
374
- const counts = countRuns(topLevelWorkflowRuns(snap.runs), snap.runs);
383
+ const displayCounts = countRuns(display, snap.runs);
375
384
  // Active + recently-ended dominate the badge counts so a finished run
376
385
  // visually persists for a beat before dropping off.
377
386
  const visibleCounts: RunCounts = {
@@ -385,7 +394,7 @@ export function buildThemedWidgetLines(
385
394
  blocked: display.filter((r) => effectiveRunStatus(r) === "blocked").length,
386
395
  failed: display.filter((r) => r.endedAt !== undefined && ["failed", "killed"].includes(effectiveRunStatus(r)))
387
396
  .length,
388
- awaiting: counts.awaiting,
397
+ awaiting: displayCounts.awaiting,
389
398
  };
390
399
 
391
400
  const themed = piTheme !== undefined;
@@ -396,7 +405,7 @@ export function buildThemedWidgetLines(
396
405
  return [themed ? themedCollapsed(visibleCounts, graphTheme) : plainCollapsed(visibleCounts)];
397
406
  }
398
407
 
399
- const total = counts.active + counts.paused + counts.quit + counts.done + counts.blocked + counts.failed;
408
+ const total = display.length;
400
409
  const subtitle = `${total} run${total === 1 ? "" : "s"}`;
401
410
 
402
411
  const badgeList = countBadges(visibleCounts, graphTheme);
@@ -15,6 +15,7 @@ import { stageQueuedUserMessageCount } from "../runs/foreground/stage-queued-use
15
15
  import { expandWorkflowGraph } from "../shared/expanded-workflow-graph.js";
16
16
  import type { StageUiBroker } from "../shared/stage-ui-broker.js";
17
17
  import type { Store } from "../shared/store.js";
18
+ import { readGraphStoreSnapshot, subscribeStoreInvalidation } from "../shared/store-observation.js";
18
19
  import type { StageSnapshot, StoreSnapshot } from "../shared/store-types.js";
19
20
  import type { GraphTheme } from "./graph-theme.js";
20
21
  import { GraphView } from "./graph-view.js";
@@ -86,7 +87,9 @@ export class WorkflowAttachPane implements Component {
86
87
  this.setToolsExpanded = opts.setToolsExpanded;
87
88
  this.footerData = opts.footerData;
88
89
  this.now = opts.now ?? Date.now;
89
- this.unsubscribeStore = this.store.subscribe((snapshot) => this._handleStoreUpdate(snapshot));
90
+ this.unsubscribeStore = subscribeStoreInvalidation(this.store, () =>
91
+ this._handleStoreUpdate(readGraphStoreSnapshot(this.store)),
92
+ );
90
93
  this.graphView = this._buildGraphView();
91
94
  const target =
92
95
  opts.initialAttachStageId !== undefined && this.runId
@@ -97,7 +100,7 @@ export class WorkflowAttachPane implements Component {
97
100
  if (target) {
98
101
  this._attachToStage(target.runId, target.stageId);
99
102
  } else {
100
- this._syncAwaitingInputKeys(this.store.snapshot());
103
+ this._syncAwaitingInputKeys(readGraphStoreSnapshot(this.store));
101
104
  this._armGraphEnterQuarantineIfRunNeedsInput();
102
105
  this._syncMouseScrollTracking();
103
106
  }
@@ -150,7 +153,7 @@ export class WorkflowAttachPane implements Component {
150
153
  return null;
151
154
  }
152
155
  private _workflowName(runId: string): string {
153
- const snap = this.store.snapshot();
156
+ const snap = readGraphStoreSnapshot(this.store);
154
157
  const run = snap.runs.find((r) => r.id === runId);
155
158
  return run?.name ?? "workflow";
156
159
  }
@@ -160,7 +163,7 @@ export class WorkflowAttachPane implements Component {
160
163
  options: { suppressInitialPromptSubmit?: boolean } = { suppressInitialPromptSubmit: true },
161
164
  ): void {
162
165
  this.graphEnterQuarantineUntil = 0;
163
- const snapshot = this.store.snapshot();
166
+ const snapshot = readGraphStoreSnapshot(this.store);
164
167
  const graphRunId = this._resolveRunId();
165
168
  this.lastGraphAwaitingInputKey = graphRunId ? this._runAwaitingInputKey(snapshot, graphRunId) : null;
166
169
  this.lastStageAwaitingInputKey = this._stageAwaitingInputKey(snapshot, runId, stageId);
@@ -227,7 +230,9 @@ export class WorkflowAttachPane implements Component {
227
230
  this.mode = "graph";
228
231
  this.stagePromptEnterQuarantineUntil = 0;
229
232
  this.lastStageAwaitingInputKey = null;
230
- this.lastGraphAwaitingInputKey = this.runId ? this._runAwaitingInputKey(this.store.snapshot(), this.runId) : null;
233
+ this.lastGraphAwaitingInputKey = this.runId
234
+ ? this._runAwaitingInputKey(readGraphStoreSnapshot(this.store), this.runId)
235
+ : null;
231
236
  this.graphEnterQuarantineUntil =
232
237
  reason === "prompt-resolved" && metadata.suppressNextGraphSubmit === true
233
238
  ? this.now() + ENTER_TRANSITION_QUARANTINE_MS
@@ -248,7 +253,7 @@ export class WorkflowAttachPane implements Component {
248
253
  this.graphEnterQuarantineUntil = 0;
249
254
  this.stagePromptEnterQuarantineUntil = 0;
250
255
  this.graphView = this._buildGraphView();
251
- this._syncAwaitingInputKeys(this.store.snapshot());
256
+ this._syncAwaitingInputKeys(readGraphStoreSnapshot(this.store));
252
257
  if (stageId !== undefined && runId) {
253
258
  const target =
254
259
  stageRunId === undefined ? this._resolveGraphStageTarget(runId, stageId) : { runId: stageRunId, stageId };
@@ -264,7 +269,7 @@ export class WorkflowAttachPane implements Component {
264
269
  rootRunId: string,
265
270
  stageId: string,
266
271
  ): { runId: string; stageId: string } | undefined {
267
- const graph = expandWorkflowGraph(this.store.snapshot(), rootRunId);
272
+ const graph = expandWorkflowGraph(readGraphStoreSnapshot(this.store), rootRunId);
268
273
  const exact = graph.stages.find((stage) => stage.id === stageId);
269
274
  const localMatches = graph.stages.filter((stage) => stage.workflowGraphTarget.stageId === stageId);
270
275
  if (exact === undefined && localMatches.length > 1) return undefined;
@@ -403,7 +408,7 @@ export class WorkflowAttachPane implements Component {
403
408
  runId && this._runNeedsInput(runId) ? this.now() + ENTER_TRANSITION_QUARANTINE_MS : 0;
404
409
  }
405
410
  private _runNeedsInput(runId: string): boolean {
406
- return this._runAwaitingInputKey(this.store.snapshot(), runId) !== null;
411
+ return this._runAwaitingInputKey(readGraphStoreSnapshot(this.store), runId) !== null;
407
412
  }
408
413
  private _runAwaitingInputKey(snapshot: StoreSnapshot, runId: string): string | null {
409
414
  const run = snapshot.runs.find((candidate) => candidate.id === runId);
@@ -422,7 +427,7 @@ export class WorkflowAttachPane implements Component {
422
427
  return keys[0]!.key;
423
428
  }
424
429
  private _stageNeedsInput(runId: string, stageId: string): boolean {
425
- return this._stageAwaitingInputKey(this.store.snapshot(), runId, stageId) !== null;
430
+ return this._stageAwaitingInputKey(readGraphStoreSnapshot(this.store), runId, stageId) !== null;
426
431
  }
427
432
  private _stageAwaitingInputKey(snapshot: StoreSnapshot, runId: string, stageId: string): string | null {
428
433
  const run = snapshot.runs.find((candidate) => candidate.id === runId);
@@ -448,7 +453,7 @@ export class WorkflowAttachPane implements Component {
448
453
  return this._stageSnapshot(runId, stageId)?.attached === true;
449
454
  }
450
455
  private _stageSnapshot(runId: string, stageId: string): StageSnapshot | undefined {
451
- const run = this.store.snapshot().runs.find((candidate) => candidate.id === runId);
456
+ const run = readGraphStoreSnapshot(this.store).runs.find((candidate) => candidate.id === runId);
452
457
  return run?.stages.find((candidate) => candidate.id === stageId);
453
458
  }
454
459
  private _stageSnapshotNeedsInput(stage: Pick<StageSnapshot, "pendingPrompt" | "inputRequest" | "status">): boolean {