@selesai/code 0.5.20 → 0.5.22

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 (302) hide show
  1. package/README.md +1 -1
  2. package/dist/core/agent-session-skill-block.test.d.ts +2 -0
  3. package/dist/core/agent-session-skill-block.test.d.ts.map +1 -0
  4. package/dist/core/agent-session-skill-block.test.js +20 -0
  5. package/dist/core/agent-session-skill-block.test.js.map +1 -0
  6. package/dist/core/agent-session.d.ts +7 -4
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +22 -12
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/skills.d.ts.map +1 -1
  11. package/dist/core/skills.js +1 -1
  12. package/dist/core/skills.js.map +1 -1
  13. package/dist/extensions/handoff-new.test.ts +8 -9
  14. package/dist/extensions/handoff-new.ts +6 -8
  15. package/dist/extensions/inline-skills.test.ts +96 -0
  16. package/dist/extensions/inline-skills.ts +92 -0
  17. package/dist/extensions/package.json +1 -0
  18. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +39 -0
  19. package/dist/extensions/pi-powerline-footer/tps.ts +172 -113
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  21. package/dist/extensions/pi-subagents/README.md +602 -220
  22. package/dist/extensions/pi-subagents/banner.png +0 -0
  23. package/dist/extensions/pi-subagents/index.ts +1 -0
  24. package/dist/extensions/pi-subagents/install.mjs +3 -3
  25. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  26. package/dist/extensions/pi-subagents/package.json +26 -14
  27. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  28. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  29. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  30. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  31. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  32. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  33. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  34. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  35. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  36. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  37. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  38. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  39. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  40. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  41. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  42. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  43. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  44. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  45. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  46. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  47. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  48. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  49. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  50. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  51. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  52. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  53. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  54. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  55. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  56. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  57. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  58. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  59. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  60. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  61. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  62. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  63. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  65. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  66. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  67. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  68. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  69. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  70. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  71. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  72. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  73. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  74. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  75. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  76. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  77. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  78. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  79. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  80. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  81. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  82. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  83. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  84. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  85. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  86. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  87. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  88. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  89. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  90. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  92. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  93. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  94. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  95. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  96. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  97. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  98. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  99. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  100. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  101. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  102. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  103. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  104. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  105. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  106. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  107. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  108. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  109. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  110. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  111. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  112. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  113. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  114. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  115. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  116. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  117. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  118. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  119. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  120. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  121. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  122. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  123. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  124. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  125. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  126. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  127. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  128. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  129. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  130. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  131. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  132. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  133. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  134. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  135. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  136. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  137. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  138. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  139. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  140. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  141. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  142. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  143. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  144. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  145. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  146. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  147. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  148. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  149. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  150. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  151. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  152. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  153. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  154. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  155. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  156. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  157. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  158. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  159. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  160. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  161. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  162. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  163. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  164. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  165. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  166. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  167. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  168. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  169. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  170. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  171. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  172. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  173. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  174. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  175. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  176. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  177. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  178. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  179. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  180. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  181. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  182. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  183. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  184. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  185. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  186. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  187. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  188. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  189. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  190. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  191. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  192. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  193. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  194. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  195. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  196. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  197. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  198. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  199. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  200. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  201. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  202. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  203. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  204. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  205. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  206. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  207. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  208. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  209. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  210. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  211. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  212. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  213. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  214. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  215. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  216. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  217. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  218. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  219. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  220. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  221. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  222. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  223. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  224. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  225. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  226. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  227. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  228. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  229. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  230. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  231. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  232. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  233. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  234. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  235. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  236. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  237. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  238. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  239. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  240. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  241. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  242. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  243. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  244. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  245. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  246. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  247. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  248. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  249. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  250. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  251. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  252. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  253. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  254. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  255. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  256. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  257. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  258. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  259. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  260. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  261. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  262. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  263. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  264. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  265. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  266. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  267. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  268. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  269. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  270. package/dist/extensions/question/batch.ts +103 -0
  271. package/dist/extensions/question/constants.ts +1 -1
  272. package/dist/extensions/question/dialog-adapter.ts +23 -1
  273. package/dist/extensions/question/helpers.ts +34 -78
  274. package/dist/extensions/question/index.ts +412 -419
  275. package/dist/extensions/question/package.json +2 -1
  276. package/dist/extensions/question/question-list.ts +21 -14
  277. package/dist/extensions/question/row-layout.ts +2 -2
  278. package/dist/extensions/question/schemas.ts +38 -39
  279. package/dist/extensions/question/selection-mode.ts +5 -7
  280. package/dist/extensions/question/tests/batch.test.ts +75 -0
  281. package/dist/extensions/question/tests/helpers.test.ts +31 -156
  282. package/dist/extensions/question/tests/question-list.test.ts +17 -3
  283. package/dist/extensions/question/tests/row-layout.test.ts +4 -4
  284. package/dist/extensions/question/tests/schemas.test.ts +30 -0
  285. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  286. package/dist/extensions/question/tests/wizard.test.ts +99 -0
  287. package/dist/extensions/question/tui-adapter.ts +2 -3
  288. package/dist/extensions/question/types.ts +75 -39
  289. package/dist/extensions/question/ui-protocol.ts +3 -3
  290. package/dist/extensions/workflow/adapter.ts +4 -76
  291. package/dist/index.d.ts +1 -1
  292. package/dist/index.d.ts.map +1 -1
  293. package/dist/index.js +1 -1
  294. package/dist/index.js.map +1 -1
  295. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  296. package/dist/modes/interactive/interactive-mode.js +10 -10
  297. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  298. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  299. package/dist/skills/workflow-creation/SKILL.md +2 -2
  300. package/docs/skills.md +8 -0
  301. package/docs/workflows.md +4 -4
  302. package/package.json +1 -1
@@ -1,6 +1,4 @@
1
- // Question extension entry point.
2
- // Owns: QuestionComponent (mode-switching + editor lifecycle + box borders),
3
- // tool registration, execute() orchestration via UIProtocol.
1
+ // Batch-first interactive question tool.
4
2
 
5
3
  import type { ExtensionAPI, KeybindingsManager, Theme } from "@selesai/code";
6
4
  import {
@@ -9,563 +7,558 @@ import {
9
7
  Editor,
10
8
  type EditorTheme,
11
9
  Key,
12
- Markdown,
13
10
  matchesKey,
14
11
  Spacer,
15
12
  Text,
16
13
  type TUI,
17
14
  truncateToWidth,
15
+ wrapTextWithAnsi,
18
16
  } from "@earendil-works/pi-tui";
19
17
 
20
- import { BOX_BORDER_LEFT, BOX_BORDER_OVERHEAD, BOX_BORDER_RIGHT, QUESTION_VERSION } from "./constants.ts";
21
- import { DialogFallback } from "./dialog-adapter.ts";
22
- import { createFreeformResponse, createSelectionResponse, formatOptionsForMessage, formatResponseSummary, getOptionsFormatError, normalizeOptions, oneLine, safeMarkdownTheme } from "./helpers.ts";
18
+ import { buildQuestionAnswers, prepareQuestions, saveDraftQuestion } from "./batch.ts";
19
+ import { BOX_BORDER_LEFT, BOX_BORDER_OVERHEAD, BOX_BORDER_RIGHT, DISABLED_SHORTCUT, QUESTION_VERSION } from "./constants.ts";
20
+ import { createSelectionResponse, createTextResponse, formatQuestionResponse, formatResponseSummary, oneLine, previewText, trimText } from "./helpers.ts";
23
21
  import { QuestionList } from "./question-list.ts";
24
22
  import { MultiSelect, SingleSelect } from "./selection-mode.ts";
25
- import { resolveShortcuts } from "./shortcuts.ts";
26
23
  import { QuestionParamsSchema } from "./schemas.ts";
27
- import type { QuestionDetails, QuestionMode, QuestionOption, QuestionResponse, RawOption, ResolvedShortcuts } from "./types.ts";
28
- import { createTUIProtocol } from "./tui-adapter.ts";
29
- import type { CustomFactory, UIProtocol } from "./ui-protocol.ts";
30
-
31
- // ---------------------------------------------------------------------------
32
- // Box borders (manual ANSI components, self-contained)
33
- // ---------------------------------------------------------------------------
24
+ import type {
25
+ DraftQuestionState,
26
+ PreparedQuestion,
27
+ QuestionAnswer,
28
+ QuestionMode,
29
+ QuestionResponse,
30
+ QuestionToolResult,
31
+ RawQuestion,
32
+ } from "./types.ts";
34
33
 
35
34
  class BoxBorderTop implements Component {
36
35
  constructor(
37
- private color: (s: string) => string,
38
- private title?: string,
39
- private titleColor?: (s: string) => string,
36
+ private color: (value: string) => string,
37
+ private title: string,
38
+ private titleColor: (value: string) => string,
40
39
  ) {}
41
40
  invalidate(): void {}
42
41
  render(width: number): string[] {
43
42
  const inner = Math.max(0, width - 2);
44
- if (!this.title || inner < this.title.length + 4) {
45
- return [this.color(`╭${"─".repeat(inner)}╮`)];
46
- }
47
43
  const label = ` ${this.title} `;
48
- const remaining = inner - 1 - label.length;
49
- const titleStyle = this.titleColor ?? this.color;
50
- return [this.color("╭─") + titleStyle(label) + this.color("─".repeat(Math.max(0, remaining)) + "╮")];
44
+ if (inner < label.length + 2) return [this.color(`╭${"─".repeat(inner)}╮`)];
45
+ return [this.color("╭─") + this.titleColor(label) + this.color(`${"─".repeat(Math.max(0, inner - label.length - 1))}╮`)];
51
46
  }
52
47
  }
53
48
 
54
49
  class BoxBorderBottom implements Component {
55
50
  constructor(
56
- private color: (s: string) => string,
57
- private label?: string,
58
- private labelColor?: (s: string) => string,
51
+ private color: (value: string) => string,
52
+ private label: string,
53
+ private labelColor: (value: string) => string,
59
54
  ) {}
60
55
  invalidate(): void {}
61
56
  render(width: number): string[] {
62
57
  const inner = Math.max(0, width - 2);
63
- if (!this.label || inner < this.label.length + 4) {
64
- return [this.color(`╰${"─".repeat(inner)}╯`)];
65
- }
66
- const tag = ` ${this.label} `;
67
- const leftDashes = inner - tag.length - 1;
68
- const style = this.labelColor ?? this.color;
69
- return [this.color("╰" + "─".repeat(Math.max(0, leftDashes))) + style(tag) + this.color("─╯")];
58
+ const label = ` ${this.label} `;
59
+ if (inner < label.length + 2) return [this.color(`╰${"─".repeat(inner)}╯`)];
60
+ return [this.color(`╰${"─".repeat(Math.max(0, inner - label.length - 1))}`) + this.labelColor(label) + this.color("─╯")];
70
61
  }
71
62
  }
72
63
 
73
- function createEditorTheme(theme: Theme): EditorTheme {
64
+ function editorTheme(theme: Theme): EditorTheme {
74
65
  return {
75
- borderColor: (s) => theme.fg("accent", s),
66
+ borderColor: (value) => theme.fg("accent", value),
76
67
  selectList: {
77
- selectedPrefix: (t) => theme.fg("accent", t),
78
- selectedText: (t) => theme.fg("accent", t),
79
- description: (t) => theme.fg("muted", t),
80
- scrollInfo: (t) => theme.fg("dim", t),
81
- noMatch: (t) => theme.fg("warning", t),
68
+ selectedPrefix: (value) => theme.fg("accent", value),
69
+ selectedText: (value) => theme.fg("accent", value),
70
+ description: (value) => theme.fg("muted", value),
71
+ scrollInfo: (value) => theme.fg("dim", value),
72
+ noMatch: (value) => theme.fg("warning", value),
82
73
  },
83
74
  };
84
75
  }
85
76
 
86
- // ---------------------------------------------------------------------------
87
- // Question component (Container-based layout)
88
- // ---------------------------------------------------------------------------
89
-
90
77
  class QuestionComponent extends Container implements Component {
91
- private mode: QuestionMode = "select";
92
- private pendingSelections: string[] = [];
93
- private freeformDraft = "";
94
- private commentDraft = "";
95
-
96
- private titleText: Text;
97
- private questionText: Text;
98
- private contextComponent?: Component;
99
- private modeContainer: Container;
100
- private helpText: Text;
101
-
78
+ private mode: QuestionMode;
79
+ private selectionValues: string[] = [];
80
+ private otherDraft = "";
102
81
  private list?: QuestionList;
103
82
  private editor?: Editor;
104
-
105
- private _focused = false;
83
+ private editorActive = true;
84
+ private focusedState = false;
85
+ private answerText = new Text("", 1, 0);
86
+ private modeContainer = new Container();
87
+ private helpText = new Text("", 1, 0);
106
88
 
107
89
  constructor(
108
- private question: string,
109
- private context: string | undefined,
110
- private options: QuestionOption[],
111
- private allowMultiple: boolean,
112
- private allowFreeform: boolean,
113
- private allowComment: boolean,
90
+ private question: PreparedQuestion,
114
91
  private tui: TUI,
115
92
  private theme: Theme,
116
93
  private keybindings: KeybindingsManager,
117
- private shortcuts: ResolvedShortcuts,
118
- private onDone: (result: QuestionResponse | null) => void,
94
+ private onCancel: () => void,
95
+ initialResponse?: QuestionResponse,
119
96
  ) {
120
97
  super();
121
-
122
- this.addChild(
123
- new BoxBorderTop(
124
- (s: string) => theme.fg("accent", s),
125
- "question",
126
- (s: string) => theme.fg("dim", theme.bold(s)),
127
- ),
128
- );
98
+ this.mode = question.type === "text" ? "text" : "select";
99
+ this.addChild(new BoxBorderTop((value) => theme.fg("accent", value), "question", (value) => theme.fg("dim", theme.bold(value))));
129
100
  this.addChild(new Spacer(1));
130
-
131
- this.titleText = new Text("", 1, 0);
132
- this.addChild(this.titleText);
133
- this.addChild(new Spacer(1));
134
-
135
- this.questionText = new Text("", 1, 0);
136
- this.addChild(this.questionText);
137
-
138
- if (this.context) {
101
+ this.addChild(new Text(theme.fg("text", theme.bold(question.question)), 1, 0));
102
+ if (question.context) {
139
103
  this.addChild(new Spacer(1));
140
- const mdTheme = safeMarkdownTheme();
141
- if (mdTheme) {
142
- this.contextComponent = new Markdown(`**Context:**\n${this.context}`, 1, 0, mdTheme);
143
- } else {
144
- this.contextComponent = new Text(`${theme.fg("accent", theme.bold("Context:"))}\n${theme.fg("dim", this.context)}`, 1, 0);
145
- }
146
- this.addChild(this.contextComponent);
104
+ this.addChild(new Text(`${theme.fg("accent", theme.bold("Context:"))}\n${theme.fg("dim", question.context)}`, 1, 0));
147
105
  }
148
-
149
106
  this.addChild(new Spacer(1));
150
-
151
- this.modeContainer = new Container();
107
+ this.addChild(this.answerText);
108
+ this.addChild(new Spacer(1));
152
109
  this.addChild(this.modeContainer);
153
-
154
110
  this.addChild(new Spacer(1));
155
- this.helpText = new Text("", 1, 0);
156
111
  this.addChild(this.helpText);
157
-
158
112
  this.addChild(new Spacer(1));
159
- this.addChild(new BoxBorderBottom((s: string) => theme.fg("accent", s), `v${QUESTION_VERSION}`, (s: string) => theme.fg("dim", s)));
160
-
161
- this.updateStaticText();
162
- this.showSelectMode();
113
+ this.addChild(new BoxBorderBottom((value) => theme.fg("accent", value), `v${QUESTION_VERSION}`, (value) => theme.fg("dim", value)));
114
+ this.restore(initialResponse);
115
+ this.rebuildMode();
163
116
  }
164
117
 
165
118
  get focused(): boolean {
166
- return this._focused;
119
+ return this.focusedState;
167
120
  }
168
121
  set focused(value: boolean) {
169
- this._focused = value;
170
- if (this.editor && (this.mode === "freeform" || this.mode === "comment")) {
171
- (this.editor as any).focused = value;
172
- }
122
+ this.focusedState = value;
123
+ if (this.editor) this.editor.focused = value && this.editorActive;
173
124
  }
174
125
 
175
- override invalidate(): void {
176
- super.invalidate();
177
- this.updateStaticText();
178
- this.updateHelpText();
126
+ get isEditing(): boolean {
127
+ return (this.mode === "text" || this.mode === "other") && this.editorActive;
179
128
  }
180
129
 
181
130
  override render(width: number): string[] {
182
131
  const innerWidth = Math.max(1, width - BOX_BORDER_OVERHEAD);
183
- const rawLines = super.render(innerWidth);
184
- const borderColor = (s: string) => this.theme.fg("accent", s);
185
- const titleColor = (s: string) => this.theme.fg("dim", this.theme.bold(s));
186
- return rawLines.map((line, index) => {
187
- if (index === 0) return new BoxBorderTop(borderColor, "question", titleColor).render(width)[0];
188
- if (index === rawLines.length - 1)
189
- return new BoxBorderBottom(borderColor, `v${QUESTION_VERSION}`, (s: string) => this.theme.fg("dim", s)).render(width)[0];
190
- const padded = truncateToWidth(line, innerWidth, "", true);
191
- return `${borderColor(BOX_BORDER_LEFT)}${padded}${borderColor(BOX_BORDER_RIGHT)}`;
132
+ const lines = super.render(innerWidth);
133
+ const border = (value: string) => this.theme.fg("accent", value);
134
+ return lines.map((line, index) => {
135
+ if (index === 0) return new BoxBorderTop(border, "question", (value) => this.theme.fg("dim", this.theme.bold(value))).render(width)[0]!;
136
+ if (index === lines.length - 1) return new BoxBorderBottom(border, `v${QUESTION_VERSION}`, (value) => this.theme.fg("dim", value)).render(width)[0]!;
137
+ return `${border(BOX_BORDER_LEFT)}${truncateToWidth(line, innerWidth, "", true)}${border(BOX_BORDER_RIGHT)}`;
192
138
  });
193
139
  }
194
140
 
195
- private updateStaticText(): void {
196
- const theme = this.theme;
197
- const title = this.mode === "comment" ? "Optional comment" : "Question";
198
- this.titleText.setText(theme.fg("accent", theme.bold(title)));
199
- this.questionText.setText(theme.fg("text", theme.bold(this.question)));
200
- if (this.contextComponent && this.context) {
201
- const mdTheme = safeMarkdownTheme();
202
- if (mdTheme && this.contextComponent instanceof Markdown) {
203
- (this.contextComponent as Markdown).setText(`**Context:**\n${this.context}`);
204
- } else {
205
- (this.contextComponent as Text).setText(`${theme.fg("accent", theme.bold("Context:"))}\n${theme.fg("dim", this.context)}`);
206
- }
207
- }
141
+ override invalidate(): void {
142
+ super.invalidate();
143
+ this.updateAnswerText();
144
+ this.updateHelp();
208
145
  }
209
146
 
210
- private updateHelpText(): void {
211
- const theme = this.theme;
212
- const kb = this.keybindings;
213
- const commentHint =
214
- this.allowComment && !this.shortcuts.commentToggle.disabled
215
- ? `${theme.fg("dim", this.shortcuts.commentToggle.spec)}${theme.fg("muted", " toggle context")}`
216
- : null;
217
-
218
- if (this.mode === "freeform" || this.mode === "comment") {
219
- const hints = [
220
- `${theme.fg("dim", kb.getKeys("tui.input.submit").join("/"))}${theme.fg("muted", this.mode === "comment" ? " submit/skip" : " submit")}`,
221
- `${theme.fg("dim", kb.getKeys("tui.input.newLine").join("/"))}${theme.fg("muted", " newline")}`,
222
- `${theme.fg("dim", "esc")}${theme.fg("muted", " back")}`,
223
- ]
224
- .filter((h): h is string => !!h)
225
- .join(" • ");
226
- this.helpText.setText(theme.fg("dim", hints));
147
+ private restore(response?: QuestionResponse): void {
148
+ if (!response) return;
149
+ if (response.kind === "text") {
150
+ this.mode = "text";
151
+ this.ensureEditor().setText(response.text);
227
152
  return;
228
153
  }
229
-
230
- if (this.allowMultiple) {
231
- const hints = [
232
- `${theme.fg("dim", "↑↓")}${theme.fg("muted", " navigate")}`,
233
- `${theme.fg("dim", "space")}${theme.fg("muted", " toggle")}`,
234
- commentHint,
235
- `${theme.fg("dim", kb.getKeys("tui.select.confirm").join("/"))}${theme.fg("muted", " submit")}`,
236
- `${theme.fg("dim", kb.getKeys("tui.select.cancel").join("/"))}${theme.fg("muted", " cancel")}`,
237
- ]
238
- .filter((h): h is string => !!h)
239
- .join(" • ");
240
- this.helpText.setText(theme.fg("dim", hints));
241
- } else {
242
- const hints = [
243
- `${theme.fg("dim", "type")}${theme.fg("muted", " filter")}`,
244
- `${theme.fg("dim", kb.getKeys("tui.editor.deleteCharBackward").join("/"))}${theme.fg("muted", " erase")}`,
245
- `${theme.fg("dim", "↑↓")}${theme.fg("muted", " navigate")}`,
246
- commentHint,
247
- `${theme.fg("dim", kb.getKeys("tui.select.confirm").join("/"))}${theme.fg("muted", " select")}`,
248
- `${theme.fg("dim", "esc")}${theme.fg("muted", " clear/cancel")}`,
249
- ]
250
- .filter((h): h is string => !!h)
251
- .join(" • ");
252
- this.helpText.setText(theme.fg("dim", hints));
154
+ this.selectionValues = [...response.values];
155
+ this.otherDraft = response.otherText ?? "";
156
+ this.ensureList().restoreSelection(response.values);
157
+ if (response.otherText) {
158
+ this.mode = "other";
159
+ this.ensureEditor().setText(response.otherText);
253
160
  }
254
161
  }
255
162
 
256
163
  private ensureList(): QuestionList {
257
164
  if (this.list) return this.list;
165
+ const multi = this.question.type === "multiselect";
258
166
  const list = new QuestionList(
259
- this.options,
260
- this.allowMultiple ? MultiSelect : SingleSelect,
261
- this.allowFreeform,
262
- this.allowComment,
167
+ this.question.options,
168
+ multi ? MultiSelect : SingleSelect,
169
+ this.question.allowOther,
170
+ false,
263
171
  this.theme,
264
172
  this.keybindings,
265
- this.shortcuts.commentToggle,
173
+ DISABLED_SHORTCUT,
266
174
  );
267
- list.onSubmit = (selections, commentEnabled) => this.handleSelectionSubmit(selections, commentEnabled);
268
- list.onCancel = () => this.onDone(null);
269
- list.onEnterFreeform = () => this.showFreeformMode();
175
+ list.onSubmit = (values) => {
176
+ this.selectionValues = values;
177
+ if (!multi) this.otherDraft = "";
178
+ this.updateAnswerText();
179
+ this.tui.requestRender();
180
+ };
181
+ list.onEnterFreeform = () => this.showOther();
182
+ list.onCancel = this.onCancel;
270
183
  this.list = list;
271
184
  return list;
272
185
  }
273
186
 
274
187
  private ensureEditor(): Editor {
275
188
  if (this.editor) return this.editor;
276
- const editor = new Editor(this.tui, createEditorTheme(this.theme));
277
- editor.onSubmit = (text: string) => this.handleEditorSubmit(text);
189
+ const editor = new Editor(this.tui, editorTheme(this.theme));
190
+ editor.onChange = () => {
191
+ this.updateAnswerText();
192
+ this.tui.requestRender();
193
+ };
194
+ editor.onSubmit = (text) => {
195
+ editor.setText(text.trim());
196
+ this.updateAnswerText();
197
+ this.tui.requestRender();
198
+ };
278
199
  this.editor = editor;
279
200
  return editor;
280
201
  }
281
202
 
282
- private handleSelectionSubmit(selections: string[], wantsComment: boolean): void {
283
- if (this.allowComment && wantsComment) {
284
- this.pendingSelections = selections;
285
- this.commentDraft = "";
286
- this.showCommentMode();
203
+ private showOther(): void {
204
+ this.mode = "other";
205
+ this.editorActive = true;
206
+ const editor = this.ensureEditor();
207
+ editor.setText(this.otherDraft);
208
+ this.rebuildMode();
209
+ }
210
+
211
+ private showSelect(): void {
212
+ if (this.mode === "other") this.otherDraft = this.ensureEditor().getText();
213
+ this.mode = "select";
214
+ this.editorActive = false;
215
+ this.rebuildMode();
216
+ }
217
+
218
+ private rebuildMode(): void {
219
+ this.modeContainer.clear();
220
+ if (this.mode === "select") {
221
+ this.modeContainer.addChild(this.ensureList());
222
+ } else {
223
+ const editor = this.ensureEditor();
224
+ editor.focused = this.focusedState && this.editorActive;
225
+ this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold(this.mode === "text" ? "Answer" : "Other answer")), 1, 0));
226
+ this.modeContainer.addChild(new Spacer(1));
227
+ this.modeContainer.addChild(editor);
228
+ }
229
+ this.updateAnswerText();
230
+ this.updateHelp();
231
+ this.invalidate();
232
+ this.tui.requestRender();
233
+ }
234
+
235
+ private updateAnswerText(): void {
236
+ const response = this.getResponse();
237
+ this.answerText.setText(
238
+ response
239
+ ? `${this.theme.fg("success", "Draft:")} ${this.theme.fg("text", oneLine(formatQuestionResponse(this.question, response), 160))}`
240
+ : this.theme.fg("dim", "No answer yet"),
241
+ );
242
+ }
243
+
244
+ private updateHelp(): void {
245
+ if (this.mode === "select") {
246
+ const multi = this.question.type === "multiselect";
247
+ this.helpText.setText(
248
+ this.theme.fg(
249
+ "dim",
250
+ multi
251
+ ? "↑↓ navigate • space toggle • enter save selection • Tab/→ next • Shift+Tab/← previous • esc cancel"
252
+ : "type filter • ↑↓ navigate • enter save choice • Tab/→ next • Shift+Tab/← previous • esc cancel",
253
+ ),
254
+ );
287
255
  return;
288
256
  }
289
- this.onDone(createSelectionResponse(selections) ?? null);
257
+ this.helpText.setText(
258
+ this.theme.fg(
259
+ "dim",
260
+ `${this.keybindings.getKeys("tui.input.submit").join("/")} save • ${this.keybindings.getKeys("tui.input.newLine").join("/")} newline • Tab next • Shift+Tab previous • esc ${this.mode === "other" ? "choices" : "blur"}`,
261
+ ),
262
+ );
290
263
  }
291
264
 
292
- private handleEditorSubmit(text: string): void {
293
- if (this.mode === "freeform") {
294
- const r = createFreeformResponse(text);
295
- this.onDone(r ?? null);
265
+ getResponse(): QuestionResponse | null {
266
+ if (this.question.type === "text") return createTextResponse(this.ensureEditor().getText());
267
+ const values = this.question.type === "multiselect" ? this.ensureList().getCheckedValues() : this.selectionValues;
268
+ const otherText = this.mode === "other" ? this.ensureEditor().getText() : this.otherDraft;
269
+ return createSelectionResponse(this.question.type === "select" && trimText(otherText) ? [] : values, otherText);
270
+ }
271
+
272
+ handleInput(data: string): void {
273
+ if (this.mode === "other" && matchesKey(data, Key.escape)) {
274
+ this.showSelect();
296
275
  return;
297
276
  }
298
- if (this.mode === "comment") {
299
- this.commentDraft = text;
300
- this.onDone(createSelectionResponse(this.pendingSelections, text) ?? null);
277
+ if (this.mode === "text" && matchesKey(data, Key.escape) && this.editorActive) {
278
+ this.editorActive = false;
279
+ this.ensureEditor().focused = false;
280
+ this.updateHelp();
281
+ this.tui.requestRender();
282
+ return;
283
+ }
284
+ if ((this.mode === "text" || this.mode === "other") && !this.editorActive) {
285
+ if (matchesKey(data, Key.enter)) {
286
+ this.editorActive = true;
287
+ this.ensureEditor().focused = this.focusedState;
288
+ this.tui.requestRender();
289
+ }
290
+ return;
301
291
  }
292
+ if (this.mode === "text" || this.mode === "other") this.ensureEditor().handleInput(data);
293
+ else this.ensureList().handleInput(data);
294
+ this.tui.requestRender();
302
295
  }
296
+ }
303
297
 
304
- private showSelectMode(): void {
305
- this.mode = "select";
306
- this.pendingSelections = [];
307
- this.modeContainer.clear();
308
- this.modeContainer.addChild(this.ensureList());
309
- this.updateHelpText();
310
- this.invalidate();
311
- this.tui.requestRender();
298
+ class ReviewComponent implements Component {
299
+ private focusedState = false;
300
+
301
+ constructor(
302
+ private questions: PreparedQuestion[],
303
+ private states: ReadonlyMap<string, DraftQuestionState>,
304
+ private theme: Theme,
305
+ private onBack: () => void,
306
+ private onCancel: () => void,
307
+ private onSubmit: () => void,
308
+ ) {}
309
+
310
+ get focused(): boolean {
311
+ return this.focusedState;
312
+ }
313
+ set focused(value: boolean) {
314
+ this.focusedState = value;
312
315
  }
313
316
 
314
- private showFreeformMode(): void {
315
- this.mode = "freeform";
316
- this.modeContainer.clear();
317
- const editor = this.ensureEditor();
318
- (editor as any).setText?.(this.freeformDraft);
319
- (editor as any).focused = this._focused;
320
- this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold("Custom response")), 1, 0));
321
- this.modeContainer.addChild(new Spacer(1));
322
- this.modeContainer.addChild(editor);
323
- this.updateHelpText();
324
- this.invalidate();
325
- this.tui.requestRender();
317
+ invalidate(): void {}
318
+
319
+ render(width: number): string[] {
320
+ const safeWidth = Math.max(12, width);
321
+ const lines: string[] = [];
322
+ for (const question of this.questions) {
323
+ const state = this.states.get(question.id);
324
+ const status = state?.status ?? "unanswered";
325
+ const marker = status === "answered" ? this.theme.fg("success", "✓") : status === "skipped" ? this.theme.fg("warning", "−") : this.theme.fg("dim", "○");
326
+ lines.push(truncateToWidth(`${marker} ${this.theme.fg("accent", this.theme.bold(question.label))} · ${this.theme.fg(status === "answered" ? "success" : status === "skipped" ? "warning" : "dim", status)}`, safeWidth, ""));
327
+ lines.push(...wrapTextWithAnsi(this.theme.fg("dim", question.question), safeWidth));
328
+ if (state?.status === "answered" && state.response) {
329
+ const preview = previewText(formatQuestionResponse(question, state.response));
330
+ lines.push(...wrapTextWithAnsi(`${this.theme.fg("dim", "Answer:")} ${this.theme.fg("text", preview)}`, safeWidth));
331
+ }
332
+ lines.push("");
333
+ }
334
+
335
+ const bodyBudget = Math.max(7, (process.stdout.rows || 24) - 9);
336
+ let body = lines;
337
+ if (lines.length > bodyBudget) {
338
+ body = lines.slice(-bodyBudget);
339
+ body[0] = this.theme.fg("dim", "↑ earlier answers");
340
+ }
341
+ const submit = this.theme.bg("selectedBg", this.theme.fg("text", " Submit "));
342
+ return [...body, truncateToWidth(submit, safeWidth, "")];
326
343
  }
327
344
 
328
- private showCommentMode(): void {
329
- this.mode = "comment";
330
- this.modeContainer.clear();
331
- const editor = this.ensureEditor();
332
- (editor as any).setText?.(this.commentDraft);
333
- (editor as any).focused = this._focused;
334
- const label = this.pendingSelections.length === 1 ? "Selected option:" : "Selected options:";
335
- this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold(label)), 1, 0));
336
- this.modeContainer.addChild(new Text(this.theme.fg("text", this.pendingSelections.join(", ")), 1, 0));
337
- this.modeContainer.addChild(new Spacer(1));
338
- this.modeContainer.addChild(editor);
339
- this.updateHelpText();
345
+ handleInput(data: string): void {
346
+ if (matchesKey(data, Key.enter)) this.onSubmit();
347
+ else if (matchesKey(data, Key.left)) this.onBack();
348
+ else if (matchesKey(data, Key.escape)) this.onCancel();
349
+ }
350
+ }
351
+
352
+ class BatchQuestionComponent extends Container implements Component {
353
+ private currentPage = 0;
354
+ private states = new Map<string, DraftQuestionState>();
355
+ private questionComponent?: QuestionComponent;
356
+ private reviewComponent?: ReviewComponent;
357
+ private focusedState = false;
358
+ private header = new Text("", 1, 0);
359
+ private content = new Container();
360
+ private legend = new Text("", 1, 0);
361
+
362
+ constructor(
363
+ private questions: PreparedQuestion[],
364
+ private tui: TUI,
365
+ private theme: Theme,
366
+ private keybindings: KeybindingsManager,
367
+ private onDone: (result: QuestionToolResult) => void,
368
+ ) {
369
+ super();
370
+ this.addChild(this.header);
371
+ this.addChild(new Spacer(1));
372
+ this.addChild(this.content);
373
+ this.addChild(new Spacer(1));
374
+ this.addChild(this.legend);
375
+ this.showPage();
376
+ }
377
+
378
+ get focused(): boolean {
379
+ return this.focusedState;
380
+ }
381
+ set focused(value: boolean) {
382
+ this.focusedState = value;
383
+ if (this.questionComponent) this.questionComponent.focused = value;
384
+ if (this.reviewComponent) this.reviewComponent.focused = value;
385
+ }
386
+
387
+ override invalidate(): void {
388
+ super.invalidate();
389
+ this.updateChrome();
390
+ }
391
+
392
+ private get isReviewPage(): boolean {
393
+ return this.currentPage === this.questions.length;
394
+ }
395
+
396
+ private cancel = (): void => this.onDone({ status: "cancelled", reason: "user" });
397
+
398
+ private updateChrome(): void {
399
+ if (this.isReviewPage) {
400
+ const answered = [...this.states.values()].filter((state) => state.status === "answered").length;
401
+ const skipped = [...this.states.values()].filter((state) => state.status === "skipped").length;
402
+ this.header.setText(this.theme.fg("accent", this.theme.bold(`Review · ${answered} answered · ${skipped} skipped · ${this.questions.length - this.states.size} unanswered`)));
403
+ this.legend.setText(this.theme.fg("dim", "Enter submit • ← edit answers • Esc cancel"));
404
+ return;
405
+ }
406
+ const question = this.questions[this.currentPage]!;
407
+ const status = this.states.get(question.id)?.status ?? "unanswered";
408
+ this.header.setText(this.theme.fg("accent", this.theme.bold(`${question.label} · Question ${this.currentPage + 1} of ${this.questions.length} · ${status}`)));
409
+ this.legend.setText(this.theme.fg("dim", "Tab/→ next • Shift+Tab/← previous • blank Next skips • answers commit only on final Submit"));
410
+ }
411
+
412
+ private showPage(): void {
413
+ this.content.clear();
414
+ this.questionComponent = undefined;
415
+ this.reviewComponent = undefined;
416
+ if (this.isReviewPage) {
417
+ this.reviewComponent = new ReviewComponent(
418
+ this.questions,
419
+ this.states,
420
+ this.theme,
421
+ () => {
422
+ this.currentPage = this.questions.length - 1;
423
+ this.showPage();
424
+ },
425
+ this.cancel,
426
+ () => this.onDone({ status: "submitted", answers: buildQuestionAnswers(this.questions, this.states) }),
427
+ );
428
+ this.reviewComponent.focused = this.focusedState;
429
+ this.content.addChild(this.reviewComponent);
430
+ } else {
431
+ const question = this.questions[this.currentPage]!;
432
+ const state = this.states.get(question.id);
433
+ this.questionComponent = new QuestionComponent(question, this.tui, this.theme, this.keybindings, this.cancel, state?.response);
434
+ this.questionComponent.focused = this.focusedState;
435
+ this.content.addChild(this.questionComponent);
436
+ }
437
+ this.updateChrome();
340
438
  this.invalidate();
341
439
  this.tui.requestRender();
342
440
  }
343
441
 
442
+ private saveCurrent(markBlankSkipped: boolean): void {
443
+ if (this.isReviewPage || !this.questionComponent) return;
444
+ const question = this.questions[this.currentPage]!;
445
+ const response = this.questionComponent.getResponse();
446
+ saveDraftQuestion(this.states, question.id, response, markBlankSkipped);
447
+ }
448
+
449
+ private goNext(): void {
450
+ this.saveCurrent(true);
451
+ if (this.currentPage < this.questions.length) this.currentPage++;
452
+ this.showPage();
453
+ }
454
+
455
+ private goPrevious(): void {
456
+ this.saveCurrent(false);
457
+ if (this.currentPage > 0) this.currentPage--;
458
+ this.showPage();
459
+ }
460
+
344
461
  handleInput(data: string): void {
345
- if (this.mode === "freeform" || this.mode === "comment") {
346
- if (matchesKey(data, Key.escape)) {
347
- this.showSelectMode();
348
- return;
349
- }
350
- if (this.keybindings.matches(data, "tui.select.cancel")) {
351
- this.onDone(null);
352
- return;
353
- }
354
- this.ensureEditor().handleInput(data);
355
- this.tui.requestRender();
462
+ if (this.isReviewPage) {
463
+ this.reviewComponent?.handleInput(data);
356
464
  return;
357
465
  }
358
- this.ensureList().handleInput(data);
359
- this.tui.requestRender();
466
+ if (matchesKey(data, Key.tab) || (!this.questionComponent?.isEditing && matchesKey(data, Key.right))) {
467
+ this.goNext();
468
+ return;
469
+ }
470
+ if (matchesKey(data, Key.shift("tab")) || (!this.questionComponent?.isEditing && matchesKey(data, Key.left))) {
471
+ this.goPrevious();
472
+ return;
473
+ }
474
+ if (!this.questionComponent?.isEditing && matchesKey(data, Key.escape) && this.questions[this.currentPage]?.type === "text") {
475
+ this.cancel();
476
+ return;
477
+ }
478
+ this.questionComponent?.handleInput(data);
360
479
  }
361
480
  }
362
481
 
363
- // ---------------------------------------------------------------------------
364
- // Tool registration
365
- // ---------------------------------------------------------------------------
482
+ function answerText(answer: QuestionAnswer): string {
483
+ if (answer.status !== "answered") return answer.status;
484
+ return formatResponseSummary(answer.response);
485
+ }
486
+
487
+ function rawQuestionLabel(question: RawQuestion | undefined, answer: QuestionAnswer): string {
488
+ if (!question || answer.status !== "answered" || answer.response.kind === "text") return answerText(answer);
489
+ const labels = answer.response.values.map((value) => question.options.find((option) => option.value === value)?.label ?? value);
490
+ if (answer.response.otherText) labels.push(`Other: ${answer.response.otherText}`);
491
+ return labels.join(", ");
492
+ }
366
493
 
367
494
  export default function questionExtension(pi: ExtensionAPI) {
368
495
  pi.registerTool({
369
496
  name: "question",
370
497
  label: "Question",
371
498
  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.",
373
- promptSnippet: "Ask the user a question when required information is missing.",
499
+ "Ask one or more typed questions in a terminal wizard. Supports select, multiselect, text, stable option values, Other answers, and partial atomic submission.",
500
+ promptSnippet: "Ask the user one or more typed questions when a decision is required.",
374
501
  promptGuidelines: [
375
- "Use question only when you cannot proceed safely or accurately without user input.",
376
- "Ask concise questions. Prefer options when choices are known.",
377
- "Set allowFreeform=true when user may need to provide a custom answer.",
378
- "Use allowMultiple=true when more than one option may apply.",
379
- "Pass a short summary via context when the question depends on prior findings.",
380
- "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
- "Do not use question for information you can infer, inspect, or compute with other tools.",
502
+ "Use question only for decisions the user must make; inspect facts yourself.",
503
+ "Send one questions array containing independent decisions that can be answered together.",
504
+ "Use stable option values separate from human-readable labels.",
505
+ "Use text questions only when known choices cannot represent the answer.",
506
+ "After question returns, continue immediately using submitted answers; skipped and unanswered are distinct states.",
382
507
  ],
383
- // Block other tool calls in the same turn until the user answers, so the
384
- // model can't batch question with side-effecting tools and run them first.
385
508
  executionMode: "sequential",
386
509
  parameters: QuestionParamsSchema,
387
510
 
388
511
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
389
- if (signal?.aborted) {
390
- return {
391
- content: [{ type: "text", text: "Question cancelled" }],
392
- details: { question: params.question, options: [], response: null, cancelled: true } satisfies QuestionDetails,
393
- };
394
- }
395
-
396
- const rawQuestion = params.question as string;
397
- // `title` is a deprecated alias for context; merge if only title is given.
398
- const rawContext = (params.context as string | undefined)?.trim() || (params.title as string | undefined)?.trim() || undefined;
399
- const allowMultiple = params.allowMultiple ?? false;
400
- const allowFreeform = params.allowFreeform ?? !((params.options ?? []).length > 0);
401
- const allowComment = params.allowComment ?? false;
402
- const timeout = params.timeout as number | undefined;
403
-
404
- const shortcuts: ResolvedShortcuts = resolveShortcuts(
405
- params.commentToggleKey as string | null | undefined,
406
- process.env.PI_QUESTION_COMMENT_TOGGLE_KEY,
407
- );
408
-
409
- const optionsFormatError = getOptionsFormatError(params.options as RawOption[] | undefined);
410
- if (optionsFormatError) {
411
- return {
412
- content: [{ type: "text", text: optionsFormatError }],
413
- isError: true,
414
- details: { question: rawQuestion, context: rawContext, options: [], response: null, cancelled: true } satisfies QuestionDetails,
415
- };
416
- }
417
-
418
- const options = normalizeOptions(params.options as RawOption[] | undefined);
419
- const normalizedContext = rawContext || undefined;
420
- const detailsBase = { question: rawQuestion, context: normalizedContext, options };
421
-
422
- const protocol: UIProtocol = createTUIProtocol(ctx);
423
-
424
- if (!protocol.hasUI) {
425
- const optionText = options.length > 0 ? `\n\nOptions:\n${formatOptionsForMessage(options)}` : "";
426
- const freeformHint = allowFreeform ? "\n\nYou can also answer freely." : "";
427
- const commentHint = allowComment ? "\n\nAfter choosing, you may add an optional comment." : "";
428
- const contextText = normalizedContext ? `\n\nContext:\n${normalizedContext}` : "";
429
- return {
430
- content: [
431
- { type: "text", text: `Question requires interactive mode. Please answer:\n\n${rawQuestion}${contextText}${optionText}${freeformHint}${commentHint}` },
432
- ],
433
- isError: true,
434
- details: { ...detailsBase, response: null, cancelled: true } satisfies QuestionDetails,
435
- };
436
- }
437
-
438
- if (options.length === 0 && !allowFreeform) {
439
- return {
440
- content: [{ type: "text", text: "Question options are empty and allowFreeform is false." }],
441
- isError: true,
442
- details: { ...detailsBase, response: null, cancelled: true } satisfies QuestionDetails,
443
- };
444
- }
445
-
446
- onUpdate?.({
447
- content: [{ type: "text", text: "Waiting for user input..." }],
448
- details: { ...detailsBase, response: null, cancelled: false } satisfies QuestionDetails,
512
+ const prepared = prepareQuestions(params.questions);
513
+ if ("error" in prepared) throw new Error(prepared.error);
514
+ if (ctx.mode !== "tui") throw new Error("Question requires terminal TUI mode.");
515
+ if (signal?.aborted) return { content: [{ type: "text", text: "Question cancelled" }], details: { status: "cancelled", reason: "user" } satisfies QuestionToolResult };
516
+
517
+ onUpdate?.({ content: [{ type: "text", text: `Waiting for answers to ${prepared.questions.length} question${prepared.questions.length === 1 ? "" : "s"}...` }] });
518
+ const result = await ctx.ui.custom<QuestionToolResult>((tui, theme, keybindings, done) => {
519
+ if (signal) signal.addEventListener("abort", () => done({ status: "cancelled", reason: "user" }), { once: true });
520
+ return new BatchQuestionComponent(prepared.questions, tui, theme, keybindings, done);
449
521
  });
522
+ if (!result) throw new Error("Question TUI did not return a result.");
450
523
 
451
- let result: QuestionResponse | null;
452
-
453
- try {
454
- const customFactory: CustomFactory = (
455
- tui: TUI,
456
- theme: Theme,
457
- keybindings: KeybindingsManager,
458
- done: (result: QuestionResponse | null) => void,
459
- ) => {
460
- if (signal) {
461
- const onAbort = () => done(null);
462
- signal.addEventListener("abort", onAbort, { once: true });
463
- }
464
- if (timeout && timeout > 0) setTimeout(() => done(null), timeout);
465
-
466
- return new QuestionComponent(
467
- rawQuestion,
468
- normalizedContext,
469
- options,
470
- allowMultiple,
471
- allowFreeform,
472
- allowComment,
473
- tui,
474
- theme,
475
- keybindings,
476
- shortcuts,
477
- done,
478
- );
479
- };
480
-
481
- const customResult = await protocol.custom(customFactory);
482
-
483
- if (customResult !== undefined) {
484
- result = customResult;
485
- } else {
486
- // RPC/headless: custom() returns undefined — degrade to dialog protocol.
487
- result = await DialogFallback.ask(
488
- { question: rawQuestion, context: normalizedContext, options, allowMultiple, allowFreeform, allowComment, shortcuts, timeout },
489
- protocol,
490
- );
491
- }
492
- } catch (error) {
493
- const message = error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error);
494
- return {
495
- content: [{ type: "text", text: `Question tool failed: ${message}` }],
496
- isError: true,
497
- details: { error: message } as unknown as QuestionDetails,
498
- };
499
- }
500
-
501
- if (result === null) {
502
- pi.events.emit("question:cancelled", { question: rawQuestion, context: normalizedContext, options });
503
- return {
504
- content: [{ type: "text", text: "User cancelled the question" }],
505
- details: { ...detailsBase, response: null, cancelled: true } satisfies QuestionDetails,
506
- };
524
+ if (result.status === "cancelled") {
525
+ pi.events.emit("question:cancelled", { reason: result.reason });
526
+ return { content: [{ type: "text", text: "User cancelled the question batch" }], details: result };
507
527
  }
508
-
509
- pi.events.emit("question:answered", { question: rawQuestion, context: normalizedContext, response: result });
528
+ pi.events.emit("question:submitted", { answers: result.answers });
510
529
  return {
511
- content: [{ type: "text", text: `User answered: ${formatResponseSummary(result)}` }],
512
- details: { ...detailsBase, response: result, cancelled: false } satisfies QuestionDetails,
530
+ content: [{ type: "text", text: JSON.stringify(result) }],
531
+ details: result,
513
532
  };
514
533
  },
515
534
 
516
- renderCall(args, theme, _context) {
517
- const question = (args.question as string) || "";
518
- const rawOptions = Array.isArray(args.options) ? args.options : [];
519
- const title = args.context ? `${args.context}: ` : args.title ? `${args.title}: ` : "";
520
- const optionCount = rawOptions.length;
521
- const flags: string[] = [];
522
- if (args.allowMultiple) flags.push("multi");
523
- if (args.allowComment) flags.push("comment");
524
- const suffix = optionCount
525
- ? theme.fg("dim", ` · ${optionCount} option${optionCount === 1 ? "" : "s"}${flags.length ? ` · ${flags.join("/")}` : ""}`)
526
- : theme.fg("dim", ` · freeform${flags.length ? ` · ${flags.join("/")}` : ""}`);
535
+ renderCall(args, theme) {
536
+ const questions = Array.isArray(args.questions) ? (args.questions as RawQuestion[]) : [];
537
+ const labels = questions.map((question, index) => question.label || question.id || `Q${index + 1}`).join(", ");
527
538
  return new Text(
528
- `${theme.fg("toolTitle", theme.bold("QUESTION"))} ${theme.fg("accent", theme.bold(oneLine(title + question, 120)))}${suffix}`,
539
+ `${theme.fg("toolTitle", theme.bold("QUESTION"))} ${theme.fg("accent", theme.bold(`${questions.length} question${questions.length === 1 ? "" : "s"}`))}${labels ? theme.fg("dim", ` · ${oneLine(labels, 100)}`) : ""}`,
529
540
  0,
530
541
  0,
531
542
  );
532
543
  },
533
544
 
534
- renderResult(result, options, theme, _context) {
535
- const details = result.details as (QuestionDetails & { error?: string }) | undefined;
536
-
537
- if (details?.error) return new Text(theme.fg("error", details.error), 0, 0);
538
-
545
+ renderResult(result, options, theme, context) {
539
546
  if (options.isPartial) {
540
- const waitingText =
541
- result.content
542
- ?.filter((part: { type?: string; text?: string }) => part?.type === "text")
543
- .map((part: { text?: string }) => part.text ?? "")
544
- .join("\n")
545
- .trim() || "Waiting for user input...";
546
- return new Text(theme.fg("muted", waitingText), 0, 0);
547
- }
548
-
549
- if (!details || details.cancelled || !details.response) {
550
- return new Text(theme.fg("warning", "question cancelled"), 0, 0);
547
+ const text = result.content.find((part: { type?: string }) => part.type === "text") as { text?: string } | undefined;
548
+ return new Text(theme.fg("muted", text?.text ?? "Waiting for user input..."), 0, 0);
551
549
  }
552
-
553
- const response = details.response;
554
- let text = theme.fg("success", theme.bold("answered: "));
555
- text += theme.fg("accent", theme.bold(oneLine(formatResponseSummary(response), 120)));
556
-
557
- if (options.expanded && details.question) {
558
- text += `\n${theme.fg("dim", `Q: ${oneLine(details.question, 200)}`)}`;
559
- if (details.context) text += `\n${theme.fg("dim", oneLine(details.context, 200))}`;
560
- if (response.kind === "selection" && details.options.length > 0) {
561
- const selected = new Set(response.selections);
562
- text += `\n${theme.fg("dim", "Options:")}`;
563
- for (const opt of details.options) {
564
- const desc = opt.description ? ` — ${opt.description}` : "";
565
- const marker = selected.has(opt.label) ? theme.fg("success", "●") : theme.fg("dim", "○");
566
- text += `\n ${marker} ${theme.fg("dim", opt.label)}${theme.fg("dim", desc)}`;
567
- }
568
- if (response.comment) text += `\n${theme.fg("dim", "Comment:")} ${theme.fg("dim", response.comment)}`;
550
+ const details = result.details as QuestionToolResult | undefined;
551
+ if (!details || details.status === "cancelled") return new Text(theme.fg("warning", "question cancelled"), 0, 0);
552
+ const answered = details.answers.filter((answer) => answer.status === "answered").length;
553
+ const skipped = details.answers.filter((answer) => answer.status === "skipped").length;
554
+ const unanswered = details.answers.length - answered - skipped;
555
+ let text = `${theme.fg("success", "submitted")} ${theme.fg("dim", `· ${answered} answered · ${skipped} skipped · ${unanswered} unanswered`)}`;
556
+ if (options.expanded) {
557
+ const questions = (((context as { args?: { questions?: RawQuestion[] } }).args?.questions ?? []) as RawQuestion[]);
558
+ for (const answer of details.answers) {
559
+ const question = questions.find((candidate, index) => (candidate.id?.trim() || `q${index + 1}`) === answer.id);
560
+ const marker = answer.status === "answered" ? theme.fg("success", "✓") : answer.status === "skipped" ? theme.fg("warning", "−") : theme.fg("dim", "○");
561
+ text += `\n${marker} ${theme.fg("accent", answer.id)}: ${theme.fg(answer.status === "answered" ? "text" : "dim", oneLine(rawQuestionLabel(question, answer), 200))}`;
569
562
  }
570
563
  }
571
564
  return new Text(text, 0, 0);