@selesai/code 0.5.20 → 0.5.21

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 (263) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  4. package/dist/extensions/pi-subagents/README.md +602 -220
  5. package/dist/extensions/pi-subagents/banner.png +0 -0
  6. package/dist/extensions/pi-subagents/index.ts +1 -0
  7. package/dist/extensions/pi-subagents/install.mjs +3 -3
  8. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  9. package/dist/extensions/pi-subagents/package.json +26 -14
  10. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  11. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  12. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  13. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  14. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  15. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  16. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  18. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  19. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  20. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  21. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  22. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  23. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  24. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  25. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  26. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  27. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  28. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  29. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  30. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  31. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  32. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  33. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  34. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  35. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  36. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  37. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  38. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  39. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  40. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  41. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  42. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  43. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  44. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  45. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  46. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  48. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  49. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  50. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  51. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  53. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  54. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  55. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  56. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  57. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  58. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  59. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  60. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  61. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  63. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  65. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  66. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  67. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  68. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  69. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  70. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  71. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  72. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  73. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  74. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  76. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  78. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  80. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  82. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  83. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  87. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  88. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  89. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  90. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  94. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  96. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  97. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  98. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  99. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  100. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  101. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  102. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  103. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  104. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  105. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  106. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  107. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  108. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  109. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  110. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  111. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  112. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  113. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  114. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  115. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  116. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  117. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  120. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  121. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  122. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  124. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  125. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  126. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  127. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  140. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  141. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  142. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  143. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  144. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  145. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  146. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  147. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  148. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  149. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  150. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  151. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  152. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  153. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  154. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  155. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  156. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  157. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  158. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  159. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  160. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  161. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  162. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  163. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  164. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  165. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  166. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  167. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  168. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  169. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  170. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  171. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  172. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  173. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  174. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  175. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  176. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  177. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  178. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  179. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  180. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  181. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  182. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  183. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  184. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  185. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  186. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  187. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  188. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  189. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  190. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  191. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  192. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  194. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  195. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  196. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  197. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  198. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  199. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  200. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  201. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  202. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  203. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  204. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  205. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  206. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  207. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  208. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  209. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  210. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  211. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  212. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  213. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  214. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  215. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  216. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  217. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  218. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  222. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  223. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  224. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  225. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  226. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  227. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  228. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  229. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  230. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  232. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  233. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  234. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  235. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  236. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  237. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  238. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  239. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  240. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  250. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  251. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  252. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  253. package/dist/extensions/question/batch.ts +39 -0
  254. package/dist/extensions/question/dialog-adapter.ts +23 -1
  255. package/dist/extensions/question/index.ts +237 -7
  256. package/dist/extensions/question/schemas.ts +28 -15
  257. package/dist/extensions/question/tests/batch.test.ts +26 -0
  258. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  259. package/dist/extensions/question/tui-adapter.ts +2 -3
  260. package/dist/extensions/question/types.ts +37 -0
  261. package/dist/extensions/question/ui-protocol.ts +3 -3
  262. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  263. package/package.json +1 -1
@@ -8,7 +8,7 @@ import {
8
8
  isCancelled,
9
9
  parseDialogSelections,
10
10
  } from "./helpers.ts";
11
- import type { QuestionOption, QuestionResponse, ResolvedQuestionParams } from "./types.ts";
11
+ import type { BatchAnswer, BatchedQuestion, QuestionOption, QuestionResponse, ResolvedQuestionParams } from "./types.ts";
12
12
  import type { FallbackProtocol, UIProtocol } from "./ui-protocol.ts";
13
13
 
14
14
  const FREEFORM_DIALOG_OPTION = "✏️ Type custom response...";
@@ -58,6 +58,28 @@ async function askViaDialogs(
58
58
  return createSelectionResponse([selected], comment);
59
59
  }
60
60
 
61
+ export async function askBatchViaDialogs(questions: BatchedQuestion[], protocol: UIProtocol, timeout?: number): Promise<BatchAnswer[] | null> {
62
+ const deadline = timeout && timeout > 0 ? Date.now() + timeout : undefined;
63
+ const answers: BatchAnswer[] = [];
64
+ for (const question of questions) {
65
+ const remaining = deadline === undefined ? undefined : deadline - Date.now();
66
+ if (remaining !== undefined && remaining <= 0) return null;
67
+ const response = await askViaDialogs(
68
+ protocol,
69
+ question.question,
70
+ question.context,
71
+ question.options,
72
+ question.allowMultiple,
73
+ question.allowFreeform,
74
+ question.allowComment,
75
+ remaining,
76
+ );
77
+ if (response === null) return null;
78
+ answers.push({ id: question.id, question: question.question, response });
79
+ }
80
+ return answers;
81
+ }
82
+
61
83
  export const DialogFallback: FallbackProtocol = {
62
84
  async ask(params, protocol) {
63
85
  return askViaDialogs(
@@ -17,14 +17,15 @@ import {
17
17
  truncateToWidth,
18
18
  } from "@earendil-works/pi-tui";
19
19
 
20
+ import { prepareBatchQuestions } from "./batch.ts";
20
21
  import { BOX_BORDER_LEFT, BOX_BORDER_OVERHEAD, BOX_BORDER_RIGHT, QUESTION_VERSION } from "./constants.ts";
21
- import { DialogFallback } from "./dialog-adapter.ts";
22
+ import { askBatchViaDialogs, DialogFallback } from "./dialog-adapter.ts";
22
23
  import { createFreeformResponse, createSelectionResponse, formatOptionsForMessage, formatResponseSummary, getOptionsFormatError, normalizeOptions, oneLine, safeMarkdownTheme } from "./helpers.ts";
23
24
  import { QuestionList } from "./question-list.ts";
24
25
  import { MultiSelect, SingleSelect } from "./selection-mode.ts";
25
26
  import { resolveShortcuts } from "./shortcuts.ts";
26
27
  import { QuestionParamsSchema } from "./schemas.ts";
27
- import type { QuestionDetails, QuestionMode, QuestionOption, QuestionResponse, RawOption, ResolvedShortcuts } from "./types.ts";
28
+ import type { BatchAnswer, BatchQuestionDetails, BatchedQuestion, QuestionDetails, QuestionMode, QuestionOption, QuestionResponse, RawOption, ResolvedShortcuts } from "./types.ts";
28
29
  import { createTUIProtocol } from "./tui-adapter.ts";
29
30
  import type { CustomFactory, UIProtocol } from "./ui-protocol.ts";
30
31
 
@@ -165,6 +166,10 @@ class QuestionComponent extends Container implements Component {
165
166
  get focused(): boolean {
166
167
  return this._focused;
167
168
  }
169
+
170
+ get isEditing(): boolean {
171
+ return this.mode === "freeform" || this.mode === "comment";
172
+ }
168
173
  set focused(value: boolean) {
169
174
  this._focused = value;
170
175
  if (this.editor && (this.mode === "freeform" || this.mode === "comment")) {
@@ -360,6 +365,171 @@ class QuestionComponent extends Container implements Component {
360
365
  }
361
366
  }
362
367
 
368
+ // ---------------------------------------------------------------------------
369
+ // Batched-question component
370
+ // ---------------------------------------------------------------------------
371
+
372
+ class BatchQuestionComponent extends Container implements Component {
373
+ private currentPage = 0;
374
+ private readonly answers = new Map<string, QuestionResponse>();
375
+ private questionComponent?: QuestionComponent;
376
+ private focusedState = false;
377
+ private header = new Text("", 1, 0);
378
+ private content = new Container();
379
+ private legend = new Text("", 1, 0);
380
+
381
+ constructor(
382
+ private questions: BatchedQuestion[],
383
+ private tui: TUI,
384
+ private theme: Theme,
385
+ private keybindings: KeybindingsManager,
386
+ private onDone: (answers: BatchAnswer[] | null) => void,
387
+ ) {
388
+ super();
389
+ this.addChild(this.header);
390
+ this.addChild(new Spacer(1));
391
+ this.addChild(this.content);
392
+ this.addChild(new Spacer(1));
393
+ this.addChild(this.legend);
394
+ this.showPage();
395
+ }
396
+
397
+ get focused(): boolean {
398
+ return this.focusedState;
399
+ }
400
+ set focused(value: boolean) {
401
+ this.focusedState = value;
402
+ if (this.questionComponent) this.questionComponent.focused = value;
403
+ }
404
+
405
+ override invalidate(): void {
406
+ super.invalidate();
407
+ this.updateChrome();
408
+ }
409
+
410
+ private get isReviewPage(): boolean {
411
+ return this.currentPage === this.questions.length;
412
+ }
413
+
414
+ private allAnswered(): boolean {
415
+ return this.questions.every((question) => this.answers.has(question.id));
416
+ }
417
+
418
+ private updateChrome(): void {
419
+ const answered = this.answers.size;
420
+ if (this.isReviewPage) {
421
+ this.header.setText(this.theme.fg("accent", this.theme.bold(`Review answers · ${answered}/${this.questions.length} answered`)));
422
+ this.legend.setText(
423
+ this.theme.fg(
424
+ "dim",
425
+ this.allAnswered()
426
+ ? "Shift+Tab/← previous • Enter submit all answers • Esc cancel"
427
+ : "Shift+Tab/← previous • Answer every question before submitting • Esc cancel",
428
+ ),
429
+ );
430
+ return;
431
+ }
432
+ const question = this.questions[this.currentPage];
433
+ this.header.setText(
434
+ this.theme.fg(
435
+ "accent",
436
+ this.theme.bold(`${question.label} · ${this.currentPage + 1}/${this.questions.length}${this.answers.has(question.id) ? " · answered" : ""}`),
437
+ ),
438
+ );
439
+ this.legend.setText(this.theme.fg("dim", "Tab next • Shift+Tab previous • ←→ navigate outside text entry • Answers save when selected"));
440
+ }
441
+
442
+ private showPage(): void {
443
+ this.content.clear();
444
+ this.questionComponent = undefined;
445
+ this.updateChrome();
446
+
447
+ if (this.isReviewPage) {
448
+ this.content.addChild(new Text(this.theme.fg("accent", this.theme.bold("Ready to submit")), 1, 0));
449
+ this.content.addChild(new Spacer(1));
450
+ for (const question of this.questions) {
451
+ const response = this.answers.get(question.id);
452
+ const marker = response ? this.theme.fg("success", "✓") : this.theme.fg("warning", "○");
453
+ const answer = response ? formatResponseSummary(response) : "Unanswered";
454
+ this.content.addChild(new Text(`${marker} ${this.theme.fg("accent", question.label)} ${this.theme.fg("dim", "—")} ${this.theme.fg(response ? "text" : "warning", answer)}`, 1, 0));
455
+ }
456
+ } else {
457
+ const question = this.questions[this.currentPage];
458
+ this.questionComponent = new QuestionComponent(
459
+ question.question,
460
+ question.context,
461
+ question.options,
462
+ question.allowMultiple,
463
+ question.allowFreeform,
464
+ question.allowComment,
465
+ this.tui,
466
+ this.theme,
467
+ this.keybindings,
468
+ question.shortcuts,
469
+ (response) => this.saveAnswer(question, response),
470
+ );
471
+ this.questionComponent.focused = this.focusedState;
472
+ this.content.addChild(this.questionComponent);
473
+ }
474
+ this.invalidate();
475
+ this.tui.requestRender();
476
+ }
477
+
478
+ private saveAnswer(question: BatchedQuestion, response: QuestionResponse | null): void {
479
+ if (response === null) {
480
+ this.onDone(null);
481
+ return;
482
+ }
483
+ this.answers.set(question.id, response);
484
+ this.goNext();
485
+ }
486
+
487
+ private goNext(): void {
488
+ if (this.currentPage < this.questions.length) this.currentPage++;
489
+ this.showPage();
490
+ }
491
+
492
+ private goPrevious(): void {
493
+ if (this.currentPage > 0) this.currentPage--;
494
+ this.showPage();
495
+ }
496
+
497
+ handleInput(data: string): void {
498
+ if (this.isReviewPage) {
499
+ if (matchesKey(data, Key.enter) && this.allAnswered()) {
500
+ this.onDone(this.questions.map((question) => ({ id: question.id, question: question.question, response: this.answers.get(question.id)! })));
501
+ return;
502
+ }
503
+ if (matchesKey(data, Key.escape) || this.keybindings.matches(data, "tui.select.cancel")) {
504
+ this.onDone(null);
505
+ return;
506
+ }
507
+ if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) this.goPrevious();
508
+ return;
509
+ }
510
+
511
+ if (matchesKey(data, Key.tab)) {
512
+ this.goNext();
513
+ return;
514
+ }
515
+ if (matchesKey(data, Key.shift("tab"))) {
516
+ this.goPrevious();
517
+ return;
518
+ }
519
+ if (!this.questionComponent?.isEditing) {
520
+ if (matchesKey(data, Key.right)) {
521
+ this.goNext();
522
+ return;
523
+ }
524
+ if (matchesKey(data, Key.left)) {
525
+ this.goPrevious();
526
+ return;
527
+ }
528
+ }
529
+ this.questionComponent?.handleInput(data);
530
+ }
531
+ }
532
+
363
533
  // ---------------------------------------------------------------------------
364
534
  // Tool registration
365
535
  // ---------------------------------------------------------------------------
@@ -369,7 +539,7 @@ export default function questionExtension(pi: ExtensionAPI) {
369
539
  name: "question",
370
540
  label: "Question",
371
541
  description:
372
- "Ask the user a question in the Pi UI and return the answer. Supports multiple-choice options, multi-select, freeform answers, optional context, and an optional comment. Use when you need user input to proceed.",
542
+ "Ask the user one question or a paged batch through the Pi UI and return the answer(s). Supports multiple-choice options, multi-select, freeform answers, optional context, comments, and a final batch review/submit page. Use when you need user input to proceed.",
373
543
  promptSnippet: "Ask the user a question when required information is missing.",
374
544
  promptGuidelines: [
375
545
  "Use question only when you cannot proceed safely or accurately without user input.",
@@ -379,6 +549,7 @@ export default function questionExtension(pi: ExtensionAPI) {
379
549
  "Pass a short summary via context when the question depends on prior findings.",
380
550
  "After the user answers, continue the task immediately using that answer; do not stop and ask the user to type continue unless the task is complete.",
381
551
  "Do not use question for information you can infer, inspect, or compute with other tools.",
552
+ "For independent related decisions, send one question call with a questions array. The user can use Tab/Shift+Tab or Left/Right to revisit pages, then presses Enter on the review page to submit all answers.",
382
553
  ],
383
554
  // Block other tool calls in the same turn until the user answers, so the
384
555
  // model can't batch question with side-effecting tools and run them first.
@@ -386,6 +557,53 @@ export default function questionExtension(pi: ExtensionAPI) {
386
557
  parameters: QuestionParamsSchema,
387
558
 
388
559
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
560
+ if (Array.isArray(params.questions)) {
561
+ const prepared = prepareBatchQuestions(params.questions);
562
+ if ("error" in prepared) {
563
+ return { content: [{ type: "text", text: prepared.error }], isError: true, details: { questions: [], answers: [], cancelled: true } satisfies BatchQuestionDetails };
564
+ }
565
+ const questions = prepared.questions;
566
+ const detailsBase = {
567
+ questions: questions.map(({ id, label, question, context, options }) => ({ id, label, question, context, options })),
568
+ };
569
+ if (signal?.aborted) {
570
+ return { content: [{ type: "text", text: "Question batch cancelled" }], details: { ...detailsBase, answers: [], cancelled: true } satisfies BatchQuestionDetails };
571
+ }
572
+
573
+ const protocol = createTUIProtocol(ctx);
574
+ if (!protocol.hasUI) {
575
+ return {
576
+ content: [{ type: "text", text: "Question batch requires interactive mode." }],
577
+ isError: true,
578
+ details: { ...detailsBase, answers: [], cancelled: true } satisfies BatchQuestionDetails,
579
+ };
580
+ }
581
+ onUpdate?.({ content: [{ type: "text", text: "Waiting for answers to question batch..." }], details: { ...detailsBase, answers: [], cancelled: false } satisfies BatchQuestionDetails });
582
+
583
+ let answers: BatchAnswer[] | null | undefined;
584
+ try {
585
+ const customFactory: CustomFactory<BatchAnswer[] | null> = (tui, theme, keybindings, done) => {
586
+ if (signal) signal.addEventListener("abort", () => done(null), { once: true });
587
+ const timeout = params.timeout as number | undefined;
588
+ if (timeout && timeout > 0) setTimeout(() => done(null), timeout);
589
+ return new BatchQuestionComponent(questions, tui, theme, keybindings, done);
590
+ };
591
+ answers = await protocol.custom(customFactory);
592
+ if (answers === undefined) answers = await askBatchViaDialogs(questions, protocol, params.timeout as number | undefined);
593
+ } catch (error) {
594
+ const message = error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error);
595
+ return { content: [{ type: "text", text: `Question batch failed: ${message}` }], isError: true, details: { ...detailsBase, answers: [], cancelled: true } satisfies BatchQuestionDetails };
596
+ }
597
+
598
+ if (!answers) {
599
+ return { content: [{ type: "text", text: "User cancelled the question batch" }], details: { ...detailsBase, answers: [], cancelled: true } satisfies BatchQuestionDetails };
600
+ }
601
+ return {
602
+ content: [{ type: "text", text: answers.map((answer) => `${answer.id}: ${formatResponseSummary(answer.response)}`).join("\n") }],
603
+ details: { ...detailsBase, answers, cancelled: false } satisfies BatchQuestionDetails,
604
+ };
605
+ }
606
+
389
607
  if (signal?.aborted) {
390
608
  return {
391
609
  content: [{ type: "text", text: "Question cancelled" }],
@@ -451,7 +669,7 @@ export default function questionExtension(pi: ExtensionAPI) {
451
669
  let result: QuestionResponse | null;
452
670
 
453
671
  try {
454
- const customFactory: CustomFactory = (
672
+ const customFactory: CustomFactory<QuestionResponse | null> = (
455
673
  tui: TUI,
456
674
  theme: Theme,
457
675
  keybindings: KeybindingsManager,
@@ -514,6 +732,15 @@ export default function questionExtension(pi: ExtensionAPI) {
514
732
  },
515
733
 
516
734
  renderCall(args, theme, _context) {
735
+ const batch = Array.isArray(args.questions) ? args.questions : undefined;
736
+ if (batch) {
737
+ const labels = batch.map((item: { label?: string; id?: string }, index: number) => item.label || item.id || `Q${index + 1}`).join(", ");
738
+ return new Text(
739
+ `${theme.fg("toolTitle", theme.bold("QUESTION"))} ${theme.fg("accent", theme.bold(`${batch.length} question${batch.length === 1 ? "" : "s"}`))}${labels ? theme.fg("dim", ` · ${oneLine(labels, 100)}`) : ""}`,
740
+ 0,
741
+ 0,
742
+ );
743
+ }
517
744
  const question = (args.question as string) || "";
518
745
  const rawOptions = Array.isArray(args.options) ? args.options : [];
519
746
  const title = args.context ? `${args.context}: ` : args.title ? `${args.title}: ` : "";
@@ -532,7 +759,7 @@ export default function questionExtension(pi: ExtensionAPI) {
532
759
  },
533
760
 
534
761
  renderResult(result, options, theme, _context) {
535
- const details = result.details as (QuestionDetails & { error?: string }) | undefined;
762
+ const details = result.details as (QuestionDetails | BatchQuestionDetails) & { error?: string } | undefined;
536
763
 
537
764
  if (details?.error) return new Text(theme.fg("error", details.error), 0, 0);
538
765
 
@@ -546,9 +773,12 @@ export default function questionExtension(pi: ExtensionAPI) {
546
773
  return new Text(theme.fg("muted", waitingText), 0, 0);
547
774
  }
548
775
 
549
- if (!details || details.cancelled || !details.response) {
550
- return new Text(theme.fg("warning", "question cancelled"), 0, 0);
776
+ if (!details || details.cancelled) return new Text(theme.fg("warning", "question cancelled"), 0, 0);
777
+
778
+ if ("answers" in details) {
779
+ return new Text(details.answers.map((answer) => `${theme.fg("success", "✓")} ${theme.fg("accent", answer.id)}: ${theme.fg("text", oneLine(formatResponseSummary(answer.response), 120))}`).join("\n"), 0, 0);
551
780
  }
781
+ if (!details.response) return new Text(theme.fg("warning", "question cancelled"), 0, 0);
552
782
 
553
783
  const response = details.response;
554
784
  let text = theme.fg("success", theme.bold("answered: "));
@@ -10,35 +10,48 @@ const OptionSchema = Type.Union([
10
10
  }),
11
11
  ]);
12
12
 
13
- export const QuestionParamsSchema = Type.Object({
13
+ const QuestionFields = {
14
14
  question: Type.String({ description: "Question to ask the user before continuing" }),
15
- title: Type.Optional(Type.String({ description: "Short title for the question UI (deprecated alias of context summary)" })),
16
- context: Type.Optional(
17
- Type.String({ description: "Relevant context summary shown before the question" }),
18
- ),
15
+ title: Type.Optional(Type.String({ description: "Short title for the question UI (deprecated alias for context summary)" })),
16
+ context: Type.Optional(Type.String({ description: "Relevant context summary shown before the question" })),
19
17
  options: Type.Optional(
20
18
  Type.Array(OptionSchema, {
21
19
  description: "Optional multiple-choice options. Each option may be a string or { label, description }.",
22
20
  }),
23
21
  ),
24
- allowMultiple: Type.Optional(
25
- Type.Boolean({ description: "Allow selecting multiple options. Default: false" }),
26
- ),
22
+ allowMultiple: Type.Optional(Type.Boolean({ description: "Allow selecting multiple options. Default: false" })),
27
23
  allowFreeform: Type.Optional(
28
24
  Type.Boolean({
29
25
  description: "Allow a custom freeform answer. Defaults to true when no options are provided, false when options exist.",
30
26
  }),
31
27
  ),
32
- allowComment: Type.Optional(
33
- Type.Boolean({ description: "Collect an optional comment after selecting one or more options. Default: false" }),
34
- ),
28
+ allowComment: Type.Optional(Type.Boolean({ description: "Collect an optional comment after selecting one or more options. Default: false" })),
35
29
  commentToggleKey: Type.Optional(
36
30
  Type.String({
37
31
  description:
38
32
  "Shortcut for toggling the optional comment/extra-context row when allowComment is true, e.g. 'ctrl+g'. Pass 'off' to disable. Default: PI_QUESTION_COMMENT_TOGGLE_KEY env var if set, otherwise 'ctrl+g'.",
39
33
  }),
40
34
  ),
41
- timeout: Type.Optional(
42
- Type.Number({ description: "Auto-dismiss after N milliseconds. Returns null (cancelled) when expired." }),
43
- ),
44
- });
35
+ };
36
+
37
+ const SingleQuestionParamsSchema = Type.Object({
38
+ ...QuestionFields,
39
+ timeout: Type.Optional(Type.Number({ description: "Auto-dismiss after N milliseconds. Returns null (cancelled) when expired." })),
40
+ });
41
+
42
+ const BatchQuestionSchema = Type.Object({
43
+ id: Type.Optional(Type.String({ description: "Unique answer id. Defaults to q1, q2, and so on." })),
44
+ label: Type.Optional(Type.String({ description: "Short page label. Defaults to Q1, Q2, and so on." })),
45
+ ...QuestionFields,
46
+ });
47
+
48
+ const BatchQuestionParamsSchema = Type.Object({
49
+ questions: Type.Array(BatchQuestionSchema, {
50
+ minItems: 1,
51
+ description:
52
+ "Questions to answer in one paged form. User can move between pages with Tab/Shift+Tab or Left/Right, then press Enter on the final review page to submit all answers.",
53
+ }),
54
+ timeout: Type.Optional(Type.Number({ description: "Auto-dismiss the whole batch after N milliseconds." })),
55
+ });
56
+
57
+ export const QuestionParamsSchema = Type.Union([SingleQuestionParamsSchema, BatchQuestionParamsSchema]);
@@ -0,0 +1,26 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { prepareBatchQuestions } from "../batch.ts";
5
+
6
+ test("prepareBatchQuestions: normalizes defaults and generated ids", () => {
7
+ const result = prepareBatchQuestions([
8
+ { question: "Pick a color", options: ["Red", "Blue"] },
9
+ { id: "notes", label: "Notes", question: "Anything else?" },
10
+ ]);
11
+ assert.ok("questions" in result);
12
+ if (!("questions" in result)) return;
13
+ assert.deepEqual(
14
+ result.questions.map(({ id, label, question, options, allowFreeform }) => ({ id, label, question, options, allowFreeform })),
15
+ [
16
+ { id: "q1", label: "Q1", question: "Pick a color", options: [{ label: "Red" }, { label: "Blue" }], allowFreeform: false },
17
+ { id: "notes", label: "Notes", question: "Anything else?", options: [], allowFreeform: true },
18
+ ],
19
+ );
20
+ });
21
+
22
+ test("prepareBatchQuestions: rejects empty batches, duplicate ids, and impossible questions", () => {
23
+ assert.deepEqual(prepareBatchQuestions([]), { error: "questions must contain at least one question." });
24
+ assert.match((prepareBatchQuestions([{ id: "same", question: "One" }, { id: "same", question: "Two" }]) as { error: string }).error, /duplicate id/);
25
+ assert.match((prepareBatchQuestions([{ question: "Choose", allowFreeform: false }]) as { error: string }).error, /options are empty/);
26
+ });
@@ -1,8 +1,8 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
 
4
- import { DialogFallback } from "../dialog-adapter.ts";
5
- import type { QuestionResponse, ResolvedQuestionParams } from "../types.ts";
4
+ import { askBatchViaDialogs, DialogFallback } from "../dialog-adapter.ts";
5
+ import type { BatchedQuestion, QuestionResponse, ResolvedQuestionParams } from "../types.ts";
6
6
  import type { UIProtocol } from "../ui-protocol.ts";
7
7
 
8
8
  // ---------------------------------------------------------------------------
@@ -134,4 +134,24 @@ test("DialogFallback: freeform cancel (input returns undefined after selecting c
134
134
  });
135
135
  const result = await DialogFallback.ask(makeParams({ allowFreeform: true }), protocol);
136
136
  assert.equal(result, null);
137
- });
137
+ });
138
+ test("askBatchViaDialogs: returns identified answers and passes remaining timeout", async () => {
139
+ const timeouts: Array<number | undefined> = [];
140
+ const protocol = fakeProtocol({
141
+ select: async (_prompt, _options, opts) => {
142
+ timeouts.push(opts?.timeout);
143
+ return "Option A";
144
+ },
145
+ });
146
+ const questions: BatchedQuestion[] = [
147
+ { ...makeParams(), id: "first", label: "First" },
148
+ { ...makeParams({ question: "Pick again" }), id: "second", label: "Second" },
149
+ ];
150
+ const result = await askBatchViaDialogs(questions, protocol, 1_000);
151
+ assert.deepEqual(result, [
152
+ { id: "first", question: "Pick one", response: { kind: "selection", selections: ["Option A"] } },
153
+ { id: "second", question: "Pick again", response: { kind: "selection", selections: ["Option A"] } },
154
+ ]);
155
+ assert.equal(timeouts.length, 2);
156
+ assert.ok(timeouts.every((timeout) => timeout !== undefined && timeout > 0 && timeout <= 1_000));
157
+ });
@@ -2,14 +2,13 @@
2
2
 
3
3
  import type { ExtensionContext } from "@selesai/code";
4
4
 
5
- import type { QuestionResponse } from "./types.ts";
6
- import type { UIProtocol } from "./ui-protocol.ts";
5
+ import type { CustomFactory, UIProtocol } from "./ui-protocol.ts";
7
6
 
8
7
  export function createTUIProtocol(ctx: ExtensionContext): UIProtocol {
9
8
  return {
10
9
  hasUI: ctx.hasUI,
11
10
  theme: ctx.ui.theme,
12
- custom: (factory) => ctx.ui.custom(factory as any) as Promise<QuestionResponse | null | undefined>,
11
+ custom: <T>(factory: CustomFactory<T>) => ctx.ui.custom(factory as any) as Promise<T | undefined>,
13
12
  select: (prompt, options, opts) => ctx.ui.select(prompt, options, opts),
14
13
  input: (prompt, placeholder, opts) => ctx.ui.input(prompt, placeholder, opts),
15
14
  };
@@ -32,6 +32,43 @@ export interface QuestionParams {
32
32
  timeout?: number;
33
33
  }
34
34
 
35
+ export interface RawBatchQuestion {
36
+ id?: string;
37
+ label?: string;
38
+ question: string;
39
+ title?: string;
40
+ context?: string;
41
+ options?: RawOption[];
42
+ allowMultiple?: boolean;
43
+ allowFreeform?: boolean;
44
+ allowComment?: boolean;
45
+ commentToggleKey?: string | null;
46
+ }
47
+
48
+ export interface BatchedQuestion {
49
+ id: string;
50
+ label: string;
51
+ question: string;
52
+ context: string | undefined;
53
+ options: QuestionOption[];
54
+ allowMultiple: boolean;
55
+ allowFreeform: boolean;
56
+ allowComment: boolean;
57
+ shortcuts: ResolvedShortcuts;
58
+ }
59
+
60
+ export interface BatchAnswer {
61
+ id: string;
62
+ question: string;
63
+ response: QuestionResponse;
64
+ }
65
+
66
+ export interface BatchQuestionDetails {
67
+ questions: Array<Pick<BatchedQuestion, "id" | "label" | "question" | "context" | "options">>;
68
+ answers: BatchAnswer[];
69
+ cancelled: boolean;
70
+ }
71
+
35
72
  // ---------------------------------------------------------------------------
36
73
  // Shortcut resolution
37
74
  // ---------------------------------------------------------------------------
@@ -11,17 +11,17 @@ export interface CustomFactoryResult {
11
11
  focused: boolean;
12
12
  }
13
13
 
14
- export type CustomFactory = (
14
+ export type CustomFactory<T> = (
15
15
  tui: TUI,
16
16
  theme: Theme,
17
17
  keybindings: KeybindingsManager,
18
- done: (result: QuestionResponse | null) => void,
18
+ done: (result: T) => void,
19
19
  ) => CustomFactoryResult;
20
20
 
21
21
  export interface UIProtocol {
22
22
  readonly hasUI: boolean;
23
23
  readonly theme: Theme;
24
- custom(factory: CustomFactory): Promise<QuestionResponse | null | undefined>;
24
+ custom<T>(factory: CustomFactory<T>): Promise<T | undefined>;
25
25
  select(prompt: string, options: string[], opts?: { timeout?: number }): Promise<string | undefined>;
26
26
  input(prompt: string, placeholder?: string, opts?: { timeout?: number }): Promise<string | undefined>;
27
27
  }
@@ -6,7 +6,9 @@ disable-model-invocation: true
6
6
 
7
7
  Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
8
8
 
9
- Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask *now* without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.
9
+ Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask *now* without guessing at answers you haven't heard yet.
10
+
11
+ For every non-empty frontier, call the `question` tool **exactly once** with its `questions` array; never print frontier questions as plain chat text. Give every item a stable `id`, short `label`, concise `question`, and `context` containing the recommended answer and why. Provide options when known and set `allowFreeform: true` when the user may need another answer. The batch UI lets the user revisit pages before pressing Enter on its final review page to submit all answers. Wait for that tool result, map returned `answers` by `id`, then recompute the next frontier.
10
12
 
11
13
  Each round the user answers reshapes the tree — settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a *later* round, not this one.
12
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selesai/code",
3
- "version": "0.5.20",
3
+ "version": "0.5.21",
4
4
  "description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
5
5
  "type": "module",
6
6
  "repository": {