@code-yeongyu/senpi 2026.9.12-3 → 2026.9.13-2

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 (270) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +3 -1
  3. package/dist/bun/cli.js +2 -3
  4. package/dist/bun/cli.js.map +1 -1
  5. package/dist/bun/runtime-modules.d.ts +3 -0
  6. package/dist/bun/runtime-modules.d.ts.map +1 -0
  7. package/dist/bun/runtime-modules.js +17 -0
  8. package/dist/bun/runtime-modules.js.map +1 -0
  9. package/dist/bun/runtime-setup.js +2 -5
  10. package/dist/bun/runtime-setup.js.map +1 -1
  11. package/dist/core/agent-session.d.ts +2 -2
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +38 -30
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/discovered-resource-scope.d.ts +23 -0
  16. package/dist/core/discovered-resource-scope.d.ts.map +1 -0
  17. package/dist/core/discovered-resource-scope.js +44 -0
  18. package/dist/core/discovered-resource-scope.js.map +1 -0
  19. package/dist/core/extensions/builtin/ask-user/notify.d.ts +24 -0
  20. package/dist/core/extensions/builtin/ask-user/notify.d.ts.map +1 -0
  21. package/dist/core/extensions/builtin/ask-user/notify.js +11 -0
  22. package/dist/core/extensions/builtin/ask-user/notify.js.map +1 -0
  23. package/dist/core/extensions/builtin/ask-user/registry.d.ts +2 -0
  24. package/dist/core/extensions/builtin/ask-user/registry.d.ts.map +1 -1
  25. package/dist/core/extensions/builtin/ask-user/registry.js.map +1 -1
  26. package/dist/core/extensions/builtin/ask-user/resume.d.ts +1 -1
  27. package/dist/core/extensions/builtin/ask-user/resume.d.ts.map +1 -1
  28. package/dist/core/extensions/builtin/ask-user/resume.js +10 -20
  29. package/dist/core/extensions/builtin/ask-user/resume.js.map +1 -1
  30. package/dist/core/extensions/builtin/ask-user/tool.d.ts +5 -2
  31. package/dist/core/extensions/builtin/ask-user/tool.d.ts.map +1 -1
  32. package/dist/core/extensions/builtin/ask-user/tool.js +41 -6
  33. package/dist/core/extensions/builtin/ask-user/tool.js.map +1 -1
  34. package/dist/core/extensions/builtin/goal/channel-state-subscriptions.d.ts +2 -1
  35. package/dist/core/extensions/builtin/goal/channel-state-subscriptions.d.ts.map +1 -1
  36. package/dist/core/extensions/builtin/goal/channel-state-subscriptions.js +1 -1
  37. package/dist/core/extensions/builtin/goal/channel-state-subscriptions.js.map +1 -1
  38. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
  39. package/dist/core/extensions/builtin/goal/monitor-continuation.js +27 -13
  40. package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
  41. package/dist/core/extensions/builtin/hooks/config-loader.d.ts +2 -0
  42. package/dist/core/extensions/builtin/hooks/config-loader.d.ts.map +1 -1
  43. package/dist/core/extensions/builtin/hooks/config-loader.js +27 -0
  44. package/dist/core/extensions/builtin/hooks/config-loader.js.map +1 -1
  45. package/dist/core/extensions/builtin/hooks/dispatcher.js +1 -0
  46. package/dist/core/extensions/builtin/hooks/dispatcher.js.map +1 -1
  47. package/dist/core/extensions/builtin/hooks/index.d.ts.map +1 -1
  48. package/dist/core/extensions/builtin/hooks/index.js +51 -2
  49. package/dist/core/extensions/builtin/hooks/index.js.map +1 -1
  50. package/dist/core/extensions/builtin/hooks/lifecycle-adapter.d.ts +18 -1
  51. package/dist/core/extensions/builtin/hooks/lifecycle-adapter.d.ts.map +1 -1
  52. package/dist/core/extensions/builtin/hooks/lifecycle-adapter.js +36 -1
  53. package/dist/core/extensions/builtin/hooks/lifecycle-adapter.js.map +1 -1
  54. package/dist/core/extensions/builtin/hooks/matcher.js +1 -0
  55. package/dist/core/extensions/builtin/hooks/matcher.js.map +1 -1
  56. package/dist/core/extensions/builtin/hooks/output-parser.js +6 -0
  57. package/dist/core/extensions/builtin/hooks/output-parser.js.map +1 -1
  58. package/dist/core/extensions/builtin/hooks/trust-storage.d.ts +2 -0
  59. package/dist/core/extensions/builtin/hooks/trust-storage.d.ts.map +1 -1
  60. package/dist/core/extensions/builtin/hooks/trust-storage.js +13 -0
  61. package/dist/core/extensions/builtin/hooks/trust-storage.js.map +1 -1
  62. package/dist/core/extensions/builtin/hooks/types.d.ts +14 -2
  63. package/dist/core/extensions/builtin/hooks/types.d.ts.map +1 -1
  64. package/dist/core/extensions/builtin/hooks/types.js +1 -1
  65. package/dist/core/extensions/builtin/hooks/types.js.map +1 -1
  66. package/dist/core/extensions/builtin/monitor-state-event.d.ts +1 -0
  67. package/dist/core/extensions/builtin/monitor-state-event.d.ts.map +1 -1
  68. package/dist/core/extensions/builtin/monitor-state-event.js.map +1 -1
  69. package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +1 -0
  70. package/dist/core/extensions/builtin/terminal/tools/bash.d.ts.map +1 -1
  71. package/dist/core/extensions/builtin/terminal/tools/bash.js +13 -2
  72. package/dist/core/extensions/builtin/terminal/tools/bash.js.map +1 -1
  73. package/dist/core/extensions/loader.d.ts.map +1 -1
  74. package/dist/core/extensions/loader.js +5 -1
  75. package/dist/core/extensions/loader.js.map +1 -1
  76. package/dist/core/extensions/runner.d.ts +5 -16
  77. package/dist/core/extensions/runner.d.ts.map +1 -1
  78. package/dist/core/extensions/runner.js +16 -5
  79. package/dist/core/extensions/runner.js.map +1 -1
  80. package/dist/core/extensions/types.d.ts +28 -5
  81. package/dist/core/extensions/types.d.ts.map +1 -1
  82. package/dist/core/extensions/types.js.map +1 -1
  83. package/dist/core/extensions/wrapper.d.ts +8 -3
  84. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  85. package/dist/core/extensions/wrapper.js +12 -6
  86. package/dist/core/extensions/wrapper.js.map +1 -1
  87. package/dist/core/keybindings.d.ts +8 -1
  88. package/dist/core/keybindings.d.ts.map +1 -1
  89. package/dist/core/keybindings.js +7 -1
  90. package/dist/core/keybindings.js.map +1 -1
  91. package/dist/core/package-manager.d.ts +4 -2
  92. package/dist/core/package-manager.d.ts.map +1 -1
  93. package/dist/core/package-manager.js +7 -2
  94. package/dist/core/package-manager.js.map +1 -1
  95. package/dist/core/pi-manifest.d.ts +2 -0
  96. package/dist/core/pi-manifest.d.ts.map +1 -1
  97. package/dist/core/pi-manifest.js +3 -0
  98. package/dist/core/pi-manifest.js.map +1 -1
  99. package/dist/core/resource-loader.d.ts +9 -0
  100. package/dist/core/resource-loader.d.ts.map +1 -1
  101. package/dist/core/resource-loader.js +69 -21
  102. package/dist/core/resource-loader.js.map +1 -1
  103. package/dist/core/settings-manager.d.ts +1 -0
  104. package/dist/core/settings-manager.d.ts.map +1 -1
  105. package/dist/core/settings-manager.js +1 -0
  106. package/dist/core/settings-manager.js.map +1 -1
  107. package/dist/core/settings-shapes.d.ts +1 -0
  108. package/dist/core/settings-shapes.d.ts.map +1 -1
  109. package/dist/core/settings-shapes.js.map +1 -1
  110. package/dist/core/source-info.d.ts +5 -1
  111. package/dist/core/source-info.d.ts.map +1 -1
  112. package/dist/core/source-info.js.map +1 -1
  113. package/dist/core/tools/bash.d.ts.map +1 -1
  114. package/dist/core/tools/bash.js +5 -0
  115. package/dist/core/tools/bash.js.map +1 -1
  116. package/dist/modes/app-server/server/skills.d.ts.map +1 -1
  117. package/dist/modes/app-server/server/skills.js +1 -0
  118. package/dist/modes/app-server/server/skills.js.map +1 -1
  119. package/dist/modes/interactive/components/ask-user-answer-chip.d.ts +17 -0
  120. package/dist/modes/interactive/components/ask-user-answer-chip.d.ts.map +1 -0
  121. package/dist/modes/interactive/components/ask-user-answer-chip.js +86 -0
  122. package/dist/modes/interactive/components/ask-user-answer-chip.js.map +1 -0
  123. package/dist/modes/interactive/components/ask-user-answer-key.d.ts +3 -1
  124. package/dist/modes/interactive/components/ask-user-answer-key.d.ts.map +1 -1
  125. package/dist/modes/interactive/components/ask-user-answer-key.js +11 -1
  126. package/dist/modes/interactive/components/ask-user-answer-key.js.map +1 -1
  127. package/dist/modes/interactive/components/ask-user-async-widget.d.ts +5 -2
  128. package/dist/modes/interactive/components/ask-user-async-widget.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/ask-user-async-widget.js +18 -12
  130. package/dist/modes/interactive/components/ask-user-async-widget.js.map +1 -1
  131. package/dist/modes/interactive/components/ask-user-countdown.d.ts +8 -0
  132. package/dist/modes/interactive/components/ask-user-countdown.d.ts.map +1 -0
  133. package/dist/modes/interactive/components/ask-user-countdown.js +23 -0
  134. package/dist/modes/interactive/components/ask-user-countdown.js.map +1 -0
  135. package/dist/modes/interactive/components/ask-user-question-keys.d.ts.map +1 -1
  136. package/dist/modes/interactive/components/ask-user-question-keys.js +85 -17
  137. package/dist/modes/interactive/components/ask-user-question-keys.js.map +1 -1
  138. package/dist/modes/interactive/components/ask-user-question-render.d.ts.map +1 -1
  139. package/dist/modes/interactive/components/ask-user-question-render.js +22 -7
  140. package/dist/modes/interactive/components/ask-user-question-render.js.map +1 -1
  141. package/dist/modes/interactive/components/ask-user-question-state.d.ts +15 -0
  142. package/dist/modes/interactive/components/ask-user-question-state.d.ts.map +1 -1
  143. package/dist/modes/interactive/components/ask-user-question-state.js +57 -13
  144. package/dist/modes/interactive/components/ask-user-question-state.js.map +1 -1
  145. package/dist/modes/interactive/components/ask-user-question.d.ts +8 -2
  146. package/dist/modes/interactive/components/ask-user-question.d.ts.map +1 -1
  147. package/dist/modes/interactive/components/ask-user-question.js +16 -8
  148. package/dist/modes/interactive/components/ask-user-question.js.map +1 -1
  149. package/dist/modes/interactive/components/config-selector.d.ts +2 -1
  150. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  151. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  152. package/dist/modes/interactive/components/custom-editor.d.ts +2 -0
  153. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/custom-editor.js +10 -1
  155. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  156. package/dist/modes/interactive/components/loaded-resource-section.d.ts +14 -0
  157. package/dist/modes/interactive/components/loaded-resource-section.d.ts.map +1 -0
  158. package/dist/modes/interactive/components/loaded-resource-section.js +25 -0
  159. package/dist/modes/interactive/components/loaded-resource-section.js.map +1 -0
  160. package/dist/modes/interactive/components/user-message.d.ts +2 -1
  161. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  162. package/dist/modes/interactive/components/user-message.js +12 -2
  163. package/dist/modes/interactive/components/user-message.js.map +1 -1
  164. package/dist/modes/interactive/interactive-mode.d.ts +14 -11
  165. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  166. package/dist/modes/interactive/interactive-mode.js +263 -165
  167. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  168. package/dist/modes/interactive/loaded-resource-scopes.d.ts +28 -0
  169. package/dist/modes/interactive/loaded-resource-scopes.d.ts.map +1 -0
  170. package/dist/modes/interactive/loaded-resource-scopes.js +107 -0
  171. package/dist/modes/interactive/loaded-resource-scopes.js.map +1 -0
  172. package/dist/modes/interactive/tips/catalog/input-tips.d.ts +1 -1
  173. package/dist/modes/interactive/tips/catalog/input-tips.js +2 -2
  174. package/dist/modes/interactive/tips/catalog/input-tips.js.map +1 -1
  175. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
  176. package/dist/modes/rpc/host-lifecycle.js +7 -0
  177. package/dist/modes/rpc/host-lifecycle.js.map +1 -1
  178. package/dist/modes/rpc/session-worker.js +5 -0
  179. package/dist/modes/rpc/session-worker.js.map +1 -1
  180. package/docs/environment-variables.md +5 -1
  181. package/docs/extensions.md +43 -5
  182. package/docs/keybindings.md +5 -2
  183. package/docs/packages.md +17 -0
  184. package/docs/rpc.md +6 -3
  185. package/docs/settings.md +5 -1
  186. package/docs/tui.md +15 -3
  187. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +28 -0
  188. package/node_modules/@code-yeongyu/senpi-codemode/README.md +16 -4
  189. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  190. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +2 -0
  191. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/session-env.ts +6 -0
  192. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-manager.ts +6 -1
  193. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/run-eval-cell.ts +31 -12
  194. package/node_modules/@earendil-works/chord/package.json +1 -1
  195. package/node_modules/@earendil-works/pi-agent-core/dist/harness/messages.d.ts +1 -1
  196. package/node_modules/@earendil-works/pi-agent-core/dist/harness/messages.d.ts.map +1 -1
  197. package/node_modules/@earendil-works/pi-agent-core/dist/harness/messages.js +1 -1
  198. package/node_modules/@earendil-works/pi-agent-core/dist/harness/messages.js.map +1 -1
  199. package/node_modules/@earendil-works/pi-agent-core/package.json +4 -4
  200. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent.lazy.d.ts +2 -0
  201. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent.lazy.d.ts.map +1 -1
  202. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent.lazy.js +6 -1
  203. package/node_modules/@earendil-works/pi-ai/dist/api/devin-agent.lazy.js.map +1 -1
  204. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.d.ts.map +1 -1
  205. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +55 -17
  206. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js.map +1 -1
  207. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.d.ts.map +1 -1
  208. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +2 -1
  209. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js.map +1 -1
  210. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts +2 -1
  211. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts.map +1 -1
  212. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +9 -0
  213. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js.map +1 -1
  214. package/node_modules/@earendil-works/pi-ai/dist/api/responses-completion-grace.d.ts +30 -0
  215. package/node_modules/@earendil-works/pi-ai/dist/api/responses-completion-grace.d.ts.map +1 -0
  216. package/node_modules/@earendil-works/pi-ai/dist/api/responses-completion-grace.js +94 -0
  217. package/node_modules/@earendil-works/pi-ai/dist/api/responses-completion-grace.js.map +1 -0
  218. package/node_modules/@earendil-works/pi-ai/dist/api/websocket-liveness.d.ts +48 -0
  219. package/node_modules/@earendil-works/pi-ai/dist/api/websocket-liveness.d.ts.map +1 -0
  220. package/node_modules/@earendil-works/pi-ai/dist/api/websocket-liveness.js +104 -0
  221. package/node_modules/@earendil-works/pi-ai/dist/api/websocket-liveness.js.map +1 -0
  222. package/node_modules/@earendil-works/pi-ai/dist/devin-provider.d.ts +6 -0
  223. package/node_modules/@earendil-works/pi-ai/dist/devin-provider.d.ts.map +1 -0
  224. package/node_modules/@earendil-works/pi-ai/dist/devin-provider.js +4 -0
  225. package/node_modules/@earendil-works/pi-ai/dist/devin-provider.js.map +1 -0
  226. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  227. package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
  228. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  229. package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
  230. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  231. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
  232. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +7 -3
  233. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
  234. package/node_modules/@earendil-works/pi-ai/package.json +10 -2
  235. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  236. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  237. package/node_modules/@earendil-works/pi-tui/README.md +11 -3
  238. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +2 -1
  239. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +1 -1
  240. package/node_modules/@earendil-works/pi-tui/dist/index.js +1 -0
  241. package/node_modules/@earendil-works/pi-tui/dist/index.js.map +1 -1
  242. package/node_modules/@earendil-works/pi-tui/dist/mouse-input.d.ts +37 -0
  243. package/node_modules/@earendil-works/pi-tui/dist/mouse-input.d.ts.map +1 -0
  244. package/node_modules/@earendil-works/pi-tui/dist/mouse-input.js +97 -0
  245. package/node_modules/@earendil-works/pi-tui/dist/mouse-input.js.map +1 -0
  246. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.d.ts +2 -0
  247. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.d.ts.map +1 -1
  248. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js +38 -0
  249. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js.map +1 -1
  250. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +25 -1
  251. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  252. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +110 -4
  253. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  254. package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.d.ts.map +1 -1
  255. package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js +5 -50
  256. package/node_modules/@earendil-works/pi-tui/dist/tui-alt-screen.js.map +1 -1
  257. package/node_modules/@earendil-works/pi-tui/dist/tui-main-screen.d.ts +12 -0
  258. package/node_modules/@earendil-works/pi-tui/dist/tui-main-screen.d.ts.map +1 -1
  259. package/node_modules/@earendil-works/pi-tui/dist/tui-main-screen.js +129 -3
  260. package/node_modules/@earendil-works/pi-tui/dist/tui-main-screen.js.map +1 -1
  261. package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts +28 -0
  262. package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts.map +1 -1
  263. package/node_modules/@earendil-works/pi-tui/dist/tui.js +160 -0
  264. package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +1 -1
  265. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  266. package/package.json +13 -9
  267. package/dist/bun/register-cursor-agent.d.ts +0 -2
  268. package/dist/bun/register-cursor-agent.d.ts.map +0 -1
  269. package/dist/bun/register-cursor-agent.js +0 -4
  270. package/dist/bun/register-cursor-agent.js.map +0 -1
