@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
@@ -6,41 +6,273 @@ import type { GraphTheme } from "./graph-theme.js";
6
6
  import { createPromptSelectList } from "./prompt-card-select.js";
7
7
  import type { PromptCardState } from "./prompt-card-state.js";
8
8
  import { graphemeParts } from "./prompt-card-text.js";
9
+ import { renderRunIdentityRows } from "./run-identity-rows.js";
10
+ import { statusColor, statusIcon } from "./status-helpers.js";
11
+
12
+ export interface PromptCardIdentity {
13
+ readonly runId: string;
14
+ readonly name: string;
15
+ readonly meta?: string;
16
+ }
9
17
 
10
18
  export interface PromptCardRenderOpts {
11
19
  readonly state: PromptCardState;
12
20
  readonly theme: GraphTheme;
13
21
  readonly width: number;
14
22
  readonly cursorOn: boolean;
23
+ /** Optional run attribution; absent preserves the legacy prompt card. */
24
+ readonly identity?: PromptCardIdentity;
25
+ /** Maximum rows to emit. Small budgets use a reduced, closed prompt surface. */
26
+ readonly maxRows?: number;
27
+ /** Wrapped question-row offset used by scrollable attached prompt surfaces. */
28
+ readonly messageOffset?: number;
29
+ }
30
+
31
+ export interface PromptCardLayout {
32
+ readonly lines: string[];
33
+ readonly totalQuestionRows: number;
34
+ readonly visibleQuestionRows: number;
35
+ }
36
+
37
+ export function renderPromptIdentityBanner(identity: PromptCardIdentity, theme: GraphTheme, width: number): string[] {
38
+ const innerWidth = Math.max(20, width - 2);
39
+ const borderColor = theme.border;
40
+ const bg = "";
41
+ const identityRows = renderRunIdentityRows({
42
+ runId: identity.runId,
43
+ name: identity.name,
44
+ meta: identity.meta,
45
+ glyph: statusIcon("awaiting_input"),
46
+ glyphColor: statusColor("awaiting_input", theme),
47
+ theme,
48
+ width: innerWidth,
49
+ idIndent: 1,
50
+ idGap: 1,
51
+ nameIndent: 4,
52
+ });
53
+ return [
54
+ makeBorderTop(borderColor, " AWAITING INPUT ", theme, innerWidth, bg),
55
+ ...identityRows.map((row) => makePaddedRow(bg, borderColor, innerWidth, row)),
56
+ makeBorderBottom(borderColor, innerWidth, bg),
57
+ ];
58
+ }
59
+
60
+ export function renderPromptRunIdBanner(identity: PromptCardIdentity, theme: GraphTheme, width: number): string[] {
61
+ const banner = renderPromptIdentityBanner(identity, theme, width);
62
+ // The canonical identity renderer appends the workflow-name row last, after
63
+ // every wrapped run-id row. Remove only that row for the middle ladder rung.
64
+ return [...banner.slice(0, -2), banner.at(-1)!];
15
65
  }
16
66
 
67
+ const STANDARD_PROMPT_FIXED_ROWS = 5;
68
+ const NORMAL_SPACING_ROWS = 3;
69
+
17
70
  /**
18
- * Render the prompt card as a list of width-safe ANSI lines, suitable to
19
- * paint over the graph body inside the overlay.
71
+ * Render the prompt surface as an optional attribution banner followed by
72
+ * the existing prompt UI. The banner contains identity only; the prompt
73
+ * question, response field, and hints remain in their existing box.
20
74
  */
21
75
  export function renderPromptCard(opts: PromptCardRenderOpts): string[] {
76
+ return renderPromptCardLayout(opts).lines;
77
+ }
78
+
79
+ /** Render a card and report its scrollable question window in question-row units. */
80
+ export function renderPromptCardLayout(opts: PromptCardRenderOpts): PromptCardLayout {
22
81
  const { state, theme, width } = opts;
23
82
  const innerWidth = Math.max(20, width - 2);
24
83
  const borderColor = theme.border;
25
84
  const bg = "";
85
+ const maxRows = opts.maxRows === undefined ? undefined : Math.max(0, Math.floor(opts.maxRows));
86
+ const messageOffset = Math.max(0, Math.floor(opts.messageOffset ?? 0));
87
+ const unattributed = renderPromptBodyBlock(
88
+ state,
89
+ theme,
90
+ innerWidth,
91
+ opts.cursorOn,
92
+ borderColor,
93
+ bg,
94
+ maxRows,
95
+ messageOffset,
96
+ );
97
+ if (opts.identity === undefined) return unattributed;
98
+
99
+ const banners = [
100
+ renderPromptIdentityBanner(opts.identity, theme, width),
101
+ renderPromptRunIdBanner(opts.identity, theme, width),
102
+ ];
103
+ for (const banner of banners) {
104
+ const promptBudget = maxRows === undefined ? undefined : Math.max(0, maxRows - banner.length);
105
+ const attributedPrompt = renderPromptBodyBlock(
106
+ state,
107
+ theme,
108
+ innerWidth,
109
+ opts.cursorOn,
110
+ borderColor,
111
+ bg,
112
+ promptBudget,
113
+ messageOffset,
114
+ );
115
+ const minimumQuestionRows = attributedPrompt.totalQuestionRows > 0 ? 1 : 0;
116
+ const bannerFits =
117
+ maxRows === undefined ||
118
+ (banner.length + minimumCompletePromptRows(state) <= maxRows &&
119
+ attributedPrompt.visibleQuestionRows >= minimumQuestionRows &&
120
+ attributedPrompt.visibleQuestionRows >= unattributed.visibleQuestionRows);
121
+ if (!bannerFits) continue;
26
122
 
123
+ return {
124
+ lines: [
125
+ ...banner,
126
+ ...attributedPrompt.lines.map((line, index) =>
127
+ index === 0 ? makeBorderTop(borderColor, "", theme, innerWidth, bg) : line,
128
+ ),
129
+ ],
130
+ totalQuestionRows: attributedPrompt.totalQuestionRows,
131
+ visibleQuestionRows: attributedPrompt.visibleQuestionRows,
132
+ };
133
+ }
134
+ return unattributed;
135
+ }
136
+
137
+ function renderPromptBodyBlock(
138
+ state: PromptCardState,
139
+ theme: GraphTheme,
140
+ innerWidth: number,
141
+ cursorOn: boolean,
142
+ borderColor: string,
143
+ bg: string,
144
+ maxRows: number | undefined,
145
+ messageOffset: number,
146
+ ): PromptCardLayout {
147
+ const messageRows = wrapText(state.prompt.message, innerWidth - 4);
148
+ const totalQuestionRows = messageRows.length;
149
+ if (maxRows !== undefined && maxRows === 0) {
150
+ return { lines: [], totalQuestionRows, visibleQuestionRows: 0 };
151
+ }
152
+ if (maxRows !== undefined && maxRows < minimumCompletePromptRows(state)) {
153
+ return renderReducedPromptBlock(state, theme, innerWidth, borderColor, bg, maxRows, messageRows, messageOffset);
154
+ }
155
+
156
+ const desiredResponseRows = responseRowCount(state);
157
+ const minimumResponseRows = minimumResponseRowCount(state);
158
+ const normallySpaced =
159
+ maxRows === undefined ||
160
+ maxRows >= STANDARD_PROMPT_FIXED_ROWS + NORMAL_SPACING_ROWS + desiredResponseRows + totalQuestionRows;
161
+ let messageCount = totalQuestionRows;
162
+ let responseCount = desiredResponseRows;
163
+ if (maxRows !== undefined) {
164
+ const fixedRows = STANDARD_PROMPT_FIXED_ROWS + (normallySpaced ? NORMAL_SPACING_ROWS : 0);
165
+ const variableBudget = maxRows - fixedRows;
166
+ messageCount = totalQuestionRows > 0 ? 1 : 0;
167
+ responseCount = minimumResponseRows;
168
+ let remaining = variableBudget - messageCount - responseCount;
169
+ const responseExtra = Math.min(remaining, Math.max(0, desiredResponseRows - responseCount));
170
+ responseCount += responseExtra;
171
+ remaining -= responseExtra;
172
+ messageCount += Math.min(remaining, Math.max(0, totalQuestionRows - messageCount));
173
+ }
174
+
175
+ const safeMessageOffset = Math.min(messageOffset, Math.max(0, totalQuestionRows - messageCount));
27
176
  const lines: string[] = [];
28
177
  lines.push(makeBorderTop(borderColor, " AWAITING INPUT ", theme, innerWidth, bg));
29
- lines.push(makePaddedRow(bg, borderColor, innerWidth, ""));
30
- for (const messageLine of wrapText(state.prompt.message, innerWidth - 4)) {
178
+ if (normallySpaced) lines.push(makePaddedRow(bg, borderColor, innerWidth, ""));
179
+ for (const messageLine of messageRows.slice(safeMessageOffset, safeMessageOffset + messageCount)) {
31
180
  lines.push(makePaddedRow(bg, borderColor, innerWidth, ` ${paint(messageLine, theme.text)}`));
32
181
  }
33
- lines.push(makePaddedRow(bg, borderColor, innerWidth, ""));
182
+ if (normallySpaced) lines.push(makePaddedRow(bg, borderColor, innerWidth, ""));
34
183
 
35
- const fieldLines = renderResponseFieldBox(state, theme, innerWidth - 4, opts.cursorOn);
36
- for (const fl of fieldLines) {
37
- lines.push(makePaddedRow(bg, borderColor, innerWidth, ` ${fl}`));
184
+ const fieldLines = renderResponseFieldBox(state, theme, innerWidth - 4, cursorOn, responseCount);
185
+ for (const fieldLine of fieldLines) {
186
+ lines.push(makePaddedRow(bg, borderColor, innerWidth, ` ${fieldLine}`));
38
187
  }
39
188
 
40
- lines.push(makePaddedRow(bg, borderColor, innerWidth, ""));
189
+ if (normallySpaced) lines.push(makePaddedRow(bg, borderColor, innerWidth, ""));
41
190
  lines.push(makePaddedRow(bg, borderColor, innerWidth, ` ${renderHints(state.prompt.kind, theme)}`));
42
191
  lines.push(makeBorderBottom(borderColor, innerWidth, bg));
43
- return lines;
192
+ return { lines, totalQuestionRows, visibleQuestionRows: messageCount };
193
+ }
194
+
195
+ function minimumCompletePromptRows(state: PromptCardState): number {
196
+ return STANDARD_PROMPT_FIXED_ROWS + minimumResponseRowCount(state) + 1;
197
+ }
198
+
199
+ function renderReducedPromptBlock(
200
+ state: PromptCardState,
201
+ theme: GraphTheme,
202
+ innerWidth: number,
203
+ borderColor: string,
204
+ bg: string,
205
+ maxRows: number,
206
+ messageRows: readonly string[],
207
+ messageOffset: number,
208
+ ): PromptCardLayout {
209
+ const totalQuestionRows = messageRows.length;
210
+ const compactHint = compactSurfaceHint(state.prompt.kind);
211
+ if (maxRows === 1) {
212
+ return {
213
+ lines: [paint(`AWAITING INPUT · ${compactHint}`, theme.textMuted, { bold: true })],
214
+ totalQuestionRows,
215
+ visibleQuestionRows: 0,
216
+ };
217
+ }
218
+ if (maxRows === 2) {
219
+ return {
220
+ lines: [
221
+ makeBorderTop(borderColor, ` AWAITING INPUT · ${compactHint} `, theme, innerWidth, bg),
222
+ makeBorderBottom(borderColor, innerWidth, bg),
223
+ ],
224
+ totalQuestionRows,
225
+ visibleQuestionRows: 0,
226
+ };
227
+ }
228
+
229
+ const availableBodyRows = maxRows - 2;
230
+ const supportingRows = availableBodyRows >= 3 ? 2 : Math.max(0, availableBodyRows - 1);
231
+ const messageCount = Math.min(totalQuestionRows, Math.max(1, availableBodyRows - supportingRows));
232
+ const safeMessageOffset = Math.min(messageOffset, Math.max(0, totalQuestionRows - messageCount));
233
+ const title = maxRows < 5 ? ` AWAITING INPUT · ${compactHint} ` : " AWAITING INPUT ";
234
+ const lines = [makeBorderTop(borderColor, title, theme, innerWidth, bg)];
235
+ for (const messageLine of messageRows.slice(safeMessageOffset, safeMessageOffset + messageCount)) {
236
+ lines.push(makePaddedRow(bg, borderColor, innerWidth, ` ${paint(messageLine, theme.text)}`));
237
+ }
238
+ if (availableBodyRows >= 2) {
239
+ lines.push(makePaddedRow(bg, borderColor, innerWidth, ` ${renderReducedResponse(state, theme)}`));
240
+ }
241
+ if (availableBodyRows >= 3) {
242
+ lines.push(makePaddedRow(bg, borderColor, innerWidth, ` ${renderHints(state.prompt.kind, theme)}`));
243
+ }
244
+ lines.push(makeBorderBottom(borderColor, innerWidth, bg));
245
+ return { lines, totalQuestionRows, visibleQuestionRows: messageCount };
246
+ }
247
+
248
+ function compactSurfaceHint(kind: PendingPrompt["kind"]): string {
249
+ switch (kind) {
250
+ case "confirm":
251
+ return "y Yes";
252
+ case "select":
253
+ return "↑↓ Choose";
254
+ case "custom":
255
+ return "enter";
256
+ default:
257
+ return "enter Submit";
258
+ }
259
+ }
260
+
261
+ function renderReducedResponse(state: PromptCardState, theme: GraphTheme): string {
262
+ switch (state.prompt.kind) {
263
+ case "confirm":
264
+ return paint("response yes / no", theme.textMuted, { bold: true });
265
+ case "select":
266
+ return paint(`response ${state.prompt.choices?.[state.selectedIndex] ?? "(no choices)"}`, theme.textMuted, {
267
+ bold: true,
268
+ });
269
+ case "input":
270
+ return paint(`response ❯ ${state.rawText}`, theme.textMuted, { bold: true });
271
+ case "editor":
272
+ return paint("response editor", theme.textMuted, { bold: true });
273
+ case "custom":
274
+ return paint("response", theme.textMuted, { bold: true });
275
+ }
44
276
  }
45
277
 
46
278
  function makeBorderTop(color: string, label: string, theme: GraphTheme, innerWidth: number, bg: string): string {
@@ -67,11 +299,30 @@ function wrapText(text: string, width: number): string[] {
67
299
  return wrapTextWithAnsi(text, width);
68
300
  }
69
301
 
302
+ function responseRowCount(state: PromptCardState): number {
303
+ switch (state.prompt.kind) {
304
+ case "select": {
305
+ const choiceCount = state.prompt.choices?.length ?? 0;
306
+ const visibleChoices = Math.max(1, Math.min(5, choiceCount));
307
+ return visibleChoices + (choiceCount > visibleChoices ? 1 : 0);
308
+ }
309
+ case "editor":
310
+ return 6;
311
+ default:
312
+ return 1;
313
+ }
314
+ }
315
+
316
+ function minimumResponseRowCount(state: PromptCardState): number {
317
+ return state.prompt.kind === "select" && (state.prompt.choices?.length ?? 0) > 1 ? 2 : 1;
318
+ }
319
+
70
320
  function renderResponseFieldBox(
71
321
  state: PromptCardState,
72
322
  theme: GraphTheme,
73
323
  usable: number,
74
324
  cursorOn: boolean,
325
+ maxContentRows: number,
75
326
  ): string[] {
76
327
  const boxWidth = Math.max(4, usable);
77
328
  const contentWidth = Math.max(1, boxWidth - 2);
@@ -80,7 +331,7 @@ function renderResponseFieldBox(
80
331
  const labelText = paint(label, theme.textMuted, { bold: true });
81
332
  const labelW = visibleWidth(labelText);
82
333
  const topFill = Math.max(0, boxWidth - labelW - 2);
83
- const rows = renderResponseField(state, theme, contentWidth, cursorOn);
334
+ const rows = renderResponseField(state, theme, contentWidth, cursorOn, maxContentRows);
84
335
  return [
85
336
  paint("╭", borderColor) + labelText + paint(`${"─".repeat(topFill)}╮`, borderColor),
86
337
  ...rows.map((row) => makeFieldRow(row, contentWidth, borderColor)),
@@ -94,16 +345,22 @@ function makeFieldRow(content: string, width: number, borderColor: string): stri
94
345
  return paint("│", borderColor) + padded + paint("│", borderColor);
95
346
  }
96
347
 
97
- function renderResponseField(state: PromptCardState, theme: GraphTheme, usable: number, cursorOn: boolean): string[] {
348
+ function renderResponseField(
349
+ state: PromptCardState,
350
+ theme: GraphTheme,
351
+ usable: number,
352
+ cursorOn: boolean,
353
+ maxRows: number,
354
+ ): string[] {
98
355
  switch (state.prompt.kind) {
99
356
  case "confirm":
100
357
  return [renderConfirmRow(state, theme, usable)];
101
358
  case "select":
102
- return renderSelectRows(state, theme, usable);
359
+ return renderSelectRows(state, theme, usable, maxRows);
103
360
  case "input":
104
361
  return [renderInputRow(state, theme, usable, cursorOn)];
105
362
  case "editor":
106
- return renderEditorRows(state, theme, usable, cursorOn);
363
+ return renderEditorRows(state, theme, usable, cursorOn, maxRows);
107
364
  case "custom":
108
365
  return [padToUsable("", usable)];
109
366
  }
@@ -123,12 +380,14 @@ function renderConfirmRow(state: PromptCardState, theme: GraphTheme, usable: num
123
380
  return padToUsable(row, usable);
124
381
  }
125
382
 
126
- function renderSelectRows(state: PromptCardState, theme: GraphTheme, usable: number): string[] {
383
+ function renderSelectRows(state: PromptCardState, theme: GraphTheme, usable: number, maxRows: number): string[] {
127
384
  const choices = state.prompt.choices ?? [];
128
385
  if (choices.length === 0) {
129
386
  return [padToUsable(paint("(no choices)", theme.dim), usable)];
130
387
  }
131
- const maxVisible = Math.min(5, choices.length);
388
+ const rowBudget = Math.max(1, Math.floor(maxRows));
389
+ const scrolls = choices.length > Math.min(5, rowBudget);
390
+ const maxVisible = Math.max(1, Math.min(5, choices.length, rowBudget - (scrolls ? 1 : 0)));
132
391
  const list = createPromptSelectList(state, theme, maxVisible);
133
392
  return list.render(usable).map((line) => padToUsable(line, usable));
134
393
  }
@@ -141,8 +400,19 @@ function renderInputRow(state: PromptCardState, theme: GraphTheme, usable: numbe
141
400
  return padToUsable(paint("❯ ", theme.accent) + withCursor, usable);
142
401
  }
143
402
 
144
- function renderEditorRows(state: PromptCardState, theme: GraphTheme, usable: number, cursorOn: boolean): string[] {
145
- const ROWS = 5;
403
+ function renderEditorRows(
404
+ state: PromptCardState,
405
+ theme: GraphTheme,
406
+ usable: number,
407
+ cursorOn: boolean,
408
+ maxRows: number,
409
+ ): string[] {
410
+ const rowBudget = Math.max(1, Math.floor(maxRows));
411
+ if (rowBudget === 1 && state.editorSubmitFocused) {
412
+ return [padToUsable(renderEditorSubmitAction(true, theme), usable)];
413
+ }
414
+ const editorRows = rowBudget === 1 ? 1 : rowBudget - 1;
415
+ const includeSubmit = rowBudget > 1;
146
416
  const allLines = state.rawText.split("\n");
147
417
  // Find the line + column the caret currently sits on.
148
418
  let acc = 0;
@@ -159,10 +429,10 @@ function renderEditorRows(state: PromptCardState, theme: GraphTheme, usable: num
159
429
  caretLine = i + 1;
160
430
  caretCol = 0;
161
431
  }
162
- const start = Math.max(0, Math.min(caretLine - Math.floor(ROWS / 2), allLines.length - ROWS));
432
+ const start = Math.max(0, Math.min(caretLine - Math.floor(editorRows / 2), allLines.length - editorRows));
163
433
  const safeStart = Math.max(0, start);
164
434
  const rows: string[] = [];
165
- for (let i = 0; i < ROWS; i++) {
435
+ for (let i = 0; i < editorRows; i++) {
166
436
  const lineIdx = safeStart + i;
167
437
  const lineText = allLines[lineIdx] ?? "";
168
438
  const isCaretLine = !state.editorSubmitFocused && lineIdx === caretLine;
@@ -174,7 +444,7 @@ function renderEditorRows(state: PromptCardState, theme: GraphTheme, usable: num
174
444
  const prefix = paint(isCaretLine ? "❯ " : " ", isCaretLine ? theme.accent : theme.dim);
175
445
  rows.push(padToUsable(prefix + withCursor, usable));
176
446
  }
177
- rows.push(padToUsable(renderEditorSubmitAction(state.editorSubmitFocused, theme), usable));
447
+ if (includeSubmit) rows.push(padToUsable(renderEditorSubmitAction(state.editorSubmitFocused, theme), usable));
178
448
  return rows;
179
449
  }
180
450
 
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  export { handlePromptCardInput, isPromptEscapeInput } from "./prompt-card-input.js";
9
- export type { PromptCardRenderOpts } from "./prompt-card-render.js";
9
+ export type { PromptCardIdentity, PromptCardRenderOpts } from "./prompt-card-render.js";
10
10
  export { renderPromptCard } from "./prompt-card-render.js";
11
11
  export type { PromptCardAction, PromptCardState } from "./prompt-card-state.js";
12
12
  export { createPromptCardState, defaultResponseFor } from "./prompt-card-state.js";
@@ -12,4 +12,3 @@ export { renderHeader } from "./header.js";
12
12
  export { computeLayout, NODE_H, NODE_W } from "./layout.js";
13
13
  export { renderNodeCard } from "./node-card.js";
14
14
  export { fmtDuration, statusColor, statusIcon } from "./status-helpers.js";
15
- export { renderToasts } from "./toast.js";
@@ -24,10 +24,10 @@ import type { FlatBandBadge } from "./chat-surface.js";
24
24
  import { renderRoundedBox } from "./chat-surface.js";
25
25
  import { BOLD, hexToAnsi, RESET } from "./color-utils.js";
26
26
  import type { GraphTheme } from "./graph-theme.js";
27
+ import { wrapIdentifierLines } from "./run-identity-rows.js";
27
28
  import { fmtDuration, statusColor, statusIcon } from "./status-helpers.js";
28
29
  import { truncateToWidth, visibleWidth } from "./text-helpers.js";
29
30
 
30
- const SHORT_ID_LEN = 6;
31
31
  const STAGE_NAME_COL = 14;
32
32
  const KEY_COL = 14;
33
33
 
@@ -56,10 +56,11 @@ export function renderRunDetail(detail: RunDetail, opts: RenderRunDetailOpts = {
56
56
 
57
57
  function renderPlain(detail: RunDetail, now: number, width: number): string {
58
58
  const out: string[] = [];
59
-
60
- const sid = shortId(detail.runId);
61
59
  const stateBadge = stateLabel(detail);
62
60
 
61
+ out.push(...renderIdentifierRows(detail.runId, width - 2));
62
+ out.push("");
63
+
63
64
  for (const [k, v] of summaryRows(detail, now)) {
64
65
  if (v === undefined) continue;
65
66
  const value = truncateToWidth(v, Math.max(1, width - 4 - KEY_COL), "…");
@@ -89,18 +90,10 @@ function renderPlain(detail: RunDetail, now: number, width: number): string {
89
90
  out.push("");
90
91
  }
91
92
 
92
- if (detail.endedAt === undefined) {
93
- const hint =
94
- detail.status === "paused"
95
- ? ` ▸ workflow resume id=${sid} continue workflow `
96
- : ` ▸ workflow interrupt id=${sid} cancel `;
97
- out.push(truncateToWidth(hint, width - 2, "…"));
98
- } else {
99
- out.push(truncateToWidth(` ▸ workflow resume id=${sid} reopen graph `, width - 2, "…"));
100
- }
93
+ out.push(...renderDetailHintRows(detail, width - 2));
101
94
 
102
95
  return renderRoundedBox({
103
- title: `RUN ${sid} ${detail.name} ${stateBadge}`,
96
+ title: `RUN ${detail.name} ${stateBadge}`,
104
97
  bodyLines: out,
105
98
  width,
106
99
  });
@@ -115,9 +108,10 @@ function renderThemed(detail: RunDetail, now: number, theme: GraphTheme, width:
115
108
  const muted = hexToAnsi(theme.textMuted);
116
109
  const dim = hexToAnsi(theme.dim);
117
110
  const text = hexToAnsi(theme.text);
118
- const accent = hexToAnsi(theme.accent);
119
111
 
120
- const sid = shortId(detail.runId);
112
+ out.push(...renderIdentifierRows(detail.runId, width - 2, theme));
113
+ out.push("");
114
+
121
115
  const badges = stateBadges(detail, theme);
122
116
 
123
117
  for (const [k, v] of summaryRows(detail, now)) {
@@ -151,25 +145,11 @@ function renderThemed(detail: RunDetail, now: number, theme: GraphTheme, width:
151
145
  out.push("");
152
146
  }
153
147
 
154
- if (detail.endedAt === undefined) {
155
- const hint =
156
- detail.status === "paused"
157
- ? ` ${dim}▸${RESET} ${accent}workflow resume id=${sid}${RESET}${dim} continue workflow${RESET} `
158
- : ` ${dim}▸${RESET} ${accent}workflow interrupt id=${sid}${RESET}${dim} cancel${RESET} `;
159
- out.push(truncateToWidth(hint, width - 2, "…"));
160
- } else {
161
- out.push(
162
- truncateToWidth(
163
- ` ${dim}▸${RESET} ${accent}workflow resume id=${sid}${RESET}${dim} reopen graph${RESET} `,
164
- width - 2,
165
- "…",
166
- ),
167
- );
168
- }
148
+ out.push(...renderDetailHintRows(detail, width - 2, theme));
169
149
 
170
150
  const badgeText = badges.length > 0 ? ` ${badges.map((b) => b.text).join(" ")}` : "";
171
151
  return renderRoundedBox({
172
- title: `RUN ${sid} ${detail.name}${badgeText}`,
152
+ title: `RUN ${detail.name}${badgeText}`,
173
153
  bodyLines: out,
174
154
  accent: theme.accent,
175
155
  theme,
@@ -389,16 +369,53 @@ function statePlain(detail: RunDetail): string {
389
369
  // Tiny formatters
390
370
  // ---------------------------------------------------------------------------
391
371
 
392
- function shortId(id: string): string {
393
- return id.length > SHORT_ID_LEN ? id.slice(0, SHORT_ID_LEN) : id;
394
- }
395
-
396
372
  function pad(s: string, n: number): string {
397
373
  const width = visibleWidth(s);
398
374
  if (width >= n) return s;
399
375
  return s + " ".repeat(n - width);
400
376
  }
401
377
 
378
+ function renderIdentifierRows(id: string, width: number, theme?: GraphTheme): string[] {
379
+ const rows = wrapIdentifierLines(id, width, " run id ", " ");
380
+ if (!theme) return rows.map((row) => `${row.prefix}${row.chunk}`);
381
+ const muted = hexToAnsi(theme.textMuted);
382
+ const accent = hexToAnsi(theme.accent);
383
+ return rows.map((row, index) => {
384
+ const prefix = index === 0 ? `${muted}${row.prefix}${RESET}` : row.prefix;
385
+ return `${prefix}${accent}${row.chunk}${RESET}`;
386
+ });
387
+ }
388
+
389
+ function renderDetailHintRows(detail: RunDetail, width: number, theme?: GraphTheme): string[] {
390
+ const prefix =
391
+ detail.endedAt === undefined
392
+ ? detail.status === "paused"
393
+ ? " ▸ workflow resume id="
394
+ : " ▸ workflow interrupt id="
395
+ : " ▸ workflow resume id=";
396
+ const suffix =
397
+ detail.endedAt === undefined
398
+ ? detail.status === "paused"
399
+ ? " continue workflow "
400
+ : " cancel "
401
+ : " reopen graph ";
402
+ const continuation = " ";
403
+ const rows = wrapIdentifierLines(detail.runId, width, prefix, continuation);
404
+ const last = rows[rows.length - 1]!;
405
+ if (visibleWidth(`${last.prefix}${last.chunk}${suffix}`) <= width) {
406
+ last.chunk += suffix;
407
+ } else {
408
+ rows.push({
409
+ prefix: continuation,
410
+ chunk: truncateToWidth(suffix.trimStart(), Math.max(1, width - visibleWidth(continuation)), "…"),
411
+ });
412
+ }
413
+ if (!theme) return rows.map((row) => `${row.prefix}${row.chunk}`);
414
+ const dim = hexToAnsi(theme.dim);
415
+ const accent = hexToAnsi(theme.accent);
416
+ return rows.map((row) => `${dim}${row.prefix}${RESET}${accent}${row.chunk}${RESET}`);
417
+ }
418
+
402
419
  function formatTime(ms: number): string {
403
420
  const d = new Date(ms);
404
421
  const hh = String(d.getUTCHours()).padStart(2, "0");
@@ -0,0 +1,97 @@
1
+ /** Shared two-row run identity rendering for workflow cards and attribution banners. */
2
+
3
+ import { BOLD, hexToAnsi, RESET } from "./color-utils.js";
4
+ import type { GraphTheme } from "./graph-theme.js";
5
+ import { visibleWidth } from "./text-helpers.js";
6
+
7
+ export interface RunIdentityRowsOpts {
8
+ /** Full run UUID; this value is never shortened. */
9
+ readonly runId: string;
10
+ /** Workflow display name shown on the second row. */
11
+ readonly name: string;
12
+ /** Optional metadata appended to the workflow name. */
13
+ readonly meta?: string;
14
+ /** Status glyph shown on the first row. */
15
+ readonly glyph: string;
16
+ /** Hex colour for the status glyph in themed output. */
17
+ readonly glyphColor?: string;
18
+ /** Hex colour for metadata in themed output. */
19
+ readonly metaColor?: string;
20
+ /** Omit for plain output. */
21
+ readonly theme?: GraphTheme;
22
+ /** Visible width available to the identity rows. Omit to disable wrapping. */
23
+ readonly width?: number;
24
+ /** Leading spaces before the glyph. Defaults to the widget's three cells. */
25
+ readonly idIndent?: number;
26
+ /** Spaces between the glyph and the full run id. Defaults to two cells. */
27
+ readonly idGap?: number;
28
+ /** Leading spaces before the workflow name. Defaults to five cells. */
29
+ readonly nameIndent?: number;
30
+ }
31
+
32
+ export interface IdentifierLine {
33
+ prefix: string;
34
+ chunk: string;
35
+ }
36
+
37
+ /**
38
+ * Hard-wrap an identifier without changing any of its characters. Prefixes
39
+ * count against each row's visible-cell budget, but are not part of the id.
40
+ */
41
+ export function wrapIdentifierLines(
42
+ id: string,
43
+ width: number,
44
+ firstPrefix: string,
45
+ continuationPrefix: string,
46
+ ): IdentifierLine[] {
47
+ const rows: IdentifierLine[] = [];
48
+ let remaining = id;
49
+ let first = true;
50
+ while (remaining.length > 0 || rows.length === 0) {
51
+ const prefix = first ? firstPrefix : continuationPrefix;
52
+ const budget = Math.max(1, width - visibleWidth(prefix));
53
+ let chunk = "";
54
+ for (const character of remaining) {
55
+ if (chunk.length > 0 && visibleWidth(`${chunk}${character}`) > budget) break;
56
+ chunk += character;
57
+ }
58
+ if (chunk.length === 0) chunk = remaining[0] ?? "";
59
+ rows.push({ prefix, chunk });
60
+ remaining = remaining.slice(chunk.length);
61
+ first = false;
62
+ }
63
+ return rows;
64
+ }
65
+
66
+ /**
67
+ * Render the canonical identity shape shared by the background widget and
68
+ * awaiting-input attribution banner. The caller supplies the status glyph so
69
+ * surfaces can preserve their existing run-state semantics.
70
+ */
71
+ export function renderRunIdentityRows(opts: RunIdentityRowsOpts): string[] {
72
+ const idIndent = Math.max(0, opts.idIndent ?? 3);
73
+ const idGap = Math.max(0, opts.idGap ?? 2);
74
+ const nameIndent = Math.max(0, opts.nameIndent ?? 5);
75
+ const idPrefix = `${" ".repeat(idIndent)}${opts.glyph}${" ".repeat(idGap)}`;
76
+ const continuationPrefix = " ".repeat(Math.max(0, idIndent + idGap - 1));
77
+ const availableWidth = opts.width === undefined ? Number.POSITIVE_INFINITY : Math.max(1, opts.width);
78
+ const chunks = wrapIdentifierLines(opts.runId, availableWidth, idPrefix, continuationPrefix);
79
+ const themed = opts.theme !== undefined;
80
+ const glyph = themed ? `${hexToAnsi(opts.glyphColor ?? opts.theme.text)}${opts.glyph}${RESET}` : opts.glyph;
81
+ const id = themed ? (text: string) => `${hexToAnsi(opts.theme!.accent)}${text}${RESET}` : (text: string) => text;
82
+ const name = themed ? `${hexToAnsi(opts.theme.text)}${BOLD}${opts.name}${RESET}` : opts.name;
83
+ const metaColor = opts.metaColor ?? opts.theme?.dim;
84
+ const meta =
85
+ themed && opts.meta !== undefined && opts.meta.length > 0
86
+ ? ` · ${hexToAnsi(metaColor ?? opts.theme.textMuted)}${opts.meta}${RESET}`
87
+ : opts.meta !== undefined && opts.meta.length > 0
88
+ ? ` · ${opts.meta}`
89
+ : "";
90
+
91
+ const rows = chunks.map((chunk, index) => {
92
+ if (index > 0) return `${chunk.prefix}${id(chunk.chunk)}`;
93
+ return `${" ".repeat(idIndent)}${glyph}${" ".repeat(idGap)}${id(chunk.chunk)}`;
94
+ });
95
+ rows.push(`${" ".repeat(nameIndent)}${name}${meta}`);
96
+ return rows;
97
+ }