@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
@@ -25,12 +25,19 @@ import type { ChatMessageRenderOptions, ReadonlyFooterDataProvider } from "@bast
25
25
  import type { Store } from "../shared/store.js";
26
26
  import type { GraphTheme } from "./graph-theme.js";
27
27
  import { GraphView } from "./graph-view.js";
28
- import { StageChatView } from "./stage-chat-view.js";
28
+ import {
29
+ StageChatView,
30
+ type StageChatDetachMetadata,
31
+ type StageChatDetachReason,
32
+ } from "./stage-chat-view.js";
33
+ import { Key, matchesKey } from "./text-helpers.js";
29
34
  import type {
30
35
  StageControlHandle,
31
36
  StageControlRegistry,
32
37
  } from "../runs/foreground/stage-control-registry.js";
33
38
  import type { StageUiBroker } from "../shared/stage-ui-broker.js";
39
+ import type { StageSnapshot, StoreSnapshot } from "../shared/store-types.js";
40
+ import { expandWorkflowGraph } from "../shared/expanded-workflow-graph.js";
34
41
 
35
42
  /**
36
43
  * Surface used to write Pi's footer/status tag while the attach pane is
@@ -112,11 +119,14 @@ export interface WorkflowAttachPaneOpts {
112
119
  * scrolling and drops non-wheel mouse bytes before they reach the editor.
113
120
  */
114
121
  setMouseScrollTracking?: (enabled: boolean) => void;
122
+ /** Optional clock injection for deterministic transition-quarantine tests. */
123
+ now?: () => number;
115
124
  }
116
125
 
117
126
  export type WorkflowAttachPaneMode = "graph" | "stage-chat";
118
127
 
119
128
  const STATUS_KEY = "pi-workflows";
129
+ const ENTER_TRANSITION_QUARANTINE_MS = 200;
120
130
 