@@ -7,7 +7,7 @@ import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { contentText, modelsAreEqual } from "@earendil-works/pi-ai";
9
9
  import * as TuiLayouts from "@earendil-works/pi-tui";
10
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, outerKittyGraphicsMode, Spacer, sanitizeTerminalLabel, setCapabilityOverrides, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
10
+ import { CombinedAutocompleteProvider, Container, decodeKittyPrintable, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, outerKittyGraphicsMode, SelectList, Spacer, sanitizeTerminalLabel, setCapabilityOverrides, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
11
11
  import chalk from "chalk";
12
12
  import { spawn, spawnSync } from "child_process";
13
13
  import { APP_COMMAND, APP_NAME, APP_TITLE, BRAND, CONFIG_DIR_NAME, DISPLAY_VERSION, expandTildePath, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, } from "../../config.js";
@@ -19,6 +19,7 @@ import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, }
19
19
  import { resolveChangelogSource } from "../../core/changelog-source.js";
20
20
  import { collectEntriesForBranchSummary } from "../../core/compaction/branch-summarization.js";
21
21
  import { AssistantEditError, assistantTextEquals } from "../../core/edited-assistant-message.js";
22
+ import { formatUserMessage } from "../../core/extensions/builtin/ask-user/format.js";
22
23
  import { buildNoticeBox } from "../../core/extensions/notice/index.js";
