@duckmind/dm-darwin-x64 0.43.4 → 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 (251) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +241 -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/index.js +20 -0
  126. package/extensions/greedysearch-dm/package.json +1 -22
  127. package/extensions/greedysearch-dm/src/formatters/results.js +8 -0
  128. package/extensions/greedysearch-dm/src/formatters/sources.js +1 -0
  129. package/extensions/greedysearch-dm/src/formatters/synthesis.js +1 -0
  130. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +20 -0
  131. package/extensions/greedysearch-dm/src/tools/shared.js +9 -0
  132. package/extensions/greedysearch-dm/src/types.js +0 -0
  133. package/extensions/greedysearch-dm/src/utils/helpers.js +1 -0
  134. package/package.json +1 -1
  135. package/extensions/dm-9router-ext/src/index.ts +0 -541
  136. package/extensions/dm-ask-user/index.ts +0 -857
  137. package/extensions/dm-context/src/context.ts +0 -158
  138. package/extensions/dm-context/src/index.ts +0 -439
  139. package/extensions/dm-context/src/utils.ts +0 -6
  140. package/extensions/dm-fff/src/index.ts +0 -1023
  141. package/extensions/dm-fff/src/query.ts +0 -87
  142. package/extensions/dm-goal/src/goal.ts +0 -1073
  143. package/extensions/dm-grill-me/index.ts +0 -1085
  144. package/extensions/dm-subagents/src/agents/agent-management.ts +0 -1052
  145. package/extensions/dm-subagents/src/agents/agent-memory.ts +0 -254
  146. package/extensions/dm-subagents/src/agents/agent-scope.ts +0 -6
  147. package/extensions/dm-subagents/src/agents/agent-selection.ts +0 -25
  148. package/extensions/dm-subagents/src/agents/agent-serializer.ts +0 -121
  149. package/extensions/dm-subagents/src/agents/agents.ts +0 -1553
  150. package/extensions/dm-subagents/src/agents/chain-serializer.ts +0 -277
  151. package/extensions/dm-subagents/src/agents/frontmatter.ts +0 -93
  152. package/extensions/dm-subagents/src/agents/identity.ts +0 -30
  153. package/extensions/dm-subagents/src/agents/proactive-skills.ts +0 -191
  154. package/extensions/dm-subagents/src/agents/skills.ts +0 -729
  155. package/extensions/dm-subagents/src/extension/config.ts +0 -39
  156. package/extensions/dm-subagents/src/extension/control-notices.ts +0 -92
  157. package/extensions/dm-subagents/src/extension/doctor.ts +0 -214
  158. package/extensions/dm-subagents/src/extension/fanout-child.ts +0 -172
  159. package/extensions/dm-subagents/src/extension/index.ts +0 -656
  160. package/extensions/dm-subagents/src/extension/rpc.ts +0 -369
  161. package/extensions/dm-subagents/src/extension/schemas.ts +0 -309
  162. package/extensions/dm-subagents/src/intercom/intercom-bridge.ts +0 -180
  163. package/extensions/dm-subagents/src/intercom/native-supervisor-channel.ts +0 -519
  164. package/extensions/dm-subagents/src/intercom/result-intercom.ts +0 -377
  165. package/extensions/dm-subagents/src/profiles/profiles.ts +0 -637
  166. package/extensions/dm-subagents/src/runs/background/async-execution.ts +0 -1065
  167. package/extensions/dm-subagents/src/runs/background/async-job-tracker.ts +0 -441
  168. package/extensions/dm-subagents/src/runs/background/async-resume.ts +0 -391
  169. package/extensions/dm-subagents/src/runs/background/async-status.ts +0 -395
  170. package/extensions/dm-subagents/src/runs/background/chain-append.ts +0 -282
  171. package/extensions/dm-subagents/src/runs/background/chain-root-attachment.ts +0 -191
  172. package/extensions/dm-subagents/src/runs/background/completion-batcher.ts +0 -166
  173. package/extensions/dm-subagents/src/runs/background/completion-dedupe.ts +0 -63
  174. package/extensions/dm-subagents/src/runs/background/control-channel.ts +0 -332
  175. package/extensions/dm-subagents/src/runs/background/fleet-view.ts +0 -515
  176. package/extensions/dm-subagents/src/runs/background/notify.ts +0 -225
  177. package/extensions/dm-subagents/src/runs/background/parallel-groups.ts +0 -45
  178. package/extensions/dm-subagents/src/runs/background/result-watcher.ts +0 -315
  179. package/extensions/dm-subagents/src/runs/background/run-id-resolver.ts +0 -84
  180. package/extensions/dm-subagents/src/runs/background/run-status.ts +0 -434
  181. package/extensions/dm-subagents/src/runs/background/scheduled-runs.ts +0 -514
  182. package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.ts +0 -368
  183. package/extensions/dm-subagents/src/runs/background/subagent-runner.ts +0 -3171
  184. package/extensions/dm-subagents/src/runs/background/top-level-async.ts +0 -13
  185. package/extensions/dm-subagents/src/runs/background/wait.ts +0 -353
  186. package/extensions/dm-subagents/src/runs/foreground/chain-clarify.ts +0 -1333
  187. package/extensions/dm-subagents/src/runs/foreground/chain-execution.ts +0 -1313
  188. package/extensions/dm-subagents/src/runs/foreground/execution.ts +0 -1239
  189. package/extensions/dm-subagents/src/runs/foreground/subagent-executor.ts +0 -3613
  190. package/extensions/dm-subagents/src/runs/shared/acceptance.ts +0 -879
  191. package/extensions/dm-subagents/src/runs/shared/chain-outputs.ts +0 -116
  192. package/extensions/dm-subagents/src/runs/shared/completion-guard.ts +0 -147
  193. package/extensions/dm-subagents/src/runs/shared/dm-args.ts +0 -271
  194. package/extensions/dm-subagents/src/runs/shared/dm-spawn.ts +0 -147
  195. package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.ts +0 -295
  196. package/extensions/dm-subagents/src/runs/shared/long-running-guard.ts +0 -175
  197. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +0 -365
  198. package/extensions/dm-subagents/src/runs/shared/model-fallback.ts +0 -292
  199. package/extensions/dm-subagents/src/runs/shared/model-scope.ts +0 -128
  200. package/extensions/dm-subagents/src/runs/shared/nested-events.ts +0 -908
  201. package/extensions/dm-subagents/src/runs/shared/nested-path.ts +0 -52
  202. package/extensions/dm-subagents/src/runs/shared/nested-render.ts +0 -115
  203. package/extensions/dm-subagents/src/runs/shared/parallel-utils.ts +0 -198
  204. package/extensions/dm-subagents/src/runs/shared/run-history.ts +0 -60
  205. package/extensions/dm-subagents/src/runs/shared/single-output.ts +0 -180
  206. package/extensions/dm-subagents/src/runs/shared/structured-output.ts +0 -77
  207. package/extensions/dm-subagents/src/runs/shared/subagent-control.ts +0 -223
  208. package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.ts +0 -342
  209. package/extensions/dm-subagents/src/runs/shared/tool-budget.ts +0 -74
  210. package/extensions/dm-subagents/src/runs/shared/turn-budget.ts +0 -52
  211. package/extensions/dm-subagents/src/runs/shared/workflow-graph.ts +0 -206
  212. package/extensions/dm-subagents/src/runs/shared/worktree.ts +0 -600
  213. package/extensions/dm-subagents/src/shared/artifacts.ts +0 -113
  214. package/extensions/dm-subagents/src/shared/atomic-json.ts +0 -86
  215. package/extensions/dm-subagents/src/shared/child-transcript.ts +0 -212
  216. package/extensions/dm-subagents/src/shared/file-coalescer.ts +0 -40
  217. package/extensions/dm-subagents/src/shared/fork-context.ts +0 -194
  218. package/extensions/dm-subagents/src/shared/formatters.ts +0 -133
  219. package/extensions/dm-subagents/src/shared/jsonl-writer.ts +0 -81
  220. package/extensions/dm-subagents/src/shared/model-info.ts +0 -78
  221. package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
  222. package/extensions/dm-subagents/src/shared/session-identity.ts +0 -10
  223. package/extensions/dm-subagents/src/shared/session-tokens.ts +0 -44
  224. package/extensions/dm-subagents/src/shared/settings.ts +0 -450
  225. package/extensions/dm-subagents/src/shared/status-format.ts +0 -49
  226. package/extensions/dm-subagents/src/shared/types.ts +0 -1257
  227. package/extensions/dm-subagents/src/shared/utils.ts +0 -554
  228. package/extensions/dm-subagents/src/slash/prompt-template-bridge.ts +0 -420
  229. package/extensions/dm-subagents/src/slash/prompt-workflows.ts +0 -330
  230. package/extensions/dm-subagents/src/slash/slash-bridge.ts +0 -176
  231. package/extensions/dm-subagents/src/slash/slash-commands.ts +0 -1296
  232. package/extensions/dm-subagents/src/slash/slash-live-state.ts +0 -292
  233. package/extensions/dm-subagents/src/tui/render-helpers.ts +0 -80
  234. package/extensions/dm-subagents/src/tui/render.ts +0 -1748
  235. package/extensions/dm-tasks/src/auto-clear.ts +0 -91
  236. package/extensions/dm-tasks/src/index.ts +0 -1145
  237. package/extensions/dm-tasks/src/process-tracker.ts +0 -140
  238. package/extensions/dm-tasks/src/task-store.ts +0 -305
  239. package/extensions/dm-tasks/src/tasks-config.ts +0 -23
  240. package/extensions/dm-tasks/src/types.ts +0 -40
  241. package/extensions/dm-tasks/src/ui/settings-menu.ts +0 -100
  242. package/extensions/dm-tasks/src/ui/task-widget.ts +0 -265
  243. package/extensions/dm-usage/index.ts +0 -1718
  244. package/extensions/greedysearch-dm/index.ts +0 -177
  245. package/extensions/greedysearch-dm/src/formatters/results.ts +0 -163
  246. package/extensions/greedysearch-dm/src/formatters/sources.ts +0 -116
  247. package/extensions/greedysearch-dm/src/formatters/synthesis.ts +0 -87
  248. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.ts +0 -370
  249. package/extensions/greedysearch-dm/src/tools/shared.ts +0 -187
  250. package/extensions/greedysearch-dm/src/types.ts +0 -110
  251. 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
- }