@bastani/atomic 0.8.13 → 0.8.14

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 (355) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/host-html-template.ts +1 -1
  4. package/dist/builtin/mcp/init.ts +15 -2
  5. package/dist/builtin/mcp/mcp-callback-server.ts +10 -9
  6. package/dist/builtin/mcp/package.json +1 -1
  7. package/dist/builtin/mcp/ui-session.ts +9 -6
  8. package/dist/builtin/subagents/CHANGELOG.md +8 -1
  9. package/dist/builtin/subagents/README.md +39 -32
  10. package/dist/builtin/subagents/package.json +1 -1
  11. package/dist/builtin/subagents/skills/subagent/SKILL.md +11 -11
  12. package/dist/builtin/subagents/src/agents/agent-management.ts +6 -1
  13. package/dist/builtin/subagents/src/agents/agent-serializer.ts +2 -0
  14. package/dist/builtin/subagents/src/agents/agents.ts +44 -19
  15. package/dist/builtin/subagents/src/extension/config.ts +16 -0
  16. package/dist/builtin/subagents/src/extension/fanout-child.ts +246 -0
  17. package/dist/builtin/subagents/src/extension/index.ts +466 -603
  18. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +6 -4
  19. package/dist/builtin/subagents/src/intercom/result-intercom.ts +109 -1
  20. package/dist/builtin/subagents/src/runs/background/async-execution.ts +124 -19
  21. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +41 -6
  22. package/dist/builtin/subagents/src/runs/background/async-resume.ts +28 -15
  23. package/dist/builtin/subagents/src/runs/background/async-status.ts +60 -30
  24. package/dist/builtin/subagents/src/runs/background/result-watcher.ts +111 -54
  25. package/dist/builtin/subagents/src/runs/background/run-id-resolver.ts +83 -0
  26. package/dist/builtin/subagents/src/runs/background/run-status.ts +79 -3
  27. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +46 -1
  28. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +66 -14
  29. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +10 -3
  30. package/dist/builtin/subagents/src/runs/foreground/execution.ts +14 -2
  31. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +320 -23
  32. package/dist/builtin/subagents/src/runs/shared/completion-guard.ts +23 -1
  33. package/dist/builtin/subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +369 -0
  34. package/dist/builtin/subagents/src/runs/shared/nested-events.ts +935 -0
  35. package/dist/builtin/subagents/src/runs/shared/nested-path.ts +52 -0
  36. package/dist/builtin/subagents/src/runs/shared/nested-render.ts +115 -0
  37. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
  38. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +82 -9
  39. package/dist/builtin/subagents/src/runs/shared/pi-spawn.ts +1 -1
  40. package/dist/builtin/subagents/src/runs/shared/single-output.ts +12 -2
  41. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +32 -10
  42. package/dist/builtin/subagents/src/runs/shared/worktree.ts +3 -2
  43. package/dist/builtin/subagents/src/shared/artifacts.ts +0 -1
  44. package/dist/builtin/subagents/src/shared/types.ts +96 -1
  45. package/dist/builtin/subagents/src/shared/utils.ts +10 -2
  46. package/dist/builtin/subagents/src/slash/slash-commands.ts +468 -625
  47. package/dist/builtin/subagents/src/tui/render.ts +1227 -2093
  48. package/dist/builtin/web-access/package.json +1 -1
  49. package/dist/builtin/workflows/CHANGELOG.md +24 -0
  50. package/dist/builtin/workflows/README.md +28 -11
  51. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +323 -40
  52. package/dist/builtin/workflows/builtin/ralph.ts +362 -176
  53. package/dist/builtin/workflows/package.json +2 -5
  54. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
  55. package/dist/builtin/workflows/skills/skill-creator/LICENSE.txt +202 -0
  56. package/dist/builtin/workflows/skills/skill-creator/SKILL.md +489 -0
  57. package/dist/builtin/workflows/skills/skill-creator/agents/analyzer.md +274 -0
  58. package/dist/builtin/workflows/skills/skill-creator/agents/comparator.md +202 -0
  59. package/dist/builtin/workflows/skills/skill-creator/agents/grader.md +223 -0
  60. package/dist/builtin/workflows/skills/skill-creator/assets/eval_review.html +146 -0
  61. package/dist/builtin/workflows/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  62. package/dist/builtin/workflows/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  63. package/dist/builtin/workflows/skills/skill-creator/references/schemas.md +430 -0
  64. package/dist/builtin/workflows/skills/skill-creator/scripts/__init__.py +0 -0
  65. package/dist/builtin/workflows/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  66. package/dist/builtin/workflows/skills/skill-creator/scripts/generate_report.py +326 -0
  67. package/dist/builtin/workflows/skills/skill-creator/scripts/improve_description.py +247 -0
  68. package/dist/builtin/workflows/skills/skill-creator/scripts/package_skill.py +136 -0
  69. package/dist/builtin/workflows/skills/skill-creator/scripts/quick_validate.py +103 -0
  70. package/dist/builtin/workflows/skills/skill-creator/scripts/run_eval.py +310 -0
  71. package/dist/builtin/workflows/skills/skill-creator/scripts/run_loop.py +328 -0
  72. package/dist/builtin/workflows/skills/skill-creator/scripts/utils.py +47 -0
  73. package/dist/builtin/workflows/src/extension/index.ts +869 -93
  74. package/dist/builtin/workflows/src/extension/render-call.ts +34 -1
  75. package/dist/builtin/workflows/src/extension/render-result.ts +126 -21
  76. package/dist/builtin/workflows/src/extension/runtime.ts +91 -3
  77. package/dist/builtin/workflows/src/extension/wiring.ts +38 -12
  78. package/dist/builtin/workflows/src/extension/workflow-schema.ts +62 -5
  79. package/dist/builtin/workflows/src/runs/background/runner.ts +3 -3
  80. package/dist/builtin/workflows/src/runs/background/status.ts +42 -8
  81. package/dist/builtin/workflows/src/runs/foreground/executor.ts +410 -95
  82. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +5 -2
  83. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +8 -0
  84. package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +6 -4
  85. package/dist/builtin/workflows/src/runs/shared/worktree.ts +3 -2
  86. package/dist/builtin/workflows/src/shared/persistence-restore.ts +138 -5
  87. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +30 -0
  88. package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +78 -120
  89. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +193 -0
  90. package/dist/builtin/workflows/src/shared/store-types.ts +26 -1
  91. package/dist/builtin/workflows/src/shared/store.ts +145 -17
  92. package/dist/builtin/workflows/src/shared/timing.ts +6 -2
  93. package/dist/builtin/workflows/src/shared/workflow-failures.ts +375 -0
  94. package/dist/builtin/workflows/src/tui/chat-surface.ts +68 -17
  95. package/dist/builtin/workflows/src/tui/connectors.ts +2 -2
  96. package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +24 -26
  97. package/dist/builtin/workflows/src/tui/graph-canvas.ts +4 -8
  98. package/dist/builtin/workflows/src/tui/graph-view.ts +17 -14
  99. package/dist/builtin/workflows/src/tui/header.ts +38 -0
  100. package/dist/builtin/workflows/src/tui/inline-form-card.ts +161 -238
  101. package/dist/builtin/workflows/src/tui/inline-form-editor.ts +68 -73
  102. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +2 -3
  103. package/dist/builtin/workflows/src/tui/inline-form-store.ts +2 -1
  104. package/dist/builtin/workflows/src/tui/inputs-overlay.ts +1 -3
  105. package/dist/builtin/workflows/src/tui/inputs-picker.ts +286 -399
  106. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +11 -0
  107. package/dist/builtin/workflows/src/tui/node-card.ts +2 -1
  108. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -1
  109. package/dist/builtin/workflows/src/tui/prompt-card.ts +46 -19
  110. package/dist/builtin/workflows/src/tui/run-detail.ts +63 -80
  111. package/dist/builtin/workflows/src/tui/session-confirm.ts +9 -3
  112. package/dist/builtin/workflows/src/tui/session-picker.ts +19 -16
  113. package/dist/builtin/workflows/src/tui/stage-chat-layout.ts +88 -0
  114. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +368 -879
  115. package/dist/builtin/workflows/src/tui/status-helpers.ts +4 -0
  116. package/dist/builtin/workflows/src/tui/status-list.ts +67 -75
  117. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +50 -12
  118. package/dist/builtin/workflows/src/tui/submit-pane.ts +164 -0
  119. package/dist/builtin/workflows/src/tui/switcher.ts +27 -4
  120. package/dist/builtin/workflows/src/tui/text-helpers.ts +98 -4
  121. package/dist/builtin/workflows/src/tui/widget.ts +90 -68
  122. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +23 -2
  123. package/dist/builtin/workflows/src/tui/workflow-list.ts +44 -68
  124. package/dist/cli/file-processor.d.ts.map +1 -1
  125. package/dist/cli/file-processor.js +2 -3
  126. package/dist/cli/file-processor.js.map +1 -1
  127. package/dist/config.d.ts.map +1 -1
  128. package/dist/config.js +3 -10
  129. package/dist/config.js.map +1 -1
  130. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  131. package/dist/core/agent-session-runtime.js +2 -1
  132. package/dist/core/agent-session-runtime.js.map +1 -1
  133. package/dist/core/agent-session-services.d.ts.map +1 -1
  134. package/dist/core/agent-session-services.js +3 -2
  135. package/dist/core/agent-session-services.js.map +1 -1
  136. package/dist/core/agent-session.d.ts +6 -0
  137. package/dist/core/agent-session.d.ts.map +1 -1
  138. package/dist/core/agent-session.js +16 -2
  139. package/dist/core/agent-session.js.map +1 -1
  140. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  141. package/dist/core/atomic-guide-command.js +8 -9
  142. package/dist/core/atomic-guide-command.js.map +1 -1
  143. package/dist/core/auth-storage.d.ts.map +1 -1
  144. package/dist/core/auth-storage.js +3 -2
  145. package/dist/core/auth-storage.js.map +1 -1
  146. package/dist/core/bash-executor.d.ts.map +1 -1
  147. package/dist/core/bash-executor.js +2 -1
  148. package/dist/core/bash-executor.js.map +1 -1
  149. package/dist/core/export-html/index.d.ts.map +1 -1
  150. package/dist/core/export-html/index.js +8 -6
  151. package/dist/core/export-html/index.js.map +1 -1
  152. package/dist/core/export-html/template.js +6 -3
  153. package/dist/core/extensions/loader.d.ts.map +1 -1
  154. package/dist/core/extensions/loader.js +12 -29
  155. package/dist/core/extensions/loader.js.map +1 -1
  156. package/dist/core/model-registry.d.ts.map +1 -1
  157. package/dist/core/model-registry.js +5 -1
  158. package/dist/core/model-registry.js.map +1 -1
  159. package/dist/core/package-manager.d.ts +8 -0
  160. package/dist/core/package-manager.d.ts.map +1 -1
  161. package/dist/core/package-manager.js +145 -58
  162. package/dist/core/package-manager.js.map +1 -1
  163. package/dist/core/prompt-templates.d.ts.map +1 -1
  164. package/dist/core/prompt-templates.js +6 -20
  165. package/dist/core/prompt-templates.js.map +1 -1
  166. package/dist/core/resource-loader.d.ts.map +1 -1
  167. package/dist/core/resource-loader.js +38 -31
  168. package/dist/core/resource-loader.js.map +1 -1
  169. package/dist/core/sdk.d.ts.map +1 -1
  170. package/dist/core/sdk.js +9 -4
  171. package/dist/core/sdk.js.map +1 -1
  172. package/dist/core/session-manager.d.ts.map +1 -1
  173. package/dist/core/session-manager.js +32 -24
  174. package/dist/core/session-manager.js.map +1 -1
  175. package/dist/core/settings-manager.d.ts.map +1 -1
  176. package/dist/core/settings-manager.js +8 -15
  177. package/dist/core/settings-manager.js.map +1 -1
  178. package/dist/core/skills.d.ts.map +1 -1
  179. package/dist/core/skills.js +8 -22
  180. package/dist/core/skills.js.map +1 -1
  181. package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts +5 -4
  182. package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts.map +1 -1
  183. package/dist/core/tools/ask-user-question/state/questionnaire-session.js +34 -11
  184. package/dist/core/tools/ask-user-question/state/questionnaire-session.js.map +1 -1
  185. package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts +1 -0
  186. package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts.map +1 -1
  187. package/dist/core/tools/ask-user-question/state/selectors/contract.js.map +1 -1
  188. package/dist/core/tools/ask-user-question/state/selectors/projections.d.ts.map +1 -1
  189. package/dist/core/tools/ask-user-question/state/selectors/projections.js +1 -0
  190. package/dist/core/tools/ask-user-question/state/selectors/projections.js.map +1 -1
  191. package/dist/core/tools/ask-user-question/state/state-reducer.d.ts +1 -2
  192. package/dist/core/tools/ask-user-question/state/state-reducer.d.ts.map +1 -1
  193. package/dist/core/tools/ask-user-question/state/state-reducer.js +26 -9
  194. package/dist/core/tools/ask-user-question/state/state-reducer.js.map +1 -1
  195. package/dist/core/tools/ask-user-question/state/state.d.ts +4 -0
  196. package/dist/core/tools/ask-user-question/state/state.d.ts.map +1 -1
  197. package/dist/core/tools/ask-user-question/state/state.js.map +1 -1
  198. package/dist/core/tools/ask-user-question/view/components/option-list-view.d.ts +1 -0
  199. package/dist/core/tools/ask-user-question/view/components/option-list-view.d.ts.map +1 -1
  200. package/dist/core/tools/ask-user-question/view/components/option-list-view.js +1 -0
  201. package/dist/core/tools/ask-user-question/view/components/option-list-view.js.map +1 -1
  202. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts +9 -6
  203. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
  204. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +28 -7
  205. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
  206. package/dist/core/tools/ask-user-question/view/props-adapter.d.ts.map +1 -1
  207. package/dist/core/tools/ask-user-question/view/props-adapter.js +4 -1
  208. package/dist/core/tools/ask-user-question/view/props-adapter.js.map +1 -1
  209. package/dist/core/tools/bash.d.ts.map +1 -1
  210. package/dist/core/tools/bash.js +56 -53
  211. package/dist/core/tools/bash.js.map +1 -1
  212. package/dist/core/tools/edit-diff.d.ts +3 -1
  213. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  214. package/dist/core/tools/edit-diff.js +8 -1
  215. package/dist/core/tools/edit-diff.js.map +1 -1
  216. package/dist/core/tools/edit.d.ts +3 -1
  217. package/dist/core/tools/edit.d.ts.map +1 -1
  218. package/dist/core/tools/edit.js +44 -81
  219. package/dist/core/tools/edit.js.map +1 -1
  220. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  221. package/dist/core/tools/file-mutation-queue.js +27 -12
  222. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  223. package/dist/core/tools/find.d.ts.map +1 -1
  224. package/dist/core/tools/find.js +2 -3
  225. package/dist/core/tools/find.js.map +1 -1
  226. package/dist/core/tools/grep.d.ts.map +1 -1
  227. package/dist/core/tools/grep.js +3 -3
  228. package/dist/core/tools/grep.js.map +1 -1
  229. package/dist/core/tools/ls.d.ts.map +1 -1
  230. package/dist/core/tools/ls.js +5 -5
  231. package/dist/core/tools/ls.js.map +1 -1
  232. package/dist/core/tools/output-accumulator.d.ts +2 -0
  233. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  234. package/dist/core/tools/output-accumulator.js +11 -4
  235. package/dist/core/tools/output-accumulator.js.map +1 -1
  236. package/dist/core/tools/path-utils.d.ts +2 -0
  237. package/dist/core/tools/path-utils.d.ts.map +1 -1
  238. package/dist/core/tools/path-utils.js +39 -21
  239. package/dist/core/tools/path-utils.js.map +1 -1
  240. package/dist/core/tools/read.d.ts.map +1 -1
  241. package/dist/core/tools/read.js +9 -8
  242. package/dist/core/tools/read.js.map +1 -1
  243. package/dist/core/tools/truncate.d.ts.map +1 -1
  244. package/dist/core/tools/truncate.js +12 -2
  245. package/dist/core/tools/truncate.js.map +1 -1
  246. package/dist/core/tools/write.d.ts.map +1 -1
  247. package/dist/core/tools/write.js +20 -35
  248. package/dist/core/tools/write.js.map +1 -1
  249. package/dist/index.d.ts +2 -1
  250. package/dist/index.d.ts.map +1 -1
  251. package/dist/index.js +4 -1
  252. package/dist/index.js.map +1 -1
  253. package/dist/main.d.ts.map +1 -1
  254. package/dist/main.js +5 -6
  255. package/dist/main.js.map +1 -1
  256. package/dist/modes/interactive/chat-input-actions.d.ts +24 -0
  257. package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -0
  258. package/dist/modes/interactive/chat-input-actions.js +179 -0
  259. package/dist/modes/interactive/chat-input-actions.js.map +1 -0
  260. package/dist/modes/interactive/components/chat-message-renderer.d.ts +1 -0
  261. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  262. package/dist/modes/interactive/components/chat-message-renderer.js +14 -3
  263. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  264. package/dist/modes/interactive/components/chat-session-host.d.ts +157 -0
  265. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -0
  266. package/dist/modes/interactive/components/chat-session-host.js +1007 -0
  267. package/dist/modes/interactive/components/chat-session-host.js.map +1 -0
  268. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  269. package/dist/modes/interactive/components/config-selector.js +1 -1
  270. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  271. package/dist/modes/interactive/components/footer.d.ts +1 -0
  272. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  273. package/dist/modes/interactive/components/footer.js +14 -5
  274. package/dist/modes/interactive/components/footer.js.map +1 -1
  275. package/dist/modes/interactive/components/index.d.ts +1 -0
  276. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  277. package/dist/modes/interactive/components/index.js +1 -0
  278. package/dist/modes/interactive/components/index.js.map +1 -1
  279. package/dist/modes/interactive/components/login-dialog.d.ts +9 -1
  280. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  281. package/dist/modes/interactive/components/login-dialog.js +29 -4
  282. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  283. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  284. package/dist/modes/interactive/interactive-mode.js +18 -67
  285. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  286. package/dist/utils/child-process.d.ts +1 -0
  287. package/dist/utils/child-process.d.ts.map +1 -1
  288. package/dist/utils/child-process.js +8 -0
  289. package/dist/utils/child-process.js.map +1 -1
  290. package/dist/utils/clipboard-native.d.ts +3 -1
  291. package/dist/utils/clipboard-native.d.ts.map +1 -1
  292. package/dist/utils/clipboard-native.js +14 -8
  293. package/dist/utils/clipboard-native.js.map +1 -1
  294. package/dist/utils/image-resize-core.d.ts +30 -0
  295. package/dist/utils/image-resize-core.d.ts.map +1 -0
  296. package/dist/utils/image-resize-core.js +124 -0
  297. package/dist/utils/image-resize-core.js.map +1 -0
  298. package/dist/utils/image-resize-worker.d.ts +2 -0
  299. package/dist/utils/image-resize-worker.d.ts.map +1 -0
  300. package/dist/utils/image-resize-worker.js +31 -0
  301. package/dist/utils/image-resize-worker.js.map +1 -0
  302. package/dist/utils/image-resize.d.ts +7 -27
  303. package/dist/utils/image-resize.d.ts.map +1 -1
  304. package/dist/utils/image-resize.js +75 -115
  305. package/dist/utils/image-resize.js.map +1 -1
  306. package/dist/utils/paths.d.ts +16 -1
  307. package/dist/utils/paths.d.ts.map +1 -1
  308. package/dist/utils/paths.js +49 -7
  309. package/dist/utils/paths.js.map +1 -1
  310. package/docs/changelog.mdx +29 -0
  311. package/docs/compaction.md +1 -1
  312. package/docs/custom-provider.md +2 -2
  313. package/docs/development.md +1 -1
  314. package/docs/docs.json +98 -143
  315. package/docs/extensions.md +29 -16
  316. package/docs/favicon.svg +29 -0
  317. package/docs/images/interactive-mode.png +0 -0
  318. package/docs/images/tree-view.png +0 -0
  319. package/docs/images/workflow-command.png +0 -0
  320. package/docs/images/workflow-graph.png +0 -0
  321. package/docs/images/workflow-input-picker.png +0 -0
  322. package/docs/images/workflow-list.png +0 -0
  323. package/docs/index.md +10 -1
  324. package/docs/logo.svg +59 -0
  325. package/docs/packages.md +3 -3
  326. package/docs/providers.md +1 -1
  327. package/docs/quickstart.md +98 -2
  328. package/docs/rpc.md +8 -8
  329. package/docs/sdk.md +23 -12
  330. package/docs/sessions.md +1 -1
  331. package/docs/skills.md +15 -1
  332. package/docs/termux.md +11 -1
  333. package/docs/themes.md +6 -6
  334. package/docs/tui.md +18 -18
  335. package/docs/usage.md +1 -1
  336. package/docs/workflows.md +172 -2
  337. package/examples/extensions/subagent/index.ts +2 -1
  338. package/package.json +6 -6
  339. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/SKILL.md +0 -0
  340. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/element-attributes.md +0 -0
  341. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/playwright-tests.md +0 -0
  342. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/request-mocking.md +0 -0
  343. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/running-code.md +0 -0
  344. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/session-management.md +0 -0
  345. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/spec-driven-testing.md +0 -0
  346. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/storage-state.md +0 -0
  347. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/test-generation.md +0 -0
  348. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/tracing.md +0 -0
  349. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/video-recording.md +0 -0
  350. /package/dist/builtin/{workflows → subagents}/skills/tdd/SKILL.md +0 -0
  351. /package/dist/builtin/{workflows → subagents}/skills/tdd/deep-modules.md +0 -0
  352. /package/dist/builtin/{workflows → subagents}/skills/tdd/interface-design.md +0 -0
  353. /package/dist/builtin/{workflows → subagents}/skills/tdd/mocking.md +0 -0
  354. /package/dist/builtin/{workflows → subagents}/skills/tdd/refactoring.md +0 -0
  355. /package/dist/builtin/{workflows → subagents}/skills/tdd/tests.md +0 -0
