pi-agent-flow 1.8.39 → 2.0.0

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 (291) hide show
  1. package/README.md +33 -37
  2. package/agents/audit.md +18 -22
  3. package/agents/build.md +20 -22
  4. package/agents/craft.md +20 -27
  5. package/agents/debug.md +21 -28
  6. package/agents/ideas.md +18 -101
  7. package/agents/scout.md +15 -19
  8. package/dist/batch/batch-bash.d.ts +2 -2
  9. package/dist/batch/batch-bash.d.ts.map +1 -1
  10. package/dist/batch/batch-bash.js +3 -3
  11. package/dist/batch/batch-bash.js.map +1 -1
  12. package/dist/batch/constants.d.ts +19 -5
  13. package/dist/batch/constants.d.ts.map +1 -1
  14. package/dist/batch/constants.js +4 -3
  15. package/dist/batch/constants.js.map +1 -1
  16. package/dist/batch/execute.d.ts +0 -1
  17. package/dist/batch/execute.d.ts.map +1 -1
  18. package/dist/batch/execute.js +101 -10
  19. package/dist/batch/execute.js.map +1 -1
  20. package/dist/batch/fuzzy-edit.d.ts +0 -6
  21. package/dist/batch/fuzzy-edit.d.ts.map +1 -1
  22. package/dist/batch/fuzzy-edit.js +1 -1
  23. package/dist/batch/fuzzy-edit.js.map +1 -1
  24. package/dist/batch/index.d.ts.map +1 -1
  25. package/dist/batch/index.js +87 -16
  26. package/dist/batch/index.js.map +1 -1
  27. package/dist/batch/render.d.ts +0 -1
  28. package/dist/batch/render.d.ts.map +1 -1
  29. package/dist/batch/render.js +7 -101
  30. package/dist/batch/render.js.map +1 -1
  31. package/dist/batch/summary.d.ts +5 -0
  32. package/dist/batch/summary.d.ts.map +1 -0
  33. package/dist/batch/summary.js +101 -0
  34. package/dist/batch/summary.js.map +1 -0
  35. package/dist/{config.d.ts → config/config.d.ts} +34 -2
  36. package/dist/config/config.d.ts.map +1 -0
  37. package/dist/{config.js → config/config.js} +157 -9
  38. package/dist/config/config.js.map +1 -0
  39. package/dist/config/log.d.ts +27 -0
  40. package/dist/config/log.d.ts.map +1 -0
  41. package/dist/config/log.js +104 -0
  42. package/dist/config/log.js.map +1 -0
  43. package/dist/{settings-resolver.d.ts → config/settings-resolver.d.ts} +9 -2
  44. package/dist/config/settings-resolver.d.ts.map +1 -0
  45. package/dist/config/settings-resolver.js +275 -0
  46. package/dist/config/settings-resolver.js.map +1 -0
  47. package/dist/core/agents.d.ts.map +1 -0
  48. package/dist/{agents.js → core/agents.js} +11 -10
  49. package/dist/core/agents.js.map +1 -0
  50. package/dist/core/delegation.d.ts +24 -0
  51. package/dist/core/delegation.d.ts.map +1 -0
  52. package/dist/core/delegation.js +55 -0
  53. package/dist/core/delegation.js.map +1 -0
  54. package/dist/core/depth.d.ts.map +1 -0
  55. package/dist/{depth.js → core/depth.js} +9 -8
  56. package/dist/core/depth.js.map +1 -0
  57. package/dist/{executor.d.ts → core/executor.d.ts} +13 -3
  58. package/dist/core/executor.d.ts.map +1 -0
  59. package/dist/{executor.js → core/executor.js} +79 -15
  60. package/dist/core/executor.js.map +1 -0
  61. package/dist/{flow.d.ts → core/flow.d.ts} +4 -1
  62. package/dist/core/flow.d.ts.map +1 -0
  63. package/dist/{flow.js → core/flow.js} +179 -25
  64. package/dist/core/flow.js.map +1 -0
  65. package/dist/{session-mode.d.ts → core/session-mode.d.ts} +2 -1
  66. package/dist/core/session-mode.d.ts.map +1 -0
  67. package/dist/{session-mode.js → core/session-mode.js} +1 -1
  68. package/dist/core/session-mode.js.map +1 -0
  69. package/dist/core/session-registry.d.ts +16 -0
  70. package/dist/core/session-registry.d.ts.map +1 -0
  71. package/dist/core/session-registry.js +30 -0
  72. package/dist/core/session-registry.js.map +1 -0
  73. package/dist/core/transitions.d.ts.map +1 -0
  74. package/dist/{transitions.js → core/transitions.js} +1 -1
  75. package/dist/core/transitions.js.map +1 -0
  76. package/dist/flow/command.d.ts +8 -0
  77. package/dist/flow/command.d.ts.map +1 -0
  78. package/dist/flow/command.js +189 -0
  79. package/dist/flow/command.js.map +1 -0
  80. package/dist/flow/continuation.d.ts +16 -0
  81. package/dist/flow/continuation.d.ts.map +1 -0
  82. package/dist/flow/continuation.js +151 -0
  83. package/dist/flow/continuation.js.map +1 -0
  84. package/dist/flow/index.d.ts +15 -0
  85. package/dist/flow/index.d.ts.map +1 -0
  86. package/dist/flow/index.js +22 -0
  87. package/dist/flow/index.js.map +1 -0
  88. package/dist/flow/settings-command.d.ts +51 -0
  89. package/dist/flow/settings-command.d.ts.map +1 -0
  90. package/dist/flow/settings-command.js +851 -0
  91. package/dist/flow/settings-command.js.map +1 -0
  92. package/dist/flow/store.d.ts +26 -0
  93. package/dist/flow/store.d.ts.map +1 -0
  94. package/dist/flow/store.js +158 -0
  95. package/dist/flow/store.js.map +1 -0
  96. package/dist/flow/template-strings.d.ts +8 -0
  97. package/dist/flow/template-strings.d.ts.map +1 -0
  98. package/dist/flow/template-strings.js +39 -0
  99. package/dist/flow/template-strings.js.map +1 -0
  100. package/dist/flow/types.d.ts +55 -0
  101. package/dist/flow/types.d.ts.map +1 -0
  102. package/dist/flow/types.js +5 -0
  103. package/dist/flow/types.js.map +1 -0
  104. package/dist/flow/warp-command.d.ts +9 -0
  105. package/dist/flow/warp-command.d.ts.map +1 -0
  106. package/dist/flow/warp-command.js +405 -0
  107. package/dist/flow/warp-command.js.map +1 -0
  108. package/dist/index.d.ts +3 -1
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +115 -32
  111. package/dist/index.js.map +1 -1
  112. package/dist/{notify-state.d.ts → notify/notify-state.d.ts} +2 -1
  113. package/dist/notify/notify-state.d.ts.map +1 -0
  114. package/dist/notify/notify-state.js.map +1 -0
  115. package/dist/notify/notify.d.ts.map +1 -0
  116. package/dist/{notify.js → notify/notify.js} +3 -2
  117. package/dist/notify/notify.js.map +1 -0
  118. package/dist/{cli-args.d.ts → snapshot/cli-args.d.ts} +4 -2
  119. package/dist/snapshot/cli-args.d.ts.map +1 -0
  120. package/dist/{cli-args.js → snapshot/cli-args.js} +10 -1
  121. package/dist/snapshot/cli-args.js.map +1 -0
  122. package/dist/snapshot/index.d.ts +2 -0
  123. package/dist/snapshot/index.d.ts.map +1 -0
  124. package/dist/snapshot/index.js +2 -0
  125. package/dist/snapshot/index.js.map +1 -0
  126. package/dist/{reasoning-strip.d.ts → snapshot/reasoning-strip.d.ts} +0 -4
  127. package/dist/snapshot/reasoning-strip.d.ts.map +1 -0
  128. package/dist/{reasoning-strip.js → snapshot/reasoning-strip.js} +2 -2
  129. package/dist/snapshot/reasoning-strip.js.map +1 -0
  130. package/dist/{runner-events.d.ts → snapshot/runner-events.d.ts} +13 -1
  131. package/dist/snapshot/runner-events.d.ts.map +1 -0
  132. package/dist/{runner-events.js → snapshot/runner-events.js} +16 -4
  133. package/dist/snapshot/runner-events.js.map +1 -0
  134. package/dist/{snapshot.d.ts → snapshot/snapshot.d.ts} +29 -3
  135. package/dist/snapshot/snapshot.d.ts.map +1 -0
  136. package/dist/{snapshot.js → snapshot/snapshot.js} +347 -39
  137. package/dist/snapshot/snapshot.js.map +1 -0
  138. package/dist/{structured-output.d.ts → snapshot/structured-output.d.ts} +1 -1
  139. package/dist/snapshot/structured-output.d.ts.map +1 -0
  140. package/dist/{structured-output.js → snapshot/structured-output.js} +13 -0
  141. package/dist/snapshot/structured-output.js.map +1 -0
  142. package/dist/{flow-prompt.d.ts → steering/flow-prompt.d.ts} +2 -2
  143. package/dist/steering/flow-prompt.d.ts.map +1 -0
  144. package/dist/{flow-prompt.js → steering/flow-prompt.js} +3 -3
  145. package/dist/steering/flow-prompt.js.map +1 -0
  146. package/dist/{sliding-prompt.d.ts → steering/sliding-prompt.d.ts} +8 -7
  147. package/dist/steering/sliding-prompt.d.ts.map +1 -0
  148. package/dist/{sliding-prompt.js → steering/sliding-prompt.js} +15 -64
  149. package/dist/steering/sliding-prompt.js.map +1 -0
  150. package/dist/{tool-utils.d.ts → steering/tool-utils.d.ts} +1 -0
  151. package/dist/steering/tool-utils.d.ts.map +1 -0
  152. package/dist/{tool-utils.js → steering/tool-utils.js} +10 -3
  153. package/dist/steering/tool-utils.js.map +1 -0
  154. package/dist/{ask-user.d.ts → tools/ask-user.d.ts} +3 -15
  155. package/dist/tools/ask-user.d.ts.map +1 -0
  156. package/dist/tools/ask-user.js +778 -0
  157. package/dist/tools/ask-user.js.map +1 -0
  158. package/dist/{timed-bash.d.ts → tools/timed-bash.d.ts} +2 -7
  159. package/dist/tools/timed-bash.d.ts.map +1 -0
  160. package/dist/{timed-bash.js → tools/timed-bash.js} +2 -2
  161. package/dist/tools/timed-bash.js.map +1 -0
  162. package/dist/{web-tool.d.ts → tools/web-tool.d.ts} +1 -1
  163. package/dist/tools/web-tool.d.ts.map +1 -0
  164. package/dist/{web-tool.js → tools/web-tool.js} +8 -7
  165. package/dist/tools/web-tool.js.map +1 -0
  166. package/dist/tui/flow-colors.d.ts +55 -0
  167. package/dist/tui/flow-colors.d.ts.map +1 -0
  168. package/dist/tui/flow-colors.js +22 -0
  169. package/dist/tui/flow-colors.js.map +1 -0
  170. package/dist/{render-utils.d.ts → tui/render-utils.d.ts} +1 -1
  171. package/dist/tui/render-utils.d.ts.map +1 -0
  172. package/dist/{render-utils.js → tui/render-utils.js} +3 -3
  173. package/dist/tui/render-utils.js.map +1 -0
  174. package/dist/tui/render.d.ts +21 -0
  175. package/dist/tui/render.d.ts.map +1 -0
  176. package/dist/tui/render.js +813 -0
  177. package/dist/tui/render.js.map +1 -0
  178. package/dist/tui/scramble/algorithm.d.ts +7 -0
  179. package/dist/tui/scramble/algorithm.d.ts.map +1 -0
  180. package/dist/tui/scramble/algorithm.js +227 -0
  181. package/dist/tui/scramble/algorithm.js.map +1 -0
  182. package/dist/tui/scramble/constants.d.ts +99 -0
  183. package/dist/tui/scramble/constants.d.ts.map +1 -0
  184. package/dist/tui/scramble/constants.js +101 -0
  185. package/dist/tui/scramble/constants.js.map +1 -0
  186. package/dist/tui/scramble/index.d.ts +6 -0
  187. package/dist/tui/scramble/index.d.ts.map +1 -0
  188. package/dist/tui/scramble/index.js +6 -0
  189. package/dist/tui/scramble/index.js.map +1 -0
  190. package/dist/tui/scramble/manager.d.ts +48 -0
  191. package/dist/tui/scramble/manager.d.ts.map +1 -0
  192. package/dist/tui/scramble/manager.js +959 -0
  193. package/dist/tui/scramble/manager.js.map +1 -0
  194. package/dist/tui/scramble/utils.d.ts +18 -0
  195. package/dist/tui/scramble/utils.d.ts.map +1 -0
  196. package/dist/tui/scramble/utils.js +145 -0
  197. package/dist/tui/scramble/utils.js.map +1 -0
  198. package/dist/tui/single-select-layout.d.ts +17 -0
  199. package/dist/tui/single-select-layout.d.ts.map +1 -0
  200. package/dist/{single-select-layout.js → tui/single-select-layout.js} +8 -25
  201. package/dist/tui/single-select-layout.js.map +1 -0
  202. package/dist/types/flow.d.ts +110 -0
  203. package/dist/types/flow.d.ts.map +1 -0
  204. package/dist/{types.js → types/flow.js} +3 -54
  205. package/dist/types/flow.js.map +1 -0
  206. package/dist/types/index.d.ts +8 -0
  207. package/dist/types/index.d.ts.map +1 -0
  208. package/dist/types/index.js +7 -0
  209. package/dist/types/index.js.map +1 -0
  210. package/dist/types/output.d.ts +104 -0
  211. package/dist/types/output.d.ts.map +1 -0
  212. package/dist/types/output.js +5 -0
  213. package/dist/types/output.js.map +1 -0
  214. package/dist/types/ui.d.ts +24 -0
  215. package/dist/types/ui.d.ts.map +1 -0
  216. package/dist/types/ui.js +55 -0
  217. package/dist/types/ui.js.map +1 -0
  218. package/package.json +7 -4
  219. package/dist/agents.d.ts.map +0 -1
  220. package/dist/agents.js.map +0 -1
  221. package/dist/ask-user.d.ts.map +0 -1
  222. package/dist/ask-user.js +0 -1405
  223. package/dist/ask-user.js.map +0 -1
  224. package/dist/batch.d.ts +0 -12
  225. package/dist/batch.d.ts.map +0 -1
  226. package/dist/batch.js +0 -11
  227. package/dist/batch.js.map +0 -1
  228. package/dist/cli-args.d.ts.map +0 -1
  229. package/dist/cli-args.js.map +0 -1
  230. package/dist/config.d.ts.map +0 -1
  231. package/dist/config.js.map +0 -1
  232. package/dist/depth.d.ts.map +0 -1
  233. package/dist/depth.js.map +0 -1
  234. package/dist/executor.d.ts.map +0 -1
  235. package/dist/executor.js.map +0 -1
  236. package/dist/flow-prompt.d.ts.map +0 -1
  237. package/dist/flow-prompt.js.map +0 -1
  238. package/dist/flow.d.ts.map +0 -1
  239. package/dist/flow.js.map +0 -1
  240. package/dist/notify-state.d.ts.map +0 -1
  241. package/dist/notify-state.js.map +0 -1
  242. package/dist/notify.d.ts.map +0 -1
  243. package/dist/notify.js.map +0 -1
  244. package/dist/reasoning-strip.d.ts.map +0 -1
  245. package/dist/reasoning-strip.js.map +0 -1
  246. package/dist/render-utils.d.ts.map +0 -1
  247. package/dist/render-utils.js.map +0 -1
  248. package/dist/render.d.ts +0 -24
  249. package/dist/render.d.ts.map +0 -1
  250. package/dist/render.js +0 -592
  251. package/dist/render.js.map +0 -1
  252. package/dist/runner-events.d.ts.map +0 -1
  253. package/dist/runner-events.js.map +0 -1
  254. package/dist/scramble.d.ts +0 -171
  255. package/dist/scramble.d.ts.map +0 -1
  256. package/dist/scramble.js +0 -2261
  257. package/dist/scramble.js.map +0 -1
  258. package/dist/session-mode.d.ts.map +0 -1
  259. package/dist/session-mode.js.map +0 -1
  260. package/dist/settings-resolver.d.ts.map +0 -1
  261. package/dist/settings-resolver.js +0 -148
  262. package/dist/settings-resolver.js.map +0 -1
  263. package/dist/single-select-layout.d.ts +0 -20
  264. package/dist/single-select-layout.d.ts.map +0 -1
  265. package/dist/single-select-layout.js.map +0 -1
  266. package/dist/sliding-prompt.d.ts.map +0 -1
  267. package/dist/sliding-prompt.js.map +0 -1
  268. package/dist/snapshot.d.ts.map +0 -1
  269. package/dist/snapshot.js.map +0 -1
  270. package/dist/spec-mode.d.ts +0 -13
  271. package/dist/spec-mode.d.ts.map +0 -1
  272. package/dist/spec-mode.js +0 -90
  273. package/dist/spec-mode.js.map +0 -1
  274. package/dist/structured-output.d.ts.map +0 -1
  275. package/dist/structured-output.js.map +0 -1
  276. package/dist/timed-bash.d.ts.map +0 -1
  277. package/dist/timed-bash.js.map +0 -1
  278. package/dist/tool-utils.d.ts.map +0 -1
  279. package/dist/tool-utils.js.map +0 -1
  280. package/dist/transitions.d.ts.map +0 -1
  281. package/dist/transitions.js.map +0 -1
  282. package/dist/types.d.ts +0 -208
  283. package/dist/types.d.ts.map +0 -1
  284. package/dist/types.js.map +0 -1
  285. package/dist/web-tool.d.ts.map +0 -1
  286. package/dist/web-tool.js.map +0 -1
  287. /package/dist/{agents.d.ts → core/agents.d.ts} +0 -0
  288. /package/dist/{depth.d.ts → core/depth.d.ts} +0 -0
  289. /package/dist/{transitions.d.ts → core/transitions.d.ts} +0 -0
  290. /package/dist/{notify-state.js → notify/notify-state.js} +0 -0
  291. /package/dist/{notify.d.ts → notify/notify.d.ts} +0 -0
package/dist/ask-user.js DELETED
@@ -1,1405 +0,0 @@
1
- /**
2
- * Ask Tool Extension - Interactive question UI for pi-coding-agent
3
- *
4
- * Refactored to use built-in TUI primitives (Container/Text/Spacer/SelectList/Editor)
5
- * and a custom box border instead of manual ANSI box drawing.
6
- */
7
- import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
8
- import { Type } from "@sinclair/typebox";
9
- import { appendStrategicHintOnce } from "./tool-utils.js";
10
- import { setPendingDecision } from "./notify-state.js";
11
- import { scrambleManager, runScrambleTimer } from "./scramble.js";
12
- import { stripAnsi } from "./render-utils.js";
13
- import { Container, decodeKittyPrintable, Editor, fuzzyFilter, Key, Markdown, matchesKey, Spacer, Text, truncateToWidth, wrapTextWithAnsi, } from "@mariozechner/pi-tui";
14
- import { renderSingleSelectRows } from "./single-select-layout.js";
15
- const ASK_USER_VERSION = "0.11.0";
16
- /**
17
- * Emit a flat `{ type: "string", enum: [...] }` JSON Schema instead of the
18
- * `anyOf`/`oneOf` shape that `Type.Union([Type.Literal()])` produces. Google's
19
- * function-calling API rejects the union form. Local copy of pi-ai's StringEnum
20
- * to avoid a peer dependency for one helper.
21
- */
22
- function StringEnum(values, options) {
23
- return Type.Unsafe({
24
- type: "string",
25
- enum: [...values],
26
- ...(options?.description ? { description: options.description } : {}),
27
- ...(options?.default !== undefined ? { default: options.default } : {}),
28
- });
29
- }
30
- function normalizeOptions(options) {
31
- return options
32
- .map((option) => {
33
- if (typeof option === "string") {
34
- return { title: option };
35
- }
36
- if (option && typeof option === "object" && typeof option.title === "string") {
37
- return { title: option.title, description: option.description };
38
- }
39
- return null;
40
- })
41
- .filter((option) => option !== null);
42
- }
43
- function formatOptionsForMessage(options) {
44
- return options
45
- .map((option, index) => {
46
- const desc = option.description ? ` — ${option.description}` : "";
47
- return `${index + 1}. ${option.title}${desc}`;
48
- })
49
- .join("\n");
50
- }
51
- function normalizeOptionalComment(text) {
52
- const trimmed = text?.trim();
53
- return trimmed ? trimmed : undefined;
54
- }
55
- function createFreeformResponse(text) {
56
- const trimmed = text?.trim();
57
- return trimmed ? { kind: "freeform", text: trimmed } : null;
58
- }
59
- function createSelectionResponse(selections, comment) {
60
- const normalizedSelections = selections.map((selection) => selection.trim()).filter(Boolean);
61
- if (normalizedSelections.length === 0)
62
- return null;
63
- const normalizedComment = normalizeOptionalComment(comment);
64
- return normalizedComment
65
- ? { kind: "selection", selections: normalizedSelections, comment: normalizedComment }
66
- : { kind: "selection", selections: normalizedSelections };
67
- }
68
- function formatResponseSummary(response) {
69
- if (response.kind === "freeform")
70
- return response.text;
71
- const selections = response.selections.join(", ");
72
- return response.comment ? `${selections} — ${response.comment}` : selections;
73
- }
74
- function buildCommentPrompt(prompt, selections) {
75
- const label = selections.length === 1 ? "Selected option" : "Selected options";
76
- const lines = selections.map((selection) => `- ${selection}`).join("\n");
77
- return `${prompt}\n\n${label}:\n${lines}`;
78
- }
79
- function parseDialogSelections(input) {
80
- return input
81
- .split(",")
82
- .map((selection) => selection.trim())
83
- .filter(Boolean);
84
- }
85
- function isCancelledInput(value) {
86
- return value === null || value === undefined;
87
- }
88
- function isSelectionResponse(response) {
89
- return response.kind === "selection";
90
- }
91
- function createSelectListTheme(theme) {
92
- return {
93
- selectedPrefix: (t) => theme.fg("accent", t),
94
- selectedText: (t) => theme.fg("accent", t),
95
- description: (t) => theme.fg("muted", t),
96
- scrollInfo: (t) => theme.fg("dim", t),
97
- noMatch: (t) => theme.fg("warning", t),
98
- };
99
- }
100
- function createEditorTheme(theme) {
101
- return {
102
- borderColor: (s) => theme.fg("accent", s),
103
- selectList: createSelectListTheme(theme),
104
- };
105
- }
106
- const BOX_BORDER_LEFT = "│ ";
107
- const BOX_BORDER_RIGHT = " │";
108
- const BOX_BORDER_OVERHEAD = BOX_BORDER_LEFT.length + BOX_BORDER_RIGHT.length;
109
- class BoxBorderTop {
110
- color;
111
- title;
112
- titleColor;
113
- constructor(color, title, titleColor) {
114
- this.color = color;
115
- this.title = title;
116
- this.titleColor = titleColor;
117
- }
118
- invalidate() { }
119
- render(width) {
120
- const inner = Math.max(0, width - 2);
121
- if (!this.title || inner < this.title.length + 4) {
122
- return [this.color(`╭${"─".repeat(inner)}╮`)];
123
- }
124
- const label = ` ${this.title} `;
125
- const remaining = inner - 1 - label.length;
126
- const titleStyle = this.titleColor ?? this.color;
127
- return [
128
- this.color("╭─") + titleStyle(label) + this.color("─".repeat(Math.max(0, remaining)) + "╮"),
129
- ];
130
- }
131
- }
132
- class BoxBorderBottom {
133
- color;
134
- label;
135
- labelColor;
136
- constructor(color, label, labelColor) {
137
- this.color = color;
138
- this.label = label;
139
- this.labelColor = labelColor;
140
- }
141
- invalidate() { }
142
- render(width) {
143
- const inner = Math.max(0, width - 2);
144
- if (!this.label || inner < this.label.length + 4) {
145
- return [this.color(`╰${"─".repeat(inner)}╯`)];
146
- }
147
- const tag = ` ${this.label} `;
148
- const leftDashes = inner - tag.length - 1;
149
- const style = this.labelColor ?? this.color;
150
- return [
151
- this.color("╰" + "─".repeat(Math.max(0, leftDashes))) + style(tag) + this.color("─╯"),
152
- ];
153
- }
154
- }
155
- function formatKeyList(keys) {
156
- return keys.join("/");
157
- }
158
- function keybindingHint(theme, keybindings, keybinding, description) {
159
- return `${theme.fg("dim", formatKeyList(keybindings.getKeys(keybinding)))}${theme.fg("muted", ` ${description}`)}`;
160
- }
161
- function literalHint(theme, key, description) {
162
- return `${theme.fg("dim", key)}${theme.fg("muted", ` ${description}`)}`;
163
- }
164
- const DISABLED_SHORTCUT = {
165
- disabled: true,
166
- spec: null,
167
- matches: ((_data) => false),
168
- };
169
- const SHORTCUT_DISABLE_VALUES = new Set(["off", "none", "disabled", ""]);
170
- function normalizeShortcutSpec(value) {
171
- if (value === undefined)
172
- return undefined;
173
- if (value === null)
174
- return null;
175
- const trimmed = value.trim().toLowerCase();
176
- if (SHORTCUT_DISABLE_VALUES.has(trimmed))
177
- return null;
178
- return trimmed;
179
- }
180
- function isValidShortcutSpec(spec) {
181
- // KeyId is canonical lowercase: modifiers (`ctrl|shift|alt|super`) joined by `+`,
182
- // plus a base key. We do a light syntactic sanity check; matchesKey() does the rest.
183
- if (!spec)
184
- return false;
185
- if (!/^[a-z0-9+_\-!@#$%^&*()|~`'":;,./<>?[\]{}=\\]+$/i.test(spec))
186
- return false;
187
- if (spec.startsWith("+") || spec.endsWith("+"))
188
- return false;
189
- if (spec.includes("++"))
190
- return false;
191
- return true;
192
- }
193
- function buildShortcut(spec) {
194
- return {
195
- disabled: false,
196
- spec,
197
- matches: (data) => matchesKey(data, spec),
198
- };
199
- }
200
- function resolveShortcut(paramValue, envValue, defaultSpec) {
201
- const candidates = [paramValue, envValue, defaultSpec];
202
- for (const raw of candidates) {
203
- const normalized = normalizeShortcutSpec(raw);
204
- if (normalized === undefined)
205
- continue; // not provided, fall through
206
- if (normalized === null)
207
- return DISABLED_SHORTCUT; // explicit disable
208
- if (isValidShortcutSpec(normalized))
209
- return buildShortcut(normalized);
210
- // Invalid spec: silently fall through to next candidate.
211
- }
212
- return DISABLED_SHORTCUT;
213
- }
214
- const ASK_OVERLAY_MAX_HEIGHT_RATIO = 0.85;
215
- const ASK_OVERLAY_WIDTH = "92%";
216
- const ASK_OVERLAY_MIN_WIDTH = 40;
217
- const SINGLE_SELECT_SPLIT_PANE_MIN_WIDTH = 84;
218
- const SINGLE_SELECT_SPLIT_PANE_LEFT_MIN_WIDTH = 32;
219
- const SINGLE_SELECT_SPLIT_PANE_RIGHT_MIN_WIDTH = 28;
220
- const SINGLE_SELECT_SPLIT_PANE_SEPARATOR = " │ ";
221
- const FREEFORM_SENTINEL = "\u270f\ufe0f Type custom response...";
222
- const COMMENT_TOGGLE_LABEL = "Add extra context after selection";
223
- const DEFAULT_OVERLAY_TOGGLE_KEY = "alt+o";
224
- const DEFAULT_COMMENT_TOGGLE_KEY = "ctrl+g";
225
- // Vim-style aliases for navigating option lists. ctrl+j/k are safe in the
226
- // searchable single-select because they don't collide with fuzzy-search input.
227
- const VIM_SELECT_UP_KEY = Key.ctrl("k");
228
- const VIM_SELECT_DOWN_KEY = Key.ctrl("j");
229
- function matchesSelectUp(data, keybindings) {
230
- return (keybindings.matches(data, "tui.select.up") ||
231
- matchesKey(data, Key.shift("tab")) ||
232
- matchesKey(data, VIM_SELECT_UP_KEY));
233
- }
234
- function matchesSelectDown(data, keybindings) {
235
- return (keybindings.matches(data, "tui.select.down") ||
236
- matchesKey(data, Key.tab) ||
237
- matchesKey(data, VIM_SELECT_DOWN_KEY));
238
- }
239
- function buildCustomUIOptions(displayMode, onHandle) {
240
- switch (displayMode) {
241
- case "inline":
242
- return undefined;
243
- case "overlay":
244
- return {
245
- overlay: true,
246
- overlayOptions: {
247
- anchor: "center",
248
- width: ASK_OVERLAY_WIDTH,
249
- minWidth: ASK_OVERLAY_MIN_WIDTH,
250
- maxHeight: "85%",
251
- margin: 1,
252
- },
253
- ...(onHandle ? { onHandle } : {}),
254
- };
255
- default: {
256
- const _exhaustive = displayMode;
257
- void _exhaustive;
258
- return {
259
- overlay: true,
260
- overlayOptions: {
261
- anchor: "center",
262
- width: ASK_OVERLAY_WIDTH,
263
- minWidth: ASK_OVERLAY_MIN_WIDTH,
264
- maxHeight: "85%",
265
- margin: 1,
266
- },
267
- ...(onHandle ? { onHandle } : {}),
268
- };
269
- }
270
- }
271
- }
272
- class MultiSelectList {
273
- options;
274
- allowFreeform;
275
- allowComment;
276
- theme;
277
- keybindings;
278
- commentToggle;
279
- selectedIndex = 0;
280
- checked = new Set();
281
- commentEnabled = false;
282
- cachedWidth;
283
- cachedLines;
284
- onCancel;
285
- onSubmit;
286
- onEnterFreeform;
287
- constructor(options, allowFreeform, allowComment, theme, keybindings, commentToggle) {
288
- this.options = options;
289
- this.allowFreeform = allowFreeform;
290
- this.allowComment = allowComment;
291
- this.theme = theme;
292
- this.keybindings = keybindings;
293
- this.commentToggle = commentToggle;
294
- }
295
- isCommentEnabled() {
296
- return this.commentEnabled;
297
- }
298
- invalidate() {
299
- this.cachedWidth = undefined;
300
- this.cachedLines = undefined;
301
- }
302
- getItemCount() {
303
- return this.options.length + (this.allowComment ? 1 : 0) + (this.allowFreeform ? 1 : 0);
304
- }
305
- getCommentToggleIndex() {
306
- return this.allowComment ? this.options.length : null;
307
- }
308
- getFreeformIndex() {
309
- return this.options.length + (this.allowComment ? 1 : 0);
310
- }
311
- isCommentToggleRow(index) {
312
- const toggleIndex = this.getCommentToggleIndex();
313
- return toggleIndex !== null && index === toggleIndex;
314
- }
315
- isFreeformRow(index) {
316
- return this.allowFreeform && index === this.getFreeformIndex();
317
- }
318
- toggle(index) {
319
- if (index < 0 || index >= this.options.length)
320
- return;
321
- if (this.checked.has(index))
322
- this.checked.delete(index);
323
- else
324
- this.checked.add(index);
325
- }
326
- toggleComment() {
327
- if (!this.allowComment)
328
- return;
329
- this.commentEnabled = !this.commentEnabled;
330
- this.invalidate();
331
- }
332
- handleInput(data) {
333
- if (this.keybindings.matches(data, "tui.select.cancel")) {
334
- this.onCancel?.();
335
- return;
336
- }
337
- const count = this.getItemCount();
338
- if (count === 0) {
339
- this.onCancel?.();
340
- return;
341
- }
342
- if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) {
343
- this.toggleComment();
344
- return;
345
- }
346
- if (matchesSelectUp(data, this.keybindings)) {
347
- this.selectedIndex = this.selectedIndex === 0 ? count - 1 : this.selectedIndex - 1;
348
- this.invalidate();
349
- return;
350
- }
351
- if (matchesSelectDown(data, this.keybindings)) {
352
- this.selectedIndex = this.selectedIndex === count - 1 ? 0 : this.selectedIndex + 1;
353
- this.invalidate();
354
- return;
355
- }
356
- const numMatch = data.match(/^[1-9]$/);
357
- if (numMatch) {
358
- const idx = Number.parseInt(numMatch[0], 10) - 1;
359
- if (idx >= 0 && idx < this.options.length) {
360
- this.toggle(idx);
361
- this.selectedIndex = Math.min(idx, count - 1);
362
- this.invalidate();
363
- }
364
- return;
365
- }
366
- if (matchesKey(data, Key.space)) {
367
- if (this.isCommentToggleRow(this.selectedIndex)) {
368
- this.toggleComment();
369
- return;
370
- }
371
- if (this.isFreeformRow(this.selectedIndex)) {
372
- this.onEnterFreeform?.();
373
- return;
374
- }
375
- this.toggle(this.selectedIndex);
376
- this.invalidate();
377
- return;
378
- }
379
- if (this.keybindings.matches(data, "tui.select.confirm")) {
380
- if (this.isCommentToggleRow(this.selectedIndex)) {
381
- this.toggleComment();
382
- return;
383
- }
384
- if (this.isFreeformRow(this.selectedIndex)) {
385
- this.onEnterFreeform?.();
386
- return;
387
- }
388
- const selectedTitles = Array.from(this.checked)
389
- .sort((a, b) => a - b)
390
- .map((i) => this.options[i]?.title)
391
- .filter((t) => !!t);
392
- const fallback = this.options[this.selectedIndex]?.title;
393
- const result = selectedTitles.length > 0 ? selectedTitles : fallback ? [fallback] : [];
394
- if (result.length > 0)
395
- this.onSubmit?.(result);
396
- else
397
- this.onCancel?.();
398
- }
399
- }
400
- render(width) {
401
- if (this.cachedLines && this.cachedWidth === width) {
402
- return this.cachedLines;
403
- }
404
- const theme = this.theme;
405
- const count = this.getItemCount();
406
- const maxVisible = Math.min(count, 10);
407
- if (count === 0) {
408
- this.cachedLines = [theme.fg("warning", "No options")];
409
- this.cachedWidth = width;
410
- return this.cachedLines;
411
- }
412
- const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), count - maxVisible));
413
- const endIndex = Math.min(startIndex + maxVisible, count);
414
- const lines = [];
415
- for (let i = startIndex; i < endIndex; i++) {
416
- const isSelected = i === this.selectedIndex;
417
- const prefix = isSelected ? theme.fg("accent", "▶") : " ";
418
- if (this.isCommentToggleRow(i)) {
419
- const checkbox = this.commentEnabled ? theme.fg("success", "[✔]") : theme.fg("dim", "[ ]");
420
- const label = isSelected
421
- ? theme.fg("accent", theme.bold(COMMENT_TOGGLE_LABEL))
422
- : theme.fg("text", theme.bold(COMMENT_TOGGLE_LABEL));
423
- lines.push(truncateToWidth(`${prefix} ${checkbox} ${label}`, width, ""));
424
- continue;
425
- }
426
- if (this.isFreeformRow(i)) {
427
- const label = theme.fg("text", theme.bold("Type something."));
428
- const desc = theme.fg("muted", "Enter a custom response");
429
- const line = `${prefix} ${label} ${theme.fg("dim", "—")} ${desc}`;
430
- lines.push(truncateToWidth(line, width, ""));
431
- continue;
432
- }
433
- const option = this.options[i];
434
- if (!option)
435
- continue;
436
- const checkbox = this.checked.has(i) ? theme.fg("success", "[✔]") : theme.fg("dim", "[ ]");
437
- const num = theme.fg("dim", `${i + 1}.`);
438
- const title = isSelected
439
- ? theme.fg("accent", theme.bold(option.title))
440
- : theme.fg("text", theme.bold(option.title));
441
- const firstLine = `${prefix} ${num} ${checkbox} ${title}`;
442
- lines.push(truncateToWidth(firstLine, width, ""));
443
- if (option.description) {
444
- const indent = " ";
445
- const wrapWidth = Math.max(10, width - indent.length);
446
- const wrapped = wrapTextWithAnsi(option.description, wrapWidth);
447
- for (const w of wrapped) {
448
- lines.push(truncateToWidth(indent + theme.fg("muted", w), width, ""));
449
- }
450
- }
451
- }
452
- if (startIndex > 0 || endIndex < count) {
453
- lines.push(theme.fg("dim", truncateToWidth(` (${this.selectedIndex + 1}/${count})`, width, "")));
454
- }
455
- this.cachedWidth = width;
456
- this.cachedLines = lines;
457
- return lines;
458
- }
459
- }
460
- class WrappedSingleSelectList {
461
- options;
462
- allowFreeform;
463
- allowComment;
464
- theme;
465
- keybindings;
466
- commentToggle;
467
- selectedIndex = 0;
468
- searchQuery = "";
469
- commentEnabled = false;
470
- maxVisibleRows = 12;
471
- cachedWidth;
472
- cachedLines;
473
- onCancel;
474
- onSubmit;
475
- onEnterFreeform;
476
- constructor(options, allowFreeform, allowComment, theme, keybindings, commentToggle) {
477
- this.options = options;
478
- this.allowFreeform = allowFreeform;
479
- this.allowComment = allowComment;
480
- this.theme = theme;
481
- this.keybindings = keybindings;
482
- this.commentToggle = commentToggle;
483
- }
484
- isCommentEnabled() {
485
- return this.commentEnabled;
486
- }
487
- setMaxVisibleRows(rows) {
488
- const next = Math.max(1, Math.floor(rows));
489
- if (next !== this.maxVisibleRows) {
490
- this.maxVisibleRows = next;
491
- this.invalidate();
492
- }
493
- }
494
- invalidate() {
495
- this.cachedWidth = undefined;
496
- this.cachedLines = undefined;
497
- }
498
- getFilteredOptions() {
499
- return fuzzyFilter(this.options, this.searchQuery, (option) => `${option.title} ${option.description ?? ""}`);
500
- }
501
- getItemCount(filteredOptions) {
502
- return filteredOptions.length + (this.allowComment ? 1 : 0) + (this.allowFreeform ? 1 : 0);
503
- }
504
- isCommentToggleRow(index, filteredOptions) {
505
- return this.allowComment && index === filteredOptions.length;
506
- }
507
- isFreeformRow(index, filteredOptions) {
508
- return this.allowFreeform && index === filteredOptions.length + (this.allowComment ? 1 : 0);
509
- }
510
- toggleComment() {
511
- if (!this.allowComment)
512
- return;
513
- this.commentEnabled = !this.commentEnabled;
514
- this.invalidate();
515
- }
516
- setSearchQuery(query) {
517
- this.searchQuery = query;
518
- this.selectedIndex = 0;
519
- this.invalidate();
520
- }
521
- popSearchCharacter() {
522
- if (!this.searchQuery)
523
- return;
524
- const characters = [...this.searchQuery];
525
- characters.pop();
526
- this.setSearchQuery(characters.join(""));
527
- }
528
- getPrintableInput(data) {
529
- const kittyPrintable = decodeKittyPrintable(data);
530
- if (kittyPrintable !== undefined)
531
- return kittyPrintable;
532
- const characters = [...data];
533
- if (characters.length !== 1)
534
- return null;
535
- const [character] = characters;
536
- if (!character)
537
- return null;
538
- const code = character.charCodeAt(0);
539
- if (code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {
540
- return null;
541
- }
542
- return character;
543
- }
544
- styleListLine(line, width, isSelected) {
545
- const trimmed = line.trim();
546
- if (trimmed.startsWith("(")) {
547
- return truncateToWidth(this.theme.fg("dim", line), width, "");
548
- }
549
- if (isSelected) {
550
- return truncateToWidth(this.theme.fg("accent", this.theme.bold(line)), width, "");
551
- }
552
- if (line.startsWith(" ")) {
553
- return truncateToWidth(this.theme.fg("muted", line), width, "");
554
- }
555
- if (line.startsWith("▶")) {
556
- return truncateToWidth(this.theme.fg("accent", this.theme.bold(line)), width, "");
557
- }
558
- return truncateToWidth(this.theme.fg("text", line), width, "");
559
- }
560
- getSplitPaneWidths(width) {
561
- if (width < SINGLE_SELECT_SPLIT_PANE_MIN_WIDTH)
562
- return null;
563
- const availableWidth = width - SINGLE_SELECT_SPLIT_PANE_SEPARATOR.length;
564
- if (availableWidth < SINGLE_SELECT_SPLIT_PANE_LEFT_MIN_WIDTH + SINGLE_SELECT_SPLIT_PANE_RIGHT_MIN_WIDTH) {
565
- return null;
566
- }
567
- const preferredLeftWidth = Math.floor(availableWidth * 0.42);
568
- const left = Math.max(SINGLE_SELECT_SPLIT_PANE_LEFT_MIN_WIDTH, Math.min(preferredLeftWidth, availableWidth - SINGLE_SELECT_SPLIT_PANE_RIGHT_MIN_WIDTH));
569
- const right = availableWidth - left;
570
- if (right < SINGLE_SELECT_SPLIT_PANE_RIGHT_MIN_WIDTH)
571
- return null;
572
- return { left, right };
573
- }
574
- buildListLines(width, filteredOptions, hideDescriptions = false) {
575
- const lines = [];
576
- const count = this.getItemCount(filteredOptions);
577
- const searchValue = this.searchQuery ? this.theme.fg("text", this.searchQuery) : this.theme.fg("dim", "type to filter");
578
- lines.push(truncateToWidth(`${this.theme.fg("accent", "Filter:")} ${searchValue}`, width, ""));
579
- if (this.searchQuery && filteredOptions.length === 0) {
580
- lines.push(truncateToWidth(this.theme.fg("warning", "No matching options"), width, ""));
581
- }
582
- if (count === 0) {
583
- if (!this.searchQuery) {
584
- lines.push(truncateToWidth(this.theme.fg("warning", "No options"), width, ""));
585
- }
586
- return lines.slice(0, this.maxVisibleRows);
587
- }
588
- const maxRows = Math.max(1, this.maxVisibleRows - lines.length);
589
- const optionRows = renderSingleSelectRows({
590
- options: filteredOptions,
591
- selectedIndex: this.selectedIndex,
592
- width,
593
- allowFreeform: this.allowFreeform,
594
- allowComment: this.allowComment,
595
- commentEnabled: this.commentEnabled,
596
- maxRows,
597
- hideDescriptions,
598
- });
599
- const optionLines = optionRows.map((row) => this.styleListLine(row.line, width, row.selected));
600
- lines.push(...optionLines);
601
- return lines.slice(0, this.maxVisibleRows);
602
- }
603
- buildPreviewLines(width, filteredOptions, maxLines) {
604
- if (maxLines <= 0)
605
- return [];
606
- let mdTheme;
607
- try {
608
- mdTheme = getMarkdownTheme();
609
- }
610
- catch { }
611
- let md = "";
612
- if (this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
613
- md += "## Additional context\n\n";
614
- md += `Currently: **${this.commentEnabled ? "Enabled" : "Disabled"}**\n\n`;
615
- md += "Turn this on when the selected option needs extra explanation before the tool submits.\n";
616
- }
617
- else if (this.isFreeformRow(this.selectedIndex, filteredOptions)) {
618
- md += "## Custom response\n\n";
619
- md += "Open the editor to write **any** answer.\n\n";
620
- md += "*Use this when none of the listed options fit.*\n";
621
- if (this.searchQuery) {
622
- md += `\n> Current filter: \`${this.searchQuery}\`\n`;
623
- }
624
- }
625
- else {
626
- const selected = filteredOptions[this.selectedIndex];
627
- if (!selected) {
628
- md += "*No option selected*\n";
629
- }
630
- else {
631
- md += `## ${selected.title}\n\n`;
632
- if (selected.description?.trim()) {
633
- md += `${selected.description}\n`;
634
- }
635
- else {
636
- md += "*No additional details provided for this option.*\n";
637
- }
638
- md += `\n---\n\nPress \`Enter\` to select this option.\n`;
639
- if (this.searchQuery) {
640
- md += `\n> Filter: \`${this.searchQuery}\`\n`;
641
- }
642
- }
643
- }
644
- let lines;
645
- if (mdTheme) {
646
- const mdComponent = new Markdown(md.trim(), 0, 0, mdTheme);
647
- lines = mdComponent.render(width);
648
- }
649
- else {
650
- lines = [];
651
- for (const line of wrapTextWithAnsi(md.trim(), Math.max(10, width))) {
652
- lines.push(truncateToWidth(line, width, ""));
653
- }
654
- }
655
- while (lines.length > 0 && lines[lines.length - 1]?.trim() === "") {
656
- lines.pop();
657
- }
658
- if (lines.length <= maxLines)
659
- return lines;
660
- if (maxLines === 1)
661
- return [truncateToWidth(this.theme.fg("dim", "…"), width, "")];
662
- const visibleLines = lines.slice(0, maxLines - 1);
663
- visibleLines.push(truncateToWidth(this.theme.fg("dim", "…"), width, ""));
664
- return visibleLines;
665
- }
666
- handleInput(data) {
667
- if (this.searchQuery && matchesKey(data, Key.escape)) {
668
- this.setSearchQuery("");
669
- return;
670
- }
671
- if (this.keybindings.matches(data, "tui.select.cancel")) {
672
- this.onCancel?.();
673
- return;
674
- }
675
- if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) {
676
- this.toggleComment();
677
- return;
678
- }
679
- const filteredOptions = this.getFilteredOptions();
680
- const count = this.getItemCount(filteredOptions);
681
- if (matchesSelectUp(data, this.keybindings) && count > 0) {
682
- this.selectedIndex = this.selectedIndex === 0 ? count - 1 : this.selectedIndex - 1;
683
- this.invalidate();
684
- return;
685
- }
686
- if (matchesSelectDown(data, this.keybindings) && count > 0) {
687
- this.selectedIndex = this.selectedIndex === count - 1 ? 0 : this.selectedIndex + 1;
688
- this.invalidate();
689
- return;
690
- }
691
- const numMatch = data.match(/^[1-9]$/);
692
- if (numMatch && filteredOptions.length > 0) {
693
- const idx = Number.parseInt(numMatch[0], 10) - 1;
694
- if (idx >= 0 && idx < filteredOptions.length) {
695
- this.selectedIndex = idx;
696
- this.invalidate();
697
- return;
698
- }
699
- }
700
- if (matchesKey(data, Key.space) && count > 0 && this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
701
- this.toggleComment();
702
- return;
703
- }
704
- if (this.keybindings.matches(data, "tui.select.confirm") && count > 0) {
705
- if (this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
706
- this.toggleComment();
707
- return;
708
- }
709
- if (this.isFreeformRow(this.selectedIndex, filteredOptions)) {
710
- this.onEnterFreeform?.();
711
- return;
712
- }
713
- const result = filteredOptions[this.selectedIndex]?.title;
714
- if (result)
715
- this.onSubmit?.(result);
716
- else
717
- this.onCancel?.();
718
- return;
719
- }
720
- if (this.keybindings.matches(data, "tui.editor.deleteCharBackward") || matchesKey(data, Key.backspace)) {
721
- this.popSearchCharacter();
722
- return;
723
- }
724
- const printableInput = this.getPrintableInput(data);
725
- if (printableInput) {
726
- this.setSearchQuery(this.searchQuery + printableInput);
727
- }
728
- }
729
- render(width) {
730
- if (this.cachedLines && this.cachedWidth === width) {
731
- return this.cachedLines;
732
- }
733
- const filteredOptions = this.getFilteredOptions();
734
- const count = this.getItemCount(filteredOptions);
735
- this.selectedIndex = count > 0 ? Math.max(0, Math.min(this.selectedIndex, count - 1)) : 0;
736
- const splitPane = this.getSplitPaneWidths(width);
737
- let lines;
738
- if (!splitPane) {
739
- lines = this.buildListLines(width, filteredOptions);
740
- }
741
- else {
742
- const listLines = this.buildListLines(splitPane.left, filteredOptions, true);
743
- const previewLines = this.buildPreviewLines(splitPane.right, filteredOptions, this.maxVisibleRows);
744
- const rowCount = Math.min(this.maxVisibleRows, Math.max(listLines.length, previewLines.length));
745
- const separator = this.theme.fg("dim", SINGLE_SELECT_SPLIT_PANE_SEPARATOR);
746
- lines = Array.from({ length: rowCount }, (_, index) => {
747
- const left = truncateToWidth(listLines[index] ?? "", splitPane.left, "", true);
748
- const right = truncateToWidth(previewLines[index] ?? "", splitPane.right, "");
749
- return `${left}${separator}${right}`;
750
- });
751
- }
752
- this.cachedWidth = width;
753
- this.cachedLines = lines;
754
- return lines;
755
- }
756
- }
757
- /**
758
- * Interactive ask UI. Uses a root Container for layout and swaps the center
759
- * component between SelectList/MultiSelectList and an Editor (freeform mode).
760
- */
761
- class AskComponent extends Container {
762
- question;
763
- context;
764
- options;
765
- allowMultiple;
766
- allowFreeform;
767
- allowComment;
768
- displayMode;
769
- tui;
770
- theme;
771
- keybindings;
772
- shortcuts;
773
- onDone;
774
- mode = "select";
775
- pendingSelections = [];
776
- freeformDraft = "";
777
- commentDraft = "";
778
- // Static layout components
779
- titleText;
780
- questionText;
781
- contextComponent;
782
- modeContainer;
783
- helpText;
784
- // Mode components
785
- singleSelectList;
786
- multiSelectList;
787
- editor;
788
- // Focusable - propagate to Editor for IME cursor positioning
789
- _focused = false;
790
- get focused() {
791
- return this._focused;
792
- }
793
- set focused(value) {
794
- this._focused = value;
795
- if (this.editor && (this.mode === "freeform" || this.mode === "comment")) {
796
- this.editor.focused = value;
797
- }
798
- }
799
- constructor(question, context, options, allowMultiple, allowFreeform, allowComment, displayMode, tui, theme, keybindings, shortcuts, onDone) {
800
- super();
801
- this.question = question;
802
- this.context = context;
803
- this.options = options;
804
- this.allowMultiple = allowMultiple;
805
- this.allowFreeform = allowFreeform;
806
- this.allowComment = allowComment;
807
- this.displayMode = displayMode;
808
- this.tui = tui;
809
- this.theme = theme;
810
- this.keybindings = keybindings;
811
- this.shortcuts = shortcuts;
812
- this.onDone = onDone;
813
- // Layout skeleton
814
- this.addChild(new BoxBorderTop((s) => theme.fg("accent", s), "ask_user", (s) => theme.fg("dim", theme.bold(s))));
815
- this.addChild(new Spacer(1));
816
- this.titleText = new Text("", 1, 0);
817
- this.addChild(this.titleText);
818
- this.addChild(new Spacer(1));
819
- this.questionText = new Text("", 1, 0);
820
- this.addChild(this.questionText);
821
- if (this.context) {
822
- this.addChild(new Spacer(1));
823
- let mdTheme;
824
- try {
825
- mdTheme = getMarkdownTheme();
826
- }
827
- catch { }
828
- if (mdTheme) {
829
- this.contextComponent = new Markdown("", 1, 0, mdTheme);
830
- }
831
- else {
832
- this.contextComponent = new Text("", 1, 0);
833
- }
834
- this.addChild(this.contextComponent);
835
- }
836
- this.addChild(new Spacer(1));
837
- this.modeContainer = new Container();
838
- this.addChild(this.modeContainer);
839
- this.addChild(new Spacer(1));
840
- this.helpText = new Text("", 1, 0);
841
- this.addChild(this.helpText);
842
- this.addChild(new Spacer(1));
843
- this.addChild(new BoxBorderBottom((s) => theme.fg("accent", s), `v${ASK_USER_VERSION}`, (s) => theme.fg("dim", s)));
844
- this.updateStaticText();
845
- this.showSelectMode();
846
- }
847
- invalidate() {
848
- super.invalidate();
849
- this.updateStaticText();
850
- this.updateHelpText();
851
- }
852
- render(width) {
853
- const innerWidth = Math.max(1, width - BOX_BORDER_OVERHEAD);
854
- if (this.mode === "select" && !this.allowMultiple) {
855
- const overlayMaxHeight = Math.max(12, Math.floor(this.tui.terminal.rows * ASK_OVERLAY_MAX_HEIGHT_RATIO));
856
- const staticLines = this.countStaticLines(innerWidth);
857
- const availableOptionRows = Math.max(4, overlayMaxHeight - staticLines);
858
- this.ensureSingleSelectList().setMaxVisibleRows(availableOptionRows);
859
- }
860
- // Render children at the inner width (excluding side border characters)
861
- const rawLines = super.render(innerWidth);
862
- // First and last lines are the top/bottom box borders — pass through at full width.
863
- // All inner lines get wrapped with side borders.
864
- const borderColor = (s) => this.theme.fg("accent", s);
865
- const titleColor = (s) => this.theme.fg("dim", this.theme.bold(s));
866
- return rawLines.map((line, index) => {
867
- if (index === 0 || index === rawLines.length - 1) {
868
- // Box top/bottom borders already rendered at innerWidth — re-render at full width
869
- if (index === 0)
870
- return new BoxBorderTop(borderColor, "ask_user", titleColor).render(width)[0];
871
- return new BoxBorderBottom(borderColor, `v${ASK_USER_VERSION}`, (s) => this.theme.fg("dim", s)).render(width)[0];
872
- }
873
- const padded = truncateToWidth(line, innerWidth, "", true);
874
- return `${borderColor(BOX_BORDER_LEFT)}${padded}${borderColor(BOX_BORDER_RIGHT)}`;
875
- });
876
- }
877
- countWrappedLines(text, width) {
878
- return Math.max(1, wrapTextWithAnsi(text, Math.max(10, width - 2)).length);
879
- }
880
- countStaticLines(width) {
881
- const titleLines = 1;
882
- const questionLines = this.countWrappedLines(this.question, width);
883
- const contextLines = this.context ? 1 + this.countWrappedLines(this.context, width) : 0;
884
- const helpLines = 1;
885
- const borderLines = 2;
886
- const spacerLines = this.context ? 6 : 5;
887
- return borderLines + spacerLines + titleLines + questionLines + contextLines + helpLines;
888
- }
889
- updateStaticText() {
890
- const theme = this.theme;
891
- const title = this.mode === "comment" ? "Optional comment" : "Question";
892
- this.titleText.setText(theme.fg("accent", theme.bold(title)));
893
- this.questionText.setText(theme.fg("text", theme.bold(this.question)));
894
- if (this.contextComponent && this.context) {
895
- if (this.contextComponent instanceof Markdown) {
896
- this.contextComponent.setText(`**Context:**\n${this.context}`);
897
- }
898
- else {
899
- this.contextComponent.setText(`${theme.fg("accent", theme.bold("Context:"))}\n${theme.fg("dim", this.context)}`);
900
- }
901
- }
902
- }
903
- updateHelpText() {
904
- const theme = this.theme;
905
- const overlayHint = this.displayMode === "overlay" && !this.shortcuts.overlayToggle.disabled
906
- ? literalHint(theme, this.shortcuts.overlayToggle.spec, "hide")
907
- : null;
908
- const commentHint = this.allowComment && !this.shortcuts.commentToggle.disabled
909
- ? literalHint(theme, this.shortcuts.commentToggle.spec, "toggle context")
910
- : null;
911
- if (this.mode === "freeform" || this.mode === "comment") {
912
- const alternateCancelKeys = this.keybindings
913
- .getKeys("tui.select.cancel")
914
- .filter((key) => key !== "escape" && key !== "esc");
915
- const hints = [
916
- keybindingHint(theme, this.keybindings, "tui.input.submit", this.mode === "comment" ? "submit/skip" : "submit"),
917
- keybindingHint(theme, this.keybindings, "tui.input.newLine", "newline"),
918
- literalHint(theme, "esc", "back"),
919
- overlayHint,
920
- alternateCancelKeys.length > 0 ? literalHint(theme, formatKeyList(alternateCancelKeys), "cancel") : null,
921
- ]
922
- .filter((hint) => !!hint)
923
- .join(" • ");
924
- this.helpText.setText(theme.fg("dim", hints));
925
- return;
926
- }
927
- if (this.allowMultiple) {
928
- const hints = [
929
- literalHint(theme, "↑↓", "navigate"),
930
- literalHint(theme, "space", "toggle"),
931
- commentHint,
932
- overlayHint,
933
- keybindingHint(theme, this.keybindings, "tui.select.confirm", "submit"),
934
- keybindingHint(theme, this.keybindings, "tui.select.cancel", "cancel"),
935
- ]
936
- .filter((hint) => !!hint)
937
- .join(" • ");
938
- this.helpText.setText(theme.fg("dim", hints));
939
- }
940
- else {
941
- const alternateCancelKeys = this.keybindings
942
- .getKeys("tui.select.cancel")
943
- .filter((key) => key !== "escape" && key !== "esc");
944
- const hints = [
945
- literalHint(theme, "type", "filter"),
946
- keybindingHint(theme, this.keybindings, "tui.editor.deleteCharBackward", "erase"),
947
- literalHint(theme, "↑↓", "navigate"),
948
- commentHint,
949
- overlayHint,
950
- keybindingHint(theme, this.keybindings, "tui.select.confirm", "select"),
951
- literalHint(theme, "esc", "clear/cancel"),
952
- alternateCancelKeys.length > 0
953
- ? literalHint(theme, formatKeyList(alternateCancelKeys), "cancel")
954
- : null,
955
- ]
956
- .filter((hint) => !!hint)
957
- .join(" • ");
958
- this.helpText.setText(theme.fg("dim", hints));
959
- }
960
- }
961
- ensureSingleSelectList() {
962
- if (this.singleSelectList)
963
- return this.singleSelectList;
964
- const list = new WrappedSingleSelectList(this.options, this.allowFreeform, this.allowComment, this.theme, this.keybindings, this.shortcuts.commentToggle);
965
- list.onSubmit = (result) => this.handleSelectionSubmit([result], list.isCommentEnabled());
966
- list.onCancel = () => this.onDone(null);
967
- list.onEnterFreeform = () => this.showFreeformMode();
968
- this.singleSelectList = list;
969
- return list;
970
- }
971
- ensureMultiSelectList() {
972
- if (this.multiSelectList)
973
- return this.multiSelectList;
974
- const list = new MultiSelectList(this.options, this.allowFreeform, this.allowComment, this.theme, this.keybindings, this.shortcuts.commentToggle);
975
- list.onCancel = () => this.onDone(null);
976
- list.onSubmit = (result) => this.handleSelectionSubmit(result, list.isCommentEnabled());
977
- list.onEnterFreeform = () => this.showFreeformMode();
978
- this.multiSelectList = list;
979
- return list;
980
- }
981
- ensureEditor() {
982
- if (this.editor)
983
- return this.editor;
984
- const editor = new Editor(this.tui, createEditorTheme(this.theme));
985
- editor.disableSubmit = false;
986
- editor.onSubmit = (text) => {
987
- this.handleEditorSubmit(text);
988
- };
989
- this.editor = editor;
990
- return editor;
991
- }
992
- saveEditorDraft() {
993
- if (!this.editor)
994
- return;
995
- const getText = this.editor.getText;
996
- if (typeof getText !== "function")
997
- return;
998
- const currentText = String(getText.call(this.editor) ?? "");
999
- if (this.mode === "freeform") {
1000
- this.freeformDraft = currentText;
1001
- }
1002
- else if (this.mode === "comment") {
1003
- this.commentDraft = currentText;
1004
- }
1005
- }
1006
- setEditorText(text) {
1007
- const editor = this.ensureEditor();
1008
- const setText = editor.setText;
1009
- if (typeof setText === "function") {
1010
- setText.call(editor, text);
1011
- }
1012
- }
1013
- handleSelectionSubmit(selections, wantsComment) {
1014
- if (this.allowComment && wantsComment) {
1015
- this.pendingSelections = selections;
1016
- this.commentDraft = "";
1017
- this.showCommentMode();
1018
- return;
1019
- }
1020
- this.onDone(createSelectionResponse(selections));
1021
- }
1022
- handleEditorSubmit(text) {
1023
- if (this.mode === "freeform") {
1024
- this.onDone(createFreeformResponse(text));
1025
- return;
1026
- }
1027
- if (this.mode === "comment") {
1028
- this.commentDraft = text;
1029
- this.onDone(createSelectionResponse(this.pendingSelections, text));
1030
- }
1031
- }
1032
- showSelectMode() {
1033
- if (this.mode === "freeform" || this.mode === "comment") {
1034
- this.saveEditorDraft();
1035
- }
1036
- this.mode = "select";
1037
- this.pendingSelections = [];
1038
- this.modeContainer.clear();
1039
- if (this.allowMultiple) {
1040
- this.modeContainer.addChild(this.ensureMultiSelectList());
1041
- }
1042
- else {
1043
- this.modeContainer.addChild(this.ensureSingleSelectList());
1044
- }
1045
- this.updateHelpText();
1046
- this.invalidate();
1047
- this.tui.requestRender();
1048
- }
1049
- showFreeformMode() {
1050
- if (this.mode === "comment") {
1051
- this.saveEditorDraft();
1052
- }
1053
- this.mode = "freeform";
1054
- this.modeContainer.clear();
1055
- const editor = this.ensureEditor();
1056
- this.setEditorText(this.freeformDraft);
1057
- editor.focused = this._focused;
1058
- this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold("Custom response")), 1, 0));
1059
- this.modeContainer.addChild(new Spacer(1));
1060
- this.modeContainer.addChild(editor);
1061
- this.updateHelpText();
1062
- this.invalidate();
1063
- this.tui.requestRender();
1064
- }
1065
- showCommentMode() {
1066
- if (this.mode === "freeform") {
1067
- this.saveEditorDraft();
1068
- }
1069
- this.mode = "comment";
1070
- this.modeContainer.clear();
1071
- const editor = this.ensureEditor();
1072
- this.setEditorText(this.commentDraft);
1073
- editor.focused = this._focused;
1074
- const selectedLabel = this.pendingSelections.length === 1 ? "Selected option:" : "Selected options:";
1075
- this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold(selectedLabel)), 1, 0));
1076
- this.modeContainer.addChild(new Text(this.theme.fg("text", this.pendingSelections.join(", ")), 1, 0));
1077
- this.modeContainer.addChild(new Spacer(1));
1078
- this.modeContainer.addChild(editor);
1079
- this.updateHelpText();
1080
- this.invalidate();
1081
- this.tui.requestRender();
1082
- }
1083
- handleInput(data) {
1084
- if (this.mode === "freeform" || this.mode === "comment") {
1085
- if (matchesKey(data, Key.escape)) {
1086
- this.showSelectMode();
1087
- return;
1088
- }
1089
- if (this.keybindings.matches(data, "tui.select.cancel")) {
1090
- this.onDone(null);
1091
- return;
1092
- }
1093
- this.ensureEditor().handleInput(data);
1094
- this.tui.requestRender();
1095
- return;
1096
- }
1097
- if (this.allowMultiple) {
1098
- this.ensureMultiSelectList().handleInput?.(data);
1099
- this.tui.requestRender();
1100
- return;
1101
- }
1102
- this.ensureSingleSelectList().handleInput?.(data);
1103
- this.tui.requestRender();
1104
- }
1105
- }
1106
- /**
1107
- * RPC/headless fallback: use dialog methods (select/input) instead of the rich TUI overlay.
1108
- * ctx.ui.custom() returns undefined in RPC mode, so we degrade gracefully.
1109
- */
1110
- async function askViaDialogs(ui, question, context, options, allowMultiple, allowFreeform, allowComment, allowCancel, timeout) {
1111
- const dialogOpts = {};
1112
- if (timeout)
1113
- dialogOpts.timeout = timeout;
1114
- if (!allowCancel)
1115
- dialogOpts.allowCancel = false;
1116
- const prompt = context ? `${question}\n\nContext:\n${context}` : question;
1117
- if (allowMultiple) {
1118
- const optionList = formatOptionsForMessage(options);
1119
- const rawSelections = await ui.input(`${prompt}\n\nOptions (select one or more):\n${optionList}`, "Type your selection(s)...", dialogOpts);
1120
- if (isCancelledInput(rawSelections))
1121
- return null;
1122
- const selections = parseDialogSelections(rawSelections);
1123
- if (selections.length === 0)
1124
- return null;
1125
- if (!allowComment) {
1126
- return createSelectionResponse(selections);
1127
- }
1128
- const comment = await ui.input(buildCommentPrompt(prompt, selections), "Optional comment (press Enter to skip)...", dialogOpts);
1129
- return createSelectionResponse(selections, comment);
1130
- }
1131
- const selectOptions = options.map((o) => o.title);
1132
- if (allowFreeform)
1133
- selectOptions.push(FREEFORM_SENTINEL);
1134
- const selected = await ui.select(prompt, selectOptions, dialogOpts);
1135
- if (isCancelledInput(selected))
1136
- return null;
1137
- if (selected === FREEFORM_SENTINEL) {
1138
- const answer = await ui.input(prompt, "Type your answer...", dialogOpts);
1139
- if (isCancelledInput(answer))
1140
- return null;
1141
- return createFreeformResponse(answer);
1142
- }
1143
- if (!allowComment) {
1144
- return createSelectionResponse([selected]);
1145
- }
1146
- const comment = await ui.input(buildCommentPrompt(prompt, [selected]), "Optional comment (press Enter to skip)...", dialogOpts);
1147
- return createSelectionResponse([selected], comment);
1148
- }
1149
- export function createAskUserTool() {
1150
- return {
1151
- name: "ask_user",
1152
- label: "Ask User",
1153
- description: "Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Ask exactly one focused question per call. Before calling, gather context with tools (read/web/ref) and pass a short summary via the context field. When presenting options, mark your recommended choice with [preferred] and place it first. Base your recommendation on evidence gathered from tools or investigation.",
1154
- promptSnippet: "Ask the user one focused question with optional multiple-choice answers to gather information interactively",
1155
- promptGuidelines: [
1156
- "Before calling ask_user, gather context with tools (read/web/ref) and pass a short summary via the context field.",
1157
- "Use ask_user when the user's intent is ambiguous, when a decision requires explicit user input, or when multiple valid options exist.",
1158
- "Ask exactly one focused question per ask_user call.",
1159
- "Do not combine multiple numbered, multipart, or unrelated questions into one ask_user prompt.",
1160
- ],
1161
- parameters: Type.Object({
1162
- question: Type.String({ description: "The question to ask the user" }),
1163
- context: Type.Optional(Type.String({
1164
- description: "Relevant context to show before the question (summary of findings)",
1165
- })),
1166
- options: Type.Optional(Type.Array(Type.Union([
1167
- Type.String({ description: "Short title for this option" }),
1168
- Type.Object({
1169
- title: Type.String({ description: "Short title for this option" }),
1170
- description: Type.Optional(Type.String({ description: "Longer description explaining this option" })),
1171
- }),
1172
- ]), { description: "List of options for the user to choose from" })),
1173
- allowMultiple: Type.Optional(Type.Boolean({ description: "Allow selecting multiple options. Default: false" })),
1174
- allowFreeform: Type.Optional(Type.Boolean({ description: "Add a freeform text option. Default: true" })),
1175
- allowComment: Type.Optional(Type.Boolean({ description: "Collect an optional comment after selecting one or more options. Default: false" })),
1176
- allowCancel: Type.Optional(Type.Boolean({ description: "Allow the user to cancel without answering. Default: true" })),
1177
- displayMode: Type.Optional(StringEnum(["overlay", "inline"], {
1178
- description: "UI rendering mode. 'overlay' shows a centered modal, 'inline' renders in-place. Default: PI_ASK_USER_DISPLAY_MODE env var if set, otherwise 'overlay'. Omit to respect the user's configured preference.",
1179
- })),
1180
- overlayToggleKey: Type.Optional(Type.String({
1181
- description: "Shortcut for hiding/showing the overlay popup (overlay mode only), e.g. 'alt+o' or 'ctrl+shift+h'. Pass 'off' to disable. Default: PI_ASK_USER_OVERLAY_TOGGLE_KEY env var if set, otherwise 'alt+o'.",
1182
- })),
1183
- commentToggleKey: Type.Optional(Type.String({
1184
- description: "Shortcut for toggling the optional comment/extra-context row when allowComment is true, e.g. 'ctrl+g'. Pass 'off' to disable. Default: PI_ASK_USER_COMMENT_TOGGLE_KEY env var if set, otherwise 'ctrl+g'.",
1185
- })),
1186
- timeout: Type.Optional(Type.Number({ description: "Auto-dismiss after N milliseconds. Returns null (cancelled) when expired." })),
1187
- }),
1188
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
1189
- setPendingDecision();
1190
- if (signal?.aborted) {
1191
- return {
1192
- content: [{ type: "text", text: "Cancelled" }],
1193
- details: { question: params.question, options: [], response: null, cancelled: true },
1194
- };
1195
- }
1196
- const { question, context, options: rawOptions = [], allowMultiple = false, allowFreeform = true, allowComment = false, allowCancel = true, displayMode, overlayToggleKey, commentToggleKey, timeout, } = params;
1197
- const envMode = process.env.PI_ASK_USER_DISPLAY_MODE;
1198
- const envDisplayMode = envMode === "overlay" || envMode === "inline" ? envMode : undefined;
1199
- const effectiveDisplayMode = displayMode ?? envDisplayMode ?? "overlay";
1200
- const shortcuts = {
1201
- overlayToggle: resolveShortcut(overlayToggleKey, process.env.PI_ASK_USER_OVERLAY_TOGGLE_KEY, DEFAULT_OVERLAY_TOGGLE_KEY),
1202
- commentToggle: resolveShortcut(commentToggleKey, process.env.PI_ASK_USER_COMMENT_TOGGLE_KEY, DEFAULT_COMMENT_TOGGLE_KEY),
1203
- };
1204
- const options = normalizeOptions(rawOptions);
1205
- const normalizedContext = context?.trim() || undefined;
1206
- if (!ctx.hasUI || !ctx.ui) {
1207
- const optionText = options.length > 0 ? `\n\nOptions:\n${formatOptionsForMessage(options)}` : "";
1208
- const freeformHint = allowFreeform ? "\n\nYou can also answer freely." : "";
1209
- const commentHint = allowComment ? "\n\nAfter choosing an option, you may add an optional comment." : "";
1210
- const contextText = normalizedContext ? `\n\nContext:\n${normalizedContext}` : "";
1211
- return {
1212
- content: [
1213
- {
1214
- type: "text",
1215
- text: `Ask requires interactive mode. Please answer:\n\n${question}${contextText}${optionText}${freeformHint}${commentHint}`,
1216
- },
1217
- ],
1218
- isError: true,
1219
- details: { question, context: normalizedContext, options, response: null, cancelled: true },
1220
- };
1221
- }
1222
- if (options.length === 0) {
1223
- const prompt = normalizedContext ? `${question}\n\nContext:\n${normalizedContext}` : question;
1224
- const inputOpts = {};
1225
- if (timeout)
1226
- inputOpts.timeout = timeout;
1227
- if (!allowCancel)
1228
- inputOpts.allowCancel = false;
1229
- const answer = await ctx.ui.input(prompt, "Type your answer...", Object.keys(inputOpts).length > 0 ? inputOpts : undefined);
1230
- const response = createFreeformResponse(answer);
1231
- if (!response) {
1232
- return {
1233
- content: [{ type: "text", text: "User cancelled the question" }],
1234
- details: { question, context: normalizedContext, options, response: null, cancelled: true },
1235
- };
1236
- }
1237
- const _result0 = {
1238
- content: [{ type: "text", text: `User answered: ${formatResponseSummary(response)}` }],
1239
- details: { question, context: normalizedContext, options, response, cancelled: false },
1240
- };
1241
- appendStrategicHintOnce(_result0);
1242
- return _result0;
1243
- }
1244
- onUpdate?.({
1245
- content: [{ type: "text", text: "Waiting for user input..." }],
1246
- details: { question, context: normalizedContext, options, response: null, cancelled: false },
1247
- });
1248
- let result;
1249
- let overlayHandle;
1250
- let removeOverlayInputListener;
1251
- let hasAnnouncedHide = false;
1252
- try {
1253
- const customFactory = (tui, theme, keybindings, done) => {
1254
- if (signal) {
1255
- const onAbort = () => done(null);
1256
- signal.addEventListener("abort", onAbort, { once: true });
1257
- }
1258
- if (timeout && timeout > 0) {
1259
- setTimeout(() => done(null), timeout);
1260
- }
1261
- return new AskComponent(question, normalizedContext, options, allowMultiple, allowFreeform, allowComment, effectiveDisplayMode, tui, theme, keybindings, shortcuts, done);
1262
- };
1263
- // Register a raw terminal input listener for the overlay-toggle key so the
1264
- // overlay can be toggled even while it is hidden (hidden overlays do not
1265
- // receive input). Inline mode does not need this because the prompt is
1266
- // already non-modal. Skipped entirely if the user disabled the shortcut.
1267
- const overlayToggle = shortcuts.overlayToggle;
1268
- if (effectiveDisplayMode === "overlay"
1269
- && !overlayToggle.disabled
1270
- && typeof ctx.ui.onTerminalInput === "function") {
1271
- removeOverlayInputListener = ctx.ui.onTerminalInput((data) => {
1272
- if (!overlayToggle.matches(data) || !overlayHandle)
1273
- return undefined;
1274
- const nextHidden = !overlayHandle.isHidden();
1275
- overlayHandle.setHidden(nextHidden);
1276
- if (nextHidden && !hasAnnouncedHide) {
1277
- hasAnnouncedHide = true;
1278
- ctx.ui.notify?.(`ask_user hidden — press ${overlayToggle.spec} to reopen`, "info");
1279
- }
1280
- return { consume: true };
1281
- });
1282
- }
1283
- const customResult = await ctx.ui.custom(customFactory, buildCustomUIOptions(effectiveDisplayMode, (handle) => {
1284
- overlayHandle = handle;
1285
- }));
1286
- if (customResult !== undefined) {
1287
- result = customResult;
1288
- }
1289
- else {
1290
- // RPC/headless mode: degrade to select()/input() dialog protocol
1291
- result = await askViaDialogs(ctx.ui, question, normalizedContext, options, allowMultiple, allowFreeform, allowComment, allowCancel, timeout);
1292
- }
1293
- }
1294
- catch (error) {
1295
- const message = error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error);
1296
- return {
1297
- content: [{ type: "text", text: `Ask tool failed: ${message}` }],
1298
- isError: true,
1299
- details: { error: message },
1300
- };
1301
- }
1302
- finally {
1303
- removeOverlayInputListener?.();
1304
- }
1305
- if (result === null) {
1306
- return {
1307
- content: [{ type: "text", text: "User cancelled the question" }],
1308
- details: { question, context: normalizedContext, options, response: null, cancelled: true },
1309
- };
1310
- }
1311
- return {
1312
- content: [{ type: "text", text: `User answered: ${formatResponseSummary(result)}` }],
1313
- details: {
1314
- question,
1315
- context: normalizedContext,
1316
- options,
1317
- response: result,
1318
- cancelled: false,
1319
- },
1320
- };
1321
- },
1322
- renderCall(args, theme) {
1323
- const question = args.question || "";
1324
- const rawOptions = Array.isArray(args.options) ? args.options : [];
1325
- let text = theme.fg("toolTitle", theme.bold("ask_user "));
1326
- text += theme.fg("muted", question);
1327
- if (rawOptions.length > 0) {
1328
- const labels = rawOptions.map((o) => typeof o === "string" ? o : o?.title ?? "");
1329
- text += "\n" + theme.fg("dim", ` ${rawOptions.length} option(s): ${labels.join(", ")}`);
1330
- }
1331
- if (args.allowMultiple) {
1332
- text += theme.fg("dim", " [multi-select]");
1333
- }
1334
- if (args.allowComment) {
1335
- text += theme.fg("dim", " [optional comment]");
1336
- }
1337
- return new Text(text, 0, 0);
1338
- },
1339
- renderResult(result, options, theme, args) {
1340
- const details = result.details;
1341
- const canAnimate = !!args?.invalidate && !!args?.state;
1342
- const now = Date.now();
1343
- const id = args?.toolCallId || args?.id || "ask_user";
1344
- if (details?.error) {
1345
- const line = theme.fg("error", `✖ ${details.error}`);
1346
- if (!canAnimate)
1347
- return new Text(line, 0, 0);
1348
- const scrambled = scrambleManager.updateText(id, "result", stripAnsi(line), now, false).content;
1349
- runScrambleTimer(args);
1350
- return new Text(scrambled, 0, 0);
1351
- }
1352
- if (options.isPartial) {
1353
- const waitingText = result.content
1354
- ?.filter((part) => part?.type === "text")
1355
- .map((part) => part.text ?? "")
1356
- .join("\n")
1357
- .trim() || "Waiting for user input...";
1358
- const line = theme.fg("muted", waitingText);
1359
- if (!canAnimate)
1360
- return new Text(line, 0, 0);
1361
- const scrambled = scrambleManager.updateText(id, "result", stripAnsi(line), now, false).content;
1362
- runScrambleTimer(args);
1363
- return new Text(scrambled, 0, 0);
1364
- }
1365
- if (!details || details.cancelled || !details.response) {
1366
- const line = theme.fg("warning", "Cancelled");
1367
- if (!canAnimate)
1368
- return new Text(line, 0, 0);
1369
- const scrambled = scrambleManager.updateText(id, "result", stripAnsi(line), now, false).content;
1370
- runScrambleTimer(args);
1371
- return new Text(scrambled, 0, 0);
1372
- }
1373
- const response = details.response;
1374
- let text = theme.fg("success", "✔ ");
1375
- if (response.kind === "freeform") {
1376
- text += theme.fg("muted", "(wrote) ");
1377
- }
1378
- text += theme.fg("accent", formatResponseSummary(response));
1379
- if (options.expanded) {
1380
- text += "\n" + theme.fg("dim", `Q: ${details.question}`);
1381
- if (details.context) {
1382
- text += "\n" + theme.fg("dim", details.context);
1383
- }
1384
- if (isSelectionResponse(response) && details.options.length > 0) {
1385
- const selectedTitles = new Set(response.selections);
1386
- text += "\n" + theme.fg("dim", "Options:");
1387
- for (const opt of details.options) {
1388
- const desc = opt.description ? ` — ${opt.description}` : "";
1389
- const marker = selectedTitles.has(opt.title) ? theme.fg("success", "●") : theme.fg("dim", "○");
1390
- text += `\n ${marker} ${theme.fg("dim", opt.title)}${theme.fg("dim", desc)}`;
1391
- }
1392
- if (response.comment) {
1393
- text += `\n${theme.fg("dim", "Comment:")} ${theme.fg("dim", response.comment)}`;
1394
- }
1395
- }
1396
- }
1397
- if (!canAnimate)
1398
- return new Text(text, 0, 0);
1399
- const scrambled = scrambleManager.updateText(id, "result", stripAnsi(text), now, false).content;
1400
- runScrambleTimer(args);
1401
- return new Text(scrambled, 0, 0);
1402
- },
1403
- };
1404
- }
1405
- //# sourceMappingURL=ask-user.js.map