@duckmind/dm-darwin-x64 0.43.4 → 0.48.5

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