@bastani/atomic 0.8.21-0 → 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 +46 -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 +101 -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
@@ -10,7 +10,7 @@
10
10
  * - input reuses the host's custom editor factory when one is installed,
11
11
  * otherwise `CustomEditor`; tests/headless fall back to the historical
12
12
  * one-line editor
13
- * - workflow notices remain lightweight workflow-specific rows because they
13
+ * - workflow notices render as compact workflow-specific cards because they
14
14
  * are not coding-agent chat messages
15
15
  *
16
16
  * Behaviour:
@@ -66,8 +66,9 @@ import {
66
66
  import type { PendingPrompt, StageNotice, StageSnapshot, StageStatus } from "../shared/store-types.js";
67
67
  import type { GraphTheme } from "./graph-theme.js";
68
68
  import type { StageControlHandle } from "../runs/foreground/stage-control-registry.js";
69
- import { isKeybindingsLike } from "./keybindings-adapter.js";
69
+ import { isKeybindingsLike, type KeybindingsLike } from "./keybindings-adapter.js";
70
70
  import { BOLD, RESET, hexBg, hexToAnsi, lerpColor } from "./color-utils.js";
71
+ import { renderWorkflowNoticeCard, type WorkflowNoticeTone } from "./workflow-notice-card.js";
71
72
  import { Key, matchesKey, visibleWidth } from "./text-helpers.js";
72
73
  import {
73
74
  fitStageChatFrame,
@@ -78,6 +79,7 @@ import {
78
79
  createPromptCardState,
79
80
  defaultResponseFor,
80
81
  handlePromptCardInput,
82
+ isPromptEscapeInput,
81
83
  renderPromptCard,
82
84
  type PromptCardState,
83
85
  } from "./prompt-card.js";
@@ -104,7 +106,7 @@ export interface StageChatViewOpts {
104
106
  */
105
107
  handle?: StageControlHandle;
106
108
  /** Called when the user presses Ctrl+D outside a paused stage (back to graph). */
107
- onDetach: () => void;
109
+ onDetach: (reason?: StageChatDetachReason, metadata?: StageChatDetachMetadata) => void;
108
110
  /** Called when the user presses Escape (close the whole popup). */
109
111
  onClose: () => void;
110
112
  /** Request a host TUI repaint after SDK events mutate local chat state. */
@@ -141,6 +143,12 @@ export interface StageChatViewOpts {
141
143
  getViewportRows?: () => number | undefined;
142
144
  /** Broker that routes stage-local custom UI, such as ask_user_question, into this node. */
143
145
  stageUiBroker?: StageUiBroker;
146
+ /**
147
+ * Ownership guard for prompt submission. The attach shell uses this to ensure
148
+ * stale/hidden/non-active stage-chat instances cannot settle a prompt unless
149
+ * the user is currently attached to this exact workflow node.
150
+ */
151
+ canSubmitPrompt?: (runId: string, stageId: string, promptId: string) => boolean;
144
152
  }
145
153
 
146
154
  /**
@@ -148,6 +156,12 @@ export interface StageChatViewOpts {
148
156
  * that read `_transcript` (tests, future serialisers) can recover the
149
157
  * canonical user-visible string without knowing about the Pi-box payload.
150
158
  */
159
+ export type StageChatDetachReason = "user" | "prompt-resolved";
160
+
161
+ export interface StageChatDetachMetadata {
162
+ readonly suppressNextGraphSubmit?: boolean;
163
+ }
164
+
151
165
  interface NoticeEntry {
152
166
  readonly role: "notice";
153
167
  readonly text: string;
@@ -158,7 +172,6 @@ interface NoticeEntry {
158
172
  readonly meta?: string;
159
173
  }
160
174
  type TranscriptEntry = NoticeEntry | ChatMessageEntry;
161
- type AgentSnapshotMessage = AgentSession["messages"][number];
162
175
 
163
176
  // ---------------------------------------------------------------------------
164
177
  // Frame budget
@@ -193,7 +206,7 @@ export class StageChatView implements Component, Focusable {
193
206
  private stageId: string;
194
207
  private workflowName: string;
195
208
  private handle: StageControlHandle | undefined;
196
- private onDetach: () => void;
209
+ private onDetach: (reason?: StageChatDetachReason, metadata?: StageChatDetachMetadata) => void;
197
210
  private onClose: () => void;
198
211
  private requestRender: (() => void) | undefined;
199
212
  private requestFocus: (() => void) | undefined;
@@ -205,17 +218,15 @@ export class StageChatView implements Component, Focusable {
205
218
  private piEditorFactory?: StageChatViewOpts["piEditorFactory"];
206
219
  private chatHost: ChatSessionHost<NoticeEntry>;
207
220
  private stageUiBroker: StageUiBroker;
221
+ private canSubmitPrompt?: (runId: string, stageId: string, promptId: string) => boolean;
208
222
  private mountedCustomUi: MountedStageCustomUi | null = null;
209
223
  private mountingRequestId: string | null = null;
210
224
  private promptState: PromptCardState | null = null;
211
225
  private promptEditor: EditorComponent | null = null;
212
226
  private promptEditorPromptId: string | null = null;
227
+ private promptEditorSubmitFromEnter = false;
213
228
  private promptScrollOffset = 0;
214
229
  private promptMaxScroll = 0;
215
- private getChatRenderSettings?: () =>
216
- | Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd">>
217
- | undefined;
218
- private footerData?: ReadonlyFooterDataProvider;
219
230
 
220
231
  /** True while a pending pause request is in flight (between ctrl+p and resolve). */
221
232
  private localPaused = false;
@@ -260,9 +271,8 @@ export class StageChatView implements Component, Focusable {
260
271
  this.piTheme = opts.piTheme;
261
272
  this.piKeybindings = opts.piKeybindings;
262
273
  this.piEditorFactory = opts.piEditorFactory;
263
- this.getChatRenderSettings = opts.getChatRenderSettings;
264
- this.footerData = opts.footerData;
265
274
  this.stageUiBroker = opts.stageUiBroker ?? stageUiBroker;
275
+ this.canSubmitPrompt = opts.canSubmitPrompt;
266
276
  this.chatHost = new ChatSessionHost<NoticeEntry>({
267
277
  style: this._chatHostStyle(),
268
278
  commands: {
@@ -364,7 +374,11 @@ export class StageChatView implements Component, Focusable {
364
374
  // `stage.setModel`, `stage.compact`, …) so they thread through the
365
375
  // transcript without a special render path.
366
376
  changed = this._absorbStageNotices(stage) || changed;
367
- changed = this._syncPromptState(stage?.pendingPrompt) || changed;
377
+ const promptChanged = this._syncPromptState(stage?.pendingPrompt);
378
+ changed = promptChanged || changed;
379
+ if (promptChanged && this.promptState && this._canSubmitPrompt(this.promptState.prompt.id)) {
380
+ this.requestFocus?.();
381
+ }
368
382
  this.chatHost.syncAnimationTick();
369
383
  if (changed) this.requestRender?.();
370
384
  });
@@ -406,7 +420,7 @@ export class StageChatView implements Component, Focusable {
406
420
  this.mountingRequestId = null;
407
421
  this.stageUiBroker.reject(
408
422
  request,
409
- new Error("pi-workflows: stage custom UI cannot mount without attached TUI host"),
423
+ new Error("atomic-workflows: stage custom UI cannot mount without attached TUI host"),
410
424
  );
411
425
  return;
412
426
  }
@@ -425,6 +439,7 @@ export class StageChatView implements Component, Focusable {
425
439
  this.chatHost.scrollToBottom();
426
440
  this.requestRender?.();
427
441
  },
442
+ () => this._canSubmitPrompt(request.id),
428
443
  );
429
444
  // Settled or superseded while mounting: drop the freshly-built component
430
445
  // instead of showing a gate the broker has already torn down.
@@ -494,23 +509,30 @@ export class StageChatView implements Component, Focusable {
494
509
 
495
510
  private _syncPromptState(prompt: PendingPrompt | undefined): boolean {
496
511
  if (!prompt) {
497
- const changed = this.promptState !== null;
512
+ const hadLivePrompt =
513
+ this.promptState !== null ||
514
+ this.promptEditor !== null ||
515
+ this.promptEditorPromptId !== null;
516
+ if (!hadLivePrompt) return false;
498
517
  this.promptState = null;
499
518
  this._disposePromptEditor();
500
- this.promptScrollOffset = 0;
501
- this.promptMaxScroll = 0;
502
- return changed;
519
+ this._resetPromptScroll();
520
+ return true;
503
521
  }
504
522
  if (!this.promptState || this.promptState.prompt.id !== prompt.id) {
505
523
  this.promptState = createPromptCardState(prompt);
506
524
  this._resetPromptEditor(prompt);
507
- this.promptScrollOffset = 0;
508
- this.promptMaxScroll = 0;
525
+ this._resetPromptScroll();
509
526
  return true;
510
527
  }
511
528
  return false;
512
529
  }
513
530
 
531
+ private _resetPromptScroll(): void {
532
+ this.promptScrollOffset = 0;
533
+ this.promptMaxScroll = 0;
534
+ }
535
+
514
536
  private _resetPromptEditor(prompt: PendingPrompt): void {
515
537
  this._disposePromptEditor();
516
538
  if ((prompt.kind !== "input" && prompt.kind !== "editor") || !this.piTui) return;
@@ -527,7 +549,9 @@ export class StageChatView implements Component, Focusable {
527
549
  this.requestRender?.();
528
550
  };
529
551
  editor.onSubmit = (text: string) => {
530
- this._resolvePromptResponse(prompt.id, text);
552
+ this._resolvePromptResponse(prompt.id, text, {
553
+ suppressNextGraphSubmit: this.promptEditorSubmitFromEnter,
554
+ });
531
555
  };
532
556
  this.promptEditor = editor;
533
557
  this.promptEditorPromptId = prompt.id;
@@ -541,17 +565,27 @@ export class StageChatView implements Component, Focusable {
541
565
  disposable?.dispose?.();
542
566
  }
543
567
 
544
- private _resolvePromptResponse(promptId: string, response: unknown): void {
568
+ private _resolvePromptResponse(
569
+ promptId: string,
570
+ response: unknown,
571
+ metadata: StageChatDetachMetadata = {},
572
+ ): void {
545
573
  const prompt = this.promptState?.prompt;
546
574
  if (!prompt || prompt.id !== promptId) return;
575
+ if (!this._canSubmitPrompt(promptId)) {
576
+ this.requestRender?.();
577
+ return;
578
+ }
547
579
  this.promptState = null;
548
580
  this._disposePromptEditor();
581
+ this._resetPromptScroll();
549
582
  // A false return means the prompt was already resolved/removed (for
550
583
  // example by run abort). The local UI is already stale, so clearing it is
551
- // the least surprising recovery path.
552
- this.store.resolveStagePendingPrompt(this.runId, this.stageId, prompt.id, response);
584
+ // the least surprising recovery path, but it must not trigger graph-mode
585
+ // transition side effects for a prompt this view did not actually settle.
586
+ const resolved = this.store.resolveStagePendingPrompt(this.runId, this.stageId, prompt.id, response);
553
587
  this.requestRender?.();
554
- this.onDetach();
588
+ if (resolved) this.onDetach("prompt-resolved", metadata);
555
589
  }
556
590
 
557
591
  // -------------------------------------------------------------------------
@@ -660,13 +694,12 @@ export class StageChatView implements Component, Focusable {
660
694
  editorRows: customUiActive ? customUiLines.length : editorLines.length,
661
695
  footerRows: footerLines.length,
662
696
  });
663
- const visiblePendingLines = pendingLines.slice(0, plan.pendingRows);
664
- const visibleWorkingLines = workingLines.slice(0, plan.workingRows);
665
- const visibleUsageLines = usageLines.slice(0, plan.usageRows);
666
- const visibleEditorLines = customUiActive
667
- ? customUiLines.slice(0, plan.editorRows)
668
- : editorLines.slice(0, plan.editorRows);
669
- const visibleFooterLines = footerLines.slice(0, plan.footerRows);
697
+ const visiblePendingLines = takeRows(pendingLines, plan.pendingRows);
698
+ const visibleWorkingLines = takeRows(workingLines, plan.workingRows);
699
+ const visibleUsageLines = takeRows(usageLines, plan.usageRows);
700
+ const editorSlotLines = customUiActive ? customUiLines : editorLines;
701
+ const visibleEditorLines = takeRows(editorSlotLines, plan.editorRows);
702
+ const visibleFooterLines = takeRows(footerLines, plan.footerRows);
670
703
  const bodyBudget = plan.bodyRows;
671
704
  if (blocked) this.chatHost.scrollToBottom();
672
705
 
@@ -675,6 +708,8 @@ export class StageChatView implements Component, Focusable {
675
708
  bodyLines = this._renderPromptBody(w, bodyBudget);
676
709
  } else if (blocked) {
677
710
  bodyLines = this._renderBlockedBody(w, bodyBudget, stage);
711
+ } else if (!readOnlyArchive && this._isPaused(stage)) {
712
+ bodyLines = this._renderPausedBody(w, bodyBudget);
678
713
  } else if (readOnlyArchive) {
679
714
  bodyLines = this._renderReadOnlyArchiveBody(w, bodyBudget, stage);
680
715
  } else {
@@ -829,6 +864,7 @@ export class StageChatView implements Component, Focusable {
829
864
  bodyLines.push("");
830
865
  bodyLines.push(...new Text(paint("your response", t.textMuted, { bold: true }), 2, 0).render(innerWidth));
831
866
  bodyLines.push(...new Text(paint(answer, answer.startsWith("(") ? t.dim : t.text), 4, 0).render(innerWidth));
867
+ bodyLines.push("");
832
868
  bodyLines.push(...new Text(
833
869
  paint("esc", t.accent, { bold: true }) +
834
870
  paint(" close", t.textMuted) +
@@ -867,6 +903,36 @@ export class StageChatView implements Component, Focusable {
867
903
  }
868
904
  }
869
905
 
906
+ private _renderPausedBody(width: number, budget: number): string[] {
907
+ const t = this.theme;
908
+ const callout: string[] = [];
909
+ callout.push(this._blank(width));
910
+ callout.push(
911
+ ...this._bannerLines(
912
+ width,
913
+ "warning",
914
+ "❚❚",
915
+ "PAUSED",
916
+ "enter resumes · ctrl+d close",
917
+ ),
918
+ );
919
+ callout.push(
920
+ ...new Text(
921
+ paint("This workflow stage is paused. Type a message below and press Enter to resume.", t.textMuted),
922
+ 2,
923
+ 0,
924
+ ).render(width),
925
+ );
926
+
927
+ const calloutRows = Math.min(callout.length, Math.max(0, budget - 1));
928
+ const transcriptBudget = Math.max(1, budget - calloutRows);
929
+ const lines = this.chatHost.renderBody(width, transcriptBudget);
930
+ lines.push(...callout.slice(0, calloutRows));
931
+ while (lines.length < budget) lines.push(this._blank(width));
932
+ if (lines.length > budget) lines.length = budget;
933
+ return lines;
934
+ }
935
+
870
936
  private _renderBlockedBody(
871
937
  width: number,
872
938
  budget: number,
@@ -977,17 +1043,20 @@ export class StageChatView implements Component, Focusable {
977
1043
  }
978
1044
 
979
1045
  private _noticeRow(entry: NoticeEntry): Component {
980
- const t = this.theme;
981
- const fromPart = entry.from ? paint(` (was ${entry.from})`, t.dim) : "";
982
- const metaPart = entry.meta ? " " + paint(entry.meta, t.dim) : "";
983
- const line =
984
- paint("~ ", t.borderDim) +
985
- paint(entry.kind, t.mauve, { bold: true }) +
986
- paint(" → ", t.borderDim) +
987
- paint(entry.value, t.text) +
988
- fromPart +
989
- metaPart;
990
- return new Text(line, 2, 0);
1046
+ const theme = this.theme;
1047
+ return {
1048
+ render(width: number): string[] {
1049
+ return renderWorkflowNoticeCard({
1050
+ ...stageNoticeCard(entry),
1051
+ fallbackText: entry.text,
1052
+ width,
1053
+ theme,
1054
+ });
1055
+ },
1056
+ invalidate() {
1057
+ /* notice entries are immutable */
1058
+ },
1059
+ };
991
1060
  }
992
1061
 
993
1062
  // -------------------------------------------------------------------------
@@ -1072,24 +1141,39 @@ export class StageChatView implements Component, Focusable {
1072
1141
  return true;
1073
1142
  }
1074
1143
 
1144
+ private _promptKeybindings(): KeybindingsLike | undefined {
1145
+ return isKeybindingsLike(this.piKeybindings) ? this.piKeybindings : undefined;
1146
+ }
1147
+
1148
+ private _canSubmitPrompt(promptId: string): boolean {
1149
+ return this.canSubmitPrompt?.(this.runId, this.stageId, promptId) ?? true;
1150
+ }
1151
+
1075
1152
  private _handlePromptInput(data: string): void {
1076
1153
  const state = this.promptState;
1077
1154
  if (!state) return;
1078
1155
  if (this.promptEditor && this.promptEditorPromptId === state.prompt.id) {
1079
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
1080
- this._resolvePromptResponse(state.prompt.id, defaultResponseFor(state.prompt));
1156
+ if (matchesKey(data, Key.ctrl("c"))) {
1157
+ this._resolvePromptResponse(state.prompt.id, defaultResponseFor(state.prompt), {
1158
+ suppressNextGraphSubmit: false,
1159
+ });
1160
+ return;
1161
+ }
1162
+ if (isPromptEscapeInput(data)) {
1163
+ this.requestRender?.();
1081
1164
  return;
1082
1165
  }
1083
1166
  setEditorFocused(this.promptEditor, this.focused);
1084
- this.promptEditor.handleInput(data);
1167
+ this.promptEditorSubmitFromEnter = matchesKey(data, Key.enter);
1168
+ try {
1169
+ this.promptEditor.handleInput(data);
1170
+ } finally {
1171
+ this.promptEditorSubmitFromEnter = false;
1172
+ }
1085
1173
  this.requestRender?.();
1086
1174
  return;
1087
1175
  }
1088
- const action = handlePromptCardInput(
1089
- data,
1090
- state,
1091
- isKeybindingsLike(this.piKeybindings) ? this.piKeybindings : undefined,
1092
- );
1176
+ const action = handlePromptCardInput(data, state, this._promptKeybindings());
1093
1177
  if (action.kind === "noop") {
1094
1178
  this.requestRender?.();
1095
1179
  return;
@@ -1098,7 +1182,9 @@ export class StageChatView implements Component, Focusable {
1098
1182
  const response = action.kind === "submit"
1099
1183
  ? action.response
1100
1184
  : defaultResponseFor(prompt);
1101
- this._resolvePromptResponse(prompt.id, response);
1185
+ this._resolvePromptResponse(prompt.id, response, {
1186
+ suppressNextGraphSubmit: action.kind === "submit" && matchesKey(data, Key.enter),
1187
+ });
1102
1188
  }
1103
1189
 
1104
1190
  // -------------------------------------------------------------------------
@@ -1107,6 +1193,10 @@ export class StageChatView implements Component, Focusable {
1107
1193
 
1108
1194
  handleInput(data: string): boolean {
1109
1195
  if (this.mountedCustomUi) {
1196
+ if (!this._canSubmitPrompt(this.mountedCustomUi.request.id)) {
1197
+ this.requestRender?.();
1198
+ return true;
1199
+ }
1110
1200
  if (matchesKey(data, Key.ctrl("d"))) {
1111
1201
  // Detach stops *viewing* the stage; it does not cancel a pending
1112
1202
  // human-input request. Release the local display only — the request
@@ -1137,7 +1227,10 @@ export class StageChatView implements Component, Focusable {
1137
1227
  this.requestRender?.();
1138
1228
  return true;
1139
1229
  }
1140
- this._syncPromptState(this._currentStage()?.pendingPrompt);
1230
+ const stage = this._currentStage();
1231
+ this._syncPromptState(stage?.pendingPrompt);
1232
+ const readOnlyArchive = this._isReadOnlyArchive(stage);
1233
+ const readOnlyPromptArchive = readOnlyArchive && stage?.promptFootprint !== undefined;
1141
1234
  if (matchesKey(data, Key.ctrl("d"))) {
1142
1235
  if (!this.promptState && this.chatHost.hasInputText()) return this.chatHost.handleInput(data);
1143
1236
  if (this._isPaused()) this.onClose();
@@ -1149,6 +1242,9 @@ export class StageChatView implements Component, Focusable {
1149
1242
  this._handlePromptInput(data);
1150
1243
  return true;
1151
1244
  }
1245
+ if (readOnlyPromptArchive && this._handlePromptScrollInput(data, true)) {
1246
+ return true;
1247
+ }
1152
1248
  if (this.chatHost.handleScrollInput(data)) {
1153
1249
  return true;
1154
1250
  }
@@ -1167,7 +1263,6 @@ export class StageChatView implements Component, Focusable {
1167
1263
  this.onClose();
1168
1264
  return true;
1169
1265
  }
1170
- const readOnlyArchive = this._isReadOnlyArchive();
1171
1266
  if (readOnlyArchive) return true;
1172
1267
  const blocked = this._isBlocked();
1173
1268
  if (matchesKey(data, Key.ctrl("f"))) {
@@ -1502,6 +1597,78 @@ function noticeSummary(n: StageNotice): string {
1502
1597
  return n.from ? `${base} (was ${n.from})` : base;
1503
1598
  }
1504
1599
 
1600
+ function stageNoticeCard(entry: NoticeEntry): {
1601
+ title: string;
1602
+ glyph: string;
1603
+ headline: string;
1604
+ tone: WorkflowNoticeTone;
1605
+ fields: Array<{ label: string; value: string | undefined; tone?: WorkflowNoticeTone | "text" | "muted" }>;
1606
+ } {
1607
+ switch (entry.kind) {
1608
+ case "abort":
1609
+ return {
1610
+ title: "STAGE ABORTED",
1611
+ glyph: "✗",
1612
+ headline: "Stage received an abort notice",
1613
+ tone: "error",
1614
+ fields: stageNoticeFields(entry, "error"),
1615
+ };
1616
+ case "compaction":
1617
+ return {
1618
+ title: "STAGE COMPACTION",
1619
+ glyph: "✓",
1620
+ headline: "Stage context was compacted",
1621
+ tone: "success",
1622
+ fields: stageNoticeFields(entry, "muted"),
1623
+ };
1624
+ case "model":
1625
+ return {
1626
+ title: "STAGE MODEL",
1627
+ glyph: "→",
1628
+ headline: "Stage model changed",
1629
+ tone: "info",
1630
+ fields: stageNoticeFields(entry),
1631
+ };
1632
+ case "thinking":
1633
+ return {
1634
+ title: "STAGE THINKING",
1635
+ glyph: "→",
1636
+ headline: "Stage thinking level changed",
1637
+ tone: "mauve",
1638
+ fields: stageNoticeFields(entry),
1639
+ };
1640
+ case "tree":
1641
+ return {
1642
+ title: "STAGE TREE",
1643
+ glyph: "◆",
1644
+ headline: "Stage branch tree changed",
1645
+ tone: "info",
1646
+ fields: stageNoticeFields(entry, "muted"),
1647
+ };
1648
+ case "mcp":
1649
+ default:
1650
+ return {
1651
+ title: "STAGE NOTICE",
1652
+ glyph: "◆",
1653
+ headline: "Workflow stage notice",
1654
+ tone: "info",
1655
+ fields: stageNoticeFields(entry, "muted"),
1656
+ };
1657
+ }
1658
+ }
1659
+
1660
+ function stageNoticeFields(
1661
+ entry: NoticeEntry,
1662
+ valueTone: WorkflowNoticeTone | "text" | "muted" = "text",
1663
+ ): Array<{ label: string; value: string | undefined; tone?: WorkflowNoticeTone | "text" | "muted" }> {
1664
+ return [
1665
+ { label: "kind", value: entry.kind, tone: "muted" },
1666
+ { label: "value", value: entry.value, tone: valueTone },
1667
+ { label: "from", value: entry.from, tone: "muted" },
1668
+ { label: "meta", value: entry.meta, tone: "muted" },
1669
+ ];
1670
+ }
1671
+
1505
1672
  function shortenId(id: string): string {
1506
1673
  return id.length > 10 ? id.slice(0, 8) : id;
1507
1674
  }
@@ -1528,6 +1695,10 @@ function editorThemeFromGraphTheme(t: GraphTheme): EditorTheme {
1528
1695
  } as EditorTheme;
1529
1696
  }
1530
1697
 
1698
+ function takeRows(lines: readonly string[], rows: number): string[] {
1699
+ return lines.slice(0, rows);
1700
+ }
1701
+
1531
1702
  interface PaintOpts {
1532
1703
  bold?: boolean;
1533
1704
  italic?: boolean;
@@ -1545,9 +1716,9 @@ function paint(text: string, fg: string, opts: PaintOpts = {}): string {
1545
1716
 
1546
1717
  function renderHintsForPrompt(kind: PendingPrompt["kind"], theme: GraphTheme): string {
1547
1718
  if (kind === "input" || kind === "editor") {
1548
- return `${paint("enter", theme.textMuted, { bold: true })} Submit · ${paint("esc/ctrl+c", theme.textMuted, { bold: true })} Skip`;
1719
+ return `${paint("enter", theme.textMuted, { bold: true })} Submit · ${paint("ctrl+c", theme.textMuted, { bold: true })} Skip`;
1549
1720
  }
1550
- return `${paint("enter", theme.textMuted, { bold: true })} Select · ${paint("esc/ctrl+c", theme.textMuted, { bold: true })} Skip`;
1721
+ return `${paint("enter", theme.textMuted, { bold: true })} Select · ${paint("ctrl+c", theme.textMuted, { bold: true })} Skip`;
1551
1722
  }
1552
1723
 
1553
1724
  /**
@@ -41,6 +41,8 @@ export function statusIcon(status: StageStatus | RunStatus): string {
41
41
  switch (status) {
42
42
  case "pending":
43
43
  return "○";
44
+ case "awaiting_input":
45
+ return "?";
44
46
  case "blocked":
45
47
  return "↑";
46
48
  case "skipped":