121
131
  export class WorkflowAttachPane implements Component {
122
132
  private store: Store;
@@ -139,12 +149,25 @@ export class WorkflowAttachPane implements Component {
139
149
  private piEditorFactory?: (tui: TUI, theme: EditorTheme, keybindings: unknown) => EditorComponent;
140
150
  private getChatRenderSettings?: () => Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd">> | undefined;
141
151
  private footerData?: ReadonlyFooterDataProvider;
152
+ private now: () => number;
142
153
 
143
154
  private mode: WorkflowAttachPaneMode = "graph";
155
+ private visible = true;
144
156
  private graphView: GraphView;
145
157
  private chatView: StageChatView | null = null;
158
+ private unsubscribeStore: (() => void) | null = null;
159
+ /** Run id for the currently attached stage chat; graph mode keeps `runId` as the root graph run. */
160
+ private attachedRunId: string | null = null;
146
161
  /** Stage id the user most recently attached to (used to seed focus). */
147
162
  private lastAttachedStageId: string | null = null;
163
+ /** Time-boxed guard for Enter leaking into graph mode during connect/detach transitions. */
164
+ private graphEnterQuarantineUntil = 0;
165
+ /** Time-boxed guard for Enter leaking from graph attach into an attached prompt. */
166
+ private stagePromptEnterQuarantineUntil = 0;
167
+ /** Last awaiting-input identity observed for graph-mode transition quarantine. */
168
+ private lastGraphAwaitingInputKey: string | null = null;
169
+ /** Last awaiting-input identity observed for attached-stage transition quarantine. */
170
+ private lastStageAwaitingInputKey: string | null = null;
148
171
 
149
172
  constructor(opts: WorkflowAttachPaneOpts) {
150
173
  this.store = opts.store;
@@ -167,12 +190,17 @@ export class WorkflowAttachPane implements Component {
167
190
  this.piEditorFactory = opts.piEditorFactory;
168
191
  this.getChatRenderSettings = opts.getChatRenderSettings;
169
192
  this.footerData = opts.footerData;
193
+ this.now = opts.now ?? Date.now;
170
194
 
195
+ this.unsubscribeStore = this.store.subscribe((snapshot) => this._handleStoreUpdate(snapshot));
171
196
  this.graphView = this._buildGraphView();
172
197
 
173
198
  if (opts.initialAttachStageId !== undefined && this.runId) {
174
- this._attachToStage(this.runId, opts.initialAttachStageId);
199
+ const target = this._resolveGraphStageTarget(this.runId, opts.initialAttachStageId);
200
+ this._attachToStage(target.runId, target.stageId);
175
201
  } else {
202
+ this._syncAwaitingInputKeys(this.store.snapshot());
203
+ this._armGraphEnterQuarantineIfRunNeedsInput();
176
204
  this._setBaseStatus();
177
205
  this._syncMouseScrollTracking();
178
206
  }
@@ -188,12 +216,15 @@ export class WorkflowAttachPane implements Component {
188
216
  onHide: this.onHide,
189
217
  onKill: this.onKill,
190
218
  onPromptResolve: this.onPromptResolve,
191
- onStageAttach: (runId, stageId) => this._attachToStage(runId, stageId),
219
+ onStageAttach: (runId, stageId) => this._attachToStage(runId, stageId, {
220
+ suppressInitialPromptSubmit: true,
221
+ }),
192
222
  onDetach: () => {
193
223
  if (this.onHide) this.onHide();
194
224
  },
195
225
  initialFocusedStageId,
196
226
  getViewportRows: this.getViewportRows,
227
+ piKeybindings: this.piKeybindings,
197
228
  // Gate the host render tick on `graph` mode. While the chat view
198
229
  // is attached, the GraphView is hidden behind the chat — firing
199
230
  // pi-tui renders for a frame the user can't see is wasted work.
@@ -226,19 +257,33 @@ export class WorkflowAttachPane implements Component {
226
257
  return run?.stages.find((s) => s.id === stageId)?.name ?? "stage";
227
258
  }
228
259
 
229
- private _attachToStage(runId: string, stageId: string): void {
230
- this.runId = runId;
260
+ private _attachToStage(
261
+ runId: string,
262
+ stageId: string,
263
+ options: { suppressInitialPromptSubmit?: boolean } = { suppressInitialPromptSubmit: true },
264
+ ): void {
265
+ this.graphEnterQuarantineUntil = 0;
266
+ const snapshot = this.store.snapshot();
267
+ const graphRunId = this._resolveRunId();
268
+ this.lastGraphAwaitingInputKey = graphRunId ? this._runAwaitingInputKey(snapshot, graphRunId) : null;
269
+ this.lastStageAwaitingInputKey = this._stageAwaitingInputKey(snapshot, runId, stageId);
270
+ this.stagePromptEnterQuarantineUntil =
271
+ options.suppressInitialPromptSubmit === true && this.lastStageAwaitingInputKey !== null
272
+ ? this.now() + ENTER_TRANSITION_QUARANTINE_MS
273
+ : 0;
274
+ this.attachedRunId = runId;
231
275
  this.lastAttachedStageId = stageId;
232
276
  const handle: StageControlHandle | undefined = this.registry?.get(runId, stageId);
233
277
  this.chatView?.dispose();
234
- this.chatView = new StageChatView({
278
+ let chatView!: StageChatView;
279
+ chatView = new StageChatView({
235
280
  store: this.store,
236
281
  graphTheme: this.theme,
237
282
  runId,
238
283
  stageId,
239
284
  workflowName: this._workflowName(runId),
240
285
  handle,
241
- onDetach: () => this._detachFromStage(),
286
+ onDetach: (reason, metadata) => this._detachFromStage(reason, metadata),
242
287
  onClose: this.onClose,
243
288
  requestRender: this.hostRequestRender,
244
289
  requestFocus: this.hostRequestFocus,
@@ -250,50 +295,126 @@ export class WorkflowAttachPane implements Component {
250
295
  footerData: this.footerData,
251
296
  getViewportRows: this.getViewportRows,
252
297
  stageUiBroker: this.stageUiBroker,
298
+ canSubmitPrompt: (candidateRunId, candidateStageId) => (
299
+ this.visible &&
300
+ this.mode === "stage-chat" &&
301
+ this.chatView === chatView &&
302
+ this.attachedRunId === candidateRunId &&
303
+ this.lastAttachedStageId === candidateStageId &&
304
+ this._isStageMarkedAttached(candidateRunId, candidateStageId)
305
+ ),
253
306
  });
254
- this.store.recordStageAttached(runId, stageId, true);
307
+ this.chatView = chatView;
255
308
  this.mode = "stage-chat";
309
+ this.store.recordStageAttached(runId, stageId, this.visible);
256
310
  this._setAttachedStatus(runId, stageId);
257
311
  this._syncMouseScrollTracking();
258
312
  }
259
313
 
260
- private _detachFromStage(): void {
261
- if (this.chatView && this.runId && this.lastAttachedStageId) {
262
- this.store.recordStageAttached(this.runId, this.lastAttachedStageId, false);
314
+ private _detachFromStage(
315
+ reason: StageChatDetachReason = "user",
316
+ metadata: StageChatDetachMetadata = {},
317
+ ): void {
318
+ if (this.chatView && this.attachedRunId && this.lastAttachedStageId) {
319
+ this.store.recordStageAttached(this.attachedRunId, this.lastAttachedStageId, false);
263
320
  }
264
321
  this.chatView?.dispose();
265
322
  this.chatView = null;
323
+ this.attachedRunId = null;
266
324
  // Rebuild graph view so the focused stage matches the node we
267
325
  // were just attached to (mockup contract: cursor still on
268
326
  // `review-a` after Ctrl+D detach).
269
327
  this.graphView.dispose();
270
328
  this.graphView = this._buildGraphView(this.lastAttachedStageId ?? undefined);
271
329
  this.mode = "graph";
330
+ this.stagePromptEnterQuarantineUntil = 0;
331
+ this.lastStageAwaitingInputKey = null;
332
+ this.lastGraphAwaitingInputKey = this.runId ? this._runAwaitingInputKey(this.store.snapshot(), this.runId) : null;
333
+ this.graphEnterQuarantineUntil =
334
+ reason === "prompt-resolved" && metadata.suppressNextGraphSubmit === true
335
+ ? this.now() + ENTER_TRANSITION_QUARANTINE_MS
336
+ : 0;
272
337
  this._setBaseStatus();
273
338
  this._syncMouseScrollTracking();
274
339
  }
275
340
 
276
341
  retarget(runId: string | null, stageId?: string): void {
277
- if (this.chatView && this.runId && this.lastAttachedStageId) {
278
- this.store.recordStageAttached(this.runId, this.lastAttachedStageId, false);
342
+ if (this.chatView && this.attachedRunId && this.lastAttachedStageId) {
343
+ this.store.recordStageAttached(this.attachedRunId, this.lastAttachedStageId, false);
279
344
  }
280
345
  this.chatView?.dispose();
281
346
  this.chatView = null;
282
347
  this.graphView.dispose();
283
348
  this.runId = runId;
349
+ this.attachedRunId = null;
284
350
  this.lastAttachedStageId = null;
285
351
  this.mode = "graph";
352
+ this.graphEnterQuarantineUntil = 0;
353
+ this.stagePromptEnterQuarantineUntil = 0;
286
354
  this.graphView = this._buildGraphView();
355
+ this._syncAwaitingInputKeys(this.store.snapshot());
287
356
 
288
357
  if (stageId !== undefined && runId) {
289
- this._attachToStage(runId, stageId);
358
+ const target = this._resolveGraphStageTarget(runId, stageId);
359
+ this._attachToStage(target.runId, target.stageId);
290
360
  return;
291
361
  }
292
362
 
363
+ this._armGraphEnterQuarantineIfRunNeedsInput();
293
364
  this._setBaseStatus();
294
365
  this._syncMouseScrollTracking();
295
366
  }
296
367
 
368
+ private _resolveGraphStageTarget(rootRunId: string, stageId: string): { runId: string; stageId: string } {
369
+ const graph = expandWorkflowGraph(this.store.snapshot(), rootRunId);
370
+ const match = graph.stages.find((stage) =>
371
+ stage.id === stageId || stage.workflowGraphTarget.stageId === stageId,
372
+ );
373
+ if (match === undefined) return { runId: rootRunId, stageId };
374
+ return {
375
+ runId: match.workflowGraphTarget.runId,
376
+ stageId: match.workflowGraphTarget.stageId,
377
+ };
378
+ }
379
+
380
+ private _handleStoreUpdate(snapshot: StoreSnapshot): void {
381
+ if (!this.visible) {
382
+ this._syncAwaitingInputKeys(snapshot);
383
+ return;
384
+ }
385
+ const runId = this._resolveRunId();
386
+ if (!runId) {
387
+ this.lastGraphAwaitingInputKey = null;
388
+ this.lastStageAwaitingInputKey = null;
389
+ return;
390
+ }
391
+ if (this.mode === "graph") {
392
+ const key = this._runAwaitingInputKey(snapshot, runId);
393
+ if (key !== null && key !== this.lastGraphAwaitingInputKey) {
394
+ this.graphEnterQuarantineUntil = this.now() + ENTER_TRANSITION_QUARANTINE_MS;
395
+ }
396
+ this.lastGraphAwaitingInputKey = key;
397
+ this.lastStageAwaitingInputKey = null;
398
+ return;
399
+ }
400
+ if (this.mode === "stage-chat" && this.attachedRunId && this.lastAttachedStageId) {
401
+ const key = this._stageAwaitingInputKey(snapshot, this.attachedRunId, this.lastAttachedStageId);
402
+ if (key !== null && key !== this.lastStageAwaitingInputKey) {
403
+ this.stagePromptEnterQuarantineUntil = this.now() + ENTER_TRANSITION_QUARANTINE_MS;
404
+ }
405
+ this.lastStageAwaitingInputKey = key;
406
+ this.lastGraphAwaitingInputKey = this._runAwaitingInputKey(snapshot, runId);
407
+ }
408
+ }
409
+
410
+ private _syncAwaitingInputKeys(snapshot: StoreSnapshot): void {
411
+ const runId = this._resolveRunId();
412
+ this.lastGraphAwaitingInputKey = runId ? this._runAwaitingInputKey(snapshot, runId) : null;
413
+ this.lastStageAwaitingInputKey = this.attachedRunId && this.lastAttachedStageId
414
+ ? this._stageAwaitingInputKey(snapshot, this.attachedRunId, this.lastAttachedStageId)
415
+ : null;
416
+ }
417
+
297
418
  private _setBaseStatus(): void {
298
419
  const runId = this._resolveRunId();
299
420
  const name = runId ? `pi-workflows/${this._workflowName(runId)}` : "pi-workflows";
@@ -306,9 +427,10 @@ export class WorkflowAttachPane implements Component {
306
427
  }
307
428
 
308
429
  setVisible(visible: boolean): void {
309
- if (this.mode === "stage-chat" && this.runId && this.lastAttachedStageId) {
310
- this.store.recordStageAttached(this.runId, this.lastAttachedStageId, visible);
311
- if (visible) this._setAttachedStatus(this.runId, this.lastAttachedStageId);
430
+ this.visible = visible;
431
+ if (this.mode === "stage-chat" && this.attachedRunId && this.lastAttachedStageId) {
432
+ this.store.recordStageAttached(this.attachedRunId, this.lastAttachedStageId, visible);
433
+ if (visible) this._setAttachedStatus(this.attachedRunId, this.lastAttachedStageId);
312
434
  else this.uiStatus?.setStatus?.(STATUS_KEY, undefined);
313
435
  return;
314
436
  }
@@ -327,6 +449,21 @@ export class WorkflowAttachPane implements Component {
327
449
  return this.mode === "graph";
328
450
  }
329
451
 
452
+ wantsFocusForAwaitingInput(snapshot: StoreSnapshot): boolean {
453
+ if (!this.visible) return false;
454
+ const runId = this._resolveRunId();
455
+ if (!runId) return false;
456
+ const run = snapshot.runs.find((candidate) => candidate.id === runId);
457
+ if (!run) return false;
458
+ if (this.mode === "graph") {
459
+ return this._runAwaitingInputKey(snapshot, runId) !== null;
460
+ }
461
+ if (this.mode !== "stage-chat" || !this.attachedRunId || !this.lastAttachedStageId) return false;
462
+ const attachedRun = snapshot.runs.find((candidate) => candidate.id === this.attachedRunId);
463
+ const stage = attachedRun?.stages.find((candidate) => candidate.id === this.lastAttachedStageId);
464
+ return stage?.attached === true && this._stageSnapshotNeedsInput(stage);
465
+ }
466
+
330
467
  render(width: number): string[] {
331
468
  if (this.mode === "stage-chat" && this.chatView) {
332
469
  return this.chatView.render(width);
@@ -335,20 +472,134 @@ export class WorkflowAttachPane implements Component {
335
472
  }
336
473
 
337
474
  handleInput(data: string): boolean | void {
475
+ if (!this.visible) return false;
338
476
  if (this.mode === "stage-chat" && this.chatView) {
477
+ if (this._shouldQuarantineStagePromptEnter(data)) return true;
339
478
  return this.chatView.handleInput(data);
340
479
  }
480
+ if (this._shouldQuarantineGraphEnter(data)) return true;
341
481
  return this.graphView.handleInput(data);
342
482
  }
343
483
 
484
+ private _shouldQuarantineStagePromptEnter(data: string): boolean {
485
+ if (this.stagePromptEnterQuarantineUntil <= 0) return false;
486
+ if (!matchesKey(data, Key.enter)) {
487
+ this.stagePromptEnterQuarantineUntil = 0;
488
+ return false;
489
+ }
490
+ if (!this.attachedRunId || !this.lastAttachedStageId) {
491
+ this.stagePromptEnterQuarantineUntil = 0;
492
+ return false;
493
+ }
494
+ if (!this._stageNeedsInput(this.attachedRunId, this.lastAttachedStageId)) {
495
+ this.stagePromptEnterQuarantineUntil = 0;
496
+ return false;
497
+ }
498
+ const now = this.now();
499
+ if (now <= this.stagePromptEnterQuarantineUntil) {
500
+ this.stagePromptEnterQuarantineUntil = now + ENTER_TRANSITION_QUARANTINE_MS;
501
+ return true;
502
+ }
503
+ this.stagePromptEnterQuarantineUntil = 0;
504
+ return false;
505
+ }
506
+
507
+ private _shouldQuarantineGraphEnter(data: string): boolean {
508
+ if (this.graphEnterQuarantineUntil <= 0) return false;
509
+ if (!matchesKey(data, Key.enter)) {
510
+ this.graphEnterQuarantineUntil = 0;
511
+ return false;
512
+ }
513
+ const now = this.now();
514
+ if (now <= this.graphEnterQuarantineUntil) {
515
+ this.graphEnterQuarantineUntil = now + ENTER_TRANSITION_QUARANTINE_MS;
516
+ return true;
517
+ }
518
+ this.graphEnterQuarantineUntil = 0;
519
+ return false;
520
+ }
521
+
522
+ private _armGraphEnterQuarantineIfRunNeedsInput(): void {
523
+ const runId = this._resolveRunId();
524
+ this.graphEnterQuarantineUntil = runId && this._runNeedsInput(runId)
525
+ ? this.now() + ENTER_TRANSITION_QUARANTINE_MS
526
+ : 0;
527
+ }
528
+
529
+ private _runNeedsInput(runId: string): boolean {
530
+ return this._runAwaitingInputKey(this.store.snapshot(), runId) !== null;
531
+ }
532
+
533
+ private _runAwaitingInputKey(snapshot: StoreSnapshot, runId: string): string | null {
534
+ const run = snapshot.runs.find((candidate) => candidate.id === runId);
535
+ if (!run) return null;
536
+ const keys: Array<{ key: string; createdAt: number }> = [];
537
+ if (run.pendingPrompt) {
538
+ keys.push({ key: `run-prompt:${run.pendingPrompt.id}`, createdAt: run.pendingPrompt.createdAt });
539
+ }
540
+ const graph = expandWorkflowGraph(snapshot, runId);
541
+ for (const stage of graph.stages) {
542
+ const key = this._stageAwaitingInputKeyFromSnapshot(stage);
543
+ if (key) keys.push(key);
544
+ }
545
+ if (keys.length === 0) return null;
546
+ keys.sort((a, b) => b.createdAt - a.createdAt);
547
+ return keys[0]!.key;
548
+ }
549
+
550
+ private _stageNeedsInput(runId: string, stageId: string): boolean {
551
+ return this._stageAwaitingInputKey(this.store.snapshot(), runId, stageId) !== null;
552
+ }
553
+
554
+ private _stageAwaitingInputKey(snapshot: StoreSnapshot, runId: string, stageId: string): string | null {
555
+ const run = snapshot.runs.find((candidate) => candidate.id === runId);
556
+ const stage = run?.stages.find((candidate) => candidate.id === stageId);
557
+ return stage ? this._stageAwaitingInputKeyFromSnapshot(stage)?.key ?? null : null;
558
+ }
559
+
560
+ private _stageAwaitingInputKeyFromSnapshot(stage: StageSnapshot): { key: string; createdAt: number } | null {
561
+ if (stage.pendingPrompt) {
562
+ return { key: `stage-prompt:${stage.id}:${stage.pendingPrompt.id}`, createdAt: stage.pendingPrompt.createdAt };
563
+ }
564
+ if (stage.inputRequest) {
565
+ return { key: `stage-input:${stage.id}:${stage.inputRequest.id}`, createdAt: stage.inputRequest.createdAt };
566
+ }
567
+ if (stage.status === "awaiting_input") {
568
+ return {
569
+ key: `stage-awaiting:${stage.id}:${stage.awaitingInputSince ?? "active"}`,
570
+ createdAt: stage.awaitingInputSince ?? stage.startedAt ?? 0,
571
+ };
572
+ }
573
+ return null;
574
+ }
575
+
576
+ private _isStageMarkedAttached(runId: string, stageId: string): boolean {
577
+ return this._stageSnapshot(runId, stageId)?.attached === true;
578
+ }
579
+
580
+ private _stageSnapshot(runId: string, stageId: string): StageSnapshot | undefined {
581
+ const run = this.store.snapshot().runs.find((candidate) => candidate.id === runId);
582
+ return run?.stages.find((candidate) => candidate.id === stageId);
583
+ }
584
+
585
+ private _stageSnapshotNeedsInput(stage: Pick<StageSnapshot, "pendingPrompt" | "inputRequest" | "status">): boolean {
586
+ return (
587
+ stage.pendingPrompt !== undefined ||
588
+ stage.inputRequest !== undefined ||
589
+ stage.status === "awaiting_input"
590
+ );
591
+ }
592
+
344
593
  invalidate(): void {
345
594
  if (this.mode === "stage-chat" && this.chatView) this.chatView.invalidate();
346
595
  else this.graphView.invalidate();
347
596
  }
348
597
 
349
598
  dispose(): void {
350
- if (this.chatView && this.runId && this.lastAttachedStageId) {
351
- this.store.recordStageAttached(this.runId, this.lastAttachedStageId, false);
599
+ this.unsubscribeStore?.();
600
+ this.unsubscribeStore = null;
601
+ if (this.chatView && this.attachedRunId && this.lastAttachedStageId) {
602
+ this.store.recordStageAttached(this.attachedRunId, this.lastAttachedStageId, false);
352
603
  }
353
604
  this.chatView?.dispose();
354
605
  this.chatView = null;
@@ -0,0 +1,184 @@
1
+ import { renderRoundedBoxLines } from "./chat-surface.js";
2
+ import { BOLD, RESET, hexToAnsi } from "./color-utils.js";
3
+ import type { GraphTheme } from "./graph-theme.js";
4
+ import { truncateToWidth, visibleWidth, wrapPlainText } from "./text-helpers.js";
5
+
6
+ export type WorkflowNoticeTone = "info" | "success" | "warning" | "error" | "mauve";
7
+
8
+ export interface WorkflowNoticeCardField {
9
+ readonly label: string;
10
+ readonly value: string | undefined;
11
+ readonly tone?: WorkflowNoticeTone | "text" | "muted";
12
+ }
13
+
14
+ export interface WorkflowNoticeCardOpts {
15
+ readonly title: string;
16
+ readonly glyph: string;
17
+ readonly headline: string;
18
+ readonly tone: WorkflowNoticeTone;
19
+ readonly fields?: readonly WorkflowNoticeCardField[];
20
+ readonly hints?: readonly string[];
21
+ readonly footer?: string;
22
+ readonly fallbackText: string;
23
+ readonly width: number;
24
+ readonly theme?: GraphTheme;
25
+ }
26
+
27
+ const MIN_CARD_WIDTH = 32;
28
+ const FIELD_LABEL_WIDTH = 9;
29
+
30
+ export function renderWorkflowNoticeCard(opts: WorkflowNoticeCardOpts): string[] {
31
+ const width = Math.max(1, Math.floor(opts.width));
32
+ if (width < MIN_CARD_WIDTH) return wrapPlainText(opts.fallbackText, width);
33
+
34
+ const theme = opts.theme;
35
+ const accent = theme ? toneColor(theme, opts.tone) : undefined;
36
+ const innerWidth = Math.max(2, width - 2);
37
+ const bodyLines: string[] = [];
38
+
39
+ appendHeadline(bodyLines, opts, innerWidth);
40
+
41
+ for (const field of opts.fields ?? []) {
42
+ if (field.value === undefined || field.value.length === 0) continue;
43
+ appendField(bodyLines, field, innerWidth, theme);
44
+ }
45
+
46
+ for (const hint of opts.hints ?? []) {
47
+ if (hint.length === 0) continue;
48
+ appendHint(bodyLines, hint, innerWidth, theme);
49
+ }
50
+
51
+ if (opts.footer && opts.footer.length > 0) {
52
+ appendFreeText(bodyLines, opts.footer, innerWidth, theme, "muted");
53
+ }
54
+
55
+ return renderRoundedBoxLines({
56
+ title: opts.title,
57
+ bodyLines,
58
+ width,
59
+ ...(theme ? { theme, accent } : {}),
60
+ });
61
+ }
62
+
63
+ function appendHeadline(
64
+ rows: string[],
65
+ opts: WorkflowNoticeCardOpts,
66
+ innerWidth: number,
67
+ ): void {
68
+ const prefix = `${opts.glyph} `;
69
+ const continuationPrefix = `${" ".repeat(visibleWidth(opts.glyph))} `;
70
+ const budget = Math.max(1, innerWidth - 1 - visibleWidth(prefix));
71
+ const lines = wrapPlainText(opts.headline, budget);
72
+ const theme = opts.theme;
73
+ const tone = theme ? toneColor(theme, opts.tone) : undefined;
74
+
75
+ for (let i = 0; i < lines.length; i++) {
76
+ const rawPrefix = i === 0 ? prefix : continuationPrefix;
77
+ const styledPrefix = i === 0
78
+ ? style(rawPrefix, theme, tone, true)
79
+ : " ".repeat(visibleWidth(rawPrefix));
80
+ const line = ` ${styledPrefix}${style(lines[i] ?? "", theme, theme?.text, true)}`;
81
+ rows.push(fit(line, innerWidth));
82
+ }
83
+ }
84
+
85
+ function appendField(
86
+ rows: string[],
87
+ field: WorkflowNoticeCardField,
88
+ innerWidth: number,
89
+ theme: GraphTheme | undefined,
90
+ ): void {
91
+ const label = truncateToWidth(field.label, FIELD_LABEL_WIDTH, "…").padEnd(FIELD_LABEL_WIDTH, " ");
92
+ const firstPrefixWidth = 1 + FIELD_LABEL_WIDTH + 1;
93
+ const firstBudget = Math.max(1, innerWidth - firstPrefixWidth);
94
+ const value = field.value ?? "";
95
+ const valueTone = field.tone ?? "text";
96
+
97
+ if (visibleWidth(value) <= firstBudget && !value.includes("\n")) {
98
+ rows.push(
99
+ fit(
100
+ ` ${style(label, theme, theme?.dim, true)} ${style(value, theme, colorForField(theme, valueTone))}`,
101
+ innerWidth,
102
+ ),
103
+ );
104
+ return;
105
+ }
106
+
107
+ rows.push(fit(` ${style(field.label, theme, theme?.dim, true)}`, innerWidth));
108
+ if (visibleWidth(value) <= innerWidth - 1 && !value.includes("\n")) {
109
+ rows.push(fit(` ${style(value, theme, colorForField(theme, valueTone))}`, innerWidth));
110
+ return;
111
+ }
112
+ const continuationPrefix = " ";
113
+ const continuationBudget = Math.max(1, innerWidth - visibleWidth(continuationPrefix));
114
+ for (const line of wrapPlainText(value, continuationBudget)) {
115
+ rows.push(fit(`${continuationPrefix}${style(line, theme, colorForField(theme, valueTone))}`, innerWidth));
116
+ }
117
+ }
118
+
119
+ function appendHint(
120
+ rows: string[],
121
+ hint: string,
122
+ innerWidth: number,
123
+ theme: GraphTheme | undefined,
124
+ ): void {
125
+ const prefix = "▸ ";
126
+ const continuationPrefix = " ";
127
+ const budget = Math.max(1, innerWidth - 1 - visibleWidth(prefix));
128
+ const lines = wrapPlainText(hint, budget);
129
+ for (let i = 0; i < lines.length; i++) {
130
+ const rawPrefix = i === 0 ? prefix : continuationPrefix;
131
+ const styledPrefix = i === 0
132
+ ? style(rawPrefix, theme, theme?.accent, true)
133
+ : " ".repeat(visibleWidth(rawPrefix));
134
+ rows.push(fit(` ${styledPrefix}${style(lines[i] ?? "", theme, theme?.textMuted)}`, innerWidth));
135
+ }
136
+ }
137
+
138
+ function appendFreeText(
139
+ rows: string[],
140
+ text: string,
141
+ innerWidth: number,
142
+ theme: GraphTheme | undefined,
143
+ tone: WorkflowNoticeCardField["tone"],
144
+ ): void {
145
+ const budget = Math.max(1, innerWidth - 1);
146
+ for (const line of wrapPlainText(text, budget)) {
147
+ rows.push(fit(` ${style(line, theme, colorForField(theme, tone ?? "text"))}`, innerWidth));
148
+ }
149
+ }
150
+
151
+ function colorForField(
152
+ theme: GraphTheme | undefined,
153
+ tone: WorkflowNoticeCardField["tone"],
154
+ ): string | undefined {
155
+ if (!theme) return undefined;
156
+ if (tone === "text") return theme.text;
157
+ if (tone === "muted") return theme.textMuted;
158
+ return toneColor(theme, tone ?? "info");
159
+ }
160
+
161
+ function toneColor(theme: GraphTheme, tone: WorkflowNoticeTone): string {
162
+ switch (tone) {
163
+ case "success":
164
+ return theme.success;
165
+ case "warning":
166
+ return theme.warning;
167
+ case "error":
168
+ return theme.error;
169
+ case "mauve":
170
+ return theme.mauve;
171
+ case "info":
172
+ default:
173
+ return theme.info;
174
+ }
175
+ }
176
+
177
+ function style(text: string, theme: GraphTheme | undefined, color: string | undefined, bold = false): string {
178
+ if (!theme || !color || text.length === 0) return text;
179
+ return `${hexToAnsi(color)}${bold ? BOLD : ""}${text}${RESET}`;
180
+ }
181
+
182
+ function fit(line: string, width: number): string {
183
+ return truncateToWidth(line, width, "…", true);
184
+ }