@@ -2,47 +2,55 @@
2
2
  * Interactive argument picker for `/workflow <name>` invocations.
3
3
  *
4
4
  * Opens when the user types `/workflow <name>` in the TUI without enough
5
- * key=value tokens to satisfy the declared schema. Mirrors the form phase of
6
- * flora131/atomic's `workflow-picker-tui.tsx` design (mauve `▎` section
7
- * label, rounded field box per input, caption row, dim footer hints), with
8
- * the workflow already chosen there is no fuzzy-list left pane.
5
+ * key=value tokens to satisfy the declared schema. Mirrors the
6
+ * `ask_user_question` dialog shape: a top rule, a compact field tab bar,
7
+ * one page of ask-style question rows, and dim footer hints. The workflow is
8
+ * already chosen, so there is no fuzzy-list pane.
9
9
  *
10
- * ▎ <workflow name>
11
- * <description, dim>
10
+ * ───────────────────────────────────────────
11
+ * ← ■ prompt ■ focus ✓ Submit →
12
12
  *
13
- * INPUTS <focused+1> / <total>
13
+ * The high-level task to plan and execute.
14
14
  *
15
- * prompt ─────────────────────────────────╮
16
- * │ Build me a TUI for… │
17
- * ╰──────────────────────────────────────────╯
18
- * text · required · The high-level task to plan and execute.
15
+ * 1. Build me a TUI for…
19
16
  *
20
- * ╭ focus ──────────────────────────────────╮
21
- * minimal ○ standard ○ exhaustive │
22
- * ╰──────────────────────────────────────────╯
23
- * select · required · How aggressively to scope the work.
24
- *
25
- * tab next · shift+tab prev · ctrl+x run · esc cancel
17
+ * ───────────────────────────────────────────
18
+ * Enter to select · ↑/↓ to navigate · Tab to switch input fields · Esc to cancel
26
19
  *
27
20
  * Field-type renderers:
28
- * - string / number : single-row text input with blinking cursor
29
- * - text : 3-row scrolling textarea (multi-line input)
30
- * - boolean : on/off toggle (space flips)
31
- * - select : radio row, ←/→ cycles choices
21
+ * - string / number : single-row ask-style input with blinking cursor
22
+ * - text : 3-row scrolling ask-style textarea
23
+ * - boolean : vertical on/off choice list (space flips)
24
+ * - select : vertical choice list, ←/→ cycles choices
32
25
  *
33
26
  * cross-ref:
34
27
  * - flora131/atomic research/designs/workflow-picker-tui.tsx (PROMPT phase)
35
28
  * - flora131/atomic packages/atomic-sdk/src/components/workflow-picker-panel.tsx
36
29
  * - src/tui/session-picker.ts (sibling overlay; same chrome + key style)
37
- * - DESIGN.md §1 Iconography (mauve `▎` for section labels)
30
+ * - DESIGN.md §5 Section Labels
38
31
  */
39
32
 
40
33
  import type { WorkflowInputEntry } from "../extension/render-result.js";
41
34
  import type { GraphTheme } from "./graph-theme.js";
42
35
  import { paint } from "./color-utils.js";
43
- import { decodePrintableKey, matchesKey, truncateToWidth, visibleWidth } from "./text-helpers.js";
36
+ import {
37
+ decodePrintableKey,
38
+ graphemes,
39
+ graphemeSegments,
40
+ Key,
41
+ matchesKey,
42
+ truncateToWidth,
43
+ visibleWidth,
44
+ wrapPlainText,
45
+ } from "./text-helpers.js";
46
+ import { renderCompactBandHeader } from "./header.js";
47
+ import {
48
+ renderAskChoiceRows,
49
+ renderSubmitControls,
50
+ } from "./submit-pane.js";
44
51
  import {
45
52
  type KeybindingsLike,
53
+ TUI_ACTION,
46
54
  deleteRange,
47
55
  lineEnd,
48
56
  lineStart,
@@ -71,8 +79,8 @@ export interface InputsPickerState {
71
79
  * `coerceValues()` converts these into typed objects at submit time.
72
80
  */
73
81
  rawText: Record<string, string>;
74
- /** True while the confirmation modal is on top of the form. */
75
- confirmOpen: boolean;
82
+ /** Reserved for older form snapshots; Submit is now a single final action. */
83
+ submitChoiceIdx: number;
76
84
  /**
77
85
  * Set of field indices that failed validation on the most recent submit
78
86
  * attempt. Used to dim the run hint and to highlight a field if the user
@@ -93,8 +101,6 @@ export interface InputsPickerRenderOpts {
93
101
  width: number;
94
102
  theme: GraphTheme;
95
103
  workflowName: string;
96
- /** Optional one-line description shown directly under the workflow chip. */
97
- description?: string;
98
104
  fields: readonly WorkflowInputEntry[];
99
105
  state: InputsPickerState;
100
106
  /** True when the blinking cursor is in its visible half-period. */
@@ -144,7 +150,7 @@ export function createInputsPickerState(
144
150
  return {
145
151
  focusedIdx,
146
152
  rawText,
147
- confirmOpen: false,
153
+ submitChoiceIdx: 0,
148
154
  invalidIndices: [],
149
155
  caret: (rawText[fields[focusedIdx]?.name ?? ""] ?? "").length,
150
156
  };
@@ -238,7 +244,7 @@ export function invalidForField(
238
244
  return null;
239
245
  }
240
246
 
241
- function computeInvalid(
247
+ export function computeInvalid(
242
248
  fields: readonly WorkflowInputEntry[],
243
249
  raw: Record<string, string>,
244
250
  ): number[] {
@@ -254,18 +260,10 @@ function computeInvalid(
254
260
  // Renderer
255
261
  // ---------------------------------------------------------------------------
256
262
 
257
- const dimSep = (theme: GraphTheme): string => paint(" · ", theme.dim);
258
-
259
- const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
260
-
261
- function graphemes(text: string): string[] {
262
- return Array.from(graphemeSegmenter.segment(text), (s) => s.segment);
263
- }
264
-
265
263
  function previousGraphemeOffset(text: string, caret: number): number {
266
264
  const c = Math.max(0, Math.min(caret, text.length));
267
265
  let prev = 0;
268
- for (const s of graphemeSegmenter.segment(text)) {
266
+ for (const s of graphemeSegments(text)) {
269
267
  if (s.index >= c) break;
270
268
  prev = s.index;
271
269
  }
@@ -274,7 +272,7 @@ function previousGraphemeOffset(text: string, caret: number): number {
274
272
 
275
273
  function nextGraphemeOffset(text: string, caret: number): number {
276
274
  const c = Math.max(0, Math.min(caret, text.length));
277
- for (const s of graphemeSegmenter.segment(text)) {
275
+ for (const s of graphemeSegments(text)) {
278
276
  if (s.index >= c) return Math.min(text.length, s.index + s.segment.length);
279
277
  if (s.index + s.segment.length > c) return s.index + s.segment.length;
280
278
  }
@@ -284,7 +282,7 @@ function nextGraphemeOffset(text: string, caret: number): number {
284
282
  function clampGraphemeOffset(text: string, caret: number): number {
285
283
  const c = Math.max(0, Math.min(caret, text.length));
286
284
  if (c === text.length) return c;
287
- for (const s of graphemeSegmenter.segment(text)) {
285
+ for (const s of graphemeSegments(text)) {
288
286
  if (s.index === c) return c;
289
287
  if (s.index > c) break;
290
288
  }
@@ -340,7 +338,7 @@ function layoutEditableText(raw: string, usable: number): TextLayoutLine[] {
340
338
  let line = "";
341
339
  let lineStart = 0;
342
340
  let lineWidth = 0;
343
- for (const s of graphemeSegmenter.segment(raw)) {
341
+ for (const s of graphemeSegments(raw)) {
344
342
  const offset = s.index;
345
343
  const g = s.segment;
346
344
  if (g === "\n") {
@@ -376,7 +374,7 @@ function visualColumnAt(text: string, caret: number): number {
376
374
 
377
375
  function offsetAtVisualColumn(text: string, targetCol: number): number {
378
376
  let col = 0;
379
- for (const s of graphemeSegmenter.segment(text)) {
377
+ for (const s of graphemeSegments(text)) {
380
378
  const w = visibleWidth(s.segment);
381
379
  if (col + w > targetCol) return s.index;
382
380
  col += w;
@@ -416,178 +414,6 @@ function caretLineDown(raw: string, caret: number): number | null {
416
414
  * Exported so the chat-history mirror (inline-form-card) renders fields
417
415
  * identically to this overlay — single source of truth for the field shape.
418
416
  */
419
- export function renderField(
420
- field: WorkflowInputEntry,
421
- raw: string,
422
- focused: boolean,
423
- caret: number,
424
- cursorOn: boolean,
425
- invalid: string | null,
426
- theme: GraphTheme,
427
- width: number,
428
- ): string[] {
429
- // Border + label colour pick. Focused fields use `accent` (blue); a field
430
- // that's currently flagged as invalid AFTER a submit attempt uses
431
- // `error` to draw the eye.
432
- const borderColor = invalid && !focused
433
- ? theme.error
434
- : focused
435
- ? theme.accent
436
- : theme.borderDim;
437
-
438
- const innerWidth = Math.max(10, width - 4); // 2 chars of border + 1 pad each side
439
- const top =
440
- paint("╭ ", borderColor) +
441
- paint(field.name, focused ? theme.text : theme.textMuted, { bold: focused }) +
442
- " " +
443
- paint("─".repeat(Math.max(0, innerWidth - field.name.length - 2)) + "╮", borderColor);
444
- const bottom = paint("╰" + "─".repeat(innerWidth) + "╯", borderColor);
445
-
446
- // Content row — branch per type.
447
- const contentInner = renderFieldContent(
448
- field,
449
- raw,
450
- focused,
451
- caret,
452
- cursorOn,
453
- innerWidth,
454
- theme,
455
- );
456
- const lines: string[] = [];
457
- lines.push(top);
458
- for (const inner of contentInner) {
459
- lines.push(paint("│ ", borderColor) + inner + paint(" │", borderColor));
460
- }
461
- lines.push(bottom);
462
-
463
- // Caption row — type · required|optional · description / invalid reason.
464
- // Composed at full length, then ANSI-clipped to the terminal width so it
465
- // never overflows into a second row regardless of how narrow the terminal
466
- // gets. On overflow the rightmost cell becomes `…`.
467
- const tagColour = invalid
468
- ? theme.error
469
- : field.required
470
- ? theme.warning
471
- : theme.dim;
472
- const tagLabel = invalid ?? (field.required ? "required" : "optional");
473
- const caption =
474
- " " +
475
- paint(field.type, theme.dim) +
476
- dimSep(theme) +
477
- paint(tagLabel, tagColour) +
478
- (field.description ? dimSep(theme) + paint(field.description, theme.dim) : "");
479
- lines.push(truncateToWidth(caption, width, "…", true));
480
- return lines;
481
- }
482
-
483
- /**
484
- * Return the inner content rows of a field, sized to fit `innerWidth - 2`
485
- * (the border + padding consume 4 cells total). Text fields are 3 rows
486
- * tall; all others are a single row.
487
- */
488
- function renderFieldContent(
489
- field: WorkflowInputEntry,
490
- raw: string,
491
- focused: boolean,
492
- caret: number,
493
- cursorOn: boolean,
494
- innerWidth: number,
495
- theme: GraphTheme,
496
- ): string[] {
497
- const usable = innerWidth - 2; // padding on both sides
498
-
499
- if (field.type === "select" && field.choices && field.choices.length > 0) {
500
- const cells = field.choices.map((choice) => {
501
- const sel = choice === raw;
502
- const marker = sel ? "●" : "○";
503
- const markerColor = sel
504
- ? focused
505
- ? theme.accent
506
- : theme.success
507
- : theme.dim;
508
- const textColor = sel
509
- ? focused
510
- ? theme.text
511
- : theme.textMuted
512
- : theme.dim;
513
- return paint(marker, markerColor) + " " + paint(choice, textColor);
514
- });
515
- return [padLine(cells.join(" "), usable)];
516
- }
517
-
518
- if (field.type === "boolean") {
519
- const on = raw === "true";
520
- const onCell =
521
- paint(on ? "●" : "○", on ? theme.accent : theme.dim) +
522
- " " +
523
- paint("on", on ? theme.text : theme.dim);
524
- const offCell =
525
- paint(!on ? "●" : "○", !on ? theme.accent : theme.dim) +
526
- " " +
527
- paint("off", !on ? theme.text : theme.dim);
528
- return [padLine(onCell + " " + offCell, usable)];
529
- }
530
-
531
- if (field.type === "text") {
532
- // 3-row scrolling textarea — wrap by terminal cell width and keep the
533
- // cursor's visual row in view. Newlines create hard row breaks.
534
- const ROWS = 3;
535
- if (raw === "") {
536
- return Array.from({ length: ROWS }, (_, i) =>
537
- i === 0
538
- ? renderInlineText("", focused, cursorOn, usable, theme, field.placeholder, true)
539
- : padLine("", usable),
540
- );
541
- }
542
- const layout = layoutEditableText(raw, usable);
543
- const safeCaret = clampGraphemeOffset(raw, caret);
544
- let cursorRow = layout.length - 1;
545
- for (let i = 0; i < layout.length; i++) {
546
- const line = layout[i]!;
547
- const next = layout[i + 1];
548
- if (safeCaret >= line.start && safeCaret < line.end) {
549
- cursorRow = i;
550
- break;
551
- }
552
- if (safeCaret === line.end) {
553
- cursorRow = next?.start === safeCaret ? i + 1 : i;
554
- }
555
- }
556
- cursorRow = Math.max(0, Math.min(cursorRow, layout.length - 1));
557
- const start = focused
558
- ? Math.max(0, Math.min(cursorRow - ROWS + 1, layout.length - ROWS))
559
- : Math.max(0, layout.length - ROWS);
560
- const rows: string[] = [];
561
- for (let i = 0; i < ROWS; i++) {
562
- const rowIdx = start + i;
563
- const line = layout[rowIdx];
564
- if (!line) {
565
- rows.push(padLine("", usable));
566
- continue;
567
- }
568
- const lineCaret = safeCaret >= line.start && safeCaret <= line.end
569
- ? safeCaret - line.start
570
- : line.text.length;
571
- rows.push(
572
- renderInlineText(
573
- line.text,
574
- focused && rowIdx === cursorRow,
575
- cursorOn,
576
- usable,
577
- theme,
578
- field.placeholder,
579
- false,
580
- lineCaret,
581
- ),
582
- );
583
- }
584
- return rows;
585
- }
586
-
587
- // string / number / integer / default — single-line input.
588
- return [renderInlineText(raw, focused, cursorOn, usable, theme, field.placeholder, raw === "", caret)];
589
- }
590
-
591
417
  /**
592
418
  * Render a single editable line. When `value` is empty and the field is
593
419
  * focused, paint a dim placeholder with the cursor sitting on its first
@@ -649,166 +475,226 @@ function padLine(s: string, usable: number): string {
649
475
  return truncateToWidth(s, usable, "…", true);
650
476
  }
651
477
 
652
- export function renderInputsPicker(opts: InputsPickerRenderOpts): string[] {
653
- const { theme, workflowName, description, fields, state, width, cursorOn } = opts;
654
- const lines: string[] = [];
478
+ function fitLine(line: string, width: number): string {
479
+ return truncateToWidth(line, Math.max(0, width), "…", true);
480
+ }
655
481
 
656
- // Header chip — name + description, matching atomic's locked-in chip.
657
- // Both rows are clipped to terminal width so a long workflow name or
658
- // description cannot push the picker into a wrap on narrow terminals.
659
- const chipPrefix = paint("▎ ", theme.mauve);
660
- const nameBudget = Math.max(0, width - 2);
661
- lines.push(
662
- chipPrefix +
663
- paint(truncateToWidth(workflowName, nameBudget, "…"), theme.text, { bold: true }),
664
- );
665
- if (description) {
666
- const descBudget = Math.max(0, width - 2);
667
- lines.push(" " + paint(truncateToWidth(description, descBudget, "…"), theme.textMuted));
482
+ function renderWorkflowHeader(
483
+ workflowName: string,
484
+ fieldCount: number,
485
+ focusedIdx: number,
486
+ theme: GraphTheme,
487
+ width: number,
488
+ ): string[] {
489
+ const current = Math.min(fieldCount, Math.max(1, focusedIdx + 1));
490
+ return renderCompactBandHeader({
491
+ label: "WORKFLOW",
492
+ subtitle: workflowName,
493
+ badges: fieldCount > 0 ? [{ text: `${current} / ${fieldCount}`, fg: theme.dim }] : [],
494
+ width,
495
+ theme,
496
+ });
497
+ }
498
+
499
+ function renderInputField(
500
+ field: WorkflowInputEntry,
501
+ raw: string,
502
+ caret: number,
503
+ cursorOn: boolean,
504
+ invalid: string | null,
505
+ focused: boolean,
506
+ theme: GraphTheme,
507
+ width: number,
508
+ ): string[] {
509
+ const boxWidth = Math.max(4, width);
510
+ const contentWidth = Math.max(1, boxWidth - 2);
511
+ const borderColor = focused ? theme.accent : theme.borderDim;
512
+ const rows = renderAskStyleInputBody(field, raw, focused ? caret : raw.length, cursorOn, focused, theme, contentWidth);
513
+ const lines = [
514
+ renderFieldTop(field.name, boxWidth, borderColor, focused, theme),
515
+ ...rows.map((row) => renderFieldRow(row, contentWidth, borderColor, theme)),
516
+ renderFieldBottom(boxWidth, borderColor),
517
+ ...renderFieldMeta(field, invalid, theme, width),
518
+ ];
519
+ return lines;
520
+ }
521
+
522
+ function renderAskStyleInputBody(
523
+ field: WorkflowInputEntry,
524
+ raw: string,
525
+ caret: number,
526
+ cursorOn: boolean,
527
+ focused: boolean,
528
+ theme: GraphTheme,
529
+ width: number,
530
+ ): string[] {
531
+ if (field.type === "select" && field.choices && field.choices.length > 0) {
532
+ const selected = Math.max(0, field.choices.indexOf(raw));
533
+ return field.choices.flatMap((choice, i) =>
534
+ renderAskChoiceRows(i + 1, focused || i !== selected ? choice : `✓ ${choice}`, focused && i === selected, theme, width),
535
+ );
668
536
  }
669
- lines.push("");
670
537
 
671
- // Section label with field counter (1-based). When the terminal is too
672
- // narrow to hold both, the counter is the priority — drop "INPUTS" first
673
- // so the user always knows which field they're on.
674
- const focusTargetCount = fields.length;
675
- const counter = `${Math.min(state.focusedIdx + 1, focusTargetCount)} / ${focusTargetCount}`;
676
- const labelLeft =
677
- paint(" ", theme.mauve) + paint("INPUTS", theme.textMuted, { bold: true });
678
- const labelLen = visibleWidth(labelLeft);
679
- if (labelLen + 1 + counter.length <= width) {
680
- const pad = width - labelLen - counter.length;
681
- lines.push(labelLeft + " ".repeat(pad) + paint(counter, theme.dim));
682
- } else if (counter.length + 2 <= width) {
683
- // Just the chip + counter, right-aligned.
684
- const pad = Math.max(0, width - 2 - counter.length);
685
- lines.push(chipPrefix + " ".repeat(pad) + paint(counter, theme.dim));
686
- } else {
687
- // Truly tiny — counter only, clipped.
688
- lines.push(paint(truncateToWidth(counter, width, "…"), theme.dim));
538
+ if (field.type === "boolean") {
539
+ const normalized = raw.trim().toLowerCase();
540
+ const hasValue = normalized.length > 0;
541
+ const on = normalized === "true" || normalized === "1";
542
+ return [
543
+ ...renderAskChoiceRows(1, focused || !hasValue || !on ? "on" : "✓ on", focused && hasValue && on, theme, width),
544
+ ...renderAskChoiceRows(2, focused || !hasValue || on ? "off" : "✓ off", focused && hasValue && !on, theme, width),
545
+ ];
546
+ }
547
+
548
+ return renderAskInputRows(field, raw, caret, cursorOn, focused, theme, width);
549
+ }
550
+
551
+ function renderAskInputRows(
552
+ field: WorkflowInputEntry,
553
+ raw: string,
554
+ caret: number,
555
+ cursorOn: boolean,
556
+ focused: boolean,
557
+ theme: GraphTheme,
558
+ width: number,
559
+ ): string[] {
560
+ const usable = Math.max(1, width);
561
+
562
+ if (field.type !== "text") {
563
+ return [renderInlineText(raw, focused, cursorOn, usable, theme, field.placeholder, raw === "", caret)];
689
564
  }
565
+
566
+ const ROWS = 3;
567
+ if (raw === "") {
568
+ return [
569
+ renderInlineText("", focused, cursorOn, usable, theme, field.placeholder, true),
570
+ ...Array.from({ length: ROWS - 1 }, () => padLine("", usable)),
571
+ ];
572
+ }
573
+
574
+ const layout = layoutEditableText(raw, usable);
575
+ const safeCaret = clampGraphemeOffset(raw, caret);
576
+ let cursorRow = layout.length - 1;
577
+ for (let i = 0; i < layout.length; i++) {
578
+ const line = layout[i]!;
579
+ const next = layout[i + 1];
580
+ if (safeCaret >= line.start && safeCaret < line.end) {
581
+ cursorRow = i;
582
+ break;
583
+ }
584
+ if (safeCaret === line.end) {
585
+ cursorRow = next?.start === safeCaret ? i + 1 : i;
586
+ }
587
+ }
588
+ cursorRow = Math.max(0, Math.min(cursorRow, layout.length - 1));
589
+ const start = Math.max(0, Math.min(cursorRow - ROWS + 1, layout.length - ROWS));
590
+ const rows: string[] = [];
591
+ for (let i = 0; i < ROWS; i++) {
592
+ const rowIdx = start + i;
593
+ const line = layout[rowIdx];
594
+ if (!line) {
595
+ rows.push(padLine("", usable));
596
+ continue;
597
+ }
598
+ const lineCaret = safeCaret >= line.start && safeCaret <= line.end
599
+ ? safeCaret - line.start
600
+ : line.text.length;
601
+ rows.push(
602
+ renderInlineText(
603
+ line.text,
604
+ focused && rowIdx === cursorRow,
605
+ cursorOn,
606
+ usable,
607
+ theme,
608
+ field.placeholder,
609
+ false,
610
+ lineCaret,
611
+ ),
612
+ );
613
+ }
614
+ return rows;
615
+ }
616
+
617
+ export function renderInputsPicker(opts: InputsPickerRenderOpts): string[] {
618
+ const { theme, workflowName, fields, state, width, cursorOn } = opts;
619
+ const lines: string[] = [];
620
+
621
+ lines.push(...renderWorkflowHeader(workflowName, fields.length, state.focusedIdx, theme, width));
690
622
  lines.push("");
691
623
 
692
- // Field blocks.
693
- for (let i = 0; i < fields.length; i++) {
694
- const f = fields[i]!;
695
- const raw = state.rawText[f.name] ?? "";
696
- const focused = i === state.focusedIdx && !state.confirmOpen;
624
+ for (let i = 0; i < fields.length; i += 1) {
625
+ const field = fields[i]!;
626
+ const raw = state.rawText[field.name] ?? "";
697
627
  const invalid = state.invalidIndices.includes(i)
698
- ? invalidForField(f, raw, i)
628
+ ? invalidForField(field, raw, i)
699
629
  : null;
700
- lines.push(...renderField(f, raw, focused, state.caret, cursorOn, invalid, theme, width));
701
- lines.push(""); // gap between fields
630
+ lines.push(...renderInputField(field, raw, state.caret, cursorOn, invalid, state.focusedIdx === i, theme, width));
631
+ lines.push("");
702
632
  }
703
633
 
704
- const anyInvalid = computeInvalid(fields, state.rawText).length > 0;
634
+ lines.push(...renderPickerSubmitControls(fields, state, theme, width));
705
635
 
706
- // Footer hints — tiered for narrow widths. The widest form ends up around
707
- // 61 visible cells; we step down to keys-with-labels-tight, keys-only,
708
- // and finally essentials-only when the terminal cannot hold the row.
709
- lines.push(renderFooterHints(width, theme, anyInvalid));
636
+ return lines.map((line) => fitLine(line, width));
637
+ }
710
638
 
711
- if (state.confirmOpen) {
712
- lines.push("");
713
- lines.push(...renderConfirmCard(opts));
714
- }
715
- return lines;
639
+ function renderFieldTop(
640
+ title: string,
641
+ width: number,
642
+ borderColor: string,
643
+ focused: boolean,
644
+ theme: GraphTheme,
645
+ ): string {
646
+ const label = ` ${title} `;
647
+ const labelText = paint(label, focused ? theme.accent : theme.textMuted, { bold: focused });
648
+ const fill = Math.max(0, width - visibleWidth(label) - 2);
649
+ return paint("╭", borderColor) + labelText + paint("─".repeat(fill) + "╮", borderColor);
716
650
  }
717
651
 
718
- /**
719
- * Footer hint row, tier-degraded so it never wraps on resize. Tiers:
720
- *
721
- * wide ( widest): tab next · shift+tab prev · ctrl+x run · esc cancel
722
- * medium (≥ keys): tab · shift+tab · ctrl+x · esc
723
- * tight (≥ short): tab · ⇧tab · ctrl+x · esc
724
- * narrow (else): ctrl+x · esc
725
- */
726
- function renderFooterHints(width: number, theme: GraphTheme, submitDisabled: boolean): string {
727
- const sep = dimSep(theme);
728
- const sepWidth = 5; // " · "
729
- const submitColor = submitDisabled ? theme.dim : theme.text;
730
- const submitLabelColor = submitDisabled ? theme.dim : theme.textMuted;
731
- const hint = (key: string, label: string, keyColor = theme.text, labelColor = theme.textMuted): string =>
732
- paint(key, keyColor) + " " + paint(label, labelColor);
733
- const keyOnly = (key: string, keyColor = theme.text): string => paint(key, keyColor);
652
+ function renderFieldRow(row: string, contentWidth: number, borderColor: string, _theme: GraphTheme): string {
653
+ const clipped = truncateToWidth(row, contentWidth, "", true);
654
+ const padded = clipped + " ".repeat(Math.max(0, contentWidth - visibleWidth(clipped)));
655
+ return paint("│", borderColor) + padded + paint("│", borderColor);
656
+ }
734
657
 
735
- const wide = [
736
- { width: 8, render: () => hint("tab", "Next") },
737
- { width: 14, render: () => hint("shift+tab", "Prev") },
738
- { width: 10, render: () => hint("ctrl+x", "Run", submitColor, submitLabelColor) },
739
- { width: 10, render: () => hint("esc", "Cancel") },
740
- ];
741
- const medium = [
742
- { width: 3, render: () => keyOnly("tab") },
743
- { width: 9, render: () => keyOnly("shift+tab") },
744
- { width: 6, render: () => keyOnly("ctrl+x", submitColor) },
745
- { width: 6, render: () => keyOnly("esc") },
746
- ];
747
- const tight = [
748
- { width: 3, render: () => keyOnly("tab") },
749
- { width: 4, render: () => keyOnly("⇧tab") },
750
- { width: 6, render: () => keyOnly("ctrl+x", submitColor) },
751
- { width: 6, render: () => keyOnly("esc") },
752
- ];
753
- const narrow = [
754
- { width: 6, render: () => keyOnly("ctrl+x", submitColor) },
755
- { width: 6, render: () => keyOnly("esc") },
756
- ];
658
+ function renderFieldBottom(width: number, borderColor: string): string {
659
+ return paint("╰" + "─".repeat(Math.max(0, width - 2)) + "", borderColor);
660
+ }
757
661
 
758
- for (const tier of [wide, medium, tight, narrow]) {
759
- const total = tier.reduce((s, h) => s + h.width, 0) + (tier.length - 1) * sepWidth;
760
- if (total <= width) {
761
- return tier.map((h) => h.render()).join(sep);
762
- }
763
- }
764
- // Truly tiny terminal show just the run+cancel keys joined by a single space.
765
- return paint("ctrl+x", submitColor) + " " + paint("esc", theme.text);
662
+ function renderFieldMeta(
663
+ field: WorkflowInputEntry,
664
+ invalid: string | null,
665
+ theme: GraphTheme,
666
+ width: number,
667
+ ): string[] {
668
+ const required = field.required ? "required" : "optional";
669
+ const text = field.description && field.description.length > 0
670
+ ? `${field.type} · ${required} · ${field.description}`
671
+ : `${field.type} · ${required}`;
672
+ const lines = wrapPlainText(text, width).map((line) => paintRequiredMetaLine(line, field.required === true, theme));
673
+ if (invalid) lines.push(...wrapPlainText(invalid, width).map((line) => paint(line, theme.error)));
674
+ return lines;
766
675
  }
767
676
 
768
- /**
769
- * Centered "ready to run" card that shows the composed slash invocation.
770
- * Returns an array of lines so the caller can splat into the master list
771
- * and each row is clipped to terminal width on its own.
772
- */
773
- function renderConfirmCard(opts: InputsPickerRenderOpts): string[] {
774
- const { theme, workflowName, fields, state, width } = opts;
775
- const values = coerceValues(fields, state.rawText);
776
- const head =
777
- paint("✓ ", theme.success) +
778
- paint("ready to run", theme.text, { bold: true });
779
- const cmdParts: string[] = [
780
- paint("/workflow ", theme.dim) + paint(workflowName, theme.text),
781
- ];
782
- for (const f of fields) {
783
- if (values[f.name] === undefined) continue;
784
- const shown = shortVal(String(state.rawText[f.name] ?? ""));
785
- cmdParts.push(
786
- paint(" ", theme.dim) +
787
- paint(f.name, theme.text) +
788
- paint("=", theme.dim) +
789
- paint(shown, theme.text),
790
- );
791
- }
792
- const prompt =
793
- paint("submit this workflow? ", theme.dim) +
794
- paint("y", theme.success, { bold: true }) +
795
- paint(" submit", theme.dim) +
796
- paint(" · ", theme.dim) +
797
- paint("n", theme.error, { bold: true }) +
798
- paint(" cancel", theme.dim);
799
- return [
800
- truncateToWidth(head, width, "…", true),
801
- "",
802
- ...cmdParts.map((row) => truncateToWidth(row, width, "…", true)),
803
- "",
804
- truncateToWidth(prompt, width, "…", true),
805
- ];
677
+ function paintRequiredMetaLine(line: string, required: boolean, theme: GraphTheme): string {
678
+ if (!required) return paint(line, theme.textMuted);
679
+ return line
680
+ .split(/(\brequired\b)/g)
681
+ .map((part) => part === "required" ? paint(part, theme.warning) : paint(part, theme.textMuted))
682
+ .join("");
806
683
  }
807
684
 
808
- function shortVal(s: string): string {
809
- const trimmed = s.replace(/\n/g, " ").trim();
810
- if (trimmed.length > 48) return trimmed.slice(0, 45) + "…";
811
- return trimmed.length === 0 ? "<empty>" : trimmed;
685
+ function renderPickerSubmitControls(
686
+ fields: readonly WorkflowInputEntry[],
687
+ state: InputsPickerState,
688
+ theme: GraphTheme,
689
+ width: number,
690
+ ): string[] {
691
+ const invalid = computeInvalid(fields, state.rawText);
692
+ return renderSubmitControls({
693
+ invalidFieldNames: invalid.map((i) => fields[i]!.name),
694
+ submitFocused: state.focusedIdx === fields.length,
695
+ theme,
696
+ width,
697
+ });
812
698
  }
813
699
 
814
700
  // ---------------------------------------------------------------------------
@@ -822,18 +708,17 @@ function shortVal(s: string): string {
822
708
  * with the coerced typed value map.
823
709
  *
824
710
  * Keys (form mode):
825
- * tab / down — next field
826
- * shift+tab / up — previous field
711
+ * tab switch input fields, then the final Submit action
712
+ * shift+tab previous input field / final Submit action
827
713
  * left / right — select: cycle choices; boolean: flip; text: caret
828
714
  * space — boolean: flip
829
715
  * enter — text: newline; otherwise: next field
830
- * ctrl+x — open confirm modal (if all required filled)
831
716
  * backspace — delete char left of caret
832
717
  * esc / ctrl+c — close picker without running
833
718
  *
834
- * Keys (confirm modal mode):
835
- * y / enterrun
836
- * n / esc — back to form
719
+ * Keys (Submit action):
720
+ * up / downmove back into the question list
721
+ * enter — submit immediately, or focus the first invalid field
837
722
  */
838
723
  export function handleInputsPickerInput(
839
724
  key: string,
@@ -848,7 +733,6 @@ export function handleInputsPickerInput(
848
733
  if (isCancelKey(key)) return { kind: "cancel" };
849
734
  return { kind: "noop" };
850
735
  }
851
- if (state.confirmOpen) return handleConfirmKey(key, state, fields);
852
736
  return handleFormKey(key, state, fields, keybindings);
853
737
  }
854
738
 
@@ -860,15 +744,15 @@ function handleFormKey(
860
744
  ): InputsPickerAction {
861
745
  // ── Global navigation (workflow form contract, not Pi actions) ──
862
746
  if (isCancelKey(key)) return { kind: "cancel" };
863
- if (matchesKey(key, "ctrl+x")) return attemptPickerSubmit(state, fields);
864
- if (matchesKey(key, "tab")) {
747
+ if (matchesKey(key, Key.tab)) {
865
748
  moveFocus(state, fields, +1);
866
749
  return { kind: "noop" };
867
750
  }
868
- if (matchesKey(key, "shift+tab")) {
751
+ if (matchesKey(key, Key.shift("tab"))) {
869
752
  moveFocus(state, fields, -1);
870
753
  return { kind: "noop" };
871
754
  }
755
+ if (state.focusedIdx === fields.length) return handleSubmitKey(key, state, fields, kb);
872
756
 
873
757
  const field = fields[state.focusedIdx]!;
874
758
  const name = field.name;
@@ -887,7 +771,7 @@ function handleFormKey(
887
771
  // KeybindingsManager so user-configured bindings work uniformly.
888
772
  const caret = Math.max(0, Math.min(state.caret, cur.length));
889
773
 
890
- if (matchesAction(kb, key, "tui.editor.cursorUp")) {
774
+ if (matchesAction(kb, key, TUI_ACTION.editorCursorUp)) {
891
775
  if (field.type === "text") {
892
776
  const nextCaret = caretLineUp(cur, caret);
893
777
  if (nextCaret !== null) {
@@ -898,7 +782,7 @@ function handleFormKey(
898
782
  moveFocus(state, fields, -1);
899
783
  return { kind: "noop" };
900
784
  }
901
- if (matchesAction(kb, key, "tui.editor.cursorDown")) {
785
+ if (matchesAction(kb, key, TUI_ACTION.editorCursorDown)) {
902
786
  if (field.type === "text") {
903
787
  const nextCaret = caretLineDown(cur, caret);
904
788
  if (nextCaret !== null) {
@@ -925,11 +809,11 @@ function handleFormKey(
925
809
  state.caret = lineEnd(cur, caret);
926
810
  return { kind: "noop" };
927
811
  }
928
- if (matchesAction(kb, key, "tui.editor.cursorLeft")) {
812
+ if (matchesAction(kb, key, TUI_ACTION.editorCursorLeft)) {
929
813
  state.caret = previousGraphemeOffset(cur, caret);
930
814
  return { kind: "noop" };
931
815
  }
932
- if (matchesAction(kb, key, "tui.editor.cursorRight")) {
816
+ if (matchesAction(kb, key, TUI_ACTION.editorCursorRight)) {
933
817
  state.caret = nextGraphemeOffset(cur, caret);
934
818
  return { kind: "noop" };
935
819
  }
@@ -978,7 +862,7 @@ function handleFormKey(
978
862
  return { kind: "noop" };
979
863
  }
980
864
  if (
981
- matchesAction(kb, key, "tui.input.submit") ||
865
+ matchesAction(kb, key, TUI_ACTION.inputSubmit) ||
982
866
  matchesAction(kb, key, "tui.input.newLine")
983
867
  ) {
984
868
  if (field.type === "text") {
@@ -1012,19 +896,15 @@ function handleSelectKey(
1012
896
  if (choices.length === 0) return { kind: "noop" };
1013
897
  const current = state.rawText[field.name] ?? choices[0]!;
1014
898
  const idx = Math.max(0, choices.indexOf(current));
1015
- if (matchesAction(kb, key, "tui.editor.cursorLeft")) {
899
+ if (matchesAction(kb, key, TUI_ACTION.selectUp) || matchesAction(kb, key, TUI_ACTION.editorCursorLeft)) {
1016
900
  state.rawText[field.name] = choices[(idx - 1 + choices.length) % choices.length]!;
1017
901
  return { kind: "noop" };
1018
902
  }
1019
- if (matchesAction(kb, key, "tui.editor.cursorRight")) {
903
+ if (matchesAction(kb, key, TUI_ACTION.selectDown) || matchesAction(kb, key, TUI_ACTION.editorCursorRight)) {
1020
904
  state.rawText[field.name] = choices[(idx + 1) % choices.length]!;
1021
905
  return { kind: "noop" };
1022
906
  }
1023
- if (matchesAction(kb, key, "tui.editor.cursorUp")) {
1024
- moveFocus(state, fields, -1);
1025
- return { kind: "noop" };
1026
- }
1027
- if (matchesAction(kb, key, "tui.editor.cursorDown")) {
907
+ if (matchesAction(kb, key, TUI_ACTION.selectConfirm) || matchesAction(kb, key, TUI_ACTION.inputSubmit)) {
1028
908
  moveFocus(state, fields, +1);
1029
909
  return { kind: "noop" };
1030
910
  }
@@ -1039,46 +919,48 @@ function handleBooleanKey(
1039
919
  kb: KeybindingsLike | undefined,
1040
920
  ): InputsPickerAction {
1041
921
  if (
1042
- matchesKey(key, "space") ||
1043
- matchesAction(kb, key, "tui.input.submit") ||
1044
- matchesAction(kb, key, "tui.editor.cursorLeft") ||
1045
- matchesAction(kb, key, "tui.editor.cursorRight")
922
+ matchesKey(key, Key.space) ||
923
+ matchesAction(kb, key, TUI_ACTION.selectUp) ||
924
+ matchesAction(kb, key, TUI_ACTION.selectDown) ||
925
+ matchesAction(kb, key, TUI_ACTION.editorCursorLeft) ||
926
+ matchesAction(kb, key, TUI_ACTION.editorCursorRight)
1046
927
  ) {
1047
928
  state.rawText[field.name] = state.rawText[field.name] === "true" ? "false" : "true";
1048
929
  return { kind: "noop" };
1049
930
  }
1050
- if (matchesAction(kb, key, "tui.editor.cursorUp")) {
1051
- moveFocus(state, fields, -1);
1052
- return { kind: "noop" };
1053
- }
1054
- if (matchesAction(kb, key, "tui.editor.cursorDown")) {
931
+ if (matchesAction(kb, key, TUI_ACTION.selectConfirm) || matchesAction(kb, key, TUI_ACTION.inputSubmit)) {
1055
932
  moveFocus(state, fields, +1);
1056
933
  return { kind: "noop" };
1057
934
  }
1058
935
  return { kind: "noop" };
1059
936
  }
1060
937
 
1061
- function handleConfirmKey(
938
+ function handleSubmitKey(
1062
939
  key: string,
1063
940
  state: InputsPickerState,
1064
941
  fields: readonly WorkflowInputEntry[],
942
+ kb: KeybindingsLike | undefined,
1065
943
  ): InputsPickerAction {
1066
- // Confirm-modal answers are single-char prompts (`y`/`n`) plus the form's
1067
- // raw esc/enter contract. These do not flow through Pi action ids because
1068
- // they're a confirmation-modal contract, not an editor-mode action.
1069
- if (key === "y" || key === "Y" || matchesKey(key, "enter")) {
1070
- return { kind: "run", values: coerceValues(fields, state.rawText) };
944
+ if (matchesAction(kb, key, TUI_ACTION.selectUp) || matchesAction(kb, key, TUI_ACTION.editorCursorUp)) {
945
+ moveFocus(state, fields, -1);
946
+ return { kind: "noop" };
1071
947
  }
1072
- if (matchesKey(key, "ctrl+c")) return { kind: "cancel" };
1073
- if (key === "n" || key === "N" || matchesKey(key, "escape")) {
1074
- state.confirmOpen = false;
948
+ if (matchesAction(kb, key, TUI_ACTION.selectDown) || matchesAction(kb, key, TUI_ACTION.editorCursorDown)) {
949
+ moveFocus(state, fields, +1);
1075
950
  return { kind: "noop" };
1076
951
  }
952
+ if (
953
+ matchesKey(key, Key.enter) ||
954
+ matchesAction(kb, key, TUI_ACTION.selectConfirm) ||
955
+ matchesAction(kb, key, TUI_ACTION.inputSubmit)
956
+ ) {
957
+ return attemptPickerSubmit(state, fields);
958
+ }
1077
959
  return { kind: "noop" };
1078
960
  }
1079
961
 
1080
962
  function isCancelKey(key: string): boolean {
1081
- return matchesKey(key, "ctrl+c") || matchesKey(key, "escape");
963
+ return matchesKey(key, Key.ctrl("c")) || matchesKey(key, Key.escape);
1082
964
  }
1083
965
 
1084
966
  function attemptPickerSubmit(
@@ -1088,13 +970,13 @@ function attemptPickerSubmit(
1088
970
  const invalid = computeInvalid(fields, state.rawText);
1089
971
  if (invalid.length > 0) {
1090
972
  state.invalidIndices = invalid;
973
+ state.submitChoiceIdx = 0;
1091
974
  state.focusedIdx = invalid[0]!;
1092
975
  state.caret = (state.rawText[fields[state.focusedIdx]!.name] ?? "").length;
1093
976
  return { kind: "noop" };
1094
977
  }
1095
978
  state.invalidIndices = [];
1096
- state.confirmOpen = true;
1097
- return { kind: "noop" };
979
+ return { kind: "run", values: coerceValues(fields, state.rawText) };
1098
980
  }
1099
981
 
1100
982
  function moveFocus(
@@ -1102,9 +984,14 @@ function moveFocus(
1102
984
  fields: readonly WorkflowInputEntry[],
1103
985
  delta: number,
1104
986
  ): void {
1105
- const n = fields.length;
1106
- if (n === 0) return;
987
+ const n = fields.length + 1;
988
+ if (n <= 1) return;
1107
989
  state.focusedIdx = (state.focusedIdx + delta + n) % n;
990
+ if (state.focusedIdx === fields.length) {
991
+ state.caret = 0;
992
+ state.submitChoiceIdx = 0;
993
+ return;
994
+ }
1108
995
  const next = fields[state.focusedIdx]!;
1109
996
  state.caret = (state.rawText[next.name] ?? "").length;
1110
997
  }