@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.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 (359) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +8 -11
  3. package/docs/prefix-cache-architecture.md +14 -0
  4. package/package.json +22 -24
  5. package/packages/ask-user/README.md +55 -107
  6. package/packages/ask-user/ask-ui.ts +306 -732
  7. package/packages/ask-user/clipboard.ts +50 -0
  8. package/packages/ask-user/config.ts +74 -78
  9. package/packages/ask-user/package.json +2 -2
  10. package/packages/ask-user/questions.ts +151 -0
  11. package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
  12. package/packages/ask-user/tools.ts +198 -432
  13. package/packages/autocomplete/package.json +1 -1
  14. package/packages/autocomplete/src/constants.ts +38 -43
  15. package/packages/autocomplete/src/sorting.ts +8 -0
  16. package/packages/background-tasks/package.json +2 -2
  17. package/packages/background-tasks/src/config.ts +0 -73
  18. package/packages/background-tasks/src/index.ts +20 -16
  19. package/packages/background-tasks/src/registry.ts +1 -171
  20. package/packages/background-tasks/src/types.ts +0 -32
  21. package/packages/btw/README.md +13 -56
  22. package/packages/btw/extensions/btw.ts +403 -1796
  23. package/packages/btw/package.json +5 -2
  24. package/packages/btw/skills/btw/SKILL.md +7 -143
  25. package/packages/compactor/README.md +56 -287
  26. package/packages/compactor/package.json +5 -9
  27. package/packages/compactor/skills/compactor/SKILL.md +13 -20
  28. package/packages/compactor/src/card.ts +90 -0
  29. package/packages/compactor/src/commands/index.ts +73 -130
  30. package/packages/compactor/src/compaction/compact-args.ts +5 -3
  31. package/packages/compactor/src/compaction/filter-noise.ts +1 -1
  32. package/packages/compactor/src/compaction/hooks.ts +487 -311
  33. package/packages/compactor/src/compaction/source.ts +178 -0
  34. package/packages/compactor/src/compaction/summarize.ts +586 -52
  35. package/packages/compactor/src/config/manager.ts +130 -127
  36. package/packages/compactor/src/config/schema.ts +32 -48
  37. package/packages/compactor/src/index.ts +40 -431
  38. package/packages/compactor/src/info-screen.ts +44 -190
  39. package/packages/compactor/src/session/recall-blocks.ts +1 -1
  40. package/packages/compactor/src/stats.ts +76 -0
  41. package/packages/compactor/src/tools/context-budget.ts +17 -34
  42. package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
  43. package/packages/compactor/src/tools/register.ts +29 -269
  44. package/packages/compactor/src/types.ts +47 -186
  45. package/packages/core/README.md +25 -0
  46. package/packages/core/child-agent.ts +1 -0
  47. package/packages/core/command-echo.ts +63 -0
  48. package/packages/core/compaction-context.ts +41 -0
  49. package/packages/core/constants.ts +8 -38
  50. package/packages/core/events.ts +0 -53
  51. package/packages/core/fusion-status.ts +4 -0
  52. package/packages/core/global.d.ts +10 -0
  53. package/packages/core/harness-messages.ts +392 -0
  54. package/packages/core/index.ts +14 -0
  55. package/packages/core/kanboard-status.ts +49 -0
  56. package/packages/core/long-horizon-owner-status.ts +46 -0
  57. package/packages/core/model-cache.ts +81 -7
  58. package/packages/core/package.json +6 -3
  59. package/packages/core/src/attach/detect.ts +163 -0
  60. package/packages/core/src/child-agent/child-args.ts +31 -0
  61. package/packages/core/src/child-agent/delivery.ts +41 -0
  62. package/packages/core/src/child-agent/index.ts +5 -0
  63. package/packages/core/src/child-agent/readers.ts +203 -0
  64. package/packages/core/src/child-agent/runtime.ts +584 -0
  65. package/packages/core/src/evidence.ts +78 -0
  66. package/packages/core/src/hints/browser.ts +278 -0
  67. package/packages/core/src/hints/crab-data.ts +19 -0
  68. package/packages/core/src/hints/index.ts +865 -0
  69. package/packages/core/src/hints/lines.ts +607 -0
  70. package/packages/core/src/hints/store.ts +120 -0
  71. package/packages/core/src/jev/chatter.ts +24 -0
  72. package/packages/core/src/jev/settings.ts +177 -14
  73. package/packages/core/src/package-colors.ts +2 -0
  74. package/packages/core/src/settings/catalog.ts +39 -15
  75. package/packages/core/src/settings/engine.ts +48 -2
  76. package/packages/core/src/settings/hub.ts +83 -20
  77. package/packages/core/src/settings/open.ts +49 -0
  78. package/packages/core/src/settings/schema.ts +27 -7
  79. package/packages/core/src/tui/hub-kit.ts +18 -5
  80. package/packages/core/src/tui/kit.ts +198 -0
  81. package/packages/core/src/tui/progress.ts +73 -0
  82. package/packages/core/src/turn/arbiter.ts +290 -0
  83. package/packages/footer/README.md +4 -6
  84. package/packages/footer/package.json +3 -3
  85. package/packages/footer/src/glance-editor.ts +19 -11
  86. package/packages/footer/src/help.ts +1 -1
  87. package/packages/footer/src/index.ts +36 -3
  88. package/packages/footer/src/presets.ts +3 -4
  89. package/packages/footer/src/rendering/icons.ts +0 -20
  90. package/packages/footer/src/segments/compactor.ts +52 -142
  91. package/packages/footer/src/segments/status-ext.ts +0 -1
  92. package/packages/fusion/README.md +14 -4
  93. package/packages/fusion/package.json +4 -3
  94. package/packages/fusion/src/child-args.ts +1 -0
  95. package/packages/fusion/src/index.ts +161 -8
  96. package/packages/fusion/src/pi-settings.ts +57 -0
  97. package/packages/fusion/src/picker.ts +186 -51
  98. package/packages/fusion/src/preset.ts +56 -2
  99. package/packages/fusion/src/prompts.ts +1 -1
  100. package/packages/fusion/src/sidekick-runtime.ts +1 -420
  101. package/packages/fusion/src/tools.ts +107 -128
  102. package/packages/fusion/src/transcript.ts +27 -85
  103. package/packages/info-screen/package.json +2 -2
  104. package/packages/input-shortcuts/README.md +18 -30
  105. package/packages/input-shortcuts/package.json +2 -2
  106. package/packages/input-shortcuts/src/burst.ts +88 -0
  107. package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
  108. package/packages/input-shortcuts/src/cleared-input.ts +97 -0
  109. package/packages/input-shortcuts/src/index.ts +99 -193
  110. package/packages/input-shortcuts/src/last-response.ts +40 -0
  111. package/packages/input-shortcuts/src/registers.ts +8 -23
  112. package/packages/input-shortcuts/src/types.ts +4 -16
  113. package/packages/input-shortcuts/src/undo-redo.ts +59 -47
  114. package/packages/kanboard/README.md +51 -47
  115. package/packages/kanboard/index.ts +293 -55
  116. package/packages/kanboard/package.json +8 -8
  117. package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
  118. package/packages/kanboard/src/badges.ts +93 -0
  119. package/packages/kanboard/src/bin.ts +4 -2
  120. package/packages/kanboard/src/commands.ts +218 -84
  121. package/packages/kanboard/src/debug.ts +20 -0
  122. package/packages/kanboard/src/guard.ts +267 -109
  123. package/packages/kanboard/src/monitor.ts +330 -0
  124. package/packages/kanboard/src/notice-buffer.ts +74 -0
  125. package/packages/kanboard/src/progress.ts +27 -0
  126. package/packages/kanboard/src/reminders.ts +234 -0
  127. package/packages/kanboard/src/settings.ts +69 -15
  128. package/packages/kanboard/src/shapes.ts +5 -0
  129. package/packages/long-horizon/index.ts +190 -13
  130. package/packages/long-horizon/package.json +3 -3
  131. package/packages/long-horizon/src/commands.ts +103 -7
  132. package/packages/long-horizon/src/compaction-brief.ts +35 -0
  133. package/packages/long-horizon/src/engine/continuation.ts +84 -15
  134. package/packages/long-horizon/src/engine/goal-state.ts +64 -13
  135. package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
  136. package/packages/long-horizon/src/engine/ralph.ts +3 -2
  137. package/packages/long-horizon/src/engine/runaway.ts +68 -11
  138. package/packages/long-horizon/src/engine/verifier.ts +19 -0
  139. package/packages/long-horizon/src/gate.ts +37 -12
  140. package/packages/long-horizon/src/judge/resolve.ts +1 -0
  141. package/packages/long-horizon/src/owner.ts +35 -2
  142. package/packages/long-horizon/src/progress.ts +87 -0
  143. package/packages/long-horizon/src/prompts/goal.ts +1 -1
  144. package/packages/long-horizon/src/runtime.ts +172 -18
  145. package/packages/long-horizon/src/settings.ts +54 -39
  146. package/packages/long-horizon/src/tools/goal.ts +6 -0
  147. package/packages/long-horizon/src/tools/graph.ts +1 -1
  148. package/packages/long-horizon/src/tools/swarm.ts +3 -3
  149. package/packages/mcp/package.json +2 -2
  150. package/packages/memory/commands.ts +7 -2
  151. package/packages/memory/index.ts +210 -32
  152. package/packages/memory/package.json +3 -3
  153. package/packages/memory/save-session.ts +264 -0
  154. package/packages/memory/settings.ts +17 -0
  155. package/packages/memory/tools.ts +253 -203
  156. package/packages/notify/package.json +2 -2
  157. package/packages/skill-registry/README.md +53 -16
  158. package/packages/skill-registry/index.ts +230 -0
  159. package/packages/skill-registry/package.json +19 -8
  160. package/packages/skill-registry/src/editor.ts +206 -0
  161. package/packages/skill-registry/src/judge.ts +289 -0
  162. package/packages/skill-registry/src/registry.ts +87 -0
  163. package/packages/skill-registry/src/settings.ts +259 -0
  164. package/packages/skill-registry/src/vault.ts +80 -0
  165. package/packages/subagents/README.md +39 -119
  166. package/packages/subagents/package.json +6 -13
  167. package/packages/subagents/skills/subagents/SKILL.md +18 -42
  168. package/packages/subagents/src/agents.ts +213 -0
  169. package/packages/subagents/src/cards.ts +182 -0
  170. package/packages/subagents/src/index.ts +609 -1084
  171. package/packages/subagents/src/manager.ts +429 -0
  172. package/packages/subagents/src/profiles.ts +130 -0
  173. package/packages/subagents/src/transcript.ts +154 -0
  174. package/packages/subagents/src/ui.ts +389 -0
  175. package/packages/unipi/bundled.js +39756 -45569
  176. package/packages/unipi/index.ts +55 -10
  177. package/packages/updater/package.json +2 -2
  178. package/packages/utility/README.md +76 -69
  179. package/packages/utility/package.json +2 -2
  180. package/packages/utility/skills/image/SKILL.md +37 -0
  181. package/packages/utility/skills/utility/SKILL.md +17 -60
  182. package/packages/utility/src/answer/extract.ts +128 -0
  183. package/packages/utility/src/answer/index.ts +213 -0
  184. package/packages/utility/src/answer/reply.ts +133 -0
  185. package/packages/utility/src/answer/web.ts +167 -0
  186. package/packages/utility/src/attach/index.ts +205 -0
  187. package/packages/utility/src/commands.ts +38 -142
  188. package/packages/utility/src/diagnostics/engine.ts +50 -5
  189. package/packages/utility/src/image/index.ts +33 -0
  190. package/packages/utility/src/image/models.ts +221 -0
  191. package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
  192. package/packages/utility/src/image/settings.ts +178 -0
  193. package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
  194. package/packages/utility/src/image/tools.ts +168 -0
  195. package/packages/utility/src/image/transports.ts +186 -0
  196. package/packages/utility/src/index.ts +68 -389
  197. package/packages/utility/src/lifecycle/cleanup.ts +116 -311
  198. package/packages/utility/src/rename/gate.ts +102 -0
  199. package/packages/utility/src/rename/index.ts +191 -0
  200. package/packages/utility/src/rename/session.ts +170 -0
  201. package/packages/utility/src/render/delegated.ts +285 -0
  202. package/packages/utility/src/render/harness.ts +683 -0
  203. package/packages/utility/src/render/parse.ts +156 -0
  204. package/packages/utility/src/render/pi-settings.ts +38 -0
  205. package/packages/utility/src/render/reply-bg.ts +307 -0
  206. package/packages/utility/src/render/simple.ts +724 -0
  207. package/packages/utility/src/render/spacing.ts +235 -0
  208. package/packages/utility/src/render/styled.ts +251 -0
  209. package/packages/utility/src/render/tools.ts +353 -0
  210. package/packages/utility/src/settings.ts +166 -233
  211. package/packages/utility/src/types.ts +1 -222
  212. package/packages/watchdog/index.ts +17 -6
  213. package/packages/watchdog/package.json +3 -3
  214. package/packages/watchdog/src/config.ts +3 -2
  215. package/packages/web-api/package.json +2 -2
  216. package/packages/workflow/README.md +3 -0
  217. package/packages/workflow/index.ts +0 -2
  218. package/packages/workflow/package.json +2 -2
  219. package/packages/workflow/src/permission/decide.ts +6 -4
  220. package/packages/workflow/src/permission/index.ts +17 -7
  221. package/packages/workflow/src/permission/settings.ts +39 -4
  222. package/packages/workflow/src/plan/index.ts +19 -4
  223. package/packages/background-tasks/extensions/delegate-child.ts +0 -1
  224. package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
  225. package/packages/background-tasks/src/context-token-budget.ts +0 -903
  226. package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
  227. package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
  228. package/packages/background-tasks/src/delegate/budget.ts +0 -415
  229. package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
  230. package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
  231. package/packages/background-tasks/src/delegate/launch.ts +0 -484
  232. package/packages/background-tasks/src/delegate/result-package.ts +0 -459
  233. package/packages/background-tasks/src/delegate/runner.ts +0 -430
  234. package/packages/background-tasks/src/delegate/seed.ts +0 -423
  235. package/packages/background-tasks/src/delegate/types.ts +0 -323
  236. package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
  237. package/packages/background-tasks/src/delegate-extension.ts +0 -654
  238. package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
  239. package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
  240. package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
  241. package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
  242. package/packages/compactor/src/compaction/build-sections.ts +0 -76
  243. package/packages/compactor/src/compaction/extract/goals.ts +0 -36
  244. package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
  245. package/packages/compactor/src/compaction/format.ts +0 -53
  246. package/packages/compactor/src/compaction/merge.ts +0 -143
  247. package/packages/compactor/src/config/presets.ts +0 -160
  248. package/packages/compactor/src/executor/executor.ts +0 -233
  249. package/packages/compactor/src/executor/runtime.ts +0 -115
  250. package/packages/compactor/src/security/evaluator.ts +0 -89
  251. package/packages/compactor/src/security/policy.ts +0 -96
  252. package/packages/compactor/src/security/scanner.ts +0 -65
  253. package/packages/compactor/src/session/auto-inject.ts +0 -60
  254. package/packages/compactor/src/session/db.ts +0 -325
  255. package/packages/compactor/src/session/extract.ts +0 -144
  256. package/packages/compactor/src/session/resume-inject.ts +0 -64
  257. package/packages/compactor/src/session/snapshot.ts +0 -327
  258. package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
  259. package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
  260. package/packages/compactor/src/tools/ctx-execute.ts +0 -23
  261. package/packages/compactor/src/tools/ctx-stats.ts +0 -96
  262. package/packages/image/README.md +0 -99
  263. package/packages/image/package.json +0 -61
  264. package/packages/image/skills/image/SKILL.md +0 -76
  265. package/packages/image/src/generate.ts +0 -256
  266. package/packages/image/src/index.ts +0 -135
  267. package/packages/image/src/models.ts +0 -519
  268. package/packages/image/src/openai-images-api.ts +0 -282
  269. package/packages/image/src/register-providers.ts +0 -209
  270. package/packages/image/src/settings.ts +0 -173
  271. package/packages/image/src/tools.ts +0 -326
  272. package/packages/input-shortcuts/src/clipboard.ts +0 -119
  273. package/packages/kanboard/src/runner.ts +0 -817
  274. package/packages/skill-registry/skills/auto/SKILL.md +0 -465
  275. package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
  276. package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
  277. package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
  278. package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
  279. package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
  280. package/packages/skill-registry/skills/debug/SKILL.md +0 -228
  281. package/packages/skill-registry/skills/document/SKILL.md +0 -120
  282. package/packages/skill-registry/skills/fix/SKILL.md +0 -212
  283. package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
  284. package/packages/skill-registry/skills/plan/SKILL.md +0 -231
  285. package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
  286. package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
  287. package/packages/skill-registry/skills/research/SKILL.md +0 -251
  288. package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
  289. package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
  290. package/packages/skill-registry/skills/work/SKILL.md +0 -218
  291. package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
  292. package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
  293. package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
  294. package/packages/subagents/agents/delegate.md +0 -14
  295. package/packages/subagents/agents/oracle.md +0 -78
  296. package/packages/subagents/agents/researcher.md +0 -52
  297. package/packages/subagents/agents/reviewer.md +0 -80
  298. package/packages/subagents/agents/scout.md +0 -50
  299. package/packages/subagents/agents/worker.md +0 -59
  300. package/packages/subagents/prompts/council.md +0 -48
  301. package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
  302. package/packages/subagents/prompts/parallel-cleanup.md +0 -59
  303. package/packages/subagents/prompts/parallel-research.md +0 -50
  304. package/packages/subagents/prompts/parallel-review.md +0 -54
  305. package/packages/subagents/prompts/review-loop.md +0 -43
  306. package/packages/subagents/skills/council-mode/SKILL.md +0 -230
  307. package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
  308. package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
  309. package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
  310. package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
  311. package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
  312. package/packages/subagents/src/acceptance.ts +0 -331
  313. package/packages/subagents/src/agent-manager.ts +0 -405
  314. package/packages/subagents/src/agent-memory.ts +0 -300
  315. package/packages/subagents/src/agent-overrides.ts +0 -183
  316. package/packages/subagents/src/agent-runner.ts +0 -271
  317. package/packages/subagents/src/async-runner.ts +0 -422
  318. package/packages/subagents/src/authority-policy.ts +0 -47
  319. package/packages/subagents/src/budgets.ts +0 -285
  320. package/packages/subagents/src/child-safety.ts +0 -140
  321. package/packages/subagents/src/config.ts +0 -249
  322. package/packages/subagents/src/conversation-viewer.ts +0 -281
  323. package/packages/subagents/src/core-compat.ts +0 -94
  324. package/packages/subagents/src/custom-agents.ts +0 -343
  325. package/packages/subagents/src/file-system-retry.ts +0 -50
  326. package/packages/subagents/src/fleet-data.ts +0 -105
  327. package/packages/subagents/src/fleet-view.ts +0 -322
  328. package/packages/subagents/src/foreground-detach.ts +0 -59
  329. package/packages/subagents/src/fork-context.ts +0 -285
  330. package/packages/subagents/src/global.d.ts +0 -7
  331. package/packages/subagents/src/guide.ts +0 -129
  332. package/packages/subagents/src/mission-state.ts +0 -133
  333. package/packages/subagents/src/mission-store.ts +0 -436
  334. package/packages/subagents/src/output-limits.ts +0 -142
  335. package/packages/subagents/src/parity-types.ts +0 -315
  336. package/packages/subagents/src/pi-args.ts +0 -167
  337. package/packages/subagents/src/result-files.ts +0 -232
  338. package/packages/subagents/src/result-watcher.ts +0 -221
  339. package/packages/subagents/src/retained-children.ts +0 -176
  340. package/packages/subagents/src/run-fanout-budget.ts +0 -356
  341. package/packages/subagents/src/scheduled-runs.ts +0 -349
  342. package/packages/subagents/src/schemas.ts +0 -263
  343. package/packages/subagents/src/slash-commands.ts +0 -80
  344. package/packages/subagents/src/supervisor-channel.ts +0 -249
  345. package/packages/subagents/src/tool-handler.ts +0 -1144
  346. package/packages/subagents/src/types.ts +0 -238
  347. package/packages/subagents/src/widget.ts +0 -457
  348. package/packages/subagents/src/workflow-script.ts +0 -737
  349. package/packages/subagents/src/workflow-worker.ts +0 -384
  350. package/packages/subagents/src/worktree.ts +0 -614
  351. package/packages/utility/src/analytics/collector.ts +0 -293
  352. package/packages/utility/src/info-screen.ts +0 -80
  353. package/packages/utility/src/lifecycle/process.ts +0 -162
  354. package/packages/utility/src/prefix-cache.ts +0 -263
  355. package/packages/utility/src/skill-discovery.ts +0 -542
  356. package/packages/utility/src/tools/env.ts +0 -113
  357. package/packages/utility/src/tui/name-badge-state.ts +0 -315
  358. package/packages/utility/src/tui/name-badge.ts +0 -109
  359. /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
@@ -1,475 +1,241 @@
1
1
  /**
2
- * @pi-unipi/ask-user — Tool registration
2
+ * @pi-unipi/ask-user — the ask_user tool
3
3
  *
4
- * Registers ask_user tool for structured user input.
4
+ * One call asks 1–4 questions (Devin's shape). Runs sequentially: pi runs a
5
+ * message's tool calls in parallel by default, and several dialogs at once
6
+ * used to hide all but the last — the rest could never be answered and the
7
+ * turn hung. One dialog at a time, every question in one call.
5
8
  */
6
9
 
10
+ import { readFileSync } from "node:fs";
7
11
  import { Type } from "typebox";
8
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
- import {
10
- ASK_USER_TOOLS,
11
- COMPACTOR_INSTRUCTION,
12
- UNIPI_EVENTS,
13
- emitEvent,
14
- withHerdrBlocked,
15
- } from "@pi-unipi/core";
16
- import type { NormalizedOption, AskUserResponse, SessionLauncherResult } from "./types.js";
17
- import { renderAskUI, createRenderCall, createRenderResult } from "./ask-ui.js";
18
- import { renderLauncherUI } from "./launcher-ui.js";
19
- import { getAskUserSettings } from "./config.js";
12
+ import { Text } from "@earendil-works/pi-tui";
13
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
14
+ import { ASK_USER_TOOLS, COMPACTOR_INSTRUCTION, emitEvent, UNIPI_EVENTS, withHerdrBlocked, type Attachment } from "@pi-unipi/core";
15
+ import { AskPanel, type PanelResult } from "./ask-ui.js";
16
+ import { getAskUserSettings, type AskUserSettings } from "./config.js";
20
17
  import { queueCompactHandoff, queueDirectHandoff } from "./handoff.js";
18
+ import { renderLauncherUI } from "./launcher-ui.js";
19
+ import { answerSummary, answersText, clarifyText, HEADER_MAX, prepareArgs, type AskParams, type AskQuestion, type QuestionAnswer } from "./questions.js";
20
+ import type { SessionLauncherResult } from "./types.js";
21
21
 
22
- /**
23
- * Whether this process is a subagent child (subagents package or fusion sidekick).
24
- * Subagent children never talk to the user directly — their lead owns ambiguity.
25
- */
22
+ /** Subagent children never talk to the user directly — their lead owns ambiguity. */
26
23
  export function isSubagentChild(env: Record<string, string | undefined> = process.env): boolean {
27
24
  return env.UNIPI_SUBAGENT_CHILD === "1";
28
25
  }
29
26
 
30
- /**
31
- * Register ask-user tools.
32
- */
27
+ export interface AskDetails {
28
+ questions: AskQuestion[];
29
+ answers?: QuestionAnswer[];
30
+ outcome: "answered" | "clarify" | "cancelled" | "action" | "unavailable";
31
+ /** Legacy single-question fields, kept for older consumers (compactor, notify). */
32
+ question?: string;
33
+ attachments?: Array<Pick<Attachment, "id" | "kind" | "path" | "name">>;
34
+ }
35
+
36
+ const OPTION = Type.Object({
37
+ label: Type.String({ description: "Display text (1–5 words)" }),
38
+ description: Type.Optional(Type.String({ description: "What this option means or its trade-offs" })),
39
+ value: Type.Optional(Type.String({ description: "Returned instead of the label when set" })),
40
+ action: Type.Optional(Type.Union([Type.Literal("end_turn"), Type.Literal("new_session")], {
41
+ description: "end_turn: stop the turn when picked · new_session: hand off to `prefill` (compact & run / run directly)",
42
+ })),
43
+ prefill: Type.Optional(Type.String({ description: "Message or /command queued by a new_session option" })),
44
+ });
45
+
46
+ const QUESTION = Type.Object({
47
+ question: Type.String({ description: "The full question" }),
48
+ header: Type.String({ description: `Short chip label, e.g. "Database" (≤${HEADER_MAX} chars)` }),
49
+ options: Type.Array(OPTION, { description: "2–4 choices. An \"Other\" free-text choice is added automatically." }),
50
+ multi_select: Type.Optional(Type.Boolean({ description: "Let the user pick several (default false)" })),
51
+ other: Type.Optional(Type.Boolean({ description: "Offer \"Other (type your own)\" (default true)" })),
52
+ });
53
+
54
+ function unavailable(questions: AskQuestion[], text: string) {
55
+ return { content: [{ type: "text" as const, text }], details: { questions, outcome: "unavailable" } as AskDetails };
56
+ }
57
+
58
+ function imageParts(attachments: readonly Attachment[]) {
59
+ const parts: Array<{ type: "image"; data: string; mimeType: string }> = [];
60
+ for (const a of attachments) {
61
+ if (a.kind !== "image") continue;
62
+ try {
63
+ parts.push({ type: "image", data: readFileSync(a.path).toString("base64"), mimeType: a.mimeType ?? "image/png" });
64
+ } catch {
65
+ // unreadable: the token stays in the text
66
+ }
67
+ }
68
+ return parts;
69
+ }
70
+
71
+ /** "[File #2]" → "[File #2: /path]" so the agent can open documents. */
72
+ function expandFileTokens(answers: QuestionAnswer[], attachments: readonly Attachment[]): QuestionAnswer[] {
73
+ const files = attachments.filter((a) => a.kind === "file");
74
+ if (!files.length) return answers;
75
+ return answers.map((a) => (a.custom_text ? { ...a, custom_text: files.reduce((t, f) => t.split(`[File #${f.id}]`).join(`[File #${f.id}: ${f.path}]`), a.custom_text) } : a));
76
+ }
77
+
78
+ /** The user's settings win over what the agent asked for. */
79
+ export function applySettings(questions: AskQuestion[], settings: AskUserSettings): AskQuestion[] {
80
+ return questions.slice(0, settings.maxQuestions).map((q) => {
81
+ if (settings.other === "always" || q.options.length === 0) {
82
+ const { other: _drop, ...rest } = q;
83
+ return rest;
84
+ }
85
+ return settings.other === "never" ? { ...q, other: false } : q;
86
+ });
87
+ }
88
+
33
89
  export function registerAskUserTools(pi: ExtensionAPI): void {
90
+ const max = getAskUserSettings().maxQuestions;
34
91
  pi.registerTool({
35
92
  name: ASK_USER_TOOLS.ASK,
36
93
  label: "Ask User",
37
94
  description:
38
- "Ask the user a question with structured options. Supports single-select, " +
39
- "multi-select, and freeform text input. Use for decisions, preferences, " +
40
- "and clarifications that require explicit user input.",
41
- promptSnippet: "Ask the user a structured question with options.",
95
+ `Ask the user 1–${max} multiple-choice question${max === 1 ? "" : "s"} in one dialog and wait for the answers. ` +
96
+ "Each question has a short header, 2–4 options (label + description) and optional multi-select; " +
97
+ "an \"Other\" free-text choice is always added. The user may skip questions.",
98
+ promptSnippet: "Ask the user multiple-choice questions and wait for the answers.",
42
99
  promptGuidelines: [
43
- "Use ask_user when you need explicit user input before proceeding.",
44
- "Good for: architectural trade-offs, ambiguous requirements, user preferences, confirming destructive operations.",
45
- "Provide clear options with labels and optional descriptions.",
46
- "Use allowMultiple for multi-select scenarios (e.g., choosing features to enable).",
47
- "Use allowFreeform: false to restrict to predefined options only.",
48
- "Use action: 'input' on an option to let the user add custom text before submitting.",
49
- "Use action: 'end_turn' on an option to let the user signal end of turn.",
50
- "Use action: 'new_session' with prefill to let the user hand off to a queued follow-up message or slash command."
100
+ "Use ask_user when a decision, preference or clarification needs the user before you continue.",
101
+ `Put every question you need right now in ONE call (1–${max} question${max === 1 ? "" : "s"}) — never several ask_user calls at once.`,
102
+ "Give each question a short header (≤16 chars) and 2–4 options with a clear description; don't add an 'Other' option yourself.",
103
+ "Use multi_select when several answers can apply.",
104
+ "Skipped questions come back as skipped: respect that, don't ask them again unless you must.",
105
+ "If the user says they're not ready, ask what they want to clarify instead of re-asking.",
106
+ "Options can carry action: 'end_turn' or action: 'new_session' with a prefill for workflow handoffs.",
51
107
  ],
52
108
  parameters: Type.Object({
53
- question: Type.String({
54
- description: "The question to ask the user",
55
- }),
56
- context: Type.Optional(
57
- Type.String({
58
- description: "Additional context shown before the question",
59
- }),
60
- ),
61
- options: Type.Optional(
62
- Type.Array(
63
- Type.Object({
64
- label: Type.String({ description: "Display label" }),
65
- description: Type.Optional(
66
- Type.String({ description: "Optional description shown below label" }),
67
- ),
68
- value: Type.Optional(
69
- Type.String({
70
- description: "Value returned when selected (defaults to label)",
71
- }),
72
- ),
73
- allowCustom: Type.Optional(
74
- Type.Boolean({
75
- description:
76
- "When true, selecting this option enters text input mode " +
77
- "so the user can add a custom comment before submitting.",
78
- }),
79
- ),
80
- action: Type.Optional(
81
- Type.Union(
82
- [
83
- Type.Literal("select"),
84
- Type.Literal("input"),
85
- Type.Literal("end_turn"),
86
- Type.Literal("new_session"),
87
- ],
88
- {
89
- description:
90
- "Special action: 'select' (default), 'input' (text input), " +
91
- "'end_turn' (signal end of turn), 'new_session' (queue handoff with prefill).",
92
- },
93
- ),
94
- ),
95
- prefill: Type.Optional(
96
- Type.String({
97
- description: "Prefill message for new_session action.",
98
- }),
99
- ),
100
- }),
101
- {
102
- description:
103
- "Multiple-choice options. Omit for freeform-only input.",
104
- },
105
- ),
106
- ),
107
- allowMultiple: Type.Optional(
108
- Type.Boolean({
109
- description: "Enable multi-select mode (default: false)",
110
- }),
111
- ),
112
- allowFreeform: Type.Optional(
113
- Type.Boolean({
114
- description: "Allow freeform text input (default: true)",
115
- }),
116
- ),
117
- timeout: Type.Optional(
118
- Type.Number({
119
- description: "Auto-dismiss after N milliseconds",
120
- }),
121
- ),
109
+ questions: Type.Array(QUESTION, { description: `1–${max} question${max === 1 ? "" : "s"}` }),
122
110
  }),
111
+ // Older calls ({ question, context, options, allowMultiple, allowFreeform, timeout }) still work.
112
+ prepareArguments: (args: unknown) => prepareArgs(args) as never,
113
+ executionMode: "sequential",
123
114
 
124
115
  async execute(_toolCallId, params, _signal, _onUpdate, ctx: ExtensionContext) {
125
- const {
126
- question,
127
- context,
128
- options: rawOptions,
129
- allowMultiple = false,
130
- allowFreeform = true,
131
- timeout,
132
- } = params as {
133
- question: string;
134
- context?: string;
135
- options?: { label: string; description?: string; value?: string; allowCustom?: boolean; action?: string; prefill?: string }[];
136
- allowMultiple?: boolean;
137
- allowFreeform?: boolean;
138
- timeout?: number;
139
- };
140
-
141
- // Subagent children have no user to talk to. Refuse loudly so the
142
- // subagent surfaces the question to its lead instead of guessing.
116
+ const settings = getAskUserSettings(ctx.cwd);
117
+ const questions = applySettings((prepareArgs(params) as AskParams).questions, settings);
143
118
  if (isSubagentChild()) {
144
- return {
145
- content: [
146
- {
147
- type: "text",
148
- text:
149
- "ask_user is not available inside a subagent. You cannot talk to the user directly — only your lead can. Do not guess or assume an answer. Stop and state the question, the options you considered, and your recommendation in your report so your lead can decide.",
150
- },
151
- ],
152
- isError: true,
153
- details: {
154
- question,
155
- response: {
156
- kind: "cancelled",
157
- comment: "ask_user is not available inside a subagent",
158
- } as AskUserResponse,
159
- },
160
- };
161
- }
162
-
163
- // Check settings
164
- const settings = getAskUserSettings();
165
- if (!settings.enabled) {
166
- return {
167
- content: [
168
- {
169
- type: "text",
170
- text: "Error: ask_user tool is disabled in settings.",
171
- },
172
- ],
173
- details: {
174
- question,
175
- response: {
176
- kind: "cancelled",
177
- comment: "Tool disabled",
178
- } as AskUserResponse,
179
- },
180
- };
181
- }
182
-
183
- // Validate requested format against allowed formats
184
- if (allowMultiple && !settings.allowedFormats.multiSelect) {
185
- return {
186
- content: [
187
- {
188
- type: "text",
189
- text: "Error: Multi-select questions are disabled in settings.",
190
- },
191
- ],
192
- details: {
193
- question,
194
- response: {
195
- kind: "cancelled",
196
- comment: "Multi-select disabled",
197
- } as AskUserResponse,
198
- },
199
- };
200
- }
201
-
202
- if (!allowMultiple && !settings.allowedFormats.singleSelect) {
203
- return {
204
- content: [
205
- {
206
- type: "text",
207
- text: "Error: Single-select questions are disabled in settings.",
208
- },
209
- ],
210
- details: {
211
- question,
212
- response: {
213
- kind: "cancelled",
214
- comment: "Single-select disabled",
215
- } as AskUserResponse,
216
- },
217
- };
218
- }
219
-
220
- if (allowFreeform && !settings.allowedFormats.freeform) {
221
- return {
222
- content: [
223
- {
224
- type: "text",
225
- text: "Error: Freeform questions are disabled in settings.",
226
- },
227
- ],
228
- details: {
229
- question,
230
- response: {
231
- kind: "cancelled",
232
- comment: "Freeform disabled",
233
- } as AskUserResponse,
234
- },
235
- };
236
- }
237
-
238
- // Validate: need UI
239
- if (!ctx.hasUI) {
240
- return {
241
- content: [
242
- {
243
- type: "text",
244
- text: "Error: UI not available (running in non-interactive mode)",
245
- },
246
- ],
247
- details: {
248
- question,
249
- response: {
250
- kind: "cancelled",
251
- comment: "No UI available",
252
- } as AskUserResponse,
253
- },
254
- };
255
- }
256
-
257
- // Validate: need options or freeform
258
- const options = rawOptions || [];
259
- if (options.length === 0 && !allowFreeform) {
260
- return {
261
- content: [
262
- {
263
- type: "text",
264
- text: "Error: No options provided and allowFreeform is false. Provide options or enable freeform.",
265
- },
266
- ],
267
- details: {
268
- question,
269
- response: {
270
- kind: "cancelled",
271
- comment: "No options and no freeform",
272
- } as AskUserResponse,
273
- },
274
- };
119
+ throw new Error(
120
+ "ask_user is not available inside a subagent. You cannot talk to the user directly — only your lead can. Do not guess an answer: state the question, the options you considered and your recommendation in your report.",
121
+ );
275
122
  }
123
+ if (!settings.enabled) return unavailable(questions, "ask_user is turned off in settings — ask in your reply instead.");
124
+ if (!ctx.hasUI) return unavailable(questions, "No interactive UI (non-interactive mode) — ask in your reply instead.");
125
+ if (questions.length === 0) throw new Error("ask_user needs at least one question with a question text.");
276
126
 
277
- // Normalize options — resolve value (defaults to label)
278
- const normalizedOptions: NormalizedOption[] = options.map((opt) => ({
279
- label: opt.label,
280
- description: opt.description,
281
- value: opt.value ?? opt.label,
282
- allowCustom: opt.allowCustom ?? false,
283
- action: (opt.action as NormalizedOption["action"]) ?? "select",
284
- prefill: opt.prefill,
285
- }));
286
- const detailsBase = {
287
- question,
288
- context,
289
- options: normalizedOptions,
290
- allowMultiple,
291
- allowFreeform,
292
- };
293
-
294
- // Emit ASK_USER_PROMPT event if notifyOnAsk is enabled
295
127
  if (settings.notifyOnAsk) {
296
128
  emitEvent(pi, UNIPI_EVENTS.ASK_USER_PROMPT, {
297
- question,
298
- context,
299
- optionCount: normalizedOptions.length,
300
- allowMultiple,
301
- allowFreeform,
129
+ question: questions.map((q) => q.question).join(" · "),
130
+ optionCount: questions.reduce((n, q) => n + q.options.length, 0),
131
+ allowMultiple: questions.some((q) => q.multi_select),
132
+ allowFreeform: questions.some((q) => q.other !== false),
302
133
  });
303
134
  }
304
135
 
305
- // Render interactive UI
306
- const result = await withHerdrBlocked(
307
- pi,
308
- "ask_user",
309
- () => ctx.ui.custom<{ response: AskUserResponse } | null>(
310
- renderAskUI({
311
- question,
312
- context,
313
- options: normalizedOptions,
314
- allowMultiple,
315
- allowFreeform,
316
- timeout,
317
- }),
318
- ),
136
+ const result = await withHerdrBlocked(pi, "ask_user", () =>
137
+ ctx.ui.custom<PanelResult>((tui, theme, _kb, done) =>
138
+ new AskPanel(tui, theme, questions, done, undefined, { escape: settings.escape, digitAdvance: settings.digitAdvance, helpLine: settings.helpLine })),
319
139
  );
140
+ const legacy = questions.length === 1 ? { question: questions[0]!.question } : {};
320
141
 
321
- // Handle cancel
322
- if (!result) {
323
- return {
324
- content: [
325
- {
326
- type: "text",
327
- text: "User cancelled the selection",
328
- },
329
- ],
330
- details: {
331
- ...detailsBase,
332
- response: {
333
- kind: "cancelled",
334
- } as AskUserResponse,
335
- },
336
- };
142
+ if (!result || result.type === "cancel") {
143
+ // Esc stops the turn, like Devin's "Canceled due to user interrupt" —
144
+ // `terminate` ends it after this call, cleanly (no abort error line).
145
+ return { content: [{ type: "text" as const, text: "The user cancelled the questions and stopped the turn. Wait for their next message." }], details: { questions, outcome: "cancelled", ...legacy } as AskDetails, terminate: true };
337
146
  }
338
-
339
- // Build response content
340
- const response = result.response;
341
- let contentText: string;
342
-
343
- switch (response.kind) {
344
- case "selection": {
345
- const selections = response.selections || [];
346
- contentText =
347
- selections.length === 1
348
- ? `User selected: ${selections[0]}`
349
- : `User selected: ${selections.join(", ")}`;
350
- break;
351
- }
352
- case "freeform":
353
- contentText = `User wrote: ${response.text}`;
354
- break;
355
- case "combined": {
356
- const selections = response.selections || [];
357
- const selText = selections.length === 1
358
- ? selections[0]
359
- : selections.join(", ");
360
- contentText = `User selected: ${selText} and wrote: ${response.text}`;
361
- break;
362
- }
363
- case "end_turn":
364
- // Abort the agent immediately — no LLM follow-up, no wasted tokens.
365
- // The tool result is still recorded in session history.
366
- ctx.abort();
367
- contentText = "User chose to end the turn.";
368
- break;
369
- case "new_session":
370
- contentText = response.prefill
371
- ? `User chose to hand off to: ${response.prefill}`
372
- : "User chose a handoff without a prefill.";
373
- break;
374
- case "timed_out":
375
- contentText = "User did not respond (timed out)";
376
- break;
377
- default:
378
- contentText = "No response";
147
+ if (result.type === "clarify") {
148
+ return { content: [{ type: "text" as const, text: clarifyText(questions, result.answers) }], details: { questions, answers: result.answers, outcome: "clarify", ...legacy } as AskDetails };
379
149
  }
380
-
381
- // Session launcher intercept: when user selects new_session, offer compact/direct/cancel
382
- if (response.kind === "new_session") {
383
- const prefill = response.prefill || "";
384
- const launcherResult = await withHerdrBlocked(
385
- pi,
386
- "ask_user: launch",
387
- () => ctx.ui.custom<SessionLauncherResult | null>(
388
- renderLauncherUI({ prefill }),
389
- ),
390
- );
391
-
392
- if (!launcherResult || launcherResult.action === "cancel") {
393
- return {
394
- content: [{ type: "text", text: "User cancelled the session launch" }],
395
- details: {
396
- ...detailsBase,
397
- response: {
398
- kind: "cancelled",
399
- comment: "Session launcher cancelled",
400
- } as AskUserResponse,
401
- },
402
- };
403
- }
404
-
405
- const handoff = launcherResult.action === "compact"
406
- ? queueCompactHandoff({
407
- pi,
408
- ctx,
409
- prefill,
410
- // Use the compactor sentinel so @pi-unipi/compactor's zero-LLM
411
- // pipeline intercepts. If compactor is not installed, Pi's built-in
412
- // LLM-based compaction runs instead.
413
- customInstructions: `${COMPACTOR_INSTRUCTION}\nPreparing for new task. Summarize previous work concisely, preserving only what's essential for: ${prefill}`,
414
- })
415
- : queueDirectHandoff(pi, ctx, prefill);
416
-
417
- if (handoff.status === "cancelled") {
418
- return {
419
- content: [{ type: "text", text: "Session launch cancelled: no prefill message was provided." }],
420
- details: {
421
- ...detailsBase,
422
- response: {
423
- kind: "cancelled",
424
- comment: "Session launcher had no prefill to queue",
425
- launchStatus: handoff.status,
426
- launchReason: handoff.reason,
427
- } as AskUserResponse,
428
- },
429
- };
430
- }
431
-
432
- if (handoff.status !== "failed") {
433
- // Handoff is scheduled/queued or recoverably editor-prefilled; abort the
434
- // current turn so the queued command can run without LLM follow-up.
435
- ctx.abort();
436
- }
437
-
438
- const launchLabel = launcherResult.action === "compact" ? "compact" : "direct";
439
- if (handoff.status === "editor_prefill") {
440
- contentText = `Queued ${launchLabel} handoff fell back to editor prefill: ${handoff.prefill}`;
441
- } else if (handoff.status === "failed") {
442
- contentText = `Failed to queue ${launchLabel} handoff: ${handoff.prefill ?? prefill}`;
443
- } else {
444
- contentText = `Queued ${launchLabel} handoff: ${handoff.prefill ?? prefill}`;
445
- }
446
-
447
- return {
448
- content: [{ type: "text", text: contentText }],
449
- details: {
450
- ...detailsBase,
451
- response: {
452
- ...response,
453
- prefill: handoff.prefill ?? prefill,
454
- launchedWith: launcherResult.action,
455
- launchStatus: handoff.status,
456
- launchReason: handoff.reason,
457
- launchError: handoff.error,
458
- },
459
- },
460
- };
150
+ if (result.type === "action") {
151
+ return runAction(pi, ctx, questions, result, legacy);
461
152
  }
462
153
 
154
+ const answers = expandFileTokens(result.answers, result.attachments);
155
+ const images = imageParts(result.attachments);
156
+ const names = result.attachments.map((a) => (a.kind === "image" ? `[Image #${a.id}]` : `[File #${a.id}]`));
463
157
  return {
464
- content: [{ type: "text", text: contentText }],
158
+ content: [{ type: "text" as const, text: answersText(questions, answers, images.length ? names : []) }, ...images],
465
159
  details: {
466
- ...detailsBase,
467
- response,
468
- },
160
+ questions,
161
+ answers,
162
+ outcome: "answered",
163
+ ...legacy,
164
+ ...(result.attachments.length ? { attachments: result.attachments.map(({ id, kind, path, name }) => ({ id, kind, path, name })) } : {}),
165
+ } as AskDetails,
469
166
  };
470
167
  },
471
168
 
472
- renderCall: createRenderCall(),
473
- renderResult: createRenderResult(),
169
+ renderCall: () => new Text("", 0, 0),
170
+ renderResult: (result, _options, theme, _context) => renderAskResult(result.details as AskDetails | undefined, theme as Theme),
171
+ renderShell: "self",
172
+ // The simple render style collapses every tool to one row — the Q→A is
173
+ // the whole point of ask_user, so keep compact `header: answer` rows
174
+ // under the `Asked user` line (utility's simpleResult hook), and hide the
175
+ // `· N output lines` meta (the answers are the output).
176
+ ...( {
177
+ simpleMeta: () => "",
178
+ simpleResult: (result: { details?: AskDetails }, theme: Theme) => askRows(result.details, theme),
179
+ } as object),
474
180
  });
475
181
  }
182
+
183
+ /** Compact Q→A rows for the simple render style's collapsed view. */
184
+ export function askRows(details: AskDetails | undefined, theme: Theme): string[] {
185
+ const questions = details?.questions ?? [];
186
+ if (details === undefined || questions.length === 0) return [];
187
+ if (details.outcome === "cancelled") return [` ${theme.fg("error", "Canceled by the user")}`];
188
+ if (details.outcome === "clarify") return [` ${theme.fg("warning", "Not ready to answer — wants to clarify first")}`];
189
+ if (details.outcome === "unavailable") return [` ${theme.fg("dim", "not shown (no interactive UI or turned off)")}`];
190
+ if (details.outcome === "action") return [` ${theme.fg("muted", "picked an action")}`];
191
+ return questions.map((q, i) => ` ${theme.fg("muted", `${q.header}: ${answerSummary(q, details.answers?.[i])}`)}`);
192
+ }
193
+
194
+ /** end_turn / new_session options (workflow handoffs). */
195
+ async function runAction(pi: ExtensionAPI, ctx: ExtensionContext, questions: AskQuestion[], result: Extract<PanelResult, { type: "action" }>, legacy: Record<string, unknown>) {
196
+ const opt = result.option;
197
+ const details = { questions, answers: result.answers, outcome: "action", ...legacy } as AskDetails;
198
+ if (opt.action === "end_turn") {
199
+ return { content: [{ type: "text" as const, text: `User chose "${opt.label}" and ended the turn.` }], details, terminate: true };
200
+ }
201
+ const prefill = opt.prefill ?? "";
202
+ const launch = await withHerdrBlocked(pi, "ask_user: launch", () => ctx.ui.custom<SessionLauncherResult | null>(renderLauncherUI({ prefill })));
203
+ if (!launch || launch.action === "cancel") {
204
+ return { content: [{ type: "text" as const, text: `User picked "${opt.label}" but cancelled the handoff.` }], details };
205
+ }
206
+ const handoff = launch.action === "compact"
207
+ ? queueCompactHandoff({ pi, ctx, prefill, customInstructions: `${COMPACTOR_INSTRUCTION}\nPreparing for new task. Summarize previous work concisely, preserving only what's essential for: ${prefill}` })
208
+ : queueDirectHandoff(pi, ctx, prefill);
209
+ if (handoff.status !== "failed" && handoff.status !== "cancelled") ctx.abort();
210
+ const text = handoff.status === "failed" ? `Failed to queue the ${launch.action} handoff: ${prefill}` : `Queued ${launch.action} handoff: ${handoff.prefill ?? prefill}`;
211
+ return { content: [{ type: "text" as const, text }], details };
212
+ }
213
+
214
+ /**
215
+ * ● Asked user 3 questions ● Asked user Coffee or tea?
216
+ * │ Planet: Jupiter moon └ Tea
217
+ * └ Last book: Dune
218
+ */
219
+ export function renderAskResult(details: AskDetails | undefined, theme: Theme): Text {
220
+ const t = theme;
221
+ const questions = details?.questions ?? [];
222
+ if (!questions.length) return new Text("", 0, 0);
223
+ const single = questions.length === 1;
224
+ const title = single ? questions[0]!.question.replace(/\s+/g, " ") : `${t.fg("dim", String(questions.length))} questions`;
225
+ const failed = details!.outcome === "cancelled" || details!.outcome === "clarify" || details!.outcome === "unavailable";
226
+ const bullet = failed ? t.fg("error", "●") : t.fg("success", "●");
227
+ const lines = [` ${bullet} Asked user ${title}`];
228
+ const rows: string[] = [];
229
+ if (details!.outcome === "cancelled") rows.push(t.fg("error", "Canceled by the user"));
230
+ else if (details!.outcome === "clarify") rows.push(t.fg("warning", "Not ready to answer — wants to clarify first"));
231
+ else if (details!.outcome === "unavailable") rows.push(t.fg("dim", "not shown (no interactive UI or turned off)"));
232
+ else {
233
+ questions.forEach((q, i) => {
234
+ const a = details!.answers?.[i];
235
+ const text = answerSummary(q, a);
236
+ rows.push(single ? t.fg("muted", text) : t.fg("muted", `${q.header}: ${text}`));
237
+ });
238
+ }
239
+ rows.forEach((r, i) => lines.push(` ${t.fg("borderMuted", i === rows.length - 1 ? "└" : "│")} ${r}`));
240
+ return new Text(lines.join("\n"), 0, 0);
241
+ }