@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
@@ -25,8 +25,10 @@
25
25
 
26
26
  import type { PiCustomComponent, PiCustomOverlayFactoryTui, PiCustomOverlayFunction } from "../extension/wiring.js";
27
27
  import type { Store } from "../shared/store.js";
28
+ import { readGraphStoreSnapshot, subscribeStoreInvalidation } from "../shared/store-observation.js";
28
29
  import type { GraphTheme } from "./graph-theme.js";
29
30
  import {
31
+ createSessionPickerResumeCandidateCache,
30
32
  createSessionPickerState,
31
33
  handleSessionPickerInput,
32
34
  renderSessionPicker,
@@ -80,6 +82,8 @@ export function openSessionPicker(
80
82
  let settled = false;
81
83
  let unsubscribe: (() => void) | null = null;
82
84
 
85
+ const resumeCandidateCache = createSessionPickerResumeCandidateCache();
86
+
83
87
  const factory = (
84
88
  tui: PiCustomOverlayFactoryTui,
85
89
  _theme: unknown,
@@ -96,14 +100,31 @@ export function openSessionPicker(
96
100
  };
97
101
  // Re-render on store changes so newly-started runs appear and
98
102
  // status icons refresh without the user having to press a key.
99
- unsubscribe = store.subscribe(() => tui.requestRender?.());
103
+ // Read one memoized payload-free projection per render rather than
104
+ // caching a snapshot from the subscription callback: the
105
+ // invalidation channel carries no snapshot, so a cached one would
106
+ // go stale and newly-started runs would never appear.
107
+ const selectRows = (): ReturnType<typeof selectRunsForPicker> => {
108
+ const snapshot = readGraphStoreSnapshot(store);
109
+ const resumeCandidateLookup =
110
+ intent === "resume" ? resumeCandidateCache({ ...snapshot, runs: store.runs() }) : undefined;
111
+ return selectRunsForPicker(
112
+ snapshot.runs,
113
+ state.query,
114
+ state.includeAll,
115
+ Date.now(),
116
+ intent,
117
+ resumeCandidateLookup,
118
+ );
119
+ };
120
+ unsubscribe = subscribeStoreInvalidation(store, () => tui.requestRender?.());
100
121
  return {
101
122
  render: (width: number) => {
102
- const rows = selectRunsForPicker(store.runs(), state.query, state.includeAll);
123
+ const rows = selectRows();
103
124
  return renderSessionPicker({ width, theme, rows, state });
104
125
  },
105
126
  handleInput: (data: string) => {
106
- const rows = selectRunsForPicker(store.runs(), state.query, state.includeAll);
127
+ const rows = selectRows();
107
128
  const action = handleSessionPickerInput(data, state, rows);
108
129
  if (action.kind === "noop") {
109
130
  tui.requestRender?.();
@@ -19,11 +19,14 @@
19
19
  */
20
20
 
21
21
  import { keyText } from "@bastani/atomic";
22
+ import { isWorkflowRunResumable, type WorkflowRunResumeCandidate } from "../durable/resume-eligibility.js";
22
23
  import { isTopLevelWorkflowRun } from "../shared/run-visibility.js";
23
- import type { RunSnapshot } from "../shared/store-types.js";
24
+ import type { RunSnapshot, StoreSnapshot } from "../shared/store-types.js";
24
25
  import { elapsedRunMs } from "../shared/timing.js";
26
+ import { workflowRunResumeCandidate } from "../shared/workflow-artifacts.js";
25
27
  import { BOLD, hexBg, hexToAnsi, RESET } from "./color-utils.js";
26
28
  import type { GraphTheme } from "./graph-theme.js";
29
+ import { type IdentifierLine, wrapIdentifierLines } from "./run-identity-rows.js";
27
30
  import { fmtDuration, statusColor, statusIcon } from "./status-helpers.js";
28
31
  import { Key, matchesKey, truncateToWidth, visibleWidth } from "./text-helpers.js";
29
32
 
@@ -55,23 +58,54 @@ export interface PickerRow {
55
58
  readonly bucket: "active" | "terminal";
56
59
  }
57
60
 
58
- const RECENT_WINDOW_MS = 60 * 60 * 1000;
61
+ export type ResumeCandidateProbe = (run: RunSnapshot) => WorkflowRunResumeCandidate;
62
+ export type ResumeCandidateLookup = (run: RunSnapshot) => WorkflowRunResumeCandidate;
63
+
64
+ /** Cache resume probes for one store revision so render and input share the same filesystem/backend work. */
65
+ export function createSessionPickerResumeCandidateCache(
66
+ probe: ResumeCandidateProbe = workflowRunResumeCandidate,
67
+ ): (snapshot: StoreSnapshot) => ResumeCandidateLookup {
68
+ let cachedVersion: number | undefined;
69
+ let cachedRuns: readonly RunSnapshot[] | undefined;
70
+ let sourceRuns = new Map<string, RunSnapshot>();
71
+ let candidates = new Map<string, WorkflowRunResumeCandidate>();
72
+
73
+ return (snapshot: StoreSnapshot): ResumeCandidateLookup => {
74
+ if (cachedVersion !== snapshot.version || cachedRuns !== snapshot.runs) {
75
+ cachedVersion = snapshot.version;
76
+ cachedRuns = snapshot.runs;
77
+ sourceRuns = new Map(snapshot.runs.map((run) => [run.id, run]));
78
+ candidates = new Map();
79
+ }
80
+ return (run: RunSnapshot): WorkflowRunResumeCandidate => {
81
+ const cached = candidates.get(run.id);
82
+ if (cached !== undefined) return cached;
83
+ const candidate = probe(sourceRuns.get(run.id) ?? run);
84
+ candidates.set(run.id, candidate);
85
+ return candidate;
86
+ };
87
+ };
88
+ }
59
89
 
90
+ const RECENT_WINDOW_MS = 60 * 60 * 1000;
60
91
  /**
61
92
  * Slice runs into picker rows. Active = `endedAt === undefined`. Terminal =
62
- * retained terminal runs. With `includeAll` false, terminal rows are limited
63
- * to the legacy recent-ended one-hour window.
64
- *
65
- * Sort: active first (newest start last in the list = bottom-of-pane),
66
- * then terminal newest-end first.
93
+ * retained terminal runs. Resume intent uses the shared resumability predicate;
94
+ * connect/attach and the legacy status list retain their broader listing.
95
+ * With `includeAll` false, terminal rows are limited to the recent-ended window.
67
96
  */
68
97
  export function selectRunsForPicker(
69
98
  runs: readonly RunSnapshot[],
70
99
  query: string,
71
100
  includeAll: boolean,
72
101
  now: number = Date.now(),
102
+ intent: "connect" | "resume" | "pause" = "connect",
103
+ resumeCandidateLookup?: ResumeCandidateLookup,
73
104
  ): PickerRow[] {
74
105
  const q = query.trim().toLowerCase();
106
+ // Incremental search, not id resolution. The user narrows a list and then
107
+ // selects a row, and the selected run's full id is what gets acted on, so
108
+ // prefix matching here never targets a run by a truncated id.
75
109
  const matches = (r: RunSnapshot): boolean => {
76
110
  if (!q) return true;
77
111
  return r.name.toLowerCase().includes(q) || r.id.startsWith(q);
@@ -82,7 +116,10 @@ export function selectRunsForPicker(
82
116
  for (const r of runs) {
83
117
  if (!isTopLevelWorkflowRun(r)) continue;
84
118
  if (!matches(r)) continue;
85
-
119
+ if (intent === "resume") {
120
+ const candidate = resumeCandidateLookup?.(r) ?? workflowRunResumeCandidate(r);
121
+ if (!isWorkflowRunResumable(candidate)) continue;
122
+ }
86
123
  const endedAt = r.endedAt;
87
124
  if (endedAt === undefined) {
88
125
  active.push({ run: r, bucket: "active" });
@@ -124,7 +161,8 @@ function renderHeader(width: number, theme: GraphTheme): string {
124
161
  const inner = Math.max(4, width - 2);
125
162
  const border = hexToAnsi(theme.border);
126
163
  const accent = hexToAnsi(theme.accent);
127
- const padded = ` ${TITLE} `;
164
+ const title = truncateToWidth(TITLE, Math.max(1, inner - 2), "…");
165
+ const padded = ` ${title} `;
128
166
  const padLen = Math.max(0, inner - visibleWidth(padded));
129
167
  const left = Math.min(2, padLen);
130
168
  const right = padLen - left;
@@ -219,45 +257,60 @@ function stageProgress(run: RunSnapshot): string {
219
257
  return `${done}/${total} stages`;
220
258
  }
221
259
 
222
- function renderRunRow(row: PickerRow, isSelected: boolean, inner: number, theme: GraphTheme, now: number): string {
260
+ function renderRunRow(row: PickerRow, isSelected: boolean, inner: number, theme: GraphTheme, now: number): string[] {
223
261
  const border = hexToAnsi(theme.border);
262
+ const panelBg = hexBg(theme.bg);
224
263
  const run = row.run;
225
264
  const icon = statusIcon(run.status);
226
- const idShort = run.id.slice(0, 8);
265
+ const iconColor = hexToAnsi(statusColor(run.status, theme));
266
+ const dim = hexToAnsi(theme.dim);
267
+ const text = hexToAnsi(theme.text);
268
+ const muted = hexToAnsi(theme.textMuted);
269
+
270
+ // The full identifier owns its own row. It is never ellipsized; narrow
271
+ // overlays wrap it into continuation rows so the box remains intact.
272
+ const idRows = wrapIdentifierLines(run.id, inner, ` ${icon} `, " ");
273
+ const renderIdRow = ({ prefix, chunk }: IdentifierLine, index: number): string => {
274
+ if (isSelected) {
275
+ return `${border}│${RESET}${hexBg(theme.accent)}${hexToAnsi(theme.backgroundElement)}${BOLD}${padTo(
276
+ `${prefix}${chunk}`,
277
+ inner,
278
+ )}${RESET}${border}│${RESET}`;
279
+ }
280
+ const content =
281
+ index === 0
282
+ ? ` ${iconColor}${icon}${RESET}${panelBg} ${dim}${chunk}${RESET}${panelBg}`
283
+ : ` ${dim}${chunk}${RESET}${panelBg}`;
284
+ return `${border}│${RESET}${panelBg}${padTo(content, inner)}${border}│${RESET}`;
285
+ };
286
+
227
287
  const elapsed = fmtElapsed(run, now);
228
288
  const progress = stageProgress(run);
229
-
230
- // Layout columns: glyph(1) idShort(8) name(flex) elapsed(R) progress(R).
231
- // Name budgeting is done by visible cell width so wide workflow names
232
- // cannot push the elapsed/progress columns through the right border.
233
289
  const elapsedCol = elapsed.padStart(8, " ");
234
290
  const progressCol = progress.padStart(10, " ");
235
291
  const rightPlain = `${elapsedCol} ${progressCol} `;
236
- const namePrefixW = visibleWidth(` ${icon} ${idShort} `);
237
- const nameBudget = Math.max(1, inner - namePrefixW - visibleWidth(rightPlain) - 1);
292
+ const rightBudget = Math.max(1, inner - 5);
293
+ const rightVisible = truncateToWidth(rightPlain, rightBudget, "…");
294
+ const right =
295
+ visibleWidth(rightVisible) < visibleWidth(rightPlain)
296
+ ? `${dim}${rightVisible}${RESET}${panelBg}`
297
+ : `${muted}${elapsedCol}${RESET}${panelBg} ${dim}${progressCol}${RESET}${panelBg} `;
298
+ const nameBudget = Math.max(1, inner - 3 - visibleWidth(rightVisible) - 1);
238
299
  const name = truncateToWidth(run.name, nameBudget, "…");
239
300
 
240
- if (isSelected) {
241
- const pillBg = hexBg(theme.accent);
242
- const pillFg = hexToAnsi(theme.backgroundElement);
243
- const left = ` ${icon} ${idShort} ${name}`;
244
- const right = rightPlain;
245
- const gap = Math.max(1, inner - visibleWidth(left) - visibleWidth(right));
246
- const content = `${left}${" ".repeat(gap)}${right}`;
247
- return `${border}│${RESET}${pillBg}${pillFg}${BOLD}${padTo(content, inner)}${RESET}${border}│${RESET}`;
248
- }
249
-
250
- const panelBg = hexBg(theme.bg);
251
- const iconColor = hexToAnsi(statusColor(run.status, theme));
252
- const dim = hexToAnsi(theme.dim);
253
- const text = hexToAnsi(theme.text);
254
- const muted = hexToAnsi(theme.textMuted);
255
-
256
- const left = ` ${iconColor}${icon}${RESET}${panelBg} ${dim}${idShort}${RESET}${panelBg} ${text}${name}${RESET}${panelBg}`;
257
- const right = `${muted}${elapsedCol}${RESET}${panelBg} ${dim}${progressCol}${RESET}${panelBg} `;
258
- const gap = Math.max(1, inner - visibleWidth(left) - visibleWidth(right));
259
- const content = `${left}${" ".repeat(gap)}${right}`;
260
- return `${border}│${RESET}${panelBg}${padTo(content, inner)}${RESET}${border}│${RESET}`;
301
+ const nameRow = isSelected
302
+ ? `${border}│${RESET}${hexBg(theme.accent)}${hexToAnsi(theme.backgroundElement)}${BOLD}${padTo(
303
+ ` ${name}${" ".repeat(Math.max(1, inner - 3 - visibleWidth(name) - visibleWidth(rightVisible)))}${rightVisible}`,
304
+ inner,
305
+ )}${RESET}${border}│${RESET}`
306
+ : `${border}│${RESET}${panelBg}${padTo(
307
+ ` ${text}${name}${RESET}${panelBg}${" ".repeat(
308
+ Math.max(1, inner - 3 - visibleWidth(name) - visibleWidth(right)),
309
+ )}${right}`,
310
+ inner,
311
+ )}${border}│${RESET}`;
312
+
313
+ return [...idRows.map(renderIdRow), nameRow];
261
314
  }
262
315
 
263
316
  function renderEmptyState(inner: number, theme: GraphTheme): string {
@@ -269,12 +322,12 @@ function renderEmptyState(inner: number, theme: GraphTheme): string {
269
322
  return `${border}│${RESET}${panelBg}${padTo(content, inner)}${RESET}${border}│${RESET}`;
270
323
  }
271
324
 
272
- const VIEWPORT = 10;
325
+ const VIEWPORT = 5;
273
326
 
274
327
  export function renderSessionPicker(opts: SessionPickerRenderOpts): string[] {
275
328
  const { width, theme, rows, state } = opts;
276
329
  const now = opts.now ?? Date.now();
277
- const inner = Math.max(40, width - 2);
330
+ const inner = Math.max(4, width - 2);
278
331
 
279
332
  const lines: string[] = [];
280
333
  lines.push(renderHeader(width, theme));
@@ -303,7 +356,7 @@ export function renderSessionPicker(opts: SessionPickerRenderOpts): string[] {
303
356
  prevBucket = row.bucket;
304
357
  }
305
358
  const absIndex = Math.max(0, start) + i;
306
- lines.push(renderRunRow(row, absIndex === sel, inner, theme, now));
359
+ lines.push(...renderRunRow(row, absIndex === sel, inner, theme, now));
307
360
  }
308
361
  lines.push(renderBlankRow(inner, theme));
309
362
  lines.push(renderBottomBorder(width, theme));
@@ -2,7 +2,12 @@ import { Box, Text } from "@earendil-works/pi-tui";
2
2
  import type { PendingPrompt, StageSnapshot } from "../shared/store-types.js";
3
3
  import { renderRoundedBoxLines } from "./chat-surface.js";
4
4
  import { hexToAnsi, RESET } from "./color-utils.js";
5
- import { renderPromptCard } from "./prompt-card.js";
5
+ import {
6
+ type PromptCardLayout,
7
+ renderPromptCardLayout,
8
+ renderPromptIdentityBanner,
9
+ renderPromptRunIdBanner,
10
+ } from "./prompt-card-render.js";
6
11
  import { bannerLines, embedOrchestratorReturnHintInWidget } from "./stage-chat-view-footer-status.js";
7
12
  import {
8
13
  blankLine,
@@ -205,24 +210,46 @@ export function renderBlockedBody(
205
210
  }
206
211
 
207
212
  export function renderPromptBody(ctx: StageChatViewContext, width: number, budget: number): string[] {
208
- const primitiveLines = renderPrimitivePromptBody(ctx, width);
209
- if (primitiveLines) {
210
- return fitPromptBodyLines(ctx, embedOrchestratorReturnHintInWidget(ctx, primitiveLines, width), width, budget);
213
+ const primitiveLayout = renderPrimitivePromptBody(ctx, width, budget);
214
+ if (primitiveLayout) {
215
+ return fitPromptBodyLines(
216
+ ctx,
217
+ embedPromptReturnHint(ctx, primitiveLayout.lines, width),
218
+ width,
219
+ budget,
220
+ Math.max(0, primitiveLayout.totalQuestionRows - primitiveLayout.visibleQuestionRows),
221
+ true,
222
+ primitiveLayout.visibleQuestionRows,
223
+ );
211
224
  }
212
225
 
213
226
  const state = ctx.promptState;
214
- const lines = state
215
- ? renderPromptCard({
216
- state,
217
- theme: ctx.theme,
218
- width,
219
- cursorOn: ctx.focused,
220
- })
221
- : [];
222
- return fitPromptBodyLines(ctx, embedOrchestratorReturnHintInWidget(ctx, lines, width), width, budget);
227
+ if (!state) return fitPromptBodyLines(ctx, [], width, budget);
228
+ const layout = renderPromptCardLayout({
229
+ state,
230
+ theme: ctx.theme,
231
+ width,
232
+ cursorOn: ctx.focused,
233
+ identity: { runId: ctx.runId, name: ctx.workflowName },
234
+ maxRows: budget,
235
+ messageOffset: ctx.promptScrollOffset,
236
+ });
237
+ return fitPromptBodyLines(
238
+ ctx,
239
+ embedPromptReturnHint(ctx, layout.lines, width),
240
+ width,
241
+ budget,
242
+ Math.max(0, layout.totalQuestionRows - layout.visibleQuestionRows),
243
+ true,
244
+ layout.visibleQuestionRows,
245
+ );
246
+ }
247
+
248
+ function embedPromptReturnHint(ctx: StageChatViewContext, lines: readonly string[], width: number): string[] {
249
+ return lines.length < 3 ? [...lines] : embedOrchestratorReturnHintInWidget(ctx, lines, width);
223
250
  }
224
251
 
225
- function renderPrimitivePromptBody(ctx: StageChatViewContext, width: number): string[] | null {
252
+ function renderPrimitivePromptBody(ctx: StageChatViewContext, width: number, budget: number): PromptCardLayout | null {
226
253
  const state = ctx.promptState;
227
254
  const editor = ctx.promptEditor;
228
255
  if (!state || !editor) return null;
@@ -230,19 +257,151 @@ function renderPrimitivePromptBody(ctx: StageChatViewContext, width: number): st
230
257
  setEditorBorderColor(editor, (text) => hexToAnsi(ctx.theme.accent) + text + RESET);
231
258
 
232
259
  const innerWidth = Math.max(2, width - 2);
233
- const bodyLines: string[] = [];
234
- const messageBox = new Box(2, 1);
235
- messageBox.addChild(new Text(paint(state.prompt.message, ctx.theme.text), 0, 0));
236
- bodyLines.push(...messageBox.render(innerWidth));
237
- bodyLines.push(...new Text(paint("response", ctx.theme.textMuted, { bold: true }), 2, 0).render(innerWidth));
238
- for (const line of editor.render(Math.max(20, innerWidth - 4))) {
239
- bodyLines.push(` ${line}`);
260
+ const messageLines = new Text(paint(state.prompt.message, ctx.theme.text), 2, 0).render(innerWidth);
261
+ const responseLines = new Text(paint("response", ctx.theme.textMuted, { bold: true }), 2, 0).render(innerWidth);
262
+ const editorLines = editor.render(Math.max(20, innerWidth - 4)).map((line) => ` ${line}`);
263
+ const hintLines = new Text(renderHintsForPrompt(state.prompt.kind, ctx.theme), 2, 0).render(innerWidth);
264
+ const identity = { runId: ctx.runId, name: ctx.workflowName };
265
+ const unattributed = renderPrimitivePromptBlockLayout(
266
+ ctx,
267
+ width,
268
+ budget,
269
+ "AWAITING INPUT",
270
+ messageLines,
271
+ responseLines,
272
+ editorLines,
273
+ hintLines,
274
+ );
275
+ const minimumPromptRows = 2 + editorLines.length + hintLines.length + (messageLines.length > 0 ? 1 : 0);
276
+ // Same degradation ladder as the standard prompt surface: two identity rows,
277
+ // then the run id alone, then no banner. The middle rung matters most here,
278
+ // because the editor and hint rows leave the least room for attribution.
279
+ const banners = [
280
+ renderPromptIdentityBanner(identity, ctx.theme, width),
281
+ renderPromptRunIdBanner(identity, ctx.theme, width),
282
+ ];
283
+ for (const banner of banners) {
284
+ const promptBudget = Math.max(0, budget - banner.length);
285
+ const attributed = renderPrimitivePromptBlockLayout(
286
+ ctx,
287
+ width,
288
+ promptBudget,
289
+ "",
290
+ messageLines,
291
+ responseLines,
292
+ editorLines,
293
+ hintLines,
294
+ );
295
+ if (
296
+ banner.length + minimumPromptRows > budget ||
297
+ attributed.visibleQuestionRows === 0 ||
298
+ attributed.visibleQuestionRows < unattributed.visibleQuestionRows
299
+ ) {
300
+ continue;
301
+ }
302
+ return {
303
+ lines: [...banner, ...attributed.lines],
304
+ totalQuestionRows: attributed.totalQuestionRows,
305
+ visibleQuestionRows: attributed.visibleQuestionRows,
306
+ };
240
307
  }
241
- bodyLines.push("");
242
- bodyLines.push(...new Text(renderHintsForPrompt(state.prompt.kind, ctx.theme), 2, 0).render(innerWidth));
308
+ return unattributed;
309
+ }
310
+
311
+ function renderPrimitivePromptBlockLayout(
312
+ ctx: StageChatViewContext,
313
+ width: number,
314
+ maxRows: number,
315
+ title: string,
316
+ messageLines: readonly string[],
317
+ responseLines: readonly string[],
318
+ editorLines: readonly string[],
319
+ hintLines: readonly string[],
320
+ ): PromptCardLayout {
321
+ const totalQuestionRows = messageLines.length;
322
+ if (maxRows === 0) return { lines: [], totalQuestionRows, visibleQuestionRows: 0 };
323
+ const fixedRows = 2 + editorLines.length + hintLines.length;
324
+ if (maxRows < fixedRows + (totalQuestionRows > 0 ? 1 : 0)) {
325
+ return renderReducedPrimitivePrompt(ctx, width, maxRows, messageLines, title);
326
+ }
327
+
328
+ let remaining = maxRows - fixedRows;
329
+ let messageCount = totalQuestionRows > 0 ? 1 : 0;
330
+ remaining -= messageCount;
331
+ const includeResponse = remaining >= responseLines.length;
332
+ if (includeResponse) remaining -= responseLines.length;
333
+ const extraMessageRows = Math.min(remaining, Math.max(0, totalQuestionRows - messageCount));
334
+ messageCount += extraMessageRows;
335
+ remaining -= extraMessageRows;
336
+ const includeBlank = remaining > 0;
337
+ const safeOffset = Math.min(ctx.promptScrollOffset, Math.max(0, totalQuestionRows - messageCount));
338
+ const bodyLines = [
339
+ ...messageLines.slice(safeOffset, safeOffset + messageCount),
340
+ ...(includeResponse ? responseLines : []),
341
+ ...editorLines,
342
+ ...(includeBlank ? [""] : []),
343
+ ...hintLines,
344
+ ];
345
+ return {
346
+ lines: renderPrimitivePromptBlock(ctx, width, title, bodyLines),
347
+ totalQuestionRows,
348
+ visibleQuestionRows: messageCount,
349
+ };
350
+ }
243
351
 
352
+ function renderReducedPrimitivePrompt(
353
+ ctx: StageChatViewContext,
354
+ width: number,
355
+ maxRows: number,
356
+ messageLines: readonly string[],
357
+ title: string,
358
+ ): PromptCardLayout {
359
+ const totalQuestionRows = messageLines.length;
360
+ const compactTitle = "AWAITING INPUT";
361
+ if (maxRows === 1) {
362
+ return {
363
+ lines: [paint(`${compactTitle} · enter Submit`, ctx.theme.textMuted, { bold: true })],
364
+ totalQuestionRows,
365
+ visibleQuestionRows: 0,
366
+ };
367
+ }
368
+ if (maxRows === 2) {
369
+ const closed = renderPrimitivePromptBlock(ctx, width, compactTitle, []);
370
+ return {
371
+ lines: [closed[0]!, closed.at(-1)!],
372
+ totalQuestionRows,
373
+ visibleQuestionRows: 0,
374
+ };
375
+ }
376
+
377
+ const availableBodyRows = maxRows - 2;
378
+ const supportingRows = availableBodyRows >= 3 ? 2 : Math.max(0, availableBodyRows - 1);
379
+ const messageCount = Math.min(totalQuestionRows, Math.max(1, availableBodyRows - supportingRows));
380
+ const safeOffset = Math.min(ctx.promptScrollOffset, Math.max(0, totalQuestionRows - messageCount));
381
+ const responseLine = paint(
382
+ availableBodyRows >= 3 ? " response" : " response · enter Submit",
383
+ ctx.theme.textMuted,
384
+ { bold: true },
385
+ );
386
+ const hintLine = renderHintsForPrompt(ctx.promptState?.prompt.kind ?? "input", ctx.theme);
387
+ return {
388
+ lines: renderPrimitivePromptBlock(ctx, width, maxRows < 5 ? compactTitle : title || "AWAITING INPUT", [
389
+ ...messageLines.slice(safeOffset, safeOffset + messageCount),
390
+ ...(availableBodyRows >= 2 ? [responseLine] : []),
391
+ ...(availableBodyRows >= 3 ? [hintLine] : []),
392
+ ]),
393
+ totalQuestionRows,
394
+ visibleQuestionRows: messageCount,
395
+ };
396
+ }
397
+ function renderPrimitivePromptBlock(
398
+ ctx: StageChatViewContext,
399
+ width: number,
400
+ title: string,
401
+ bodyLines: readonly string[],
402
+ ): string[] {
244
403
  return renderRoundedBoxLines({
245
- title: "AWAITING INPUT",
404
+ title,
246
405
  bodyLines,
247
406
  width,
248
407
  theme: ctx.theme,
@@ -255,10 +414,15 @@ export function fitPromptBodyLines(
255
414
  lines: readonly string[],
256
415
  width: number,
257
416
  budget: number,
417
+ maxScroll = Math.max(0, lines.length - budget),
418
+ scrollApplied = false,
419
+ visibleRows = Math.min(lines.length, budget),
258
420
  ): string[] {
259
- ctx.promptMaxScroll = Math.max(0, lines.length - budget);
421
+ ctx.promptMaxScroll = Math.max(0, maxScroll);
422
+ ctx.promptVisibleRows = Math.max(0, visibleRows);
260
423
  ctx.promptScrollOffset = Math.max(0, Math.min(ctx.promptScrollOffset, ctx.promptMaxScroll));
261
- const framed = lines.slice(ctx.promptScrollOffset, ctx.promptScrollOffset + budget);
424
+ const start = scrollApplied ? 0 : ctx.promptScrollOffset;
425
+ const framed = lines.slice(start, start + budget);
262
426
  while (framed.length < budget) framed.push(blankLine(width));
263
427
  return framed;
264
428
  }
@@ -2,6 +2,7 @@ import type { AgentSession } from "@bastani/atomic";
2
2
  import { Box, Text } from "@earendil-works/pi-tui";
3
3
  import type { StageSnapshot } from "../shared/store-types.js";
4
4
  import { hexToAnsi, RESET } from "./color-utils.js";
5
+ import { wrapIdentifierLines } from "./run-identity-rows.js";
5
6
  import {
6
7
  bgFn,
7
8
  blendBg,
@@ -17,30 +18,63 @@ import { truncateToWidth, visibleWidth } from "./text-helpers.js";
17
18
  export function renderHeader(ctx: StageChatViewContext, width: number, stage: StageSnapshot | undefined): string[] {
18
19
  const t = ctx.theme;
19
20
  const stageName = stage?.name ?? "stage";
20
- const left =
21
+ const sid = ctx.handle?.sessionId ?? stage?.sessionId;
22
+ const prefixWidth = visibleWidth(" STAGE ");
23
+ const separatorWidth = visibleWidth(" / ");
24
+ const meta = sid ? `session ${sid}` : "";
25
+ const rightWidth = meta ? visibleWidth(meta) + 1 : 0;
26
+ const singleRowNameBudget = width - prefixWidth - separatorWidth - rightWidth - (meta ? 1 : 0);
27
+
28
+ const fullNameWidth = visibleWidth(ctx.workflowName) + visibleWidth(stageName);
29
+ if (!sid || singleRowNameBudget >= fullNameWidth) {
30
+ const names = fitHeaderNames(ctx.workflowName, stageName, Math.max(2, singleRowNameBudget));
31
+ const left = renderHeaderLeft(ctx, names.workflow, names.stage);
32
+ const right = meta ? `${paint(meta, t.dim)} ` : "";
33
+ const gap = Math.max(0, width - visibleWidth(left) - visibleWidth(right));
34
+ return [left + " ".repeat(gap) + right];
35
+ }
36
+
37
+ const names = fitHeaderNames(ctx.workflowName, stageName, Math.max(2, width - prefixWidth - separatorWidth));
38
+ const left = renderHeaderLeft(ctx, names.workflow, names.stage);
39
+ const lines = [left + " ".repeat(Math.max(0, width - visibleWidth(left)))];
40
+ if (visibleWidth(meta) + 1 <= width) {
41
+ lines.push(`${" ".repeat(Math.max(0, width - visibleWidth(meta) - 1))}${paint(meta, t.dim)} `);
42
+ return lines;
43
+ }
44
+ for (const row of wrapIdentifierLines(sid, width, " ", " ")) {
45
+ const value = `${row.prefix}${paint(row.chunk, t.dim)}`;
46
+ lines.push(value + " ".repeat(Math.max(0, width - visibleWidth(value))));
47
+ }
48
+ return lines;
49
+ }
50
+
51
+ function renderHeaderLeft(ctx: StageChatViewContext, workflowName: string, stageName: string): string {
52
+ const t = ctx.theme;
53
+ return (
21
54
  paint(" ", t.mauve, { bold: true }) +
22
55
  paint("STAGE", t.textMuted, { bold: true }) +
23
56
  " " +
24
- paint(ctx.workflowName, t.textMuted) +
57
+ paint(workflowName, t.textMuted) +
25
58
  paint(" / ", t.dim) +
26
- paint(stageName, t.text, { bold: true });
27
- const meta = headerMeta(ctx, stage);
28
- const right = meta ? `${paint(meta, t.dim)} ` : "";
29
- const leftW = visibleWidth(ctx.workflowName) + visibleWidth(stageName) + visibleWidth(" STAGE / ") + 1;
30
- const rightW = visibleWidth(meta) + (meta ? 1 : 0);
31
- const gap = Math.max(1, width - leftW - rightW);
32
- return [left + " ".repeat(gap) + right];
59
+ paint(stageName, t.text, { bold: true })
60
+ );
33
61
  }
34
62
 
35
- function headerMeta(ctx: StageChatViewContext, stage: StageSnapshot | undefined): string {
36
- const parts: string[] = [];
37
- const sid = ctx.handle?.sessionId ?? stage?.sessionId;
38
- if (sid) parts.push(`session ${shortenId(sid)}`);
39
- return parts.join(" · ");
40
- }
41
-
42
- function shortenId(id: string): string {
43
- return id.length > 10 ? id.slice(0, 8) : id;
63
+ function fitHeaderNames(workflowName: string, stageName: string, budget: number): { workflow: string; stage: string } {
64
+ const available = Math.max(2, budget);
65
+ const workflowWidth = visibleWidth(workflowName);
66
+ const stageWidth = visibleWidth(stageName);
67
+ let workflowBudget = Math.min(workflowWidth, Math.max(1, Math.ceil(available / 2)));
68
+ let stageBudget = Math.min(stageWidth, Math.max(1, available - workflowBudget));
69
+ let remaining = available - workflowBudget - stageBudget;
70
+ const workflowExtra = Math.min(remaining, Math.max(0, workflowWidth - workflowBudget));
71
+ workflowBudget += workflowExtra;
72
+ remaining -= workflowExtra;
73
+ stageBudget += Math.min(remaining, Math.max(0, stageWidth - stageBudget));
74
+ return {
75
+ workflow: truncateToWidth(workflowName, workflowBudget, "…"),
76
+ stage: truncateToWidth(stageName, stageBudget, "…"),
77
+ };
44
78
  }
45
79
 
46
80
  export function sepRule(ctx: StageChatViewContext, width: number): string {
@@ -56,7 +56,7 @@ export function handleStageChatInput(ctx: StageChatViewContext, data: string): b
56
56
  return ctx.chatHost.handleInput(data);
57
57
  }
58
58
  if (isAbortableStreamingSession(ctx)) {
59
- void ctx.chatHost.interrupt();
59
+ void ctx.chatHost.interrupt({ restoreQueuedMessages: true });
60
60
  return true;
61
61
  }
62
62
  ctx.onClose();
@@ -160,8 +160,8 @@ function handlePromptInput(ctx: StageChatViewContext, data: string): void {
160
160
  function handlePromptScrollInput(ctx: StageChatViewContext, data: string, includeKeyboard = true): boolean {
161
161
  const mouse = parseTerminalMouseInput(data);
162
162
  const wheelDirection = mouse ? terminalMouseWheelDirection(mouse) : null;
163
- const wheelDeltaRows =
164
- wheelDirection === "up" ? -PROMPT_SCROLL_STEP_ROWS : wheelDirection === "down" ? PROMPT_SCROLL_STEP_ROWS : 0;
163
+ const wheelStep = promptScrollStep(ctx, PROMPT_SCROLL_STEP_ROWS);
164
+ const wheelDeltaRows = wheelDirection === "up" ? -wheelStep : wheelDirection === "down" ? wheelStep : 0;
165
165
  if (wheelDeltaRows !== 0) {
166
166
  scrollPromptBy(ctx, wheelDeltaRows);
167
167
  return true;
@@ -169,11 +169,11 @@ function handlePromptScrollInput(ctx: StageChatViewContext, data: string, includ
169
169
  if (mouse) return true;
170
170
  if (!includeKeyboard) return false;
171
171
  if (matchesKey(data, "pageUp")) {
172
- scrollPromptBy(ctx, -promptPageSize(ctx));
172
+ scrollPromptBy(ctx, -promptScrollStep(ctx, promptPageSize(ctx)));
173
173
  return true;
174
174
  }
175
175
  if (matchesKey(data, "pageDown")) {
176
- scrollPromptBy(ctx, promptPageSize(ctx));
176
+ scrollPromptBy(ctx, promptScrollStep(ctx, promptPageSize(ctx)));
177
177
  return true;
178
178
  }
179
179
  if (!ctx.promptEditor && matchesKey(data, "home")) {
@@ -189,6 +189,10 @@ function handlePromptScrollInput(ctx: StageChatViewContext, data: string, includ
189
189
  return false;
190
190
  }
191
191
 
192
+ function promptScrollStep(ctx: StageChatViewContext, requestedRows: number): number {
193
+ return Math.max(1, Math.min(requestedRows, Math.max(1, ctx.promptVisibleRows)));
194
+ }
195
+
192
196
  function scrollPromptBy(ctx: StageChatViewContext, deltaRows: number): void {
193
197
  ctx.promptScrollOffset = Math.max(0, Math.min(ctx.promptMaxScroll, ctx.promptScrollOffset + deltaRows));
194
198
  ctx.requestRender?.();