@duckmind/dm-darwin-x64 0.43.2 → 0.43.8

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 (256) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +242 -49
  3. package/extensions/dm-9router-ext/package.json +2 -19
  4. package/extensions/dm-9router-ext/src/index.js +5 -0
  5. package/extensions/dm-ask-user/index.js +10 -0
  6. package/extensions/dm-ask-user/package.json +1 -21
  7. package/extensions/dm-cli-anything/package.json +0 -6
  8. package/extensions/dm-context/package.json +2 -12
  9. package/extensions/dm-context/src/context.js +1 -0
  10. package/extensions/dm-context/src/index.js +7 -0
  11. package/extensions/dm-context/src/utils.js +1 -0
  12. package/extensions/dm-cua/package.json +0 -7
  13. package/extensions/dm-fff/package.json +1 -28
  14. package/extensions/dm-fff/src/index.js +15 -0
  15. package/extensions/dm-fff/src/query.js +1 -0
  16. package/extensions/dm-goal/package.json +2 -31
  17. package/extensions/dm-goal/src/goal.js +43 -0
  18. package/extensions/dm-grill-me/index.js +174 -0
  19. package/extensions/dm-grill-me/package.json +1 -26
  20. package/extensions/dm-image2/package.json +0 -7
  21. package/extensions/dm-subagents/package.json +1 -32
  22. package/extensions/dm-subagents/src/agents/agent-management.js +37 -0
  23. package/extensions/dm-subagents/src/agents/agent-memory.js +6 -0
  24. package/extensions/dm-subagents/src/agents/agent-scope.js +1 -0
  25. package/extensions/dm-subagents/src/agents/agent-selection.js +1 -0
  26. package/extensions/dm-subagents/src/agents/agent-serializer.js +7 -0
  27. package/extensions/dm-subagents/src/agents/agents.js +11 -0
  28. package/extensions/dm-subagents/src/agents/chain-serializer.js +12 -0
  29. package/extensions/dm-subagents/src/agents/frontmatter.js +6 -0
  30. package/extensions/dm-subagents/src/agents/identity.js +1 -0
  31. package/extensions/dm-subagents/src/agents/proactive-skills.js +1 -0
  32. package/extensions/dm-subagents/src/agents/skills.js +6 -0
  33. package/extensions/dm-subagents/src/extension/config.js +2 -0
  34. package/extensions/dm-subagents/src/extension/control-notices.js +4 -0
  35. package/extensions/dm-subagents/src/extension/doctor.js +16 -0
  36. package/extensions/dm-subagents/src/extension/fanout-child.js +231 -0
  37. package/extensions/dm-subagents/src/extension/index.js +367 -0
  38. package/extensions/dm-subagents/src/extension/rpc.js +8 -0
  39. package/extensions/dm-subagents/src/extension/schemas.js +1 -0
  40. package/extensions/dm-subagents/src/extension/{tool-description.ts → tool-description.js} +6 -126
  41. package/extensions/dm-subagents/src/intercom/intercom-bridge.js +13 -0
  42. package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +5 -0
  43. package/extensions/dm-subagents/src/intercom/result-intercom.js +3 -0
  44. package/extensions/dm-subagents/src/profiles/profiles.js +3 -0
  45. package/extensions/dm-subagents/src/runs/background/async-execution.js +46 -0
  46. package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +14 -0
  47. package/extensions/dm-subagents/src/runs/background/async-resume.js +8 -0
  48. package/extensions/dm-subagents/src/runs/background/async-status.js +12 -0
  49. package/extensions/dm-subagents/src/runs/background/chain-append.js +2 -0
  50. package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +1 -0
  51. package/extensions/dm-subagents/src/runs/background/completion-batcher.js +1 -0
  52. package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +1 -0
  53. package/extensions/dm-subagents/src/runs/background/control-channel.js +1 -0
  54. package/extensions/dm-subagents/src/runs/background/fleet-view.js +17 -0
  55. package/extensions/dm-subagents/src/runs/background/notify.js +3 -0
  56. package/extensions/dm-subagents/src/runs/background/parallel-groups.js +1 -0
  57. package/extensions/dm-subagents/src/runs/background/result-watcher.js +8 -0
  58. package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +4 -0
  59. package/extensions/dm-subagents/src/runs/background/run-status.js +23 -0
  60. package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +4 -0
  61. package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +11 -0
  62. package/extensions/dm-subagents/src/runs/background/subagent-runner.js +78 -0
  63. package/extensions/dm-subagents/src/runs/background/top-level-async.js +1 -0
  64. package/extensions/dm-subagents/src/runs/background/wait.js +11 -0
  65. package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +12 -0
  66. package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +102 -0
  67. package/extensions/dm-subagents/src/runs/foreground/execution.js +51 -0
  68. package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +228 -0
  69. package/extensions/dm-subagents/src/runs/shared/acceptance.js +3 -0
  70. package/extensions/dm-subagents/src/runs/shared/chain-outputs.js +1 -0
  71. package/extensions/dm-subagents/src/runs/shared/completion-guard.js +3 -0
  72. package/extensions/dm-subagents/src/runs/shared/dm-args.js +1 -0
  73. package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +1 -0
  74. package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +1 -0
  75. package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +1 -0
  76. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +1 -0
  77. package/extensions/dm-subagents/src/runs/shared/model-fallback.js +1 -0
  78. package/extensions/dm-subagents/src/runs/shared/model-scope.js +1 -0
  79. package/extensions/dm-subagents/src/runs/shared/nested-events.js +8 -0
  80. package/extensions/dm-subagents/src/runs/shared/nested-path.js +1 -0
  81. package/extensions/dm-subagents/src/runs/shared/nested-render.js +1 -0
  82. package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +5 -0
  83. package/extensions/dm-subagents/src/runs/shared/run-history.js +5 -0
  84. package/extensions/dm-subagents/src/runs/shared/single-output.js +14 -0
  85. package/extensions/dm-subagents/src/runs/shared/structured-output.js +1 -0
  86. package/extensions/dm-subagents/src/runs/shared/subagent-control.js +5 -0
  87. package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +22 -0
  88. package/extensions/dm-subagents/src/runs/shared/tool-budget.js +1 -0
  89. package/extensions/dm-subagents/src/runs/shared/turn-budget.js +7 -0
  90. package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +1 -0
  91. package/extensions/dm-subagents/src/runs/shared/worktree.js +3 -0
  92. package/extensions/dm-subagents/src/shared/artifacts.js +2 -0
  93. package/extensions/dm-subagents/src/shared/atomic-json.js +1 -0
  94. package/extensions/dm-subagents/src/shared/child-transcript.js +5 -0
  95. package/extensions/dm-subagents/src/shared/file-coalescer.js +1 -0
  96. package/extensions/dm-subagents/src/shared/fork-context.js +6 -0
  97. package/extensions/dm-subagents/src/shared/formatters.js +9 -0
  98. package/extensions/dm-subagents/src/shared/jsonl-writer.js +2 -0
  99. package/extensions/dm-subagents/src/shared/model-info.js +1 -0
  100. package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.js +1 -0
  101. package/extensions/dm-subagents/src/shared/session-identity.js +1 -0
  102. package/extensions/dm-subagents/src/shared/session-tokens.js +2 -0
  103. package/extensions/dm-subagents/src/shared/settings.js +23 -0
  104. package/extensions/dm-subagents/src/shared/status-format.js +1 -0
  105. package/extensions/dm-subagents/src/shared/types.js +8 -0
  106. package/extensions/dm-subagents/src/shared/utils.js +2 -0
  107. package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +1 -0
  108. package/extensions/dm-subagents/src/slash/prompt-workflows.js +7 -0
  109. package/extensions/dm-subagents/src/slash/slash-bridge.js +1 -0
  110. package/extensions/dm-subagents/src/slash/slash-commands.js +38 -0
  111. package/extensions/dm-subagents/src/slash/slash-live-state.js +6 -0
  112. package/extensions/dm-subagents/src/tui/render-helpers.js +1 -0
  113. package/extensions/dm-subagents/src/tui/render.js +4 -0
  114. package/extensions/dm-tasks/package.json +1 -30
  115. package/extensions/dm-tasks/src/auto-clear.js +1 -0
  116. package/extensions/dm-tasks/src/index.js +212 -0
  117. package/extensions/dm-tasks/src/process-tracker.js +1 -0
  118. package/extensions/dm-tasks/src/task-store.js +1 -0
  119. package/extensions/dm-tasks/src/tasks-config.js +1 -0
  120. package/extensions/dm-tasks/src/types.js +0 -0
  121. package/extensions/dm-tasks/src/ui/settings-menu.js +1 -0
  122. package/extensions/dm-tasks/src/ui/task-widget.js +1 -0
  123. package/extensions/dm-usage/index.js +9 -0
  124. package/extensions/dm-usage/package.json +1 -18
  125. package/extensions/greedysearch-dm/bin/search.mjs +135 -135
  126. package/extensions/greedysearch-dm/index.js +20 -0
  127. package/extensions/greedysearch-dm/package.json +1 -22
  128. package/extensions/greedysearch-dm/src/formatters/results.js +8 -0
  129. package/extensions/greedysearch-dm/src/formatters/sources.js +1 -0
  130. package/extensions/greedysearch-dm/src/formatters/synthesis.js +1 -0
  131. package/extensions/greedysearch-dm/src/search/engines.mjs +9 -9
  132. package/extensions/greedysearch-dm/src/search/paths.mjs +1 -0
  133. package/extensions/greedysearch-dm/src/search/research.mjs +100 -100
  134. package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +10 -10
  135. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +20 -0
  136. package/extensions/greedysearch-dm/src/tools/shared.js +9 -0
  137. package/extensions/greedysearch-dm/src/types.js +0 -0
  138. package/extensions/greedysearch-dm/src/utils/helpers.js +1 -0
  139. package/package.json +1 -1
  140. package/extensions/dm-9router-ext/src/index.ts +0 -541
  141. package/extensions/dm-ask-user/index.ts +0 -857
  142. package/extensions/dm-context/src/context.ts +0 -158
  143. package/extensions/dm-context/src/index.ts +0 -439
  144. package/extensions/dm-context/src/utils.ts +0 -6
  145. package/extensions/dm-fff/src/index.ts +0 -1023
  146. package/extensions/dm-fff/src/query.ts +0 -87
  147. package/extensions/dm-goal/src/goal.ts +0 -1073
  148. package/extensions/dm-grill-me/index.ts +0 -1085
  149. package/extensions/dm-subagents/src/agents/agent-management.ts +0 -1052
  150. package/extensions/dm-subagents/src/agents/agent-memory.ts +0 -254
  151. package/extensions/dm-subagents/src/agents/agent-scope.ts +0 -6
  152. package/extensions/dm-subagents/src/agents/agent-selection.ts +0 -25
  153. package/extensions/dm-subagents/src/agents/agent-serializer.ts +0 -121
  154. package/extensions/dm-subagents/src/agents/agents.ts +0 -1553
  155. package/extensions/dm-subagents/src/agents/chain-serializer.ts +0 -277
  156. package/extensions/dm-subagents/src/agents/frontmatter.ts +0 -93
  157. package/extensions/dm-subagents/src/agents/identity.ts +0 -30
  158. package/extensions/dm-subagents/src/agents/proactive-skills.ts +0 -191
  159. package/extensions/dm-subagents/src/agents/skills.ts +0 -729
  160. package/extensions/dm-subagents/src/extension/config.ts +0 -39
  161. package/extensions/dm-subagents/src/extension/control-notices.ts +0 -92
  162. package/extensions/dm-subagents/src/extension/doctor.ts +0 -214
  163. package/extensions/dm-subagents/src/extension/fanout-child.ts +0 -172
  164. package/extensions/dm-subagents/src/extension/index.ts +0 -656
  165. package/extensions/dm-subagents/src/extension/rpc.ts +0 -369
  166. package/extensions/dm-subagents/src/extension/schemas.ts +0 -309
  167. package/extensions/dm-subagents/src/intercom/intercom-bridge.ts +0 -180
  168. package/extensions/dm-subagents/src/intercom/native-supervisor-channel.ts +0 -519
  169. package/extensions/dm-subagents/src/intercom/result-intercom.ts +0 -377
  170. package/extensions/dm-subagents/src/profiles/profiles.ts +0 -637
  171. package/extensions/dm-subagents/src/runs/background/async-execution.ts +0 -1065
  172. package/extensions/dm-subagents/src/runs/background/async-job-tracker.ts +0 -441
  173. package/extensions/dm-subagents/src/runs/background/async-resume.ts +0 -391
  174. package/extensions/dm-subagents/src/runs/background/async-status.ts +0 -395
  175. package/extensions/dm-subagents/src/runs/background/chain-append.ts +0 -282
  176. package/extensions/dm-subagents/src/runs/background/chain-root-attachment.ts +0 -191
  177. package/extensions/dm-subagents/src/runs/background/completion-batcher.ts +0 -166
  178. package/extensions/dm-subagents/src/runs/background/completion-dedupe.ts +0 -63
  179. package/extensions/dm-subagents/src/runs/background/control-channel.ts +0 -332
  180. package/extensions/dm-subagents/src/runs/background/fleet-view.ts +0 -515
  181. package/extensions/dm-subagents/src/runs/background/notify.ts +0 -225
  182. package/extensions/dm-subagents/src/runs/background/parallel-groups.ts +0 -45
  183. package/extensions/dm-subagents/src/runs/background/result-watcher.ts +0 -315
  184. package/extensions/dm-subagents/src/runs/background/run-id-resolver.ts +0 -84
  185. package/extensions/dm-subagents/src/runs/background/run-status.ts +0 -434
  186. package/extensions/dm-subagents/src/runs/background/scheduled-runs.ts +0 -514
  187. package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.ts +0 -368
  188. package/extensions/dm-subagents/src/runs/background/subagent-runner.ts +0 -3171
  189. package/extensions/dm-subagents/src/runs/background/top-level-async.ts +0 -13
  190. package/extensions/dm-subagents/src/runs/background/wait.ts +0 -353
  191. package/extensions/dm-subagents/src/runs/foreground/chain-clarify.ts +0 -1333
  192. package/extensions/dm-subagents/src/runs/foreground/chain-execution.ts +0 -1313
  193. package/extensions/dm-subagents/src/runs/foreground/execution.ts +0 -1239
  194. package/extensions/dm-subagents/src/runs/foreground/subagent-executor.ts +0 -3613
  195. package/extensions/dm-subagents/src/runs/shared/acceptance.ts +0 -879
  196. package/extensions/dm-subagents/src/runs/shared/chain-outputs.ts +0 -116
  197. package/extensions/dm-subagents/src/runs/shared/completion-guard.ts +0 -147
  198. package/extensions/dm-subagents/src/runs/shared/dm-args.ts +0 -271
  199. package/extensions/dm-subagents/src/runs/shared/dm-spawn.ts +0 -147
  200. package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.ts +0 -295
  201. package/extensions/dm-subagents/src/runs/shared/long-running-guard.ts +0 -175
  202. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +0 -365
  203. package/extensions/dm-subagents/src/runs/shared/model-fallback.ts +0 -292
  204. package/extensions/dm-subagents/src/runs/shared/model-scope.ts +0 -128
  205. package/extensions/dm-subagents/src/runs/shared/nested-events.ts +0 -908
  206. package/extensions/dm-subagents/src/runs/shared/nested-path.ts +0 -52
  207. package/extensions/dm-subagents/src/runs/shared/nested-render.ts +0 -115
  208. package/extensions/dm-subagents/src/runs/shared/parallel-utils.ts +0 -198
  209. package/extensions/dm-subagents/src/runs/shared/run-history.ts +0 -60
  210. package/extensions/dm-subagents/src/runs/shared/single-output.ts +0 -180
  211. package/extensions/dm-subagents/src/runs/shared/structured-output.ts +0 -77
  212. package/extensions/dm-subagents/src/runs/shared/subagent-control.ts +0 -223
  213. package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.ts +0 -342
  214. package/extensions/dm-subagents/src/runs/shared/tool-budget.ts +0 -74
  215. package/extensions/dm-subagents/src/runs/shared/turn-budget.ts +0 -52
  216. package/extensions/dm-subagents/src/runs/shared/workflow-graph.ts +0 -206
  217. package/extensions/dm-subagents/src/runs/shared/worktree.ts +0 -600
  218. package/extensions/dm-subagents/src/shared/artifacts.ts +0 -113
  219. package/extensions/dm-subagents/src/shared/atomic-json.ts +0 -86
  220. package/extensions/dm-subagents/src/shared/child-transcript.ts +0 -212
  221. package/extensions/dm-subagents/src/shared/file-coalescer.ts +0 -40
  222. package/extensions/dm-subagents/src/shared/fork-context.ts +0 -194
  223. package/extensions/dm-subagents/src/shared/formatters.ts +0 -133
  224. package/extensions/dm-subagents/src/shared/jsonl-writer.ts +0 -81
  225. package/extensions/dm-subagents/src/shared/model-info.ts +0 -78
  226. package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
  227. package/extensions/dm-subagents/src/shared/session-identity.ts +0 -10
  228. package/extensions/dm-subagents/src/shared/session-tokens.ts +0 -44
  229. package/extensions/dm-subagents/src/shared/settings.ts +0 -450
  230. package/extensions/dm-subagents/src/shared/status-format.ts +0 -49
  231. package/extensions/dm-subagents/src/shared/types.ts +0 -1257
  232. package/extensions/dm-subagents/src/shared/utils.ts +0 -554
  233. package/extensions/dm-subagents/src/slash/prompt-template-bridge.ts +0 -420
  234. package/extensions/dm-subagents/src/slash/prompt-workflows.ts +0 -330
  235. package/extensions/dm-subagents/src/slash/slash-bridge.ts +0 -176
  236. package/extensions/dm-subagents/src/slash/slash-commands.ts +0 -1296
  237. package/extensions/dm-subagents/src/slash/slash-live-state.ts +0 -292
  238. package/extensions/dm-subagents/src/tui/render-helpers.ts +0 -80
  239. package/extensions/dm-subagents/src/tui/render.ts +0 -1748
  240. package/extensions/dm-tasks/src/auto-clear.ts +0 -91
  241. package/extensions/dm-tasks/src/index.ts +0 -1145
  242. package/extensions/dm-tasks/src/process-tracker.ts +0 -140
  243. package/extensions/dm-tasks/src/task-store.ts +0 -305
  244. package/extensions/dm-tasks/src/tasks-config.ts +0 -23
  245. package/extensions/dm-tasks/src/types.ts +0 -40
  246. package/extensions/dm-tasks/src/ui/settings-menu.ts +0 -100
  247. package/extensions/dm-tasks/src/ui/task-widget.ts +0 -265
  248. package/extensions/dm-usage/index.ts +0 -1718
  249. package/extensions/greedysearch-dm/index.ts +0 -177
  250. package/extensions/greedysearch-dm/src/formatters/results.ts +0 -163
  251. package/extensions/greedysearch-dm/src/formatters/sources.ts +0 -116
  252. package/extensions/greedysearch-dm/src/formatters/synthesis.ts +0 -87
  253. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.ts +0 -370
  254. package/extensions/greedysearch-dm/src/tools/shared.ts +0 -187
  255. package/extensions/greedysearch-dm/src/types.ts +0 -110
  256. package/extensions/greedysearch-dm/src/utils/helpers.ts +0 -40
@@ -1,857 +0,0 @@
1
- /**
2
- * dm-ask-user — Interactive form tool for DM
3
- *
4
- * A powerful tool that the LLM can call to ask the user one or more questions
5
- * using rich form controls: radio buttons, checkboxes, and text inputs.
6
- * Each question type supports an optional "Other..." escape hatch for custom input.
7
- *
8
- * Question types:
9
- * - radio: Single-select from options (with optional custom "Other")
10
- * - checkbox: Multi-select from options (with optional custom "Other")
11
- * - text: Free-form text input
12
- *
13
- * Navigation:
14
- * - Tab / Shift+Tab to move between questions
15
- * - Up/Down to navigate options within a question
16
- * - Space to toggle checkboxes
17
- * - Enter to select radio / submit text / advance
18
- * - Esc to cancel
19
- */
20
-
21
- import type { ExtensionAPI } from "@duckmind/dm-coding-agent";
22
- import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, visibleWidth } from "@duckmind/dm-tui";
23
- import { Type } from "@sinclair/typebox";
24
-
25
- // ─── Types ───────────────────────────────────────────────────────────────────
26
-
27
- interface QuestionOption {
28
- value: string;
29
- label: string;
30
- description?: string;
31
- }
32
-
33
- interface Question {
34
- id: string;
35
- type: "radio" | "checkbox" | "text";
36
- prompt: string;
37
- label?: string;
38
- options?: QuestionOption[];
39
- allowOther?: boolean;
40
- required?: boolean;
41
- placeholder?: string;
42
- default?: string | string[];
43
- }
44
-
45
- interface NormalizedQuestion extends Question {
46
- label: string;
47
- options: QuestionOption[];
48
- allowOther: boolean;
49
- required: boolean;
50
- }
51
-
52
- interface Answer {
53
- id: string;
54
- type: "radio" | "checkbox" | "text";
55
- value: string | string[];
56
- wasCustom: boolean;
57
- }
58
-
59
- interface FormResult {
60
- title?: string;
61
- questions: NormalizedQuestion[];
62
- answers: Answer[];
63
- cancelled: boolean;
64
- }
65
-
66
- // ─── Schema ──────────────────────────────────────────────────────────────────
67
-
68
- const OptionSchema = Type.Object({
69
- value: Type.String({ description: "Value returned when selected" }),
70
- label: Type.String({ description: "Display label" }),
71
- description: Type.Optional(Type.String({ description: "Help text shown below the label" })),
72
- });
73
-
74
- const QuestionSchema = Type.Object({
75
- id: Type.String({ description: "Unique identifier for this question" }),
76
- type: Type.Unsafe<"radio" | "checkbox" | "text">({
77
- type: "string",
78
- enum: ["radio", "checkbox", "text"],
79
- description: "Question type: radio (single-select), checkbox (multi-select), or text (free input)",
80
- }),
81
- prompt: Type.String({ description: "The question text to display" }),
82
- label: Type.Optional(Type.String({ description: "Short label for tab bar (defaults to Q1, Q2...)" })),
83
- options: Type.Optional(Type.Array(OptionSchema, { description: "Options for radio/checkbox types" })),
84
- allowOther: Type.Optional(
85
- Type.Boolean({ description: "Add an 'Other...' option with text input (default: true for radio/checkbox)" }),
86
- ),
87
- required: Type.Optional(Type.Boolean({ description: "Whether an answer is required (default: true)" })),
88
- placeholder: Type.Optional(Type.String({ description: "Placeholder for text inputs" })),
89
- default: Type.Optional(
90
- Type.Union([Type.String(), Type.Array(Type.String())], {
91
- description: "Default value(s). String for radio/text, string[] for checkbox",
92
- }),
93
- ),
94
- });
95
-
96
- const AskUserQuestionParams = Type.Object({
97
- title: Type.Optional(Type.String({ description: "Form title displayed at the top" })),
98
- description: Type.Optional(Type.String({ description: "Brief context or instructions shown under the title" })),
99
- questions: Type.Array(QuestionSchema, {
100
- description: "One or more questions to ask. Use radio for single-select, checkbox for multi-select, text for free input",
101
- }),
102
- });
103
-
104
- // ─── Helpers ─────────────────────────────────────────────────────────────────
105
-
106
- function normalize(questions: Question[]): NormalizedQuestion[] {
107
- return questions.map((q, i) => ({
108
- ...q,
109
- label: q.label || `Q${i + 1}`,
110
- options: q.options || [],
111
- allowOther: q.type === "text" ? false : q.allowOther !== false,
112
- required: q.required !== false,
113
- }));
114
- }
115
-
116
- function wrapText(text: string, maxWidth: number): string[] {
117
- const words = text.split(" ");
118
- const lines: string[] = [];
119
- let current = "";
120
- for (const word of words) {
121
- if (!current) {
122
- current = word;
123
- } else if (current.length + 1 + word.length <= maxWidth) {
124
- current += ` ${word}`;
125
- } else {
126
- lines.push(current);
127
- current = word;
128
- }
129
- }
130
- if (current) lines.push(current);
131
- return lines.length ? lines : [""];
132
- }
133
-
134
- function errorResult(msg: string): {
135
- content: { type: "text"; text: string }[];
136
- details: FormResult;
137
- } {
138
- return {
139
- content: [{ type: "text", text: msg }],
140
- details: { questions: [], answers: [], cancelled: true },
141
- };
142
- }
143
-
144
- // ─── Symbols ─────────────────────────────────────────────────────────────────
145
-
146
- const SYM = {
147
- radioOn: "◉",
148
- radioOff: "○",
149
- checkOn: "☑",
150
- checkOff: "☐",
151
- pointer: "❯",
152
- dot: "·",
153
- check: "✓",
154
- pencil: "✎",
155
- submit: "✓",
156
- };
157
-
158
- // ─── Extension ───────────────────────────────────────────────────────────────
159
-
160
- export default function askUserQuestion(pi: ExtensionAPI) {
161
- pi.registerTool({
162
- name: "ask_user_question",
163
- label: "Ask User",
164
- description: `Ask the user one or more questions using an interactive form. Supports three question types:
165
- - **radio**: Single-select from predefined options (like multiple choice)
166
- - **checkbox**: Multi-select from options (pick all that apply)
167
- - **text**: Free-form text input
168
-
169
- Each radio/checkbox question can include an "Other..." option that lets the user type a custom answer.
170
-
171
- Use this tool when you need user input to proceed — for clarifying requirements, getting preferences, confirming decisions, or choosing between alternatives. Prefer this over asking plain-text questions in your response.`,
172
- promptSnippet: "Ask the user interactive questions with radio, checkbox, or text inputs",
173
- promptGuidelines: [
174
- "Use ask_user_question instead of asking questions in plain text when you need structured user input.",
175
- "Prefer radio for single-choice, checkbox for multi-choice, text for open-ended answers.",
176
- "Always include an 'Other' escape hatch (allowOther: true) unless the options are exhaustive.",
177
- "Group related questions in a single call rather than making multiple separate calls.",
178
- ],
179
- parameters: AskUserQuestionParams as any,
180
-
181
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
182
- if (!ctx.hasUI) {
183
- return errorResult("Error: UI not available (running in non-interactive mode)");
184
- }
185
- if (!params.questions.length) {
186
- return errorResult("Error: No questions provided");
187
- }
188
-
189
- const questions = normalize(params.questions as Question[]);
190
- const isMulti = questions.length > 1;
191
- const totalTabs = questions.length + (isMulti ? 1 : 0); // +1 for Submit tab
192
-
193
- const result = await ctx.ui.custom<FormResult>((tui, theme, _kb, done) => {
194
- // ── State ────────────────────────────────────────────────
195
- let currentTab = 0;
196
- let cursorIdx = 0; // cursor within current question's options
197
- let otherMode = false; // typing into "Other..." editor
198
- let otherQuestionId: string | null = null;
199
- let cachedLines: string[] | undefined;
200
-
201
- // Answers store
202
- const radioAnswers = new Map<string, { value: string; label: string; wasCustom: boolean }>();
203
- const checkAnswers = new Map<string, Set<string>>(); // id -> set of selected values
204
- const checkCustom = new Map<string, string>(); // id -> custom "other" text
205
- const textAnswers = new Map<string, string>();
206
-
207
- // Initialize defaults
208
- for (const q of questions) {
209
- if (q.type === "checkbox") {
210
- const defaults = new Set<string>();
211
- if (Array.isArray(q.default)) {
212
- for (const v of q.default) defaults.add(v);
213
- }
214
- checkAnswers.set(q.id, defaults);
215
- } else if (q.type === "text" && typeof q.default === "string") {
216
- textAnswers.set(q.id, q.default);
217
- } else if (q.type === "radio" && typeof q.default === "string") {
218
- const opt = q.options.find((o) => o.value === q.default);
219
- if (opt) radioAnswers.set(q.id, { value: opt.value, label: opt.label, wasCustom: false });
220
- }
221
- }
222
-
223
- // Editor for "Other" and "text" fields
224
- const editorTheme: EditorTheme = {
225
- borderColor: (s) => theme.fg("accent", s),
226
- selectList: {
227
- selectedPrefix: (t) => theme.fg("accent", t),
228
- selectedText: (t) => theme.fg("accent", t),
229
- description: (t) => theme.fg("muted", t),
230
- scrollInfo: (t) => theme.fg("dim", t),
231
- noMatch: (t) => theme.fg("warning", t),
232
- },
233
- };
234
- const editor = new Editor(tui, editorTheme);
235
-
236
- function getNextTab(): number {
237
- if (currentTab < questions.length - 1) {
238
- return currentTab + 1;
239
- }
240
- return questions.length; // Submit tab
241
- }
242
-
243
- function advanceTab() {
244
- if (!(questions.length > 1)) {
245
- finishSubmit(false);
246
- } else {
247
- switchTab(getNextTab());
248
- }
249
- }
250
-
251
- function refresh() {
252
- cachedLines = undefined;
253
- tui.requestRender();
254
- }
255
-
256
- function curQ(): NormalizedQuestion | undefined {
257
- return questions[currentTab];
258
- }
259
-
260
- /** Save "Other" editor text to the appropriate answer store and exit otherMode. */
261
- function saveOtherModeText() {
262
- if (!otherMode || !otherQuestionId) return;
263
- const t = editor.getText().trim();
264
- const oq = questions.find((q) => q.id === otherQuestionId);
265
- if (oq?.type === "radio" && t) {
266
- radioAnswers.set(oq.id, { value: t, label: t, wasCustom: true });
267
- } else if (oq?.type === "checkbox" && t) {
268
- checkCustom.set(oq.id, t);
269
- }
270
- otherMode = false;
271
- otherQuestionId = null;
272
- editor.setText("");
273
- }
274
-
275
- /** Total selectable rows for the current question */
276
- function optionCount(q: NormalizedQuestion): number {
277
- if (q.type === "text") return 0;
278
- return q.options.length + (q.allowOther ? 1 : 0);
279
- }
280
-
281
- function isAnswered(q: NormalizedQuestion): boolean {
282
- if (q.type === "radio") return radioAnswers.has(q.id);
283
- if (q.type === "checkbox") {
284
- const set = checkAnswers.get(q.id);
285
- const custom = checkCustom.get(q.id);
286
- return (set != null && set.size > 0) || (custom != null && custom.trim().length > 0);
287
- }
288
- if (q.type === "text") {
289
- return (textAnswers.get(q.id)?.trim() ?? "").length > 0;
290
- }
291
- return false;
292
- }
293
-
294
- function allRequired(): boolean {
295
- return questions.every((q) => !q.required || isAnswered(q));
296
- }
297
-
298
- function switchTab(idx: number) {
299
- // Save text editor state
300
- saveEditorText();
301
- currentTab = ((idx % totalTabs) + totalTabs) % totalTabs;
302
- cursorIdx = 0;
303
- otherMode = false;
304
- otherQuestionId = null;
305
-
306
- // If switching to a text question, load its value
307
- const q = curQ();
308
- if (q?.type === "text") {
309
- editor.setText(textAnswers.get(q.id) ?? "");
310
- }
311
- refresh();
312
- }
313
-
314
- function saveEditorText() {
315
- const q = curQ();
316
- if (!q) return;
317
- if (q.type === "text") {
318
- const t = editor.getText().trim();
319
- if (t) textAnswers.set(q.id, t);
320
- else textAnswers.delete(q.id);
321
- }
322
- }
323
-
324
- function finishSubmit(cancelled: boolean) {
325
- saveEditorText();
326
- const answers: Answer[] = [];
327
- for (const q of questions) {
328
- if (q.type === "radio") {
329
- const a = radioAnswers.get(q.id);
330
- answers.push({
331
- id: q.id,
332
- type: "radio",
333
- value: a?.value ?? "",
334
- wasCustom: a?.wasCustom ?? false,
335
- });
336
- } else if (q.type === "checkbox") {
337
- const set = checkAnswers.get(q.id) ?? new Set();
338
- const custom = checkCustom.get(q.id)?.trim();
339
- const values = [...set];
340
- if (custom) values.push(custom);
341
- answers.push({ id: q.id, type: "checkbox", value: values, wasCustom: !!custom });
342
- } else {
343
- const t = textAnswers.get(q.id) ?? "";
344
- answers.push({ id: q.id, type: "text", value: t, wasCustom: true });
345
- }
346
- }
347
- done({ title: params.title, questions, answers, cancelled });
348
- }
349
-
350
- // ── Editor submit (for "Other" mode) ────────────────────
351
- editor.onSubmit = (value) => {
352
- const trimmed = value.trim();
353
- if (otherMode && otherQuestionId) {
354
- const q = questions.find((q) => q.id === otherQuestionId);
355
- if (q?.type === "radio" && trimmed) {
356
- radioAnswers.set(q.id, { value: trimmed, label: trimmed, wasCustom: true });
357
- } else if (q?.type === "checkbox" && trimmed) {
358
- checkCustom.set(q.id, trimmed);
359
- }
360
- otherMode = false;
361
- otherQuestionId = null;
362
- editor.setText("");
363
-
364
- // Auto-advance
365
- advanceTab();
366
- return;
367
- }
368
-
369
- // Text question submit (fallback — Enter is normally intercepted in handleInput
370
- // before reaching the editor, but handle it here defensively using `value`
371
- // since editor state is already cleared by the time onSubmit fires)
372
- const q = curQ();
373
- if (q?.type === "text") {
374
- const trimmedValue = value.trim();
375
- if (trimmedValue) {
376
- textAnswers.set(q.id, trimmedValue);
377
- } else {
378
- textAnswers.delete(q.id);
379
- }
380
- advanceTab();
381
- }
382
- };
383
-
384
- // ── Input handling ───────────────────────────────────────
385
-
386
- function handleInput(data: string) {
387
- // "Other" editor mode
388
- if (otherMode) {
389
- if (matchesKey(data, Key.escape)) {
390
- otherMode = false;
391
- otherQuestionId = null;
392
- editor.setText("");
393
- refresh();
394
- return;
395
- }
396
- // Enter: capture text directly from editor (before it clears itself) and advance
397
- if (matchesKey(data, Key.enter)) {
398
- saveOtherModeText();
399
- advanceTab();
400
- return;
401
- }
402
- // Tab navigation in multi-question forms: save text and switch tab
403
- if (isMulti && (matchesKey(data, Key.tab) || matchesKey(data, Key.shift("tab")))) {
404
- saveOtherModeText();
405
- switchTab(currentTab + (matchesKey(data, Key.shift("tab")) ? -1 : 1));
406
- return;
407
- }
408
- editor.handleInput(data);
409
- refresh();
410
- return;
411
- }
412
-
413
- // Text question — route most input to editor
414
- const q = curQ();
415
- if (q?.type === "text") {
416
- // Enter: save text (editor still has content here) and advance
417
- if (matchesKey(data, Key.enter)) {
418
- saveEditorText();
419
- advanceTab();
420
- return;
421
- }
422
- // Tab navigation still works
423
- if (isMulti && (matchesKey(data, Key.tab) || matchesKey(data, Key.shift("tab")))) {
424
- saveEditorText();
425
- switchTab(currentTab + (matchesKey(data, Key.shift("tab")) ? -1 : 1));
426
- return;
427
- }
428
- if (matchesKey(data, Key.escape)) {
429
- finishSubmit(true);
430
- return;
431
- }
432
- editor.handleInput(data);
433
- refresh();
434
- return;
435
- }
436
-
437
- // Submit tab (multi-question only)
438
- if (isMulti && currentTab === questions.length) {
439
- if (matchesKey(data, Key.enter) && allRequired()) {
440
- finishSubmit(false);
441
- return;
442
- }
443
- if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
444
- switchTab(0);
445
- return;
446
- }
447
- if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
448
- switchTab(currentTab - 1);
449
- return;
450
- }
451
- if (matchesKey(data, Key.escape)) {
452
- finishSubmit(true);
453
- return;
454
- }
455
- return;
456
- }
457
-
458
- if (!q) return;
459
-
460
- // Tab navigation (multi)
461
- if (isMulti) {
462
- if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
463
- switchTab(currentTab + 1);
464
- return;
465
- }
466
- if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
467
- switchTab(currentTab - 1);
468
- return;
469
- }
470
- }
471
-
472
- // Arrow navigation
473
- const total = optionCount(q);
474
- if (matchesKey(data, Key.up)) {
475
- cursorIdx = Math.max(0, cursorIdx - 1);
476
- refresh();
477
- return;
478
- }
479
- if (matchesKey(data, Key.down)) {
480
- cursorIdx = Math.min(total - 1, cursorIdx + 1);
481
- refresh();
482
- return;
483
- }
484
-
485
- // Escape
486
- if (matchesKey(data, Key.escape)) {
487
- finishSubmit(true);
488
- return;
489
- }
490
-
491
- // Radio select
492
- if (q.type === "radio" && matchesKey(data, Key.enter)) {
493
- const isOther = q.allowOther && cursorIdx === q.options.length;
494
- if (isOther) {
495
- otherMode = true;
496
- otherQuestionId = q.id;
497
- // Pre-fill with existing custom answer
498
- const existing = radioAnswers.get(q.id);
499
- editor.setText(existing?.wasCustom ? existing.label : "");
500
- refresh();
501
- return;
502
- }
503
- const opt = q.options[cursorIdx];
504
- if (opt) {
505
- radioAnswers.set(q.id, { value: opt.value, label: opt.label, wasCustom: false });
506
- advanceTab();
507
- }
508
- return;
509
- }
510
-
511
- // Checkbox toggle (space only)
512
- if (q.type === "checkbox" && matchesKey(data, Key.space)) {
513
- const isOther = q.allowOther && cursorIdx === q.options.length;
514
- if (isOther) {
515
- otherMode = true;
516
- otherQuestionId = q.id;
517
- editor.setText(checkCustom.get(q.id) ?? "");
518
- refresh();
519
- return;
520
- }
521
- const opt = q.options[cursorIdx];
522
- if (opt) {
523
- const set = checkAnswers.get(q.id) ?? new Set();
524
- if (set.has(opt.value)) set.delete(opt.value);
525
- else set.add(opt.value);
526
- checkAnswers.set(q.id, set);
527
- refresh();
528
- }
529
- return;
530
- }
531
-
532
- // Checkbox: Enter submits (single) or advances (multi)
533
- if (q.type === "checkbox" && matchesKey(data, Key.enter)) {
534
- advanceTab();
535
- return;
536
- }
537
- }
538
-
539
- // ── Render ───────────────────────────────────────────────
540
-
541
- function render(width: number): string[] {
542
- if (cachedLines) return cachedLines;
543
-
544
- const lines: string[] = [];
545
- const maxW = Math.min(width, 120);
546
- const add = (s: string) => lines.push(truncateToWidth(s, maxW));
547
- const hr = () => add(theme.fg("accent", "─".repeat(maxW)));
548
-
549
- hr();
550
-
551
- // Title & description
552
- if (params.title) {
553
- add(` ${theme.fg("accent", theme.bold(params.title))}`);
554
- }
555
- if (params.description) {
556
- add(` ${theme.fg("muted", params.description)}`);
557
- }
558
- if (params.title || params.description) lines.push("");
559
-
560
- // Tab bar (multi-question)
561
- if (isMulti) {
562
- const tabs: string[] = [];
563
- for (let i = 0; i < questions.length; i++) {
564
- const isActive = i === currentTab;
565
- const answered = isAnswered(questions[i]);
566
- const lbl = questions[i].label;
567
- const icon = answered ? theme.fg("success", SYM.check) : theme.fg("dim", SYM.dot);
568
- const text = ` ${icon} ${lbl} `;
569
- tabs.push(
570
- isActive ? theme.bg("selectedBg", theme.fg("text", text)) : theme.fg(answered ? "success" : "muted", text),
571
- );
572
- }
573
- // Submit tab
574
- const isSubmitTab = currentTab === questions.length;
575
- const canSubmit = allRequired();
576
- const submitText = ` ${SYM.submit} Submit `;
577
- tabs.push(
578
- isSubmitTab
579
- ? theme.bg("selectedBg", theme.fg("text", submitText))
580
- : theme.fg(canSubmit ? "success" : "dim", submitText),
581
- );
582
- add(` ${tabs.join(theme.fg("dim", "│"))}`);
583
- lines.push("");
584
- }
585
-
586
- const q = curQ();
587
-
588
- // ── Submit tab ───────────────────────────────────────
589
- if (isMulti && currentTab === questions.length) {
590
- add(` ${theme.fg("accent", theme.bold("Review & Submit"))}`);
591
- lines.push("");
592
-
593
- for (const question of questions) {
594
- const label = theme.fg("muted", `${question.label}:`);
595
- if (question.type === "radio") {
596
- const a = radioAnswers.get(question.id);
597
- if (a) {
598
- const prefix = a.wasCustom ? theme.fg("dim", "(wrote) ") : "";
599
- add(` ${label} ${prefix}${a.label}`);
600
- } else {
601
- add(` ${label} ${theme.fg("warning", "(unanswered)")}`);
602
- }
603
- } else if (question.type === "checkbox") {
604
- const set = checkAnswers.get(question.id) ?? new Set();
605
- const custom = checkCustom.get(question.id)?.trim();
606
- const all = [...set];
607
- if (custom) all.push(`${theme.fg("dim", "(wrote)")} ${custom}`);
608
- if (all.length) {
609
- add(` ${label} ${all.join(", ")}`);
610
- } else {
611
- add(` ${label} ${theme.fg("warning", "(unanswered)")}`);
612
- }
613
- } else {
614
- const t = textAnswers.get(question.id)?.trim();
615
- if (t) {
616
- add(` ${label} ${truncateToWidth(t, maxW - visibleWidth(question.label) - 5)}`);
617
- } else {
618
- add(` ${label} ${theme.fg("warning", "(unanswered)")}`);
619
- }
620
- }
621
- }
622
-
623
- lines.push("");
624
- if (allRequired()) {
625
- add(` ${theme.fg("success", "Press Enter to submit")}`);
626
- } else {
627
- const missing = questions
628
- .filter((q) => q.required && !isAnswered(q))
629
- .map((q) => q.label)
630
- .join(", ");
631
- add(` ${theme.fg("warning", `Required: ${missing}`)}`);
632
- }
633
-
634
- lines.push("");
635
- add(theme.fg("dim", " Tab/←→ navigate questions • Enter submit • Esc cancel"));
636
- hr();
637
- cachedLines = lines;
638
- return lines;
639
- }
640
-
641
- if (!q) {
642
- hr();
643
- cachedLines = lines;
644
- return lines;
645
- }
646
-
647
- // ── Question prompt ──────────────────────────────────
648
- const typeTag =
649
- q.type === "radio"
650
- ? theme.fg("dim", "[single-select]")
651
- : q.type === "checkbox"
652
- ? theme.fg("dim", "[multi-select]")
653
- : theme.fg("dim", "[text]");
654
-
655
- const promptLines = wrapText(q.prompt, maxW - 2);
656
- for (let i = 0; i < promptLines.length; i++) {
657
- const isLast = i === promptLines.length - 1;
658
- add(` ${theme.fg("text", theme.bold(promptLines[i]))}${isLast ? ` ${typeTag}` : ""}`);
659
- }
660
- if (q.required) {
661
- add(` ${theme.fg("warning", "*required")}`);
662
- }
663
- lines.push("");
664
-
665
- // ── Radio options ────────────────────────────────────
666
- if (q.type === "radio") {
667
- const selected = radioAnswers.get(q.id);
668
- for (let i = 0; i < q.options.length; i++) {
669
- const opt = q.options[i];
670
- const isCursor = i === cursorIdx;
671
- const isSelected = selected?.value === opt.value && !selected.wasCustom;
672
- const bullet = isSelected ? theme.fg("accent", SYM.radioOn) : theme.fg("dim", SYM.radioOff);
673
- const pointer = isCursor ? theme.fg("accent", SYM.pointer) : " ";
674
- const color = isCursor ? "accent" : isSelected ? "text" : "muted";
675
- add(` ${pointer} ${bullet} ${theme.fg(color, opt.label)}`);
676
- if (opt.description) {
677
- add(` ${theme.fg("dim", opt.description)}`);
678
- }
679
- }
680
- if (q.allowOther) {
681
- const isCursor = cursorIdx === q.options.length;
682
- const isSelected = selected?.wasCustom === true;
683
- const bullet = isSelected ? theme.fg("accent", SYM.radioOn) : theme.fg("dim", SYM.radioOff);
684
- const pointer = isCursor ? theme.fg("accent", SYM.pointer) : " ";
685
- const label = isSelected ? `Other: ${selected.label}` : "Other...";
686
- add(` ${pointer} ${bullet} ${theme.fg(isCursor ? "accent" : "muted", label)}`);
687
-
688
- if (otherMode) {
689
- lines.push("");
690
- add(` ${theme.fg("muted", " Your answer:")}`);
691
- for (const line of editor.render(maxW - 6)) {
692
- add(` ${line}`);
693
- }
694
- }
695
- }
696
- }
697
-
698
- // ── Checkbox options ─────────────────────────────────
699
- if (q.type === "checkbox") {
700
- const set = checkAnswers.get(q.id) ?? new Set();
701
- for (let i = 0; i < q.options.length; i++) {
702
- const opt = q.options[i];
703
- const isCursor = i === cursorIdx;
704
- const isChecked = set.has(opt.value);
705
- const box = isChecked ? theme.fg("accent", SYM.checkOn) : theme.fg("dim", SYM.checkOff);
706
- const pointer = isCursor ? theme.fg("accent", SYM.pointer) : " ";
707
- const color = isCursor ? "accent" : isChecked ? "text" : "muted";
708
- add(` ${pointer} ${box} ${theme.fg(color, opt.label)}`);
709
- if (opt.description) {
710
- add(` ${theme.fg("dim", opt.description)}`);
711
- }
712
- }
713
- if (q.allowOther) {
714
- const isCursor = cursorIdx === q.options.length;
715
- const custom = checkCustom.get(q.id)?.trim();
716
- const box = custom ? theme.fg("accent", SYM.checkOn) : theme.fg("dim", SYM.checkOff);
717
- const pointer = isCursor ? theme.fg("accent", SYM.pointer) : " ";
718
- const label = custom ? `Other: ${custom}` : "Other...";
719
- add(` ${pointer} ${box} ${theme.fg(isCursor ? "accent" : "muted", label)}`);
720
-
721
- if (otherMode) {
722
- lines.push("");
723
- add(` ${theme.fg("muted", " Your answer:")}`);
724
- for (const line of editor.render(maxW - 6)) {
725
- add(` ${line}`);
726
- }
727
- }
728
- }
729
- }
730
-
731
- // ── Text input ───────────────────────────────────────
732
- if (q.type === "text") {
733
- if (q.placeholder && !editor.getText()) {
734
- add(` ${theme.fg("dim", q.placeholder)}`);
735
- }
736
- for (const line of editor.render(maxW - 4)) {
737
- add(` ${line}`);
738
- }
739
- }
740
-
741
- // ── Footer ───────────────────────────────────────────
742
- lines.push("");
743
- if (otherMode) {
744
- add(theme.fg("dim", " Enter submit • Esc go back"));
745
- } else if (q.type === "text") {
746
- const nav = isMulti ? "Tab/←→ navigate • " : "";
747
- add(theme.fg("dim", ` ${nav}Enter submit • Esc cancel`));
748
- } else if (q.type === "checkbox") {
749
- const nav = isMulti ? "Tab/←→ navigate • " : "";
750
- add(theme.fg("dim", ` ↑↓ navigate • Space toggle • ${nav}Enter ${isMulti ? "next" : "submit"} • Esc cancel`));
751
- } else {
752
- const nav = isMulti ? "Tab/←→ navigate • " : "";
753
- add(theme.fg("dim", ` ↑↓ navigate • ${nav}Enter select • Esc cancel`));
754
- }
755
- hr();
756
-
757
- cachedLines = lines;
758
- return lines;
759
- }
760
-
761
- // Initialize: if first question is text, load editor
762
- const firstQ = questions[0];
763
- if (firstQ?.type === "text") {
764
- editor.setText(textAnswers.get(firstQ.id) ?? "");
765
- }
766
-
767
- return {
768
- render,
769
- invalidate: () => {
770
- cachedLines = undefined;
771
- },
772
- handleInput,
773
- };
774
- });
775
-
776
- // ── Format result ────────────────────────────────────────────
777
-
778
- if (result.cancelled) {
779
- return {
780
- content: [{ type: "text", text: "User cancelled the form" }],
781
- details: result,
782
- };
783
- }
784
-
785
- const answerLines: string[] = [];
786
- for (const a of result.answers) {
787
- const q = questions.find((q) => q.id === a.id);
788
- const label = q?.label || a.id;
789
- if (a.type === "radio") {
790
- const prefix = a.wasCustom ? "(wrote) " : "";
791
- answerLines.push(`${label}: ${prefix}${a.value}`);
792
- } else if (a.type === "checkbox") {
793
- const values = Array.isArray(a.value) ? a.value : [a.value];
794
- if (values.length === 0) {
795
- answerLines.push(`${label}: (none selected)`);
796
- } else {
797
- answerLines.push(`${label}: ${values.join(", ")}`);
798
- }
799
- } else {
800
- answerLines.push(`${label}: ${a.value || "(empty)"}`);
801
- }
802
- }
803
-
804
- return {
805
- content: [{ type: "text", text: answerLines.join("\n") }],
806
- details: result,
807
- };
808
- },
809
-
810
- // ── Custom rendering ─────────────────────────────────────────────
811
-
812
- renderCall(args, theme, _context) {
813
- const qs = (args.questions as Question[]) || [];
814
- const title = args.title as string | undefined;
815
- let text = theme.fg("toolTitle", theme.bold("ask_user_question "));
816
- if (title) {
817
- text += theme.fg("accent", title) + " ";
818
- }
819
- text += theme.fg("muted", `${qs.length} question${qs.length !== 1 ? "s" : ""}`);
820
- const types = [...new Set(qs.map((q) => q.type))].join(", ");
821
- if (types) {
822
- text += theme.fg("dim", ` (${types})`);
823
- }
824
- return new Text(text, 0, 0);
825
- },
826
-
827
- renderResult(result, _options, theme, _context) {
828
- const details = result.details as FormResult | undefined;
829
- if (!details) {
830
- const text = result.content[0];
831
- return new Text(text?.type === "text" ? text.text : "", 0, 0);
832
- }
833
-
834
- if (details.cancelled) {
835
- return new Text(theme.fg("warning", "Cancelled"), 0, 0);
836
- }
837
-
838
- const lines = details.answers.map((a) => {
839
- const q = details.questions.find((q) => q.id === a.id);
840
- const label = q?.label || a.id;
841
-
842
- if (a.type === "radio") {
843
- const prefix = a.wasCustom ? theme.fg("dim", "(wrote) ") : "";
844
- return `${theme.fg("success", SYM.check)} ${theme.fg("accent", label)}: ${prefix}${a.value}`;
845
- }
846
- if (a.type === "checkbox") {
847
- const values = Array.isArray(a.value) ? a.value : [a.value];
848
- const display = values.length ? values.join(", ") : theme.fg("dim", "(none)");
849
- return `${theme.fg("success", SYM.check)} ${theme.fg("accent", label)}: ${display}`;
850
- }
851
- return `${theme.fg("success", SYM.check)} ${theme.fg("accent", label)}: ${a.value || theme.fg("dim", "(empty)")}`;
852
- });
853
-
854
- return new Text(lines.join("\n"), 0, 0);
855
- },
856
- });
857
- }