23
24
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
24
25
  import { appendUncaughtCrashLog } from "../../core/hidden-stdout-log.js";
@@ -53,9 +54,11 @@ import { abortedMessageForRendering } from "./aborted-error-label.js";
53
54
  import { createChatViewport } from "./chat-viewport.js";
54
55
  import { transferCompactionQueue, waitForPromptDisposition, } from "./compaction-queue-transfer.js";
55
56
  import { ArminComponent } from "./components/armin.js";
57
+ import { getAskUserAnswerHeaders, parseAskUserAnswerFrame } from "./components/ask-user-answer-chip.js";
56
58
  import { matchesAskUserAnswerKey } from "./components/ask-user-answer-key.js";
57
- import { ASK_USER_WIDGET_KEY, AskUserAsyncWidget, buildCommentResponse, buildTimedOutResponse, } from "./components/ask-user-async-widget.js";
59
+ import { ASK_USER_WIDGET_KEY, AskUserAsyncWidget, buildCommentResponse, buildTimedOutResponse, unansweredIds, } from "./components/ask-user-async-widget.js";
58
60
  import { AskUserQuestionComponent } from "./components/ask-user-question.js";
61
+ import { formatCountdownLabel } from "./components/ask-user-question-state.js";
59
62
  import { AssistantMessageComponent } from "./components/assistant-message.js";
60
63
  import { BashExecutionComponent } from "./components/bash-execution.js";
61
64
  import { BorderedLoader } from "./components/bordered-loader.js";
@@ -74,6 +77,7 @@ import { ExtensionSelectorComponent } from "./components/extension-selector.js";
74
77
  import { FavoriteModelsSelectorComponent } from "./components/favorite-models-selector.js";
75
78
  import { FooterComponent, formatTokens } from "./components/footer.js";
76
79
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
80
+ import { LoadedResourceSection } from "./components/loaded-resource-section.js";
77
81
  import { LoginDialogComponent } from "./components/login-dialog.js";
78
82
  import { createMermaidMarkdownTransformer } from "./components/mermaid.js";
79
83
  import { getModelFullId, mergeFavoritePatternsForPersist, } from "./components/model-favorites.js";
@@ -97,12 +101,13 @@ import { editFileInExternalEditor, editInExternalEditor } from "./external-edito
97
101
  import { GrokChrome } from "./grok/chrome.js";
98
102
  import { restoreInteractiveStderr, takeOverInteractiveStderr } from "./interactive-stderr-guard.js";
99
103
  import { applyKeybindingsFileEdit, seedKeybindingsFile } from "./keybindings-command.js";
104
+ import { buildResourceScopeGroups, formatResourceScopeGroups, getDisplaySourceInfo, getScopeAutocompleteTag, isPackageSourceInfo, isSystemResource, } from "./loaded-resource-scopes.js";
100
105
  import { describeLoginFailure } from "./login-outcome.js";
101
106
  import { refreshModelCatalogs } from "./model-catalog-refresh.js";
102
107
  import { getModelSearchText } from "./model-search.js";
103
108
  import { isRiskyMainModel, RISKY_MAIN_MODEL_WARNING } from "./risky-main-model-warning.js";
104
109
  import { DEFAULT_SMOOTH_FPS, StreamingRevealController } from "./streaming-reveal.js";
105
- import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, onThemeChange, setRegisteredThemes, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
110
+ import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getSelectListTheme, getThemeByName, onThemeChange, setRegisteredThemes, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
106
111
  import { InteractiveThemeController } from "./theme/theme-controller.js";
107
112
  import { buildFavoriteCycleStatusMessage } from "./tips/favorite-messages.js";
108
113
  import { recordTipShown } from "./tips/history-writer.js";
@@ -494,6 +499,9 @@ export class InteractiveMode {
494
499
  : DEFAULT_SMOOTH_FPS;
495
500
  this.ui.setMaxRenderFps(fps);
496
501
  }
502
+ get shownQuestion() {
503
+ return this.shownQuestionId === undefined ? undefined : this.pendingQuestions.get(this.shownQuestionId);
504
+ }
497
505
  // Convenience accessors
498
506
  // The session may be the local AgentSession or the shared-host RPC proxy; the
499
507
  // four reads widened on InteractiveSession must be awaited at every call site.
@@ -596,7 +604,11 @@ export class InteractiveMode {
596
604
  this.extensionInput = undefined;
597
605
  this.extensionEditor = undefined;
598
606
  this.askUserQuestion = undefined;
599
- this.asyncQuestion = undefined;
607
+ this.pendingQuestions = new Map();
608
+ this.pendingOrder = [];
609
+ this.questionSurface = "collapsed";
610
+ this.composerDestination = { kind: "chat" };
611
+ this.questionArrivalEpochMs = Date.now();
600
612
  this.extensionTerminalInputSubscriptions = new Set();
601
613
  // Extension widgets (components rendered above/below the editor)
602
614
  this.extensionWidgetsAbove = new Map();
@@ -724,7 +736,7 @@ export class InteractiveMode {
724
736
  if (!sourceInfo) {
725
737
  return undefined;
726
738
  }
727
- const scopePrefix = sourceInfo.scope === "user" ? "u" : sourceInfo.scope === "project" ? "p" : "t";
739
+ const scopePrefix = getScopeAutocompleteTag(sourceInfo.scope);
728
740
  const source = sourceInfo.source.trim();
729
741
  if (source === "auto" || source === "local" || source === "cli") {
730
742
  return scopePrefix;
@@ -1121,8 +1133,10 @@ export class InteractiveMode {
1121
1133
  return `${APP_TITLE} - ${cwdBasename}`;
1122
1134
  }
1123
1135
  applyTerminalTitle() {
1136
+ const questionHeader = this.shownQuestion?.request.questions[0]?.header ?? this.blockingQuestionHeader;
1124
1137
  this.ui.terminal.setTitle(this.activeToolTerminalTitle ??
1125
1138
  this.activeToolExecutionTerminalTitle ??
1139
+ (questionHeader === undefined ? undefined : `? ${questionHeader}`) ??
1126
1140
  this.extensionTerminalTitle ??
1127
1141
  this.getNormalTerminalTitle());
1128
1142
  }
@@ -1563,84 +1577,16 @@ export class InteractiveMode {
1563
1577
  });
1564
1578
  }
1565
1579
  getDisplaySourceInfo(sourceInfo) {
1566
- const source = sourceInfo?.source ?? "local";
1567
- const scope = sourceInfo?.scope ?? "project";
1568
- if (source === "local") {
1569
- if (scope === "user") {
1570
- return { label: "user", color: "muted" };
1571
- }
1572
- if (scope === "project") {
1573
- return { label: "project", color: "muted" };
1574
- }
1575
- if (scope === "temporary") {
1576
- return { label: "path", scopeLabel: "temp", color: "muted" };
1577
- }
1578
- return { label: "path", color: "muted" };
1579
- }
1580
- if (source === "cli") {
1581
- return {
1582
- label: "path",
1583
- scopeLabel: scope === "temporary" ? "temp" : undefined,
1584
- color: "muted",
1585
- };
1586
- }
1587
- const scopeLabel = scope === "user" ? "user" : scope === "project" ? "project" : scope === "temporary" ? "temp" : undefined;
1588
- return { label: source, scopeLabel, color: "accent" };
1589
- }
1590
- getScopeGroup(sourceInfo) {
1591
- const source = sourceInfo?.source ?? "local";
1592
- const scope = sourceInfo?.scope ?? "project";
1593
- if (source === "cli" || scope === "temporary")
1594
- return "path";
1595
- if (scope === "user")
1596
- return "user";
1597
- if (scope === "project")
1598
- return "project";
1599
- return "path";
1580
+ return getDisplaySourceInfo(sourceInfo);
1600
1581
  }
1601
1582
  isPackageSource(sourceInfo) {
1602
- const source = sourceInfo?.source ?? "";
1603
- return source.startsWith("npm:") || source.startsWith("git:");
1583
+ return isPackageSourceInfo(sourceInfo);
1604
1584
  }
1605
1585
  buildScopeGroups(items) {
1606
- const groups = {
1607
- user: { scope: "user", paths: [], packages: new Map() },
1608
- project: { scope: "project", paths: [], packages: new Map() },
1609
- path: { scope: "path", paths: [], packages: new Map() },
1610
- };
1611
- for (const item of items) {
1612
- const groupKey = this.getScopeGroup(item.sourceInfo);
1613
- const group = groups[groupKey];
1614
- const source = item.sourceInfo?.source ?? "local";
1615
- if (this.isPackageSource(item.sourceInfo)) {
1616
- const list = group.packages.get(source) ?? [];
1617
- list.push(item);
1618
- group.packages.set(source, list);
1619
- }
1620
- else {
1621
- group.paths.push(item);
1622
- }
1623
- }
1624
- return [groups.project, groups.user, groups.path].filter((group) => group.paths.length > 0 || group.packages.size > 0);
1586
+ return buildResourceScopeGroups(items);
1625
1587
  }
1626
1588
  formatScopeGroups(groups, options) {
1627
- const lines = [];
1628
- for (const group of groups) {
1629
- lines.push(` ${theme.fg("accent", group.scope)}`);
1630
- const sortedPaths = [...group.paths].sort((a, b) => a.path.localeCompare(b.path));
1631
- for (const item of sortedPaths) {
1632
- lines.push(theme.fg("dim", ` ${options.formatPath(item)}`));
1633
- }
1634
- const sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));
1635
- for (const [source, items] of sortedPackages) {
1636
- lines.push(` ${theme.fg("mdLink", source)}`);
1637
- const sortedPackagePaths = [...items].sort((a, b) => a.path.localeCompare(b.path));
1638
- for (const item of sortedPackagePaths) {
1639
- lines.push(theme.fg("dim", ` ${options.formatPackagePath(item, source)}`));
1640
- }
1641
- }
1642
- }
1643
- return lines.join("\n");
1589
+ return formatResourceScopeGroups(groups, options);
1644
1590
  }
1645
1591
  findSourceInfoForPath(p, sourceInfos) {
1646
1592
  const exact = sourceInfos.get(p);
@@ -1722,15 +1668,19 @@ export class InteractiveMode {
1722
1668
  const sectionHeader = (name, color = "mdHeading") => theme.fg(color, `[${name}]`);
1723
1669
  const formatCompactList = (items, options) => {
1724
1670
  const labels = items.map((item) => item.trim()).filter((item) => item.length > 0);
1671
+ if (labels.length === 0) {
1672
+ return "";
1673
+ }
1725
1674
  if (options?.sort !== false) {
1726
1675
  labels.sort((a, b) => a.localeCompare(b));
1727
1676
  }
1728
1677
  return theme.fg("dim", ` ${labels.join(", ")}`);
1729
1678
  };
1679
+ // System resources are left out of the compact body; a section with nothing else to show stays
1680
+ // hidden until the listing is expanded, where the system group lists them.
1730
1681
  const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
1731
- const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
1682
+ const section = new LoadedResourceSection(collapsedBody.length === 0 ? "" : `${sectionHeader(name, color)}\n${collapsedBody}`, `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState());
1732
1683
  this.loadedResourcesContainer.addChild(section);
1733
- this.loadedResourcesContainer.addChild(new Spacer(1));
1734
1684
  };
1735
1685
  const skillsResult = this.session.resourceLoader.getSkills();
1736
1686
  const promptsResult = this.session.resourceLoader.getPrompts();
@@ -1789,7 +1739,7 @@ export class InteractiveMode {
1789
1739
  formatPath: (item) => this.formatDisplayPath(item.path),
1790
1740
  formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
1791
1741
  });
1792
- const skillCompactList = formatCompactList(skills.map((skill) => skill.name));
1742
+ const skillCompactList = formatCompactList(skills.filter((skill) => !isSystemResource(skill.sourceInfo)).map((skill) => skill.name));
1793
1743
  addLoadedSection("Skills", skillCompactList, skillList);
1794
1744
  }
1795
1745
  const templates = this.session.promptTemplates;
@@ -1809,7 +1759,9 @@ export class InteractiveMode {
1809
1759
  return template ? `/${template.name}` : this.formatDisplayPath(item.path);
1810
1760
  },
1811
1761
  });
1812
- const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`));
1762
+ const promptCompactList = formatCompactList(templates
1763
+ .filter((template) => !isSystemResource(template.sourceInfo))
1764
+ .map((template) => `/${template.name}`));
1813
1765
  addLoadedSection("Prompts", promptCompactList, templateList);
1814
1766
  }
1815
1767
  if (extensions.length > 0) {
@@ -1818,7 +1770,7 @@ export class InteractiveMode {
1818
1770
  formatPath: (item) => this.formatExtensionDisplayPath(item.path),
1819
1771
  formatPackagePath: (item) => this.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)),
1820
1772
  });
1821
- const extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions));
1773
+ const extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions.filter((extension) => !isSystemResource(extension.sourceInfo))));
1822
1774
  addLoadedSection("Extensions", extensionCompactList, extList, "mdHeading");
1823
1775
  }
1824
1776
  // Show loaded themes (excluding built-in)
@@ -1833,7 +1785,9 @@ export class InteractiveMode {
1833
1785
  formatPath: (item) => this.formatDisplayPath(item.path),
1834
1786
  formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
1835
1787
  });
1836
- const themeCompactList = formatCompactList(customThemes.map((loadedTheme) => loadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath, loadedTheme.sourceInfo)));
1788
+ const themeCompactList = formatCompactList(customThemes
1789
+ .filter((loadedTheme) => !isSystemResource(loadedTheme.sourceInfo))
1790
+ .map((loadedTheme) => loadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath, loadedTheme.sourceInfo)));
1837
1791
  addLoadedSection("Themes", themeCompactList, themeList);
1838
1792
  }
1839
1793
  }
@@ -2144,10 +2098,19 @@ export class InteractiveMode {
2144
2098
  return false;
2145
2099
  };
2146
2100
  }
2101
+ async withBlockedHostDialog(id, label, show) {
2102
+ this.session.emitExtensionEvent("herdr:blocked", { active: true, label, id });
2103
+ try {
2104
+ return await show();
2105
+ }
2106
+ finally {
2107
+ this.session.emitExtensionEvent("herdr:blocked", { active: false, id });
2108
+ }
2109
+ }
2147
2110
  async handleHostUiRequest(request) {
2148
2111
  switch (request.method) {
2149
2112
  case "select": {
2150
- const value = await this.showExtensionSelector(request.title ?? "", request.options ?? []);
2113
+ const value = await this.withBlockedHostDialog(request.id, request.title ?? "", () => this.showExtensionSelector(request.title ?? "", request.options ?? []));
2151
2114
  return value === undefined
2152
2115
  ? { type: "extension_ui_response", id: request.id, cancelled: true }
2153
2116
  : { type: "extension_ui_response", id: request.id, value };
@@ -2156,16 +2119,16 @@ export class InteractiveMode {
2156
2119
  return {
2157
2120
  type: "extension_ui_response",
2158
2121
  id: request.id,
2159
- confirmed: await this.showExtensionConfirm(request.title ?? "", request.message ?? ""),
2122
+ confirmed: await this.withBlockedHostDialog(request.id, request.title ?? "", () => this.showExtensionConfirm(request.title ?? "", request.message ?? "")),
2160
2123
  };
2161
2124
  case "input": {
2162
- const value = await this.showExtensionInput(request.title ?? "", request.placeholder);
2125
+ const value = await this.withBlockedHostDialog(request.id, request.title ?? "", () => this.showExtensionInput(request.title ?? "", request.placeholder));
2163
2126
  return value === undefined
2164
2127
  ? { type: "extension_ui_response", id: request.id, cancelled: true }
2165
2128
  : { type: "extension_ui_response", id: request.id, value };
2166
2129
  }
2167
2130
  case "editor": {
2168
- const value = await this.showExtensionEditor(request.title ?? "", request.prefill);
2131
+ const value = await this.withBlockedHostDialog(request.id, request.title ?? "", () => this.showExtensionEditor(request.title ?? "", request.prefill));
2169
2132
  return value === undefined
2170
2133
  ? { type: "extension_ui_response", id: request.id, cancelled: true }
2171
2134
  : { type: "extension_ui_response", id: request.id, value };
@@ -2189,6 +2152,7 @@ export class InteractiveMode {
2189
2152
  timeoutMs: request.timeout ?? 0,
2190
2153
  }, {
2191
2154
  timeout: remainingMs,
2155
+ notifyArrival: request.askedAtMs === undefined || request.askedAtMs >= this.questionArrivalEpochMs,
2192
2156
  onProgress: (draft) => {
2193
2157
  lastDraft = draft;
2194
2158
  if (progressTimer !== undefined)
@@ -2601,11 +2565,13 @@ export class InteractiveMode {
2601
2565
  if (this.askUserQuestion) {
2602
2566
  this.hideQuestionOverlay();
2603
2567
  }
2604
- this.asyncQuestion?.finish({
2605
- status: "cancelled",
2606
- answers: {},
2607
- unanswered: this.asyncQuestion.request.questions.map((question) => question.id),
2608
- });
2568
+ for (const state of this.pendingQuestions.values()) {
2569
+ state.finish({
2570
+ status: "cancelled",
2571
+ answers: {},
2572
+ unanswered: state.request.questions.map((question) => question.id),
2573
+ });
2574
+ }
2609
2575
  this.ui.hideOverlay();
2610
2576
  this.clearExtensionTerminalInputListeners();
2611
2577
  this.setExtensionFooter(undefined);
@@ -2759,9 +2725,9 @@ export class InteractiveMode {
2759
2725
  }
2760
2726
  createExtensionUIContext() {
2761
2727
  return {
2762
- select: (title, options, opts) => this.showExtensionSelector(title, options, opts),
2763
- confirm: (title, message, opts) => this.showExtensionConfirm(title, message, opts),
2764
- input: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),
2728
+ select: (title, options, opts) => this.withBlockedHostDialog(crypto.randomUUID(), title, () => this.showExtensionSelector(title, options, opts)),
2729
+ confirm: (title, message, opts) => this.withBlockedHostDialog(crypto.randomUUID(), title, () => this.showExtensionConfirm(title, message, opts)),
2730
+ input: (title, placeholder, opts) => this.withBlockedHostDialog(crypto.randomUUID(), title, () => this.showExtensionInput(title, placeholder, opts)),
2765
2731
  // Async answers are delivered by the ask-user extension, not here: the
2766
2732
  // widget only resolves the question, so the model sees exactly one
2767
2733
  // framed user message no matter which surface answered.
@@ -2787,7 +2753,7 @@ export class InteractiveMode {
2787
2753
  pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
2788
2754
  setEditorText: (text) => this.editor.setText(text),
2789
2755
  getEditorText: () => this.getExpandedEditorText(),
2790
- editor: (title, prefill) => this.showExtensionEditor(title, prefill),
2756
+ editor: (title, prefill) => this.withBlockedHostDialog(crypto.randomUUID(), title, () => this.showExtensionEditor(title, prefill)),
2791
2757
  addAutocompleteProvider: (factory) => {
2792
2758
  this.autocompleteProviderWrappers.push(factory);
2793
2759
  this.setupAutocompleteProvider();
@@ -2955,6 +2921,8 @@ export class InteractiveMode {
2955
2921
  opts?.signal?.removeEventListener("abort", onAbort);
2956
2922
  this.workingMessage = previousWorkingMessage;
2957
2923
  this.updateWorkingIndicatorMessage();
2924
+ this.blockingQuestionHeader = undefined;
2925
+ this.applyTerminalTitle();
2958
2926
  this.hideQuestionOverlay();
2959
2927
  resolve(response);
2960
2928
  };
@@ -2974,11 +2942,16 @@ export class InteractiveMode {
2974
2942
  return;
2975
2943
  }
2976
2944
  opts?.signal?.addEventListener("abort", onAbort, { once: true });
2945
+ this.blockingQuestionHeader = request.questions[0]?.header;
2946
+ this.applyTerminalTitle();
2947
+ if (opts?.notifyArrival !== false && this.settingsManager.getAskUserSettings().bell)
2948
+ this.ui.terminal.write("\x07");
2977
2949
  this.workingMessage = "Waiting for your answer";
2978
2950
  this.updateWorkingIndicatorMessage();
2979
2951
  this.askUserQuestion = new AskUserQuestionComponent(request, (response) => finish(response), {
2980
2952
  tui: this.ui,
2981
2953
  timeoutMs: opts?.timeout ?? request.timeoutMs,
2954
+ getDeadlineAtMs: opts?.getDeadlineAtMs,
2982
2955
  onProgress: opts?.onProgress,
2983
2956
  });
2984
2957
  this.disposeActiveSelector();
@@ -2994,82 +2967,199 @@ export class InteractiveMode {
2994
2967
  hideQuestionOverlay() {
2995
2968
  this.askUserQuestion?.dispose();
2996
2969
  this.askUserQuestion = undefined;
2970
+ this.questionSurface = "collapsed";
2997
2971
  this.editorContainer.clear();
2998
2972
  this.editorContainer.addChild(this.editor);
2999
2973
  this.ui.setFocus(this.editor);
3000
2974
  this.ui.requestRender();
3001
2975
  }
3002
- /**
3003
- * Park a waitForAnswer=false question behind a one-line widget above the
3004
- * editor. The turn keeps running; the promise settles when the user submits
3005
- * from the expanded component, types an ordinary reply (comment), the idle
3006
- * countdown expires, or the caller aborts. A newer async question supersedes
3007
- * a pending one, which resolves as cancelled.
3008
- */
2976
+ /** Queue an async request without replacing another request or taking editor focus. */
3009
2977
  showAsyncQuestion(request, opts) {
3010
- return new Promise((resolve) => {
3011
- const cancelled = () => ({
2978
+ const existing = this.pendingQuestions.get(request.requestId);
2979
+ if (existing)
2980
+ return existing.completion;
2981
+ const cancelled = () => ({
2982
+ status: "cancelled",
2983
+ answers: {},
2984
+ unanswered: request.questions.map((question) => question.id),
2985
+ });
2986
+ if (opts?.signal?.aborted)
2987
+ return Promise.resolve(cancelled());
2988
+ const completion = Promise.withResolvers();
2989
+ const onAbort = () => state.finish(cancelled());
2990
+ const state = {
2991
+ request,
2992
+ timeoutMs: opts?.timeout ?? request.timeoutMs,
2993
+ askedAtMs: Date.now(),
2994
+ completion: completion.promise,
2995
+ getDeadlineAtMs: opts?.getDeadlineAtMs,
2996
+ onProgress: opts?.onProgress,
2997
+ draft: { answers: {} },
2998
+ finish: (response) => {
2999
+ if (this.pendingQuestions.get(request.requestId) !== state)
3000
+ return;
3001
+ this.pendingQuestions.delete(request.requestId);
3002
+ this.pendingOrder = this.pendingOrder.filter((id) => id !== request.requestId);
3003
+ opts?.signal?.removeEventListener("abort", onAbort);
3004
+ if (this.composerDestination.kind === "answer" &&
3005
+ this.composerDestination.requestId === request.requestId) {
3006
+ this.setComposerReply();
3007
+ if (this.editor.getText() !== "")
3008
+ this.showStatus("That question is no longer pending");
3009
+ }
3010
+ if (this.pendingOrder.length === 0 && this.questionSurface === "list") {
3011
+ this.ui.hideOverlay();
3012
+ this.questionSurface = "collapsed";
3013
+ }
3014
+ if (this.shownQuestionId === request.requestId) {
3015
+ if (this.questionSurface === "expanded")
3016
+ this.hideQuestionOverlay();
3017
+ this.shownQuestionId = this.pendingOrder[0];
3018
+ }
3019
+ this.refreshAsyncWidget();
3020
+ completion.resolve(response);
3021
+ },
3022
+ };
3023
+ this.pendingQuestions.set(request.requestId, state);
3024
+ this.pendingOrder.push(request.requestId);
3025
+ this.shownQuestionId ??= request.requestId;
3026
+ if (opts?.notifyArrival !== false && this.settingsManager.getAskUserSettings().bell)
3027
+ this.ui.terminal.write("\x07");
3028
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
3029
+ this.refreshAsyncWidget();
3030
+ return completion.promise;
3031
+ }
3032
+ /** Only the visible surface ticks; extension deadlines remain authoritative. */
3033
+ refreshAsyncWidget() {
3034
+ this.applyTerminalTitle();
3035
+ const state = this.shownQuestion;
3036
+ if (!state || this.questionSurface === "expanded") {
3037
+ this.setExtensionWidget(ASK_USER_WIDGET_KEY, undefined);
3038
+ return;
3039
+ }
3040
+ this.setExtensionWidget(ASK_USER_WIDGET_KEY, (tui) => new AskUserAsyncWidget({
3041
+ request: state.request,
3042
+ draft: state.draft,
3043
+ timeoutMs: state.timeoutMs,
3044
+ getDeadlineAtMs: state.getDeadlineAtMs,
3045
+ pendingCount: this.pendingOrder.length,
3046
+ tui,
3047
+ onExpire: () => state.finish(buildTimedOutResponse(state.request, state.draft, Date.now() - state.askedAtMs)),
3048
+ }));
3049
+ }
3050
+ /** Intercept question chords and first typed input before the editor inserts it. */
3051
+ handleAskUserShortcut(data) {
3052
+ const state = this.shownQuestion;
3053
+ if (!state ||
3054
+ this.askUserQuestion ||
3055
+ this.ui.hasOverlay() ||
3056
+ this.extensionSelector ||
3057
+ this.extensionInput ||
3058
+ this.extensionEditor)
3059
+ return false;
3060
+ if (matchesAskUserAnswerKey(data))
3061
+ return this.expandPendingQuestion();
3062
+ if (this.editor.getText() !== "" ||
3063
+ this.ui.getFocusedComponent() !== this.editor ||
3064
+ this.defaultEditor.isShowingAutocomplete())
3065
+ return false;
3066
+ if (this.keybindings.matches(data, "app.question.next")) {
3067
+ if (this.pendingOrder.length < 2)
3068
+ return false;
3069
+ const index = this.pendingOrder.indexOf(state.request.requestId);
3070
+ this.shownQuestionId = this.pendingOrder[(index + 1) % this.pendingOrder.length];
3071
+ this.refreshAsyncWidget();
3072
+ return true;
3073
+ }
3074
+ const unanswered = unansweredIds(state.request, state.draft);
3075
+ const questionIndex = state.request.questions.findIndex((question) => question.id === unanswered[0]);
3076
+ const question = state.request.questions[questionIndex];
3077
+ if (/^[1-9]$/.test(data) && question?.options[Number(data) - 1]) {
3078
+ this.expandPendingQuestion(state.request.requestId, questionIndex, data);
3079
+ return true;
3080
+ }
3081
+ const printable = decodeKittyPrintable(data) ?? data;
3082
+ if (data.startsWith("\x1b[200~") ||
3083
+ (printable !== "" && !/[\x00-\x1f\x7f]/.test(printable) && !/^[/!]/.test(printable))) {
3084
+ this.setComposerReply(state.request.requestId);
3085
+ }
3086
+ return false;
3087
+ }
3088
+ setComposerReply(requestId) {
3089
+ this.composerDestination = requestId === undefined ? { kind: "chat" } : { kind: "answer", requestId };
3090
+ const state = requestId === undefined ? undefined : this.pendingQuestions.get(requestId);
3091
+ this.defaultEditor.setReplyLabel(state
3092
+ ? theme.fg("muted", `↳ reply to ${state.request.questions[0].header} · ${keyText("app.message.followUp")} sends as message`)
3093
+ : undefined);
3094
+ this.ui.requestRender();
3095
+ }
3096
+ async handleAnswerCommand(argument) {
3097
+ const state = this.shownQuestion;
3098
+ if (!state) {
3099
+ this.showStatus("No question is pending.");
3100
+ return;
3101
+ }
3102
+ if (argument === "skip") {
3103
+ const response = {
3012
3104
  status: "cancelled",
3013
- answers: {},
3014
- unanswered: request.questions.map((question) => question.id),
3105
+ answers: state.draft.answers ?? {},
3106
+ unanswered: unansweredIds(state.request, state.draft),
3107
+ };
3108
+ state.finish(response);
3109
+ await state.completion;
3110
+ this.showStatus("The user dismissed the question.");
3111
+ // Ordinary cancellation/abort stays silent in the builtin. Only this
3112
+ // explicit command acknowledges dismissal, including on host bridges.
3113
+ await this.session.sendUserMessage(formatUserMessage(response, state.request.requestId, state.request.questions), {
3114
+ deliverAs: this.session.isStreaming ? "steer" : "followUp",
3015
3115
  });
3016
- if (opts?.signal?.aborted) {
3017
- resolve(cancelled());
3116
+ return;
3117
+ }
3118
+ if (argument !== "") {
3119
+ const id = /^[1-9]\d*$/.test(argument) ? this.pendingOrder[Number(argument) - 1] : undefined;
3120
+ if (id === undefined) {
3121
+ this.showStatus("Choose a pending question number or /answer skip.");
3018
3122
  return;
3019
3123
  }
3020
- this.asyncQuestion?.finish(cancelled());
3021
- const onAbort = () => state.finish(cancelled());
3022
- const state = {
3023
- request,
3024
- timeoutMs: opts?.timeout ?? request.timeoutMs,
3025
- onProgress: opts?.onProgress,
3026
- draft: { answers: {} },
3027
- finish: (response) => {
3028
- if (this.asyncQuestion !== state)
3029
- return;
3030
- this.asyncQuestion = undefined;
3031
- opts?.signal?.removeEventListener("abort", onAbort);
3032
- if (this.askUserQuestion)
3033
- this.hideQuestionOverlay();
3034
- this.setExtensionWidget(ASK_USER_WIDGET_KEY, undefined);
3035
- resolve(response);
3036
- },
3124
+ this.expandPendingQuestion(id);
3125
+ return;
3126
+ }
3127
+ if (this.pendingOrder.length === 1) {
3128
+ this.expandPendingQuestion();
3129
+ return;
3130
+ }
3131
+ const list = new SelectList(this.pendingOrder.map((id) => {
3132
+ const pending = this.pendingQuestions.get(id);
3133
+ const remaining = (pending.getDeadlineAtMs?.() ?? pending.askedAtMs + pending.timeoutMs) - Date.now();
3134
+ return {
3135
+ value: id,
3136
+ label: `${pending.request.questions[0].header} · ${formatCountdownLabel(Date.now() - pending.askedAtMs)} ago · ${formatCountdownLabel(remaining)} remaining`,
3037
3137
  };
3038
- this.asyncQuestion = state;
3039
- opts?.signal?.addEventListener("abort", onAbort, { once: true });
3040
- this.refreshAsyncWidget(state);
3041
- });
3042
- }
3043
- /** (Re)render the collapsed widget with a fresh idle countdown. */
3044
- refreshAsyncWidget(state) {
3045
- this.setExtensionWidget(ASK_USER_WIDGET_KEY, (tui) => {
3046
- const startedAt = Date.now();
3047
- return new AskUserAsyncWidget({
3048
- request: state.request,
3049
- draft: state.draft,
3050
- timeoutMs: state.timeoutMs,
3051
- tui,
3052
- onExpire: () => state.finish(buildTimedOutResponse(state.request, state.draft, Date.now() - startedAt)),
3053
- });
3054
- });
3055
- }
3056
- /** Editor shortcut: expand the pending async question into the full component. */
3057
- handleAskUserShortcut(data) {
3058
- if (!this.asyncQuestion || this.askUserQuestion || !matchesAskUserAnswerKey(data))
3059
- return false;
3060
- return this.expandPendingQuestion();
3138
+ }), 10, getSelectListTheme());
3139
+ const close = () => {
3140
+ this.ui.hideOverlay();
3141
+ this.questionSurface = "collapsed";
3142
+ };
3143
+ list.onCancel = close;
3144
+ list.onSelect = ({ value }) => {
3145
+ close();
3146
+ this.expandPendingQuestion(value);
3147
+ };
3148
+ this.questionSurface = "list";
3149
+ this.ui.showOverlay(list, { width: "80%", anchor: "bottom-center" });
3061
3150
  }
3062
3151
  /**
3063
3152
  * Key-free entry into the pending async question (empty Enter, /answer):
3064
3153
  * mounts the full component in place of the editor. Returns false when
3065
3154
  * nothing is pending or the component is already open.
3066
3155
  */
3067
- expandPendingQuestion() {
3068
- const state = this.asyncQuestion;
3156
+ expandPendingQuestion(requestId = this.shownQuestionId, initialQuestionIndex, initialInput) {
3157
+ const state = requestId === undefined ? undefined : this.pendingQuestions.get(requestId);
3069
3158
  if (!state || this.askUserQuestion)
3070
3159
  return false;
3160
+ this.shownQuestionId = state.request.requestId;
3071
3161
  const component = new AskUserQuestionComponent(state.request, (response) => {
3072
- if (this.asyncQuestion !== state)
3162
+ if (this.pendingQuestions.get(state.request.requestId) !== state)
3073
3163
  return;
3074
3164
  if (response.status !== "cancelled") {
3075
3165
  state.finish(response);
@@ -3077,21 +3167,28 @@ export class InteractiveMode {
3077
3167
  }
3078
3168
  // Esc collapses back to the widget; the question stays pending.
3079
3169
  this.hideQuestionOverlay();
3080
- this.refreshAsyncWidget(state);
3170
+ this.refreshAsyncWidget();
3081
3171
  }, {
3082
3172
  tui: this.ui,
3083
3173
  timeoutMs: state.timeoutMs,
3174
+ getDeadlineAtMs: state.getDeadlineAtMs,
3175
+ initialDraft: state.draft,
3176
+ initialQuestionIndex,
3084
3177
  onProgress: (draft) => {
3085
3178
  state.draft = draft;
3086
3179
  state.onProgress?.(draft);
3087
3180
  },
3088
3181
  });
3089
3182
  this.askUserQuestion = component;
3183
+ this.questionSurface = "expanded";
3184
+ this.refreshAsyncWidget();
3090
3185
  this.disposeActiveSelector();
3091
3186
  this.editorContainer.clear();
3092
3187
  this.editorContainer.addChild(component);
3093
3188
  this.ui.setFocus(component);
3094
3189
  this.ui.requestRender();
3190
+ if (initialInput !== undefined)
3191
+ component.handleInput(initialInput);
3095
3192
  return true;
3096
3193
  }
3097
3194
  /**
@@ -3100,9 +3197,11 @@ export class InteractiveMode {
3100
3197
  * the raw text. Returns false when nothing is pending.
3101
3198
  */
3102
3199
  submitAsyncQuestionComment(text) {
3103
- const state = this.asyncQuestion;
3200
+ const destination = this.composerDestination;
3201
+ const state = destination.kind === "answer" ? this.pendingQuestions.get(destination.requestId) : undefined;
3104
3202
  if (!state)
3105
3203
  return false;
3204
+ this.setComposerReply();
3106
3205
  this.editor.addToHistory?.(text);
3107
3206
  this.editor.setText("");
3108
3207
  state.finish(buildCommentResponse(state.request, state.draft, text));
@@ -3580,12 +3679,8 @@ export class InteractiveMode {
3580
3679
  this.expandPendingQuestion();
3581
3680
  return;
3582
3681
  }
3583
- // A pending async question claims ordinary text as its comment answer;
3584
- // slash and bash commands keep their normal routing.
3585
- if (!text.startsWith("/") &&
3586
- !text.startsWith("!") &&
3587
- this.asyncQuestion &&
3588
- this.submitAsyncQuestionComment(text))
3682
+ // Only an explicitly bound reply claims ordinary text; pre-arrival drafts and history stay chat.
3683
+ if (!text.startsWith("/") && !text.startsWith("!") && this.submitAsyncQuestionComment(text))
3589
3684
  return;
3590
3685
  // Handle commands
3591
3686
  if (text === "/settings") {
@@ -3649,10 +3744,9 @@ export class InteractiveMode {
3649
3744
  await this.handleKeybindingsCommand();
3650
3745
  return;
3651
3746
  }
3652
- if (text === "/answer") {
3747
+ if (text === "/answer" || text.startsWith("/answer ")) {
3653
3748
  this.editor.setText("");
3654
- if (!this.expandPendingQuestion())
3655
- this.showStatus("No question is pending.");
3749
+ await this.handleAnswerCommand(text.slice("/answer".length).trim());
3656
3750
  return;
3657
3751
  }
3658
3752
  if (text === "/hotkeys") {
@@ -4594,7 +4688,8 @@ export class InteractiveMode {
4594
4688
  }
4595
4689
  }
4596
4690
  else {
4597
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers());
4691
+ const answer = parseAskUserAnswerFrame(textContent);
4692
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad, this.getMarkdownTransformers(), answer ? getAskUserAnswerHeaders(this.sessionManager.getBranch(), answer.requestId) : undefined);
4598
4693
  this.chatContainer.addChild(userComponent);
4599
4694
  }
4600
4695
  if (options?.populateHistory) {
@@ -5191,6 +5286,7 @@ export class InteractiveMode {
5191
5286
  }
5192
5287
  }
5193
5288
  async handleFollowUp() {
5289
+ this.setComposerReply();
5194
5290
  const text = this.getExpandedEditorText().trim();
5195
5291
  if (!text)
5196
5292
  return;
@@ -7612,6 +7708,7 @@ export class InteractiveMode {
7612
7708
  const followUp = this.getAppKeyDisplay("app.message.followUp");
7613
7709
  const dequeue = this.getAppKeyDisplay("app.message.dequeue");
7614
7710
  const answerQuestion = this.getAppKeyDisplay("app.question.answer");
7711
+ const nextQuestion = this.getAppKeyDisplay("app.question.next");
7615
7712
  const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
7616
7713
  let hotkeys = `
7617
7714
  **Navigation**
@@ -7655,7 +7752,8 @@ export class InteractiveMode {
7655
7752
  | \`${copyMessage}\` | Copy last assistant message |
7656
7753
  | \`${followUp}\` | Queue follow-up message |
7657
7754
  | \`${dequeue}\` | Restore queued messages |
7658
- | \`${answerQuestion}\` | Open the pending question (also: Enter on an empty editor, or /answer) |
7755
+ | \`${answerQuestion}\` | Open the pending question before dequeue (also: empty Enter; /answer lists requests) |
7756
+ | \`${nextQuestion}\` | Cycle pending questions from an empty composer |
7659
7757
  | \`${pasteImage}\` | Paste image or text from clipboard |
7660
7758
  | \`/\` | Slash commands |
7661
7759
  | \`!\` | Run bash command |