@bastani/atomic 0.8.21 → 0.8.22-0

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 (235) hide show
  1. package/CHANGELOG.md +40 -9
  2. package/dist/builtin/intercom/broker/broker.ts +3 -3
  3. package/dist/builtin/intercom/config.ts +3 -3
  4. package/dist/builtin/intercom/index.ts +1 -1
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/intercom/ui/compose.ts +2 -2
  7. package/dist/builtin/mcp/host-html-template.ts +0 -3
  8. package/dist/builtin/mcp/package.json +1 -1
  9. package/dist/builtin/subagents/CHANGELOG.md +13 -4
  10. package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
  11. package/dist/builtin/subagents/agents/debugger.md +6 -6
  12. package/dist/builtin/subagents/package.json +1 -1
  13. package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
  14. package/dist/builtin/subagents/skills/browser-use/SKILL.md +234 -0
  15. package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +76 -0
  16. package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +92 -0
  17. package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
  18. package/dist/builtin/subagents/src/agents/skills.ts +19 -1
  19. package/dist/builtin/subagents/src/extension/index.ts +24 -22
  20. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +7 -1
  21. package/dist/builtin/subagents/src/runs/background/async-execution.ts +23 -7
  22. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +98 -3
  23. package/dist/builtin/subagents/src/runs/background/async-status.ts +3 -1
  24. package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -1
  25. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +3 -0
  26. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +37 -12
  27. package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +15 -15
  28. package/dist/builtin/subagents/src/runs/foreground/execution.ts +26 -2
  29. package/dist/builtin/subagents/src/runs/shared/nested-render.ts +1 -1
  30. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +7 -0
  31. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +28 -1
  32. package/dist/builtin/subagents/src/shared/fast-mode.ts +80 -0
  33. package/dist/builtin/subagents/src/shared/formatters.ts +4 -2
  34. package/dist/builtin/subagents/src/shared/types.ts +4 -2
  35. package/dist/builtin/subagents/src/shared/utils.ts +3 -61
  36. package/dist/builtin/subagents/src/tui/render.ts +303 -157
  37. package/dist/builtin/web-access/package.json +1 -1
  38. package/dist/builtin/workflows/CHANGELOG.md +95 -35
  39. package/dist/builtin/workflows/README.md +228 -41
  40. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +535 -541
  41. package/dist/builtin/workflows/builtin/goal.ts +39 -25
  42. package/dist/builtin/workflows/builtin/open-claude-design.ts +66 -69
  43. package/dist/builtin/workflows/builtin/ralph.ts +21 -21
  44. package/dist/builtin/workflows/package.json +6 -5
  45. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
  46. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +2 -2
  47. package/dist/builtin/workflows/src/extension/discovery.ts +25 -146
  48. package/dist/builtin/workflows/src/extension/dispatcher.ts +72 -24
  49. package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +363 -0
  50. package/dist/builtin/workflows/src/extension/index.ts +690 -352
  51. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +99 -62
  52. package/dist/builtin/workflows/src/extension/render-call.ts +2 -1
  53. package/dist/builtin/workflows/src/extension/render-result.ts +9 -3
  54. package/dist/builtin/workflows/src/extension/renderers.ts +5 -3
  55. package/dist/builtin/workflows/src/extension/runtime.ts +68 -33
  56. package/dist/builtin/workflows/src/extension/status-writer.ts +1 -1
  57. package/dist/builtin/workflows/src/extension/wiring.ts +34 -13
  58. package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +142 -0
  59. package/dist/builtin/workflows/src/extension/workflow-schema.ts +4 -4
  60. package/dist/builtin/workflows/src/index.ts +2 -0
  61. package/dist/builtin/workflows/src/intercom/result-intercom.ts +1 -1
  62. package/dist/builtin/workflows/src/runs/background/runner.ts +6 -4
  63. package/dist/builtin/workflows/src/runs/background/status.ts +45 -21
  64. package/dist/builtin/workflows/src/runs/foreground/executor.ts +624 -52
  65. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +1 -1
  66. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +80 -24
  67. package/dist/builtin/workflows/src/runs/shared/validate-inputs.ts +61 -24
  68. package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +32 -10
  69. package/dist/builtin/workflows/src/sdk-surface.ts +6 -0
  70. package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +178 -0
  71. package/dist/builtin/workflows/src/shared/persistence-restore.ts +92 -12
  72. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +21 -3
  73. package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +1 -2
  74. package/dist/builtin/workflows/src/shared/run-visibility.ts +9 -0
  75. package/dist/builtin/workflows/src/shared/schema-introspection.ts +121 -0
  76. package/dist/builtin/workflows/src/shared/serializable.ts +132 -0
  77. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +91 -9
  78. package/dist/builtin/workflows/src/shared/store-types.ts +31 -3
  79. package/dist/builtin/workflows/src/shared/store.ts +58 -14
  80. package/dist/builtin/workflows/src/shared/types.ts +105 -40
  81. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +129 -13
  82. package/dist/builtin/workflows/src/tui/chat-surface.ts +6 -1
  83. package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +3 -2
  84. package/dist/builtin/workflows/src/tui/graph-canvas.ts +1 -1
  85. package/dist/builtin/workflows/src/tui/graph-view.ts +91 -65
  86. package/dist/builtin/workflows/src/tui/inline-form-card.ts +1 -1
  87. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +3 -2
  88. package/dist/builtin/workflows/src/tui/inputs-overlay.ts +3 -2
  89. package/dist/builtin/workflows/src/tui/inputs-picker.ts +8 -7
  90. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
  91. package/dist/builtin/workflows/src/tui/node-card.ts +34 -8
  92. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -11
  93. package/dist/builtin/workflows/src/tui/prompt-card.ts +98 -50
  94. package/dist/builtin/workflows/src/tui/session-list.ts +7 -2
  95. package/dist/builtin/workflows/src/tui/session-picker.ts +2 -0
  96. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +226 -55
  97. package/dist/builtin/workflows/src/tui/status-helpers.ts +2 -0
  98. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +37 -158
  99. package/dist/builtin/workflows/src/tui/toast.ts +2 -2
  100. package/dist/builtin/workflows/src/tui/widget.ts +53 -12
  101. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +270 -19
  102. package/dist/builtin/workflows/src/tui/workflow-notice-card.ts +184 -0
  103. package/dist/builtin/workflows/src/workflows/define-workflow.ts +138 -43
  104. package/dist/config.d.ts +9 -0
  105. package/dist/config.d.ts.map +1 -1
  106. package/dist/config.js +45 -0
  107. package/dist/config.js.map +1 -1
  108. package/dist/core/agent-session.d.ts +27 -9
  109. package/dist/core/agent-session.d.ts.map +1 -1
  110. package/dist/core/agent-session.js +196 -17
  111. package/dist/core/agent-session.js.map +1 -1
  112. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  113. package/dist/core/atomic-guide-command.js +2 -2
  114. package/dist/core/atomic-guide-command.js.map +1 -1
  115. package/dist/core/codex-fast-mode.d.ts +36 -0
  116. package/dist/core/codex-fast-mode.d.ts.map +1 -0
  117. package/dist/core/codex-fast-mode.js +117 -0
  118. package/dist/core/codex-fast-mode.js.map +1 -0
  119. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  120. package/dist/core/compaction/branch-summarization.js +1 -1
  121. package/dist/core/compaction/branch-summarization.js.map +1 -1
  122. package/dist/core/compaction/compaction.d.ts.map +1 -1
  123. package/dist/core/compaction/compaction.js +1 -1
  124. package/dist/core/compaction/compaction.js.map +1 -1
  125. package/dist/core/extensions/index.d.ts +4 -1
  126. package/dist/core/extensions/index.d.ts.map +1 -1
  127. package/dist/core/extensions/index.js +1 -0
  128. package/dist/core/extensions/index.js.map +1 -1
  129. package/dist/core/extensions/loader.d.ts +7 -2
  130. package/dist/core/extensions/loader.d.ts.map +1 -1
  131. package/dist/core/extensions/loader.js +23 -8
  132. package/dist/core/extensions/loader.js.map +1 -1
  133. package/dist/core/extensions/reactive-widget.d.ts +58 -0
  134. package/dist/core/extensions/reactive-widget.d.ts.map +1 -0
  135. package/dist/core/extensions/reactive-widget.js +182 -0
  136. package/dist/core/extensions/reactive-widget.js.map +1 -0
  137. package/dist/core/extensions/runner.d.ts.map +1 -1
  138. package/dist/core/extensions/runner.js +1 -0
  139. package/dist/core/extensions/runner.js.map +1 -1
  140. package/dist/core/extensions/types.d.ts +26 -12
  141. package/dist/core/extensions/types.d.ts.map +1 -1
  142. package/dist/core/extensions/types.js.map +1 -1
  143. package/dist/core/messages.d.ts +1 -1
  144. package/dist/core/messages.d.ts.map +1 -1
  145. package/dist/core/messages.js +8 -2
  146. package/dist/core/messages.js.map +1 -1
  147. package/dist/core/model-registry.d.ts +4 -0
  148. package/dist/core/model-registry.d.ts.map +1 -1
  149. package/dist/core/model-registry.js +11 -0
  150. package/dist/core/model-registry.js.map +1 -1
  151. package/dist/core/resource-loader.d.ts +9 -1
  152. package/dist/core/resource-loader.d.ts.map +1 -1
  153. package/dist/core/resource-loader.js +49 -21
  154. package/dist/core/resource-loader.js.map +1 -1
  155. package/dist/core/sdk.d.ts.map +1 -1
  156. package/dist/core/sdk.js +22 -13
  157. package/dist/core/sdk.js.map +1 -1
  158. package/dist/core/session-manager.d.ts +7 -5
  159. package/dist/core/session-manager.d.ts.map +1 -1
  160. package/dist/core/session-manager.js +5 -3
  161. package/dist/core/session-manager.js.map +1 -1
  162. package/dist/core/settings-manager.d.ts +16 -0
  163. package/dist/core/settings-manager.d.ts.map +1 -1
  164. package/dist/core/settings-manager.js +64 -5
  165. package/dist/core/settings-manager.js.map +1 -1
  166. package/dist/core/slash-commands.d.ts.map +1 -1
  167. package/dist/core/slash-commands.js +1 -0
  168. package/dist/core/slash-commands.js.map +1 -1
  169. package/dist/core/system-prompt.d.ts.map +1 -1
  170. package/dist/core/system-prompt.js +7 -4
  171. package/dist/core/system-prompt.js.map +1 -1
  172. package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
  173. package/dist/core/tools/ask-user-question/ask-user-question.js +2 -2
  174. package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
  175. package/dist/index.d.ts +4 -3
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +3 -2
  178. package/dist/index.js.map +1 -1
  179. package/dist/main.d.ts +3 -0
  180. package/dist/main.d.ts.map +1 -1
  181. package/dist/main.js +12 -0
  182. package/dist/main.js.map +1 -1
  183. package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -1
  184. package/dist/modes/interactive/chat-input-actions.js.map +1 -1
  185. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  186. package/dist/modes/interactive/components/diff.js +0 -1
  187. package/dist/modes/interactive/components/diff.js.map +1 -1
  188. package/dist/modes/interactive/components/fast-mode-selector.d.ts +27 -0
  189. package/dist/modes/interactive/components/fast-mode-selector.d.ts.map +1 -0
  190. package/dist/modes/interactive/components/fast-mode-selector.js +105 -0
  191. package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -0
  192. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  193. package/dist/modes/interactive/components/footer.js +7 -12
  194. package/dist/modes/interactive/components/footer.js.map +1 -1
  195. package/dist/modes/interactive/components/index.d.ts +1 -0
  196. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  197. package/dist/modes/interactive/components/index.js +1 -0
  198. package/dist/modes/interactive/components/index.js.map +1 -1
  199. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  200. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  201. package/dist/modes/interactive/interactive-mode.js +132 -30
  202. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  203. package/dist/modes/print-mode.d.ts.map +1 -1
  204. package/dist/modes/print-mode.js +53 -6
  205. package/dist/modes/print-mode.js.map +1 -1
  206. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  207. package/dist/modes/rpc/rpc-mode.js +3 -0
  208. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  209. package/docs/compaction.md +1 -1
  210. package/docs/custom-provider.md +2 -2
  211. package/docs/development.md +2 -2
  212. package/docs/docs.json +2 -2
  213. package/docs/extensions.md +18 -13
  214. package/docs/providers.md +5 -1
  215. package/docs/quickstart.md +5 -3
  216. package/docs/rpc.md +5 -5
  217. package/docs/sdk.md +12 -12
  218. package/docs/settings.md +18 -0
  219. package/docs/themes.md +6 -6
  220. package/docs/tui.md +20 -18
  221. package/docs/usage.md +2 -0
  222. package/docs/workflows.md +403 -39
  223. package/examples/extensions/qna.ts +2 -2
  224. package/package.json +4 -4
  225. package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +0 -392
  226. package/dist/builtin/subagents/skills/playwright-cli/references/element-attributes.md +0 -23
  227. package/dist/builtin/subagents/skills/playwright-cli/references/playwright-tests.md +0 -39
  228. package/dist/builtin/subagents/skills/playwright-cli/references/request-mocking.md +0 -87
  229. package/dist/builtin/subagents/skills/playwright-cli/references/running-code.md +0 -241
  230. package/dist/builtin/subagents/skills/playwright-cli/references/session-management.md +0 -225
  231. package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
  232. package/dist/builtin/subagents/skills/playwright-cli/references/storage-state.md +0 -275
  233. package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +0 -134
  234. package/dist/builtin/subagents/skills/playwright-cli/references/tracing.md +0 -139
  235. package/dist/builtin/subagents/skills/playwright-cli/references/video-recording.md +0 -143
@@ -13,7 +13,7 @@
13
13
  * - No decorative progress bar. Counts live in the header pills.
14
14
  *
15
15
  * cross-ref:
16
- * - github.com/flora131/atomic packages/atomic-sdk/src/components/session-graph-panel.tsx
16
+ * - github.com/bastani-inc/atomic packages/atomic-sdk/src/components/session-graph-panel.tsx
17
17
  * - DESIGN.md §4 (Elevation), §5 (Components)
18
18
  */
19
19
  import type { Component } from "@earendil-works/pi-tui";
@@ -31,17 +31,21 @@ import type {
31
31
  StoreSnapshot,
32
32
  RunSnapshot,
33
33
  } from "../shared/store-types.js";
34
- import { elapsedStageMs } from "../shared/timing.js";
35
34
  import type { GraphTheme } from "./graph-theme.js";
36
35
  import type { SwitcherState } from "./switcher.js";
37
36
  import type { LayoutNode } from "./layout.js";
37
+ import {
38
+ expandWorkflowGraph,
39
+ expandedStageTarget,
40
+ type ExpandedWorkflowGraph,
41
+ type ExpandedWorkflowStage,
42
+ } from "../shared/expanded-workflow-graph.js";
38
43
  import { computeLayout, NODE_W, NODE_H } from "./layout.js";
39
44
  import { renderHeader, renderOutlinePill } from "./header.js";
40
45
  import { renderNodeCard } from "./node-card.js";
41
46
  import { renderSwitcher, filterStages } from "./switcher.js";
42
47
  import { renderToasts, createToastManager } from "./toast.js";
43
48
  import { hexToAnsi, hexBg, RESET, BOLD } from "./color-utils.js";
44
- import { fmtDuration } from "./status-helpers.js";
45
49
  import { GraphCanvas } from "./graph-canvas.js";
46
50
  import {
47
51
  createPromptCardState,
@@ -50,6 +54,7 @@ import {
50
54
  renderPromptCard,
51
55
  type PromptCardState,
52
56
  } from "./prompt-card.js";
57
+ import { isKeybindingsLike, type KeybindingsLike } from "./keybindings-adapter.js";
53
58
 
54
59
  export type GraphViewMode = "overlay" | "widget";
55
60
 
@@ -115,6 +120,8 @@ export interface GraphViewOpts {
115
120
  * `overlay-adapter.ts`).
116
121
  */
117
122
  requestRender?: () => void;
123
+ /** Host Pi keybindings manager used by run-level prompt cards. */
124
+ piKeybindings?: unknown;
118
125
  }
119
126
 
120
127
  const HINT_KEYS: Array<{ key: string; label: string }> = [
@@ -177,6 +184,7 @@ export class GraphView implements Component {
177
184
  private initialFocusedStageId?: string;
178
185
  private getViewportRows?: () => number | undefined;
179
186
  private requestRender?: () => void;
187
+ private piKeybindings?: unknown;
180
188
 
181
189
  /** Active HIL prompt state, set when `_rebuildLayout` sees a new prompt id. */
182
190
  private promptState: PromptCardState | null = null;
@@ -187,6 +195,7 @@ export class GraphView implements Component {
187
195
  private toastManager = createToastManager();
188
196
  private detailsExpanded = true;
189
197
  private cachedLayout: LayoutNode[] = [];
198
+ private expandedGraph: ExpandedWorkflowGraph = { stages: [], targets: new Map() };
190
199
  private currentSnapshot: StoreSnapshot | null = null;
191
200
  private graphScrollOffset = 0;
192
201
  private graphScrollColOffset = 0;
@@ -211,6 +220,7 @@ export class GraphView implements Component {
211
220
  this.initialFocusedStageId = opts.initialFocusedStageId;
212
221
  this.getViewportRows = opts.getViewportRows;
213
222
  this.requestRender = opts.requestRender;
223
+ this.piKeybindings = opts.piKeybindings;
214
224
 
215
225
  this._unsubscribe = this.store.subscribe((snap) => {
216
226
  this.currentSnapshot = snap;
@@ -242,6 +252,7 @@ export class GraphView implements Component {
242
252
  const run = this._getCurrentRun();
243
253
  if (!run) {
244
254
  this.cachedLayout = [];
255
+ this.expandedGraph = { stages: [], targets: new Map() };
245
256
  this.focusedIndex = 0;
246
257
  this.graphScrollOffset = 0;
247
258
  this.graphScrollColOffset = 0;
@@ -262,7 +273,9 @@ export class GraphView implements Component {
262
273
  // the same node the user just attached to.
263
274
  if (this.initialFocusedStageId !== undefined) {
264
275
  const idx = this.cachedLayout.findIndex(
265
- (n) => n.stage.id === this.initialFocusedStageId,
276
+ (n) =>
277
+ n.stage.id === this.initialFocusedStageId ||
278
+ expandedStageTarget(this.expandedGraph, n.stage.id)?.stageId === this.initialFocusedStageId,
266
279
  );
267
280
  if (idx >= 0 && idx !== this.focusedIndex) {
268
281
  this.focusedIndex = idx;
@@ -316,31 +329,37 @@ export class GraphView implements Component {
316
329
  }
317
330
 
318
331
  private _awaitingInputKey(stage: StageSnapshot): { key: string; createdAt: number } | null {
332
+ const target = expandedStageTarget(this.expandedGraph, stage.id);
333
+ const prefix = target ? `${target.runId}:${target.stageId}` : stage.id;
319
334
  if (stage.pendingPrompt) {
320
335
  return {
321
- key: `prompt:${stage.id}:${stage.pendingPrompt.id}`,
336
+ key: `prompt:${prefix}:${stage.pendingPrompt.id}`,
322
337
  createdAt: stage.pendingPrompt.createdAt,
323
338
  };
324
339
  }
325
340
  if (stage.inputRequest) {
326
341
  return {
327
- key: `input-request:${stage.id}:${stage.inputRequest.id}`,
342
+ key: `input-request:${prefix}:${stage.inputRequest.id}`,
328
343
  createdAt: stage.inputRequest.createdAt,
329
344
  };
330
345
  }
331
346
  if (stage.status === "awaiting_input") {
332
347
  return {
333
- key: `awaiting:${stage.id}:${stage.awaitingInputSince ?? "active"}`,
348
+ key: `awaiting:${prefix}:${stage.awaitingInputSince ?? "active"}`,
334
349
  createdAt: stage.awaitingInputSince ?? stage.startedAt ?? 0,
335
350
  };
336
351
  }
337
352
  return null;
338
353
  }
339
354
 
340
- private _graphStages(run: RunSnapshot): StageSnapshot[] {
341
- const hasStagePrompt = run.stages.some((stage) => stage.pendingPrompt !== undefined);
342
- if (!hasStagePrompt) return [...run.stages];
343
- return run.stages.filter((stage) => {
355
+ private _graphStages(run: RunSnapshot): ExpandedWorkflowStage[] {
356
+ this.expandedGraph = this.currentSnapshot
357
+ ? expandWorkflowGraph(this.currentSnapshot, run.id)
358
+ : { stages: [], targets: new Map() };
359
+ const stages = [...this.expandedGraph.stages];
360
+ const hasStagePrompt = stages.some((stage) => stage.pendingPrompt !== undefined);
361
+ if (!hasStagePrompt) return stages;
362
+ return stages.filter((stage) => {
344
363
  // Prompt-node injection can leave unstarted author stages in the store
345
364
  // while the prompt node owns focus; hide only these inert placeholders.
346
365
  const isUnstartedPlaceholder =
@@ -394,11 +413,12 @@ export class GraphView implements Component {
394
413
  if (!run) {
395
414
  return [`${hexToAnsi(this.graphTheme.dim)}no active workflow${RESET}`];
396
415
  }
397
- const headerLines = renderHeader(run, { width, theme: this.graphTheme });
398
- const counts = this._counts(run);
416
+ const displayStages = this._displayStages(run);
417
+ const headerLines = renderHeader({ ...run, stages: displayStages }, { width, theme: this.graphTheme });
418
+ const counts = this._counts(displayStages);
399
419
  const trailer =
400
420
  `${hexToAnsi(this.graphTheme.dim)}` +
401
- `${counts.completed}/${run.stages.length} done` +
421
+ `${counts.completed}/${displayStages.length} done` +
402
422
  (counts.running > 0 ? ` · ${counts.running} running` : "") +
403
423
  (counts.failed > 0 ? ` · ${counts.failed} failed` : "") +
404
424
  (counts.blocked > 0 ? ` · ${counts.blocked} blocked` : "") +
@@ -473,7 +493,7 @@ export class GraphView implements Component {
473
493
 
474
494
  // 1. Header chrome (3 rows: outline pill + session name + counts).
475
495
  lines.push(
476
- ...renderHeader(run, { width: frameWidth, theme: this.graphTheme }),
496
+ ...renderHeader({ ...run, stages: this._displayStages(run) }, { width: frameWidth, theme: this.graphTheme }),
477
497
  );
478
498
 
479
499
  // 2. Graph occupies the full body. No section labels, no focused-
@@ -502,7 +522,7 @@ export class GraphView implements Component {
502
522
  lines[row] = this._blankRow(frameWidth);
503
523
  }
504
524
  const switcherWidth = Math.min(60, Math.max(40, frameWidth - 8));
505
- const switcherLines = renderSwitcher(run.stages, this.switcherState, {
525
+ const switcherLines = renderSwitcher(this._displayStages(run), this.switcherState, {
506
526
  width: switcherWidth,
507
527
  theme: this.graphTheme,
508
528
  });
@@ -520,8 +540,9 @@ export class GraphView implements Component {
520
540
 
521
541
  // 4. Pending HIL prompt — floats over the graph body, centred. The
522
542
  // chat editor remains free regardless: the overlay is the only
523
- // surface that interacts with the prompt.
524
- if (this.promptState) {
543
+ // surface that interacts with the prompt. When the stage switcher is
544
+ // open it owns the body/input, so hide the prompt card until it closes.
545
+ if (this.promptState && !this.switcherOpen) {
525
546
  const cardWidth = Math.min(72, Math.max(40, frameWidth - 6));
526
547
  const cardLines = renderPromptCard({
527
548
  state: this.promptState,
@@ -677,7 +698,7 @@ export class GraphView implements Component {
677
698
  focused,
678
699
  pulsePhase,
679
700
  theme: this.graphTheme,
680
- stages: run.stages,
701
+ stages: this.cachedLayout.map((layoutNode) => layoutNode.stage),
681
702
  });
682
703
  for (let li = 0; li < cardLines.length; li++) {
683
704
  const rowIdx = node.y + li;
@@ -796,7 +817,7 @@ export class GraphView implements Component {
796
817
  this._clampGraphScroll(totalRows, bodyRows);
797
818
  }
798
819
 
799
- private _focusedGraphRowRange(frameWidth: number): { start: number; end: number } | null {
820
+ private _focusedGraphRowRange(_frameWidth: number): { start: number; end: number } | null {
800
821
  const node = this.cachedLayout[this.focusedIndex];
801
822
  if (!node) return null;
802
823
  return { start: node.y, end: node.y + NODE_H - 1 };
@@ -1014,37 +1035,13 @@ export class GraphView implements Component {
1014
1035
  return `${bg}${" ".repeat(leftPad)}${RESET}${cardLine}${bg}${" ".repeat(rightPadLen)}${RESET}`;
1015
1036
  }
1016
1037
 
1017
- /** Overlay a fixed-width panel on a row while preserving graph cells
1018
- * outside the panel bounds. Used by the stage switcher so the picker
1019
- * does not erase nodes to its right. */
1020
- private _overlayInline(
1021
- base: string,
1022
- overlay: string,
1023
- leftPad: number,
1024
- totalWidth: number,
1025
- ): string {
1026
- const overlayWidth = Math.min(
1027
- Math.max(0, totalWidth - leftPad),
1028
- visibleWidth(overlay),
1029
- );
1030
- const left = this._sliceColumns(base, 0, leftPad);
1031
- const panel = truncateToWidth(overlay, overlayWidth, "", true);
1032
- const right = this._sliceColumns(
1033
- base,
1034
- leftPad + overlayWidth,
1035
- totalWidth,
1036
- );
1037
- const merged = `${left}${panel}${right}`;
1038
- const pad = Math.max(0, totalWidth - visibleWidth(merged));
1039
- return `${merged}${hexBg(this.graphTheme.bg)}${" ".repeat(pad)}${RESET}`;
1038
+ private _displayStages(run: RunSnapshot): StageSnapshot[] {
1039
+ return this.cachedLayout.length > 0
1040
+ ? this.cachedLayout.map((layoutNode) => layoutNode.stage)
1041
+ : [...run.stages];
1040
1042
  }
1041
1043
 
1042
- private _duration(stage: StageSnapshot): string {
1043
- const elapsed = elapsedStageMs(stage);
1044
- return elapsed === undefined ? "" : fmtDuration(elapsed);
1045
- }
1046
-
1047
- private _counts(run: RunSnapshot): {
1044
+ private _counts(stages: readonly StageSnapshot[]): {
1048
1045
  pending: number;
1049
1046
  running: number;
1050
1047
  awaiting_input: number;
@@ -1064,7 +1061,7 @@ export class GraphView implements Component {
1064
1061
  failed: 0,
1065
1062
  skipped: 0,
1066
1063
  };
1067
- for (const s of run.stages) c[s.status]++;
1064
+ for (const s of stages) c[s.status]++;
1068
1065
  return c;
1069
1066
  }
1070
1067
 
@@ -1074,25 +1071,40 @@ export class GraphView implements Component {
1074
1071
 
1075
1072
  /** Returns true if consumed. */
1076
1073
  handleInput(data: string): boolean {
1077
- // Pending HIL prompt owns input — once a prompt is active, every key
1078
- // routes to it until the user submits or skips. This is what keeps
1079
- // the chat editor free: the workflow author called ctx.ui.editor()
1080
- // long ago in a background promise; only the overlay handles the
1081
- // response. The graph nav resumes after `_resolvePrompt` clears
1082
- // `promptState` (mirrored from the store via `_syncPromptState`).
1083
- if (this.promptState) {
1084
- return this._handlePromptInput(data);
1085
- }
1086
1074
  if (this.switcherOpen) {
1087
1075
  return this._handleSwitcherInput(data);
1088
1076
  }
1077
+ // Stage-local HIL is represented by graph nodes and remains graph-first;
1078
+ // only the legacy run-level prompt card sets `promptState`. Keep that
1079
+ // fallback answerable, but let a narrow set of non-text graph controls
1080
+ // through first so the workflow overlay can still be detached or scrolled
1081
+ // instead of feeling modal while a prompt is visible. Printable keys such
1082
+ // as "/" belong to the prompt card while legacy run-level text/editor
1083
+ // prompts own input.
1084
+ if (this.promptState) {
1085
+ if (this._isNonTextGraphControlBeforePrompt(data)) {
1086
+ return this._handleGraphInput(data);
1087
+ }
1088
+ return this._handlePromptInput(data);
1089
+ }
1089
1090
  return this._handleGraphInput(data);
1090
1091
  }
1091
1092
 
1093
+ private _promptKeybindings(): KeybindingsLike | undefined {
1094
+ return isKeybindingsLike(this.piKeybindings) ? this.piKeybindings : undefined;
1095
+ }
1096
+
1097
+ private _isNonTextGraphControlBeforePrompt(data: string): boolean {
1098
+ return (
1099
+ this._mouseWheelDeltaRows(data) !== 0 ||
1100
+ matchesKey(data, Key.ctrl("d"))
1101
+ );
1102
+ }
1103
+
1092
1104
  private _handlePromptInput(data: string): boolean {
1093
1105
  const state = this.promptState;
1094
1106
  if (!state) return false;
1095
- const action = handlePromptCardInput(data, state);
1107
+ const action = handlePromptCardInput(data, state, this._promptKeybindings());
1096
1108
  if (action.kind === "noop") return true;
1097
1109
  const runId = this.runId;
1098
1110
  if (!runId) return true;
@@ -1181,8 +1193,12 @@ export class GraphView implements Component {
1181
1193
  // the overlay's setHidden() flag (not unmount); Escape/Ctrl+C closes.
1182
1194
  if (matchesKey(data, "q")) {
1183
1195
  const run = this._getCurrentRun();
1184
- if (run && run.endedAt === undefined && this.onKill) {
1185
- this.onKill(run.id);
1196
+ const targetRunId = this._focusedStageTarget()?.runId ?? run?.id;
1197
+ const targetRun = targetRunId !== undefined
1198
+ ? this.currentSnapshot?.runs.find((candidate) => candidate.id === targetRunId)
1199
+ : undefined;
1200
+ if (targetRun && targetRun.endedAt === undefined && this.onKill) {
1201
+ this.onKill(targetRun.id);
1186
1202
  }
1187
1203
  this.onClose?.();
1188
1204
  return true;
@@ -1199,8 +1215,7 @@ export class GraphView implements Component {
1199
1215
  }
1200
1216
 
1201
1217
  private _handleSwitcherInput(data: string): boolean {
1202
- const run = this._getCurrentRun();
1203
- const stages = run?.stages ?? [];
1218
+ const stages = this.cachedLayout.map((layoutNode) => layoutNode.stage);
1204
1219
 
1205
1220
  if (matchesKey(data, Key.escape)) {
1206
1221
  this.switcherOpen = false;
@@ -1326,10 +1341,21 @@ export class GraphView implements Component {
1326
1341
  const node = this.cachedLayout[this.focusedIndex];
1327
1342
  const run = this._getCurrentRun();
1328
1343
  if (!node || !run) return false;
1329
- this.onStageAttach(run.id, node.stage.id);
1344
+ const target = expandedStageTarget(this.expandedGraph, node.stage.id) ?? {
1345
+ runId: run.id,
1346
+ stageId: node.stage.id,
1347
+ };
1348
+ this.onStageAttach(target.runId, target.stageId);
1330
1349
  return true;
1331
1350
  }
1332
1351
 
1352
+ private _focusedStageTarget(): { runId: string; stageId: string } | undefined {
1353
+ const node = this.cachedLayout[this.focusedIndex];
1354
+ if (!node) return undefined;
1355
+ const target = expandedStageTarget(this.expandedGraph, node.stage.id);
1356
+ return target ? { runId: target.runId, stageId: target.stageId } : undefined;
1357
+ }
1358
+
1333
1359
  private _setFocusedIndex(index: number): void {
1334
1360
  const max = Math.max(0, this.cachedLayout.length - 1);
1335
1361
  const next = Math.max(0, Math.min(index, max));
@@ -283,7 +283,7 @@ function renderCaretLine(
283
283
  raw: string,
284
284
  caret: number,
285
285
  usable: number,
286
- theme: GraphTheme,
286
+ _theme: GraphTheme,
287
287
  color: string,
288
288
  ): string {
289
289
  const safe = clampGraphemeOffset(raw, caret);
@@ -30,6 +30,7 @@ import type {
30
30
  PiEditorFactory,
31
31
  } from "../extension/wiring.js";
32
32
  import type { WorkflowInputEntry } from "../extension/render-result.js";
33
+ import type { WorkflowInputValues } from "../shared/types.js";
33
34
  import type { GraphTheme } from "./graph-theme.js";
34
35
  import { renderInlineCard } from "./inline-form-card.js";
35
36
  import { InlineFormEditor } from "./inline-form-editor.js";
@@ -51,14 +52,14 @@ interface FormMessageDetails {
51
52
  * care which surface was used.
52
53
  */
53
54
  export type InlineFormResult =
54
- | { kind: "run"; values: Record<string, unknown> }
55
+ | { kind: "run"; values: WorkflowInputValues }
55
56
  | { kind: "cancel" }
56
57
  | { kind: "unsupported" };
57
58
 
58
59
  export interface OpenInlineFormOpts {
59
60
  workflowName: string;
60
61
  fields: readonly WorkflowInputEntry[];
61
- prefilled?: Record<string, unknown>;
62
+ prefilled?: WorkflowInputValues;
62
63
  theme: GraphTheme;
63
64
  }
64
65
 
@@ -38,6 +38,7 @@ import type {
38
38
  } from "../extension/wiring.js";
39
39
  import type { WorkflowInputEntry } from "../extension/render-result.js";
40
40
  import type { GraphTheme } from "./graph-theme.js";
41
+ import type { WorkflowInputValues } from "../shared/types.js";
41
42
  import {
42
43
  coerceValues,
43
44
  createInputsPickerState,
@@ -50,7 +51,7 @@ export interface InputsUiSurface {
50
51
  }
51
52
 
52
53
  export type InputsPickerResult =
53
- | { kind: "run"; values: Record<string, unknown> }
54
+ | { kind: "run"; values: WorkflowInputValues }
54
55
  | { kind: "cancel" };
55
56
 
56
57
  export interface OpenInputsPickerOpts {
@@ -58,7 +59,7 @@ export interface OpenInputsPickerOpts {
58
59
  fields: WorkflowInputEntry[];
59
60
  /** Prefilled values (e.g. from `key=value` slash args). The form
60
61
  * seeds these into the form so the user doesn't re-type what they typed. */
61
- prefilled?: Record<string, unknown>;
62
+ prefilled?: WorkflowInputValues;
62
63
  theme: GraphTheme;
63
64
  }
64
65
 
@@ -24,13 +24,14 @@
24
24
  * - select : vertical choice list, ←/→ cycles choices
25
25
  *
26
26
  * cross-ref:
27
- * - flora131/atomic research/designs/workflow-picker-tui.tsx (PROMPT phase)
28
- * - flora131/atomic packages/atomic-sdk/src/components/workflow-picker-panel.tsx
27
+ * - bastani-inc/atomic research/designs/workflow-picker-tui.tsx (PROMPT phase)
28
+ * - bastani-inc/atomic packages/atomic-sdk/src/components/workflow-picker-panel.tsx
29
29
  * - src/tui/session-picker.ts (sibling overlay; same chrome + key style)
30
30
  * - DESIGN.md §5 Section Labels
31
31
  */
32
32
 
33
33
  import type { WorkflowInputEntry } from "../extension/render-result.js";
34
+ import type { WorkflowInputValues, WorkflowSerializableValue } from "../shared/types.js";
34
35
  import type { GraphTheme } from "./graph-theme.js";
35
36
  import { paint } from "./color-utils.js";
36
37
  import {
@@ -95,7 +96,7 @@ export interface InputsPickerState {
95
96
  export type InputsPickerAction =
96
97
  | { kind: "noop" }
97
98
  | { kind: "cancel" }
98
- | { kind: "run"; values: Record<string, unknown> };
99
+ | { kind: "run"; values: WorkflowInputValues };
99
100
 
100
101
  export interface InputsPickerRenderOpts {
101
102
  width: number;
@@ -119,7 +120,7 @@ export interface InputsPickerRenderOpts {
119
120
  */
120
121
  export function createInputsPickerState(
121
122
  fields: readonly WorkflowInputEntry[],
122
- prefilled: Record<string, unknown> = {},
123
+ prefilled: WorkflowInputValues = {},
123
124
  ): InputsPickerState {
124
125
  const rawText: Record<string, string> = {};
125
126
  for (const f of fields) {
@@ -169,8 +170,8 @@ export function createInputsPickerState(
169
170
  export function coerceValues(
170
171
  fields: readonly WorkflowInputEntry[],
171
172
  raw: Record<string, string>,
172
- ): Record<string, unknown> {
173
- const out: Record<string, unknown> = {};
173
+ ): WorkflowInputValues {
174
+ const out: Record<string, WorkflowSerializableValue> = {};
174
175
  for (const f of fields) {
175
176
  const v = raw[f.name] ?? "";
176
177
  if (v === "" && !f.required) continue; // skip empty optionals
@@ -198,7 +199,7 @@ export function coerceValues(
198
199
  (v.startsWith("[") && v.endsWith("]"))
199
200
  ) {
200
201
  try {
201
- out[f.name] = JSON.parse(v) as unknown;
202
+ out[f.name] = JSON.parse(v) as WorkflowSerializableValue;
202
203
  break;
203
204
  } catch {
204
205
  // fall through
@@ -46,6 +46,8 @@ export const TUI_ACTION = {
46
46
  inputSubmit: "tui.input.submit",
47
47
  selectUp: "tui.select.up",
48
48
  selectDown: "tui.select.down",
49
+ selectPageUp: "tui.select.pageUp",
50
+ selectPageDown: "tui.select.pageDown",
49
51
  selectConfirm: "tui.select.confirm",
50
52
  } as const;
51
53
 
@@ -18,7 +18,7 @@
18
18
  * the same ANSI shape Pi's renderer uses for every other styled run.
19
19
  *
20
20
  * cross-ref:
21
- * - github.com/flora131/atomic packages/atomic-sdk/src/components/node-card.tsx
21
+ * - github.com/bastani-inc/atomic packages/atomic-sdk/src/components/node-card.tsx
22
22
  * - DESIGN.md §5 "Node Cards (orchestrator graph)"
23
23
  * - src/tui/graph-theme.ts `deriveGraphThemeFromPiTheme` — the
24
24
  * accent/surface tokens used below are sourced from Pi's live
@@ -120,8 +120,32 @@ function durationText(stage: StageSnapshot): string {
120
120
 
121
121
  function metaText(stage: StageSnapshot): string {
122
122
  const deps = stage.parentIds.length;
123
- if (deps === 0) return "root";
124
- return deps === 1 ? "1 dep" : `${deps} deps`;
123
+ const dependencyText = deps === 0 ? "root" : deps === 1 ? "1 dep" : `${deps} deps`;
124
+ return stage.fastMode === true ? `${dependencyText} · fast` : dependencyText;
125
+ }
126
+
127
+ function shortRunId(runId: string): string {
128
+ return runId.length <= 8 ? runId : runId.slice(0, 8);
129
+ }
130
+
131
+ function workflowChildSummaryText(stage: StageSnapshot): string {
132
+ const child = stage.workflowChild ?? stage.workflowChildRun;
133
+ if (child === undefined) return durationText(stage);
134
+ return `↳ ${child.workflow}`;
135
+ }
136
+
137
+ function workflowChildMetaText(stage: StageSnapshot): string {
138
+ const completed = stage.workflowChild;
139
+ if (completed !== undefined) {
140
+ const outputCount = Object.keys(completed.outputs).length;
141
+ const outputs = outputCount === 1 ? "1 out" : `${outputCount} outs`;
142
+ return `run ${shortRunId(completed.runId)} · ${outputs}`;
143
+ }
144
+
145
+ const live = stage.workflowChildRun;
146
+ if (live !== undefined) return `run ${shortRunId(live.runId)} · live`;
147
+
148
+ return metaText(stage);
125
149
  }
126
150
 
127
151
  function statusLabel(status: StageStatus): string {
@@ -248,13 +272,15 @@ export function renderNodeCard(stage: StageSnapshot, opts: NodeCardOpts): string
248
272
  const top = `${bg}${bc}╭${topMiddle}╮${RESET}`;
249
273
  const bottom = `${bg}${bc}╰${"─".repeat(innerWidth)}╯${RESET}`;
250
274
 
251
- // Interior — compact status + duration. Each `│` border is followed
252
- // by a `bg`-primed centred run so the inner cells stay on the card
253
- // stratum without leaving the cards visually hollow.
275
+ // Interior — compact status + duration. Child workflow boundary
276
+ // stages otherwise look like empty completed nodes, so use the first
277
+ // body row for the child workflow identity and the final row for a
278
+ // terse child-run summary. This keeps the graph dense while making
279
+ // the boundary explain what actually ran.
254
280
  const bodyText =
255
281
  stage.status === "blocked"
256
282
  ? blockedBadgeText(stage, opts.stages, innerWidth)
257
- : durationText(stage);
283
+ : workflowChildSummaryText(stage);
258
284
  const bodyHex = durationColor(stage.status, theme);
259
285
  const statusText = `${statusIcon(stage.status)} ${statusLabel(stage.status)}`;
260
286
  const statusLine =
@@ -271,7 +297,7 @@ export function renderNodeCard(stage: StageSnapshot, opts: NodeCardOpts): string
271
297
  `${bg}${bc}│${RESET}`;
272
298
  const metaLine =
273
299
  `${bg}${bc}│${RESET}` +
274
- centreColored(metaText(stage), innerWidth, theme.dim, bg) +
300
+ centreColored(workflowChildMetaText(stage), innerWidth, theme.dim, bg) +
275
301
  `${bg}${bc}│${RESET}`;
276
302
 
277
303
  const interior: string[] =
@@ -115,6 +115,8 @@ export interface BuildGraphOverlayAdapterOpts {
115
115
  * inspection.
116
116
  */
117
117
  onKillRun?: (runId: string) => void;
118
+ /** Optional clock injection for deterministic attach-pane transition tests. */
119
+ now?: () => number;
118
120
  }
119
121
 
120
122
  export function buildGraphOverlayAdapter(
@@ -177,21 +179,11 @@ export function buildGraphOverlayAdapter(
177
179
  }
178
180
  }
179
181
 
180
- function snapshotHasAwaitingInput(snapshot: StoreSnapshot): boolean {
181
- return snapshot.runs.some(
182
- (run) => run.pendingPrompt !== undefined || run.stages.some(
183
- (stage) => stage.status === "awaiting_input"
184
- || stage.pendingPrompt !== undefined
185
- || stage.inputRequest !== undefined,
186
- ),
187
- );
188
- }
189
-
190
182
  function refocusVisibleOverlayForAwaitingInput(snapshot: StoreSnapshot): void {
191
- if (!snapshotHasAwaitingInput(snapshot)) return;
192
183
  if (currentHandle === null) return;
193
184
  if (currentHandle.isHidden()) return;
194
185
  if (currentHandle.isFocused()) return;
186
+ if (currentView?.wantsFocusForAwaitingInput(snapshot) !== true) return;
195
187
  currentHandle.focus();
196
188
  }
197
189
 
@@ -318,6 +310,7 @@ export function buildGraphOverlayAdapter(
318
310
  currentHandle?.focus();
319
311
  },
320
312
  setMouseScrollTracking,
313
+ now: buildOpts.now,
321
314
  } as ConstructorParameters<typeof WorkflowAttachPane>[0] & {
322
315
  piTui?: PiCustomOverlayFactoryTui;
323
316
  piTheme?: PiTheme;