@kolisachint/hoocode-agent 0.4.105 → 0.4.107

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 (289) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +4 -4
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +0 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +2 -2
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/agent-session.d.ts +1 -3
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +2 -2
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/bash-executor.d.ts.map +1 -1
  16. package/dist/core/bash-executor.js +1 -1
  17. package/dist/core/bash-executor.js.map +1 -1
  18. package/dist/core/extension-mcp-servers.d.ts +1 -1
  19. package/dist/core/extension-mcp-servers.d.ts.map +1 -1
  20. package/dist/core/extension-mcp-servers.js +1 -1
  21. package/dist/core/extension-mcp-servers.js.map +1 -1
  22. package/dist/core/extensions/plugins/index.d.ts.map +1 -1
  23. package/dist/core/extensions/plugins/index.js +1 -1
  24. package/dist/core/extensions/plugins/index.js.map +1 -1
  25. package/dist/core/extensions/types.d.ts +1 -3
  26. package/dist/core/extensions/types.d.ts.map +1 -1
  27. package/dist/core/extensions/types.js.map +1 -1
  28. package/dist/core/index.d.ts +1 -1
  29. package/dist/core/index.d.ts.map +1 -1
  30. package/dist/core/index.js.map +1 -1
  31. package/dist/core/mode-prompts.d.ts +1 -1
  32. package/dist/core/mode-prompts.d.ts.map +1 -1
  33. package/dist/core/mode-prompts.js +1 -1
  34. package/dist/core/mode-prompts.js.map +1 -1
  35. package/dist/core/package-manager.d.ts.map +1 -1
  36. package/dist/core/package-manager.js +1 -1
  37. package/dist/core/package-manager.js.map +1 -1
  38. package/dist/core/sdk.d.ts.map +1 -1
  39. package/dist/core/sdk.js +1 -2
  40. package/dist/core/sdk.js.map +1 -1
  41. package/dist/core/session-manager.d.ts +2 -97
  42. package/dist/core/session-manager.d.ts.map +1 -1
  43. package/dist/core/session-manager.js +4 -65
  44. package/dist/core/session-manager.js.map +1 -1
  45. package/dist/core/settings-manager.d.ts.map +1 -1
  46. package/dist/core/settings-manager.js +2 -3
  47. package/dist/core/settings-manager.js.map +1 -1
  48. package/dist/core/telemetry.d.ts.map +1 -1
  49. package/dist/core/telemetry.js +1 -1
  50. package/dist/core/telemetry.js.map +1 -1
  51. package/dist/core/timings.d.ts +1 -1
  52. package/dist/core/timings.d.ts.map +1 -1
  53. package/dist/core/timings.js +2 -2
  54. package/dist/core/timings.js.map +1 -1
  55. package/dist/core/tools/{browser-continue.d.ts → browser/browser-continue.d.ts} +1 -1
  56. package/dist/core/tools/browser/browser-continue.d.ts.map +1 -0
  57. package/dist/core/tools/{browser-continue.js → browser/browser-continue.js} +2 -2
  58. package/dist/core/tools/browser/browser-continue.js.map +1 -0
  59. package/dist/core/tools/{browser-run.d.ts → browser/browser-run.d.ts} +2 -2
  60. package/dist/core/tools/browser/browser-run.d.ts.map +1 -0
  61. package/dist/core/tools/{browser-run.js → browser/browser-run.js} +2 -2
  62. package/dist/core/tools/browser/browser-run.js.map +1 -0
  63. package/dist/core/tools/browser/browsertools-shared.d.ts.map +1 -0
  64. package/dist/core/tools/{browsertools-shared.js → browser/browsertools-shared.js} +1 -1
  65. package/dist/core/tools/browser/browsertools-shared.js.map +1 -0
  66. package/dist/core/tools/browser/index.d.ts +8 -0
  67. package/dist/core/tools/browser/index.d.ts.map +1 -0
  68. package/dist/core/tools/browser/index.js +8 -0
  69. package/dist/core/tools/browser/index.js.map +1 -0
  70. package/dist/core/tools/{docedit.d.ts → doc/docedit.d.ts} +2 -2
  71. package/dist/core/tools/doc/docedit.d.ts.map +1 -0
  72. package/dist/core/tools/{docedit.js → doc/docedit.js} +7 -7
  73. package/dist/core/tools/doc/docedit.js.map +1 -0
  74. package/dist/core/tools/{docgrep.d.ts → doc/docgrep.d.ts} +2 -2
  75. package/dist/core/tools/doc/docgrep.d.ts.map +1 -0
  76. package/dist/core/tools/{docgrep.js → doc/docgrep.js} +6 -6
  77. package/dist/core/tools/doc/docgrep.js.map +1 -0
  78. package/dist/core/tools/{docpeek.d.ts → doc/docpeek.d.ts} +2 -2
  79. package/dist/core/tools/{docpeek.js.map → doc/docpeek.d.ts.map} +1 -1
  80. package/dist/core/tools/{docpeek.js → doc/docpeek.js} +6 -6
  81. package/dist/core/tools/doc/docpeek.js.map +1 -0
  82. package/dist/core/tools/{docread.d.ts → doc/docread.d.ts} +2 -2
  83. package/dist/core/tools/doc/docread.d.ts.map +1 -0
  84. package/dist/core/tools/{docread.js → doc/docread.js} +6 -6
  85. package/dist/core/tools/doc/docread.js.map +1 -0
  86. package/dist/core/tools/{docscan.d.ts → doc/docscan.d.ts} +2 -2
  87. package/dist/core/tools/doc/docscan.d.ts.map +1 -0
  88. package/dist/core/tools/{docscan.js → doc/docscan.js} +6 -6
  89. package/dist/core/tools/doc/docscan.js.map +1 -0
  90. package/dist/core/tools/{docwrite.d.ts → doc/docwrite.d.ts} +1 -1
  91. package/dist/core/tools/doc/docwrite.d.ts.map +1 -0
  92. package/dist/core/tools/{docwrite.js → doc/docwrite.js} +6 -6
  93. package/dist/core/tools/doc/docwrite.js.map +1 -0
  94. package/dist/core/tools/doc/index.d.ts +11 -0
  95. package/dist/core/tools/doc/index.d.ts.map +1 -0
  96. package/dist/core/tools/doc/index.js +11 -0
  97. package/dist/core/tools/doc/index.js.map +1 -0
  98. package/dist/core/tools/grep.d.ts.map +1 -1
  99. package/dist/core/tools/grep.js +1 -1
  100. package/dist/core/tools/grep.js.map +1 -1
  101. package/dist/core/tools/index.d.ts +42 -28
  102. package/dist/core/tools/index.d.ts.map +1 -1
  103. package/dist/core/tools/index.js +61 -199
  104. package/dist/core/tools/index.js.map +1 -1
  105. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  106. package/dist/core/tools/output-accumulator.js +1 -1
  107. package/dist/core/tools/output-accumulator.js.map +1 -1
  108. package/dist/core/tools/read.d.ts.map +1 -1
  109. package/dist/core/tools/read.js +1 -1
  110. package/dist/core/tools/read.js.map +1 -1
  111. package/dist/extensions/core/ask-options.d.ts +10 -0
  112. package/dist/extensions/core/ask-options.d.ts.map +1 -0
  113. package/dist/extensions/core/ask-options.js +83 -0
  114. package/dist/extensions/core/ask-options.js.map +1 -0
  115. package/dist/extensions/core/config.d.ts +80 -0
  116. package/dist/extensions/core/config.d.ts.map +1 -0
  117. package/dist/extensions/core/config.js +110 -0
  118. package/dist/extensions/core/config.js.map +1 -0
  119. package/dist/extensions/core/cost.d.ts +11 -0
  120. package/dist/extensions/core/cost.d.ts.map +1 -0
  121. package/dist/extensions/core/cost.js +64 -0
  122. package/dist/extensions/core/cost.js.map +1 -0
  123. package/dist/extensions/core/hoo-core.d.ts +16 -179
  124. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  125. package/dist/extensions/core/hoo-core.js +27 -1663
  126. package/dist/extensions/core/hoo-core.js.map +1 -1
  127. package/dist/extensions/core/loop.d.ts +15 -0
  128. package/dist/extensions/core/loop.d.ts.map +1 -0
  129. package/dist/extensions/core/loop.js +235 -0
  130. package/dist/extensions/core/loop.js.map +1 -0
  131. package/dist/extensions/core/marketplace.d.ts +17 -0
  132. package/dist/extensions/core/marketplace.d.ts.map +1 -0
  133. package/dist/extensions/core/marketplace.js +167 -0
  134. package/dist/extensions/core/marketplace.js.map +1 -0
  135. package/dist/extensions/core/mcp-loader.d.ts +26 -0
  136. package/dist/extensions/core/mcp-loader.d.ts.map +1 -0
  137. package/dist/extensions/core/mcp-loader.js +391 -0
  138. package/dist/extensions/core/mcp-loader.js.map +1 -0
  139. package/dist/extensions/core/modes.d.ts +56 -0
  140. package/dist/extensions/core/modes.d.ts.map +1 -0
  141. package/dist/extensions/core/modes.js +268 -0
  142. package/dist/extensions/core/modes.js.map +1 -0
  143. package/dist/extensions/core/permission-gate.d.ts +10 -0
  144. package/dist/extensions/core/permission-gate.d.ts.map +1 -0
  145. package/dist/extensions/core/permission-gate.js +167 -0
  146. package/dist/extensions/core/permission-gate.js.map +1 -0
  147. package/dist/extensions/core/scaffold.d.ts +9 -0
  148. package/dist/extensions/core/scaffold.d.ts.map +1 -0
  149. package/dist/extensions/core/scaffold.js +146 -0
  150. package/dist/extensions/core/scaffold.js.map +1 -0
  151. package/dist/extensions/core/thinking-escalation.d.ts +18 -0
  152. package/dist/extensions/core/thinking-escalation.d.ts.map +1 -0
  153. package/dist/extensions/core/thinking-escalation.js +70 -0
  154. package/dist/extensions/core/thinking-escalation.js.map +1 -0
  155. package/dist/index.d.ts +2 -3
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +3 -4
  158. package/dist/index.js.map +1 -1
  159. package/dist/main.d.ts.map +1 -1
  160. package/dist/main.js +2 -2
  161. package/dist/main.js.map +1 -1
  162. package/dist/modes/interactive/command-executor.d.ts +0 -5
  163. package/dist/modes/interactive/command-executor.d.ts.map +1 -1
  164. package/dist/modes/interactive/command-executor.js +0 -7
  165. package/dist/modes/interactive/command-executor.js.map +1 -1
  166. package/dist/modes/interactive/components/branch-summary-message.d.ts +1 -1
  167. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  168. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  169. package/dist/modes/interactive/components/compaction-summary-message.d.ts +1 -1
  170. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  171. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  172. package/dist/modes/interactive/components/custom-message.d.ts +1 -1
  173. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  175. package/dist/modes/interactive/components/index.d.ts +0 -2
  176. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/index.js +0 -2
  178. package/dist/modes/interactive/components/index.js.map +1 -1
  179. package/dist/modes/interactive/extension-chrome.d.ts +56 -0
  180. package/dist/modes/interactive/extension-chrome.d.ts.map +1 -0
  181. package/dist/modes/interactive/extension-chrome.js +172 -0
  182. package/dist/modes/interactive/extension-chrome.js.map +1 -0
  183. package/dist/modes/interactive/extension-dialogs.d.ts +82 -0
  184. package/dist/modes/interactive/extension-dialogs.d.ts.map +1 -0
  185. package/dist/modes/interactive/extension-dialogs.js +259 -0
  186. package/dist/modes/interactive/extension-dialogs.js.map +1 -0
  187. package/dist/modes/interactive/interactive-mode.d.ts +11 -161
  188. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  189. package/dist/modes/interactive/interactive-mode.js +263 -1571
  190. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  191. package/dist/modes/interactive/resource-display.d.ts +59 -0
  192. package/dist/modes/interactive/resource-display.d.ts.map +1 -0
  193. package/dist/modes/interactive/resource-display.js +499 -0
  194. package/dist/modes/interactive/resource-display.js.map +1 -0
  195. package/dist/modes/interactive/startup-checks.d.ts +17 -0
  196. package/dist/modes/interactive/startup-checks.d.ts.map +1 -0
  197. package/dist/modes/interactive/startup-checks.js +102 -0
  198. package/dist/modes/interactive/startup-checks.js.map +1 -0
  199. package/dist/modes/interactive/team-focus.d.ts +56 -0
  200. package/dist/modes/interactive/team-focus.d.ts.map +1 -0
  201. package/dist/modes/interactive/team-focus.js +167 -0
  202. package/dist/modes/interactive/team-focus.js.map +1 -0
  203. package/dist/modes/interactive/voice/voice-controller.d.ts +66 -0
  204. package/dist/modes/interactive/voice/voice-controller.d.ts.map +1 -0
  205. package/dist/modes/interactive/voice/voice-controller.js +312 -0
  206. package/dist/modes/interactive/voice/voice-controller.js.map +1 -0
  207. package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -0
  208. package/dist/modes/interactive/voice/voice-panel.js.map +1 -0
  209. package/dist/modes/interactive/{voice-transcribe.d.ts → voice/voice-transcribe.d.ts} +16 -1
  210. package/dist/modes/interactive/voice/voice-transcribe.d.ts.map +1 -0
  211. package/dist/modes/interactive/{voice-transcribe.js → voice/voice-transcribe.js} +31 -4
  212. package/dist/modes/interactive/voice/voice-transcribe.js.map +1 -0
  213. package/dist/modes/rpc/rpc-client.d.ts +1 -2
  214. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  215. package/dist/modes/rpc/rpc-client.js.map +1 -1
  216. package/dist/modes/rpc/rpc-types.d.ts +1 -2
  217. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  218. package/dist/modes/rpc/rpc-types.js.map +1 -1
  219. package/dist/resources-cli.d.ts.map +1 -1
  220. package/dist/resources-cli.js +1 -1
  221. package/dist/resources-cli.js.map +1 -1
  222. package/dist/utils/tools-manager.d.ts.map +1 -1
  223. package/dist/utils/tools-manager.js +1 -1
  224. package/dist/utils/tools-manager.js.map +1 -1
  225. package/dist/utils/version-check.d.ts.map +1 -1
  226. package/dist/utils/version-check.js +2 -2
  227. package/dist/utils/version-check.js.map +1 -1
  228. package/docs/project-local-resources.md +1 -1
  229. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  230. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  231. package/examples/extensions/sandbox/package.json +1 -1
  232. package/examples/extensions/with-deps/package.json +1 -1
  233. package/package.json +4 -4
  234. package/dist/core/compaction/branch-summarization.d.ts +0 -88
  235. package/dist/core/compaction/branch-summarization.d.ts.map +0 -1
  236. package/dist/core/compaction/branch-summarization.js +0 -243
  237. package/dist/core/compaction/branch-summarization.js.map +0 -1
  238. package/dist/core/compaction/compaction.d.ts +0 -123
  239. package/dist/core/compaction/compaction.d.ts.map +0 -1
  240. package/dist/core/compaction/compaction.js +0 -635
  241. package/dist/core/compaction/compaction.js.map +0 -1
  242. package/dist/core/compaction/index.d.ts +0 -7
  243. package/dist/core/compaction/index.d.ts.map +0 -1
  244. package/dist/core/compaction/index.js +0 -7
  245. package/dist/core/compaction/index.js.map +0 -1
  246. package/dist/core/compaction/utils.d.ts +0 -38
  247. package/dist/core/compaction/utils.d.ts.map +0 -1
  248. package/dist/core/compaction/utils.js +0 -156
  249. package/dist/core/compaction/utils.js.map +0 -1
  250. package/dist/core/messages.d.ts +0 -124
  251. package/dist/core/messages.d.ts.map +0 -1
  252. package/dist/core/messages.js +0 -221
  253. package/dist/core/messages.js.map +0 -1
  254. package/dist/core/tools/browser-continue.d.ts.map +0 -1
  255. package/dist/core/tools/browser-continue.js.map +0 -1
  256. package/dist/core/tools/browser-run.d.ts.map +0 -1
  257. package/dist/core/tools/browser-run.js.map +0 -1
  258. package/dist/core/tools/browsertools-shared.d.ts.map +0 -1
  259. package/dist/core/tools/browsertools-shared.js.map +0 -1
  260. package/dist/core/tools/docedit.d.ts.map +0 -1
  261. package/dist/core/tools/docedit.js.map +0 -1
  262. package/dist/core/tools/docgrep.d.ts.map +0 -1
  263. package/dist/core/tools/docgrep.js.map +0 -1
  264. package/dist/core/tools/docpeek.d.ts.map +0 -1
  265. package/dist/core/tools/docread.d.ts.map +0 -1
  266. package/dist/core/tools/docread.js.map +0 -1
  267. package/dist/core/tools/docscan.d.ts.map +0 -1
  268. package/dist/core/tools/docscan.js.map +0 -1
  269. package/dist/core/tools/docwrite.d.ts.map +0 -1
  270. package/dist/core/tools/docwrite.js.map +0 -1
  271. package/dist/core/tools/output-compression.d.ts +0 -53
  272. package/dist/core/tools/output-compression.d.ts.map +0 -1
  273. package/dist/core/tools/output-compression.js +0 -392
  274. package/dist/core/tools/output-compression.js.map +0 -1
  275. package/dist/modes/interactive/components/armin.d.ts +0 -34
  276. package/dist/modes/interactive/components/armin.d.ts.map +0 -1
  277. package/dist/modes/interactive/components/armin.js +0 -333
  278. package/dist/modes/interactive/components/armin.js.map +0 -1
  279. package/dist/modes/interactive/components/daxnuts.d.ts +0 -23
  280. package/dist/modes/interactive/components/daxnuts.d.ts.map +0 -1
  281. package/dist/modes/interactive/components/daxnuts.js +0 -140
  282. package/dist/modes/interactive/components/daxnuts.js.map +0 -1
  283. package/dist/modes/interactive/components/voice-panel.d.ts.map +0 -1
  284. package/dist/modes/interactive/components/voice-panel.js.map +0 -1
  285. package/dist/modes/interactive/voice-transcribe.d.ts.map +0 -1
  286. package/dist/modes/interactive/voice-transcribe.js.map +0 -1
  287. /package/dist/core/tools/{browsertools-shared.d.ts → browser/browsertools-shared.d.ts} +0 -0
  288. /package/dist/modes/interactive/{components → voice}/voice-panel.d.ts +0 -0
  289. /package/dist/modes/interactive/{components → voice}/voice-panel.js +0 -0
@@ -6,32 +6,27 @@ import * as crypto from "node:crypto";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
+ import { createCompactionSummaryMessage } from "@kolisachint/hoocode-agent-core";
9
10
  import { getProviders, } from "@kolisachint/hoocode-ai";
10
11
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, } from "@kolisachint/hoocode-tui";
11
- import { spawn, spawnSync } from "child_process";
12
- import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDocsPath, VERSION } from "../../config.js";
12
+ import { spawnSync } from "child_process";
13
+ import { APP_NAME, APP_TITLE, getAuthPath, getDocsPath, VERSION } from "../../config.js";
13
14
  import { parseSkillBlock } from "../../core/agent-session.js";
14
15
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
15
16
  import { KeybindingsManager } from "../../core/keybindings.js";
16
- import { createCompactionSummaryMessage } from "../../core/messages.js";
17
17
  import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
18
- import { DefaultPackageManager } from "../../core/package-manager.js";
19
18
  import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
20
19
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
21
20
  import { SessionManager } from "../../core/session-manager.js";
22
21
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
23
22
  import { taskStore } from "../../core/task-store.js";
24
- import { TeamApprovalCoordinator } from "../../core/team-approvals.js";
25
23
  import { buildCompactWordmark } from "../../core/wordmark.js";
26
- import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
27
24
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
28
25
  import { parseGitUrl } from "../../utils/git.js";
29
- import { getCwdRelativePath } from "../../utils/paths.js";
30
26
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
31
27
  import { ensureTool } from "../../utils/tools-manager.js";
32
28
  import { checkForNewHooCodeVersion } from "../../utils/version-check.js";
33
29
  import { CommandExecutor } from "./command-executor.js";
34
- import { AskOptionsComponent } from "./components/ask-options.js";
35
30
  import { AssistantMessageComponent } from "./components/assistant-message.js";
36
31
  import { BashExecutionComponent } from "./components/bash-execution.js";
37
32
  import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
@@ -39,10 +34,7 @@ import { CompactionSummaryMessageComponent } from "./components/compaction-summa
39
34
  import { CountdownTimer } from "./components/countdown-timer.js";
40
35
  import { CustomEditor } from "./components/custom-editor.js";
41
36
  import { CustomMessageComponent } from "./components/custom-message.js";
42
- import { DaxnutsComponent } from "./components/daxnuts.js";
43
37
  import { DynamicBorder } from "./components/dynamic-border.js";
44
- import { ExtensionEditorComponent } from "./components/extension-editor.js";
45
- import { ExtensionInputComponent } from "./components/extension-input.js";
46
38
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
47
39
  import { FooterComponent } from "./components/footer.js";
48
40
  import { keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
@@ -54,29 +46,17 @@ import { SessionSelectorComponent } from "./components/session-selector.js";
54
46
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
55
47
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
56
48
  import { TaskPanelComponent } from "./components/task-panel.js";
57
- import { TeamAttachPanelComponent } from "./components/team-attach-panel.js";
58
49
  import { ToolExecutionComponent } from "./components/tool-execution.js";
59
50
  import { TreeSelectorComponent } from "./components/tree-selector.js";
60
51
  import { UserMessageComponent } from "./components/user-message.js";
61
52
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
62
- import { VoicePanel } from "./components/voice-panel.js";
53
+ import { ExtensionChrome } from "./extension-chrome.js";
54
+ import { ExtensionDialogs } from "./extension-dialogs.js";
55
+ import { ExpandableText, formatDisplayPath, isExpandable, showLoadedResources as renderLoadedResources, } from "./resource-display.js";
56
+ import { checkForPackageUpdates, checkTmuxKeyboardSetup, getChangelogForDisplay } from "./startup-checks.js";
57
+ import { TeamFocusController } from "./team-focus.js";
63
58
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
64
- import { startVoiceTranscribe, VoiceDaemon } from "./voice-transcribe.js";
65
- function isExpandable(obj) {
66
- return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
67
- }
68
- class ExpandableText extends Text {
69
- getCollapsedText;
70
- getExpandedText;
71
- constructor(getCollapsedText, getExpandedText, expanded = false, paddingX = 0, paddingY = 0) {
72
- super(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY);
73
- this.getCollapsedText = getCollapsedText;
74
- this.getExpandedText = getExpandedText;
75
- }
76
- setExpanded(expanded) {
77
- this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
78
- }
79
- }
59
+ import { VoiceController } from "./voice/voice-controller.js";
80
60
  const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
81
61
  function isDeadTerminalError(error) {
82
62
  if (!error || typeof error !== "object" || !("code" in error)) {
@@ -85,12 +65,6 @@ function isDeadTerminalError(error) {
85
65
  const code = error.code;
86
66
  return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
87
67
  }
88
- // Mirrors voicetools' own default trailing-silence window (see `--silence-ms` /
89
- // VOICE_SILENCE_MS upstream). Only used to drive the cosmetic countdown in the
90
- // panel; the actual cutoff decision is made by the binary, not this timer.
91
- const VOICE_SILENCE_MS = 600;
92
- const VOICE_UNAVAILABLE_MESSAGE = "Voice input failed: voicetools binary unavailable and could not be downloaded. " +
93
- "Install it, set VOICETOOLS_BIN, or ensure a published release exists for this platform.";
94
68
  const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth: billed per token as extra usage, not plan limits.";
95
69
  function isAnthropicSubscriptionAuthKey(apiKey) {
96
70
  return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
@@ -122,19 +96,7 @@ export class InteractiveMode {
122
96
  defaultEditor;
123
97
  editor;
124
98
  editorComponentFactory;
125
- voiceSession;
126
- // Persistent `voicetools serve` process, once probed successfully. Stays alive
127
- // (and warm) across captures for the life of the interactive session.
128
- voiceDaemon;
129
- // Sticky for the session: set once `serve` is confirmed unsupported (old binary)
130
- // so later presses skip straight to the per-press `transcribe` fallback.
131
- voiceDaemonUnsupported = false;
132
- /** True while the voicetools binary is being resolved/downloaded before a session starts. */
133
- voiceStarting = false;
134
- /** True while a capture (daemon or legacy) is in flight. */
135
- voiceActive = false;
136
- /** The live multi-line status panel for the current capture (undefined when idle). */
137
- voicePanel;
99
+ voice;
138
100
  autocompleteProvider;
139
101
  autocompleteProviderWrappers = [];
140
102
  fdPath;
@@ -195,30 +157,20 @@ export class InteractiveMode {
195
157
  // Shutdown state
196
158
  shutdownRequested = false;
197
159
  // Extension UI state
198
- extensionSelector = undefined;
199
- extensionInput = undefined;
200
- askOptions = undefined;
201
- extensionEditor = undefined;
160
+ dialogs;
202
161
  extensionTerminalInputUnsubscribers = new Set();
203
162
  // Extension widgets (components rendered above/below the editor)
204
- extensionWidgetsAbove = new Map();
205
- extensionWidgetsBelow = new Map();
163
+ chrome;
206
164
  widgetContainerAbove;
207
165
  widgetContainerBelow;
208
166
  // Task panel shown just above the editor (active subagent tasks)
209
167
  taskPanel;
210
168
  // hooteams team client (--team): steering + attach share its single SSE stream
211
- teamClient = undefined;
212
- teamAttachPanel = undefined;
213
- teamAttachHandle = undefined;
214
- // Custom footer from extension (undefined = use built-in footer)
215
- customFooter = undefined;
169
+ teamFocus;
216
170
  // Header container that holds the built-in or custom header
217
171
  headerContainer;
218
172
  // Built-in header (logo + keybinding hints + changelog)
219
173
  builtInHeader = undefined;
220
- // Custom header from extension (undefined = use built-in header)
221
- customHeader = undefined;
222
174
  // Convenience accessors
223
175
  get session() {
224
176
  return this.runtimeHost.session;
@@ -273,9 +225,8 @@ export class InteractiveMode {
273
225
  stopLoadingAnimation: () => self.stopLoadingAnimation(),
274
226
  findExactModelMatch: (searchTerm) => self.findExactModelMatch(searchTerm),
275
227
  maybeWarnAboutAnthropicSubscriptionAuth: (model) => self.maybeWarnAboutAnthropicSubscriptionAuth(model),
276
- checkDaxnutsEasterEgg: (model) => self.checkDaxnutsEasterEgg(model),
277
228
  showModelSelector: (searchTerm) => self.showModelSelector(searchTerm),
278
- showExtensionConfirm: (title, message) => self.showExtensionConfirm(title, message),
229
+ showExtensionConfirm: (title, message) => self.dialogs.confirm(title, message),
279
230
  promptForMissingSessionCwd: (error) => self.promptForMissingSessionCwd(error),
280
231
  handleFatalRuntimeError: (prefix, error) => self.handleFatalRuntimeError(prefix, error),
281
232
  };
@@ -332,10 +283,42 @@ export class InteractiveMode {
332
283
  this.footer = new FooterComponent(this.session, this.footerDataProvider);
333
284
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
334
285
  this.footerDataProvider.setSubagentEnabled(this.session.getActiveToolNames().includes("Task"));
286
+ this.chrome = new ExtensionChrome({
287
+ ui: this.ui,
288
+ widgetContainerAbove: this.widgetContainerAbove,
289
+ widgetContainerBelow: this.widgetContainerBelow,
290
+ headerContainer: this.headerContainer,
291
+ footer: this.footer,
292
+ footerDataProvider: this.footerDataProvider,
293
+ getBuiltInHeader: () => this.builtInHeader,
294
+ isToolOutputExpanded: () => this.toolOutputExpanded,
295
+ });
335
296
  this.taskPanel = new TaskPanelComponent(this.ui);
336
- this.taskPanel.onNudge = (role) => this.showTeamNudgeInput(role);
337
- this.taskPanel.onAttach = (role) => this.showTeamAttach(role);
338
- this.taskPanel.onExitFocus = () => this.exitTeamFocus();
297
+ this.dialogs = new ExtensionDialogs({
298
+ ui: this.ui,
299
+ editorContainer: this.editorContainer,
300
+ keybindings: this.keybindings,
301
+ getEditor: () => this.editor,
302
+ });
303
+ this.teamFocus = new TeamFocusController({
304
+ ui: this.ui,
305
+ taskPanel: this.taskPanel,
306
+ editorContainer: this.editorContainer,
307
+ getEditor: () => this.editor,
308
+ showStatus: (message) => this.showStatus(message),
309
+ showWarning: (message) => this.showWarning(message),
310
+ showAskOptions: (questions, opts) => this.dialogs.showAskOptions(questions, opts),
311
+ isAskOptionsOpen: () => this.dialogs.isAskOptionsOpen(),
312
+ });
313
+ this.voice = new VoiceController({
314
+ ui: this.ui,
315
+ statusContainer: this.statusContainer,
316
+ sendEditorInput: (data) => this.editor.handleInput(data),
317
+ showError: (message) => this.showError(message),
318
+ });
319
+ this.taskPanel.onNudge = (role) => this.teamFocus.showNudgeInput(role);
320
+ this.taskPanel.onAttach = (role) => this.teamFocus.showAttach(role);
321
+ this.taskPanel.onExitFocus = () => this.teamFocus.exitFocus();
339
322
  // Load hide thinking block setting
340
323
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
341
324
  // Register themes from resource loader and initialize
@@ -486,7 +469,10 @@ export class InteractiveMode {
486
469
  return;
487
470
  this.registerSignalHandlers();
488
471
  // Load changelog (only show new entries, skip for resumed sessions)
489
- this.changelogMarkdown = this.getChangelogForDisplay();
472
+ this.changelogMarkdown = getChangelogForDisplay({
473
+ hasMessages: this.session.state.messages.length > 0,
474
+ settingsManager: this.settingsManager,
475
+ });
490
476
  // Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
491
477
  // Both are needed: fd for autocomplete, rg for grep tool and bash commands
492
478
  const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
@@ -500,7 +486,7 @@ export class InteractiveMode {
500
486
  ? buildCompactWordmark({
501
487
  appName: APP_NAME,
502
488
  version: this.version,
503
- cwd: this.formatDisplayPath(this.sessionManager.getCwd()),
489
+ cwd: formatDisplayPath(this.sessionManager.getCwd()),
504
490
  accent: (text) => theme.fg("accent", text),
505
491
  dim: (text) => theme.fg("dim", text),
506
492
  muted: (text) => theme.fg("muted", text),
@@ -523,7 +509,7 @@ export class InteractiveMode {
523
509
  hint("app.tools.expand", "to expand tools"),
524
510
  hint("app.thinking.toggle", "to expand thinking"),
525
511
  hint("app.tasks.cycleView", "to cycle task panel view"),
526
- ...(this.teamClient ? [hint("app.team.focus", "to focus team roster")] : []),
512
+ ...(this.teamFocus.connected ? [hint("app.team.focus", "to focus team roster")] : []),
527
513
  hint("app.editor.external", "for external editor"),
528
514
  rawKeyHint("/", "for commands"),
529
515
  rawKeyHint("!", "to run bash"),
@@ -546,7 +532,7 @@ export class InteractiveMode {
546
532
  this.ui.addChild(this.chatContainer);
547
533
  this.ui.addChild(this.pendingMessagesContainer);
548
534
  this.ui.addChild(this.statusContainer);
549
- this.renderWidgets(); // Initialize with default spacer
535
+ this.chrome.renderWidgets(); // Initialize with default spacer
550
536
  this.ui.addChild(this.widgetContainerAbove);
551
537
  this.ui.addChild(this.taskPanel);
552
538
  this.ui.addChild(this.editorContainer);
@@ -605,13 +591,13 @@ export class InteractiveMode {
605
591
  }
606
592
  });
607
593
  // Start package update check asynchronously
608
- this.checkForPackageUpdates().then((updates) => {
594
+ checkForPackageUpdates(this.sessionManager.getCwd(), this.settingsManager).then((updates) => {
609
595
  if (updates.length > 0) {
610
596
  this.showPackageUpdateNotification(updates);
611
597
  }
612
598
  });
613
599
  // Check tmux keyboard setup asynchronously
614
- this.checkTmuxKeyboardSetup().then((warning) => {
600
+ checkTmuxKeyboardSetup().then((warning) => {
615
601
  if (warning) {
616
602
  this.showWarning(warning);
617
603
  }
@@ -662,98 +648,6 @@ export class InteractiveMode {
662
648
  }
663
649
  }
664
650
  }
665
- async checkForPackageUpdates() {
666
- if (process.env.HOOCODE_OFFLINE ?? process.env.PI_OFFLINE) {
667
- return [];
668
- }
669
- try {
670
- const packageManager = new DefaultPackageManager({
671
- cwd: this.sessionManager.getCwd(),
672
- agentDir: getAgentDir(),
673
- settingsManager: this.settingsManager,
674
- });
675
- const updates = await packageManager.checkForAvailableUpdates();
676
- return updates.map((update) => update.displayName);
677
- }
678
- catch {
679
- return [];
680
- }
681
- }
682
- async checkTmuxKeyboardSetup() {
683
- if (!process.env.TMUX)
684
- return undefined;
685
- const runTmuxShow = (option) => {
686
- return new Promise((resolve) => {
687
- const proc = spawn("tmux", ["show", "-gv", option], {
688
- stdio: ["ignore", "pipe", "ignore"],
689
- });
690
- let stdout = "";
691
- const timer = setTimeout(() => {
692
- proc.kill();
693
- resolve(undefined);
694
- }, 2000);
695
- proc.stdout?.on("data", (data) => {
696
- stdout += data.toString();
697
- });
698
- proc.on("error", () => {
699
- clearTimeout(timer);
700
- resolve(undefined);
701
- });
702
- proc.on("close", (code) => {
703
- clearTimeout(timer);
704
- resolve(code === 0 ? stdout.trim() : undefined);
705
- });
706
- });
707
- };
708
- const [extendedKeys, extendedKeysFormat] = await Promise.all([
709
- runTmuxShow("extended-keys"),
710
- runTmuxShow("extended-keys-format"),
711
- ]);
712
- // If we couldn't query tmux (timeout, sandbox, etc.), don't warn
713
- if (extendedKeys === undefined)
714
- return undefined;
715
- if (extendedKeys !== "on" && extendedKeys !== "always") {
716
- return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
717
- }
718
- if (extendedKeysFormat === "xterm") {
719
- return `tmux extended-keys-format is xterm. ${APP_NAME} works best with csi-u. Add \`set -g extended-keys-format csi-u\` to ~/.tmux.conf and restart tmux.`;
720
- }
721
- return undefined;
722
- }
723
- /**
724
- * Get changelog entries to display on startup.
725
- * Only shows new entries since last seen version, skips for resumed sessions.
726
- */
727
- getChangelogForDisplay() {
728
- // Skip changelog for resumed/continued sessions (already have messages)
729
- if (this.session.state.messages.length > 0) {
730
- return undefined;
731
- }
732
- const lastVersion = this.settingsManager.getLastChangelogVersion();
733
- const changelogPath = getChangelogPath();
734
- const entries = parseChangelog(changelogPath);
735
- if (!lastVersion) {
736
- // Fresh install - record the latest entry's version (not VERSION, which may
737
- // overshoot the latest entry and silently swallow it once it appears).
738
- // Fall back to VERSION only if no entries exist yet.
739
- const seedVersion = entries.length > 0 ? `${entries[0].major}.${entries[0].minor}.${entries[0].patch}` : VERSION;
740
- this.settingsManager.setLastChangelogVersion(seedVersion);
741
- this.reportInstallTelemetry(seedVersion);
742
- return undefined;
743
- }
744
- const newEntries = getNewEntries(entries, lastVersion);
745
- if (newEntries.length > 0) {
746
- const latest = newEntries[0];
747
- const latestVersion = `${latest.major}.${latest.minor}.${latest.patch}`;
748
- this.settingsManager.setLastChangelogVersion(latestVersion);
749
- this.reportInstallTelemetry(latestVersion);
750
- return newEntries.map((e) => e.content).join("\n\n");
751
- }
752
- return undefined;
753
- }
754
- reportInstallTelemetry(_version) {
755
- // Disabled in HooCode fork: this is forked from pi (upstream pi.dev install-telemetry endpoint is not run by this fork).
756
- }
757
651
  getMarkdownThemeWithSettings() {
758
652
  return {
759
653
  ...getMarkdownTheme(),
@@ -763,475 +657,24 @@ export class InteractiveMode {
763
657
  // =========================================================================
764
658
  // Extension System
765
659
  // =========================================================================
766
- formatDisplayPath(p) {
767
- const home = os.homedir();
768
- let result = p;
769
- // Replace home directory with ~
770
- if (result.startsWith(home)) {
771
- result = `~${result.slice(home.length)}`;
772
- }
773
- return result;
774
- }
775
- formatExtensionDisplayPath(path) {
776
- let result = this.formatDisplayPath(path);
777
- result = result.replace(/\/index\.ts$/, "").replace(/\/index\.js$/, "");
778
- return result;
779
- }
780
- formatContextPath(p) {
781
- const cwd = path.resolve(this.sessionManager.getCwd());
782
- const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p);
783
- const relativePath = getCwdRelativePath(absolutePath, cwd);
784
- if (relativePath !== undefined) {
785
- return relativePath;
786
- }
787
- return this.formatDisplayPath(absolutePath);
788
- }
789
660
  getStartupExpansionState() {
790
661
  return this.options.verbose || this.toolOutputExpanded;
791
662
  }
792
- /**
793
- * Get a short path relative to the package root for display.
794
- */
795
- getShortPath(fullPath, sourceInfo) {
796
- const baseDir = sourceInfo?.baseDir;
797
- if (baseDir && this.isPackageSource(sourceInfo)) {
798
- const relativePath = path.relative(path.resolve(baseDir), path.resolve(fullPath));
799
- if (relativePath &&
800
- relativePath !== "." &&
801
- !relativePath.startsWith("..") &&
802
- !relativePath.startsWith(`..${path.sep}`) &&
803
- !path.isAbsolute(relativePath)) {
804
- return relativePath.replace(/\\/g, "/");
805
- }
806
- }
807
- const source = sourceInfo?.source ?? "";
808
- const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
809
- if (npmMatch && source.startsWith("npm:")) {
810
- return npmMatch[2];
811
- }
812
- const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
813
- if (gitMatch && source.startsWith("git:")) {
814
- return gitMatch[1];
815
- }
816
- return this.formatDisplayPath(fullPath);
817
- }
818
- getCompactPathLabel(resourcePath, sourceInfo) {
819
- const shortPath = this.getShortPath(resourcePath, sourceInfo);
820
- const normalizedPath = shortPath.replace(/\\/g, "/");
821
- const segments = normalizedPath.split("/").filter((segment) => segment.length > 0 && segment !== "~");
822
- if (segments.length > 0) {
823
- return segments[segments.length - 1];
824
- }
825
- return shortPath;
826
- }
827
- getCompactPackageSourceLabel(sourceInfo) {
828
- const source = sourceInfo?.source ?? "";
829
- if (source.startsWith("npm:")) {
830
- return source.slice("npm:".length) || source;
831
- }
832
- const gitSource = parseGitUrl(source);
833
- if (gitSource) {
834
- return gitSource.path || source;
835
- }
836
- return source;
837
- }
838
- getCompactExtensionLabel(resourcePath, sourceInfo) {
839
- if (!this.isPackageSource(sourceInfo)) {
840
- return this.getCompactPathLabel(resourcePath, sourceInfo);
841
- }
842
- const sourceLabel = this.getCompactPackageSourceLabel(sourceInfo);
843
- if (!sourceLabel) {
844
- return this.getCompactPathLabel(resourcePath, sourceInfo);
845
- }
846
- const shortPath = this.getShortPath(resourcePath, sourceInfo).replace(/\\/g, "/");
847
- const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
848
- const parsedPath = path.posix.parse(packagePath);
849
- if (parsedPath.name === "index") {
850
- return !parsedPath.dir || parsedPath.dir === "." ? sourceLabel : `${sourceLabel}:${parsedPath.dir}`;
851
- }
852
- return `${sourceLabel}:${packagePath}`;
853
- }
854
- getCompactDisplayPathSegments(resourcePath) {
855
- return this.formatDisplayPath(resourcePath)
856
- .replace(/\\/g, "/")
857
- .split("/")
858
- .filter((segment) => segment.length > 0 && segment !== "~");
859
- }
860
- getCompactNonPackageExtensionLabel(resourcePath, index, allPaths) {
861
- const segments = allPaths[index]?.segments;
862
- if (!segments || segments.length === 0) {
863
- return this.getCompactPathLabel(resourcePath);
864
- }
865
- for (let segmentCount = 1; segmentCount <= segments.length; segmentCount += 1) {
866
- const candidate = segments.slice(-segmentCount).join("/");
867
- const isUnique = allPaths.every((item, itemIndex) => {
868
- if (itemIndex === index) {
869
- return true;
870
- }
871
- return item.segments.slice(-segmentCount).join("/") !== candidate;
872
- });
873
- if (isUnique) {
874
- return candidate;
875
- }
876
- }
877
- return segments.join("/");
878
- }
879
- getCompactExtensionLabels(extensions) {
880
- const nonPackageExtensions = extensions
881
- .map((extension) => {
882
- const segments = this.getCompactDisplayPathSegments(extension.path);
883
- const lastSegment = segments[segments.length - 1];
884
- if (segments.length > 1 && (lastSegment === "index.ts" || lastSegment === "index.js")) {
885
- segments.pop();
886
- }
887
- return {
888
- path: extension.path,
889
- sourceInfo: extension.sourceInfo,
890
- segments,
891
- };
892
- })
893
- .filter((extension) => !this.isPackageSource(extension.sourceInfo));
894
- return extensions.map((extension) => {
895
- if (extension.displayName) {
896
- return extension.displayName;
897
- }
898
- if (this.isPackageSource(extension.sourceInfo)) {
899
- return this.getCompactExtensionLabel(extension.path, extension.sourceInfo);
900
- }
901
- const nonPackageIndex = nonPackageExtensions.findIndex((item) => item.path === extension.path);
902
- if (nonPackageIndex === -1) {
903
- return this.getCompactPathLabel(extension.path, extension.sourceInfo);
904
- }
905
- return this.getCompactNonPackageExtensionLabel(extension.path, nonPackageIndex, nonPackageExtensions);
906
- });
907
- }
908
- getDisplaySourceInfo(sourceInfo) {
909
- const source = sourceInfo?.source ?? "local";
910
- const scope = sourceInfo?.scope ?? "project";
911
- if (source === "local") {
912
- if (scope === "user") {
913
- return { label: "user", color: "muted" };
914
- }
915
- if (scope === "project") {
916
- return { label: "project", color: "muted" };
917
- }
918
- if (scope === "temporary") {
919
- return { label: "path", scopeLabel: "temp", color: "muted" };
920
- }
921
- return { label: "path", color: "muted" };
922
- }
923
- if (source === "cli") {
924
- return { label: "path", scopeLabel: scope === "temporary" ? "temp" : undefined, color: "muted" };
925
- }
926
- const scopeLabel = scope === "user" ? "user" : scope === "project" ? "project" : scope === "temporary" ? "temp" : undefined;
927
- return { label: source, scopeLabel, color: "accent" };
928
- }
929
- getScopeGroup(sourceInfo) {
930
- const source = sourceInfo?.source ?? "local";
931
- const scope = sourceInfo?.scope ?? "project";
932
- if (source === "cli" || scope === "temporary")
933
- return "path";
934
- if (scope === "user")
935
- return "user";
936
- if (scope === "project")
937
- return "project";
938
- return "path";
939
- }
940
- isPackageSource(sourceInfo) {
941
- const source = sourceInfo?.source ?? "";
942
- return source.startsWith("npm:") || source.startsWith("git:");
943
- }
944
- buildScopeGroups(items) {
945
- const groups = {
946
- user: { scope: "user", paths: [], packages: new Map() },
947
- project: { scope: "project", paths: [], packages: new Map() },
948
- path: { scope: "path", paths: [], packages: new Map() },
949
- };
950
- for (const item of items) {
951
- const groupKey = this.getScopeGroup(item.sourceInfo);
952
- const group = groups[groupKey];
953
- const source = item.sourceInfo?.source ?? "local";
954
- if (this.isPackageSource(item.sourceInfo)) {
955
- const list = group.packages.get(source) ?? [];
956
- list.push(item);
957
- group.packages.set(source, list);
958
- }
959
- else {
960
- group.paths.push(item);
961
- }
962
- }
963
- return [groups.project, groups.user, groups.path].filter((group) => group.paths.length > 0 || group.packages.size > 0);
964
- }
965
- formatScopeGroups(groups, options) {
966
- const lines = [];
967
- for (const group of groups) {
968
- lines.push(` ${theme.fg("accent", group.scope)}`);
969
- const sortedPaths = [...group.paths].sort((a, b) => a.path.localeCompare(b.path));
970
- for (const item of sortedPaths) {
971
- lines.push(theme.fg("dim", ` ${options.formatPath(item)}`));
972
- }
973
- const sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));
974
- for (const [source, items] of sortedPackages) {
975
- lines.push(` ${theme.fg("mdLink", source)}`);
976
- const sortedPackagePaths = [...items].sort((a, b) => a.path.localeCompare(b.path));
977
- for (const item of sortedPackagePaths) {
978
- lines.push(theme.fg("dim", ` ${options.formatPackagePath(item, source)}`));
979
- }
980
- }
981
- }
982
- return lines.join("\n");
983
- }
984
- findSourceInfoForPath(p, sourceInfos) {
985
- const exact = sourceInfos.get(p);
986
- if (exact)
987
- return exact;
988
- let current = p;
989
- while (current.includes("/")) {
990
- current = current.substring(0, current.lastIndexOf("/"));
991
- const parent = sourceInfos.get(current);
992
- if (parent)
993
- return parent;
994
- }
995
- return undefined;
996
- }
997
- formatPathWithSource(p, sourceInfo) {
998
- if (sourceInfo) {
999
- const shortPath = this.getShortPath(p, sourceInfo);
1000
- const { label, scopeLabel } = this.getDisplaySourceInfo(sourceInfo);
1001
- const labelText = scopeLabel ? `${label} (${scopeLabel})` : label;
1002
- return `${labelText} ${shortPath}`;
1003
- }
1004
- return this.formatDisplayPath(p);
1005
- }
1006
- formatDiagnostics(diagnostics, sourceInfos) {
1007
- const lines = [];
1008
- // Group collision diagnostics by name
1009
- const collisions = new Map();
1010
- const otherDiagnostics = [];
1011
- for (const d of diagnostics) {
1012
- if (d.type === "collision" && d.collision) {
1013
- const list = collisions.get(d.collision.name) ?? [];
1014
- list.push(d);
1015
- collisions.set(d.collision.name, list);
1016
- }
1017
- else {
1018
- otherDiagnostics.push(d);
1019
- }
1020
- }
1021
- // Format collision diagnostics grouped by name
1022
- for (const [name, collisionList] of collisions) {
1023
- const first = collisionList[0]?.collision;
1024
- if (!first)
1025
- continue;
1026
- lines.push(theme.fg("warning", ` "${name}" collision:`));
1027
- lines.push(theme.fg("dim", ` ${theme.fg("success", "✓")} ${this.formatPathWithSource(first.winnerPath, this.findSourceInfoForPath(first.winnerPath, sourceInfos))}`));
1028
- for (const d of collisionList) {
1029
- if (d.collision) {
1030
- lines.push(theme.fg("dim", ` ${theme.fg("warning", "✗")} ${this.formatPathWithSource(d.collision.loserPath, this.findSourceInfoForPath(d.collision.loserPath, sourceInfos))} (skipped)`));
1031
- }
1032
- }
1033
- }
1034
- for (const d of otherDiagnostics) {
1035
- if (d.path) {
1036
- const formattedPath = this.formatPathWithSource(d.path, this.findSourceInfoForPath(d.path, sourceInfos));
1037
- lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${formattedPath}`));
1038
- lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${d.message}`));
1039
- }
1040
- else {
1041
- lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${d.message}`));
1042
- }
1043
- }
1044
- return lines.join("\n");
1045
- }
663
+ /** Render the startup/reload resource listing (see resource-display.ts). */
1046
664
  showLoadedResources(options) {
1047
- const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
1048
- const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
1049
- if (!showListing && !showDiagnostics) {
1050
- return;
1051
- }
1052
- const sectionHeader = (name, color = "mdHeading") => theme.fg(color, `[${name}]`);
1053
- const formatCompactList = (items, options) => {
1054
- const labels = items.map((item) => item.trim()).filter((item) => item.length > 0);
1055
- if (options?.sort !== false) {
1056
- labels.sort((a, b) => a.localeCompare(b));
1057
- }
1058
- return theme.fg("dim", ` ${labels.join(", ")}`);
1059
- };
1060
- const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
1061
- const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
1062
- this.chatContainer.addChild(section);
1063
- this.chatContainer.addChild(new Spacer(1));
1064
- };
1065
- const skillsResult = this.session.resourceLoader.getSkills();
1066
- const promptsResult = this.session.resourceLoader.getPrompts();
1067
- const themesResult = this.session.resourceLoader.getThemes();
1068
- const extensions = options?.extensions ??
1069
- this.session.resourceLoader
1070
- .getExtensions()
1071
- .extensions.filter((extension) => !extension.internal)
1072
- .map((extension) => ({
1073
- path: extension.path,
1074
- sourceInfo: extension.sourceInfo,
1075
- displayName: extension.displayName,
1076
- }));
1077
- const sourceInfos = new Map();
1078
- for (const extension of extensions) {
1079
- if (extension.sourceInfo) {
1080
- sourceInfos.set(extension.path, extension.sourceInfo);
1081
- }
1082
- }
1083
- for (const skill of skillsResult.skills) {
1084
- if (skill.sourceInfo) {
1085
- sourceInfos.set(skill.filePath, skill.sourceInfo);
1086
- }
1087
- }
1088
- for (const prompt of promptsResult.prompts) {
1089
- if (prompt.sourceInfo) {
1090
- sourceInfos.set(prompt.filePath, prompt.sourceInfo);
1091
- }
1092
- }
1093
- for (const loadedTheme of themesResult.themes) {
1094
- if (loadedTheme.sourcePath && loadedTheme.sourceInfo) {
1095
- sourceInfos.set(loadedTheme.sourcePath, loadedTheme.sourceInfo);
1096
- }
1097
- }
1098
- if (showListing) {
1099
- this.chatContainer.addChild(new Spacer(1));
1100
- const { agentsFiles: contextFiles, warnings: contextWarnings } = this.session.resourceLoader.getAgentsFiles();
1101
- const skills = skillsResult.skills;
1102
- const templates = this.session.promptTemplates;
1103
- const loadedThemes = themesResult.themes;
1104
- const customThemes = loadedThemes.filter((t) => t.sourcePath);
1105
- const totalItems = contextFiles.length + skills.length + templates.length + extensions.length + customThemes.length;
1106
- // Meta items: active mode and subagent system prompt (always shown)
1107
- const metaItems = [];
1108
- const rawMode = this.footerDataProvider.getActiveMode().replace(" + subagent", "");
1109
- metaItems.push(`mode/${rawMode}`);
1110
- if (this.footerDataProvider.getSubagentEnabled()) {
1111
- metaItems.push("subagent_system_prompt");
1112
- }
1113
- if (totalItems > 0 && totalItems <= 5) {
1114
- const allCompactItems = [...metaItems];
1115
- if (contextFiles.length > 0) {
1116
- allCompactItems.push(...contextFiles.map((contextFile) => this.formatContextPath(contextFile.path)));
1117
- }
1118
- if (skills.length > 0) {
1119
- allCompactItems.push(...skills.map((skill) => skill.name));
1120
- }
1121
- if (templates.length > 0) {
1122
- allCompactItems.push(...templates.map((template) => `/${template.name}`));
1123
- }
1124
- if (extensions.length > 0) {
1125
- allCompactItems.push(...this.getCompactExtensionLabels(extensions));
1126
- }
1127
- if (customThemes.length > 0) {
1128
- allCompactItems.push(...customThemes.map((loadedTheme) => loadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath, loadedTheme.sourceInfo)));
1129
- }
1130
- this.chatContainer.addChild(new Text(`${theme.fg("mdHeading", "[Resources]")} ${theme.fg("dim", allCompactItems.join(", "))}`, 0, 0));
1131
- }
1132
- else if (totalItems === 0) {
1133
- this.chatContainer.addChild(new Text(`${theme.fg("mdHeading", "[Resources]")} ${theme.fg("dim", metaItems.join(", "))}`, 0, 0));
1134
- }
1135
- else {
1136
- addLoadedSection("Resources", formatCompactList(metaItems), formatCompactList(metaItems));
1137
- if (contextFiles.length > 0) {
1138
- this.chatContainer.addChild(new Spacer(1));
1139
- const contextList = contextFiles
1140
- .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
1141
- .join("\n");
1142
- const contextCompactList = formatCompactList(contextFiles.map((contextFile) => this.formatContextPath(contextFile.path)), { sort: false });
1143
- addLoadedSection("Context", contextCompactList, contextList);
1144
- }
1145
- if (skills.length > 0) {
1146
- const groups = this.buildScopeGroups(skills.map((skill) => ({ path: skill.filePath, sourceInfo: skill.sourceInfo })));
1147
- const skillList = this.formatScopeGroups(groups, {
1148
- formatPath: (item) => this.formatDisplayPath(item.path),
1149
- formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
1150
- });
1151
- const skillCompactList = formatCompactList(skills.map((skill) => skill.name));
1152
- addLoadedSection("Skills", skillCompactList, skillList);
1153
- }
1154
- if (templates.length > 0) {
1155
- const groups = this.buildScopeGroups(templates.map((template) => ({ path: template.filePath, sourceInfo: template.sourceInfo })));
1156
- const templateByPath = new Map(templates.map((t) => [t.filePath, t]));
1157
- const templateList = this.formatScopeGroups(groups, {
1158
- formatPath: (item) => {
1159
- const template = templateByPath.get(item.path);
1160
- return template ? `/${template.name}` : this.formatDisplayPath(item.path);
1161
- },
1162
- formatPackagePath: (item) => {
1163
- const template = templateByPath.get(item.path);
1164
- return template ? `/${template.name}` : this.formatDisplayPath(item.path);
1165
- },
1166
- });
1167
- const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`));
1168
- addLoadedSection("Prompts", promptCompactList, templateList);
1169
- }
1170
- if (extensions.length > 0) {
1171
- const groups = this.buildScopeGroups(extensions);
1172
- const extList = this.formatScopeGroups(groups, {
1173
- formatPath: (item) => item.displayName ?? this.formatExtensionDisplayPath(item.path),
1174
- formatPackagePath: (item) => item.displayName ?? this.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)),
1175
- });
1176
- const extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions));
1177
- addLoadedSection("Extensions", extensionCompactList, extList, "mdHeading");
1178
- }
1179
- if (customThemes.length > 0) {
1180
- const groups = this.buildScopeGroups(customThemes.map((loadedTheme) => ({
1181
- path: loadedTheme.sourcePath,
1182
- sourceInfo: loadedTheme.sourceInfo,
1183
- })));
1184
- const themeList = this.formatScopeGroups(groups, {
1185
- formatPath: (item) => this.formatDisplayPath(item.path),
1186
- formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
1187
- });
1188
- const themeCompactList = formatCompactList(customThemes.map((loadedTheme) => loadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath, loadedTheme.sourceInfo)));
1189
- addLoadedSection("Themes", themeCompactList, themeList);
1190
- }
1191
- }
1192
- if (contextWarnings.length > 0) {
1193
- for (const warning of contextWarnings) {
1194
- this.chatContainer.addChild(new Text(theme.fg("warning", warning), 0, 0));
1195
- }
1196
- }
1197
- }
1198
- if (showDiagnostics) {
1199
- const skillDiagnostics = skillsResult.diagnostics;
1200
- if (skillDiagnostics.length > 0) {
1201
- const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
1202
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1203
- this.chatContainer.addChild(new Spacer(1));
1204
- }
1205
- const promptDiagnostics = promptsResult.diagnostics;
1206
- if (promptDiagnostics.length > 0) {
1207
- const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
1208
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1209
- this.chatContainer.addChild(new Spacer(1));
1210
- }
1211
- const extensionDiagnostics = [];
1212
- const extensionErrors = this.session.resourceLoader.getExtensions().errors;
1213
- if (extensionErrors.length > 0) {
1214
- for (const error of extensionErrors) {
1215
- extensionDiagnostics.push({ type: "error", message: error.error, path: error.path });
1216
- }
1217
- }
1218
- const commandDiagnostics = this.session.extensionRunner.getCommandDiagnostics();
1219
- extensionDiagnostics.push(...commandDiagnostics);
1220
- extensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.session.extensionRunner));
1221
- const shortcutDiagnostics = this.session.extensionRunner.getShortcutDiagnostics();
1222
- extensionDiagnostics.push(...shortcutDiagnostics);
1223
- if (extensionDiagnostics.length > 0) {
1224
- const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
1225
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1226
- this.chatContainer.addChild(new Spacer(1));
1227
- }
1228
- const themeDiagnostics = themesResult.diagnostics;
1229
- if (themeDiagnostics.length > 0) {
1230
- const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
1231
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1232
- this.chatContainer.addChild(new Spacer(1));
1233
- }
1234
- }
665
+ renderLoadedResources({
666
+ chatContainer: this.chatContainer,
667
+ getCwd: () => this.sessionManager.getCwd(),
668
+ getResourceLoader: () => this.session.resourceLoader,
669
+ getPromptTemplates: () => this.session.promptTemplates,
670
+ getExtensionRunner: () => this.session.extensionRunner,
671
+ getActiveMode: () => this.footerDataProvider.getActiveMode(),
672
+ getSubagentEnabled: () => this.footerDataProvider.getSubagentEnabled(),
673
+ quietStartup: () => this.settingsManager.getQuietStartup(),
674
+ verbose: this.options.verbose ?? false,
675
+ isExpanded: () => this.getStartupExpansionState(),
676
+ getBuiltInCommandConflictDiagnostics: (runner) => this.getBuiltInCommandConflictDiagnostics(runner),
677
+ }, options);
1235
678
  }
1236
679
  /**
1237
680
  * Initialize the extension system with TUI-based UI context.
@@ -1468,72 +911,11 @@ export class InteractiveMode {
1468
911
  }
1469
912
  this.ui.requestRender();
1470
913
  }
1471
- /**
1472
- * Set an extension widget (string array or custom component).
1473
- */
1474
- setExtensionWidget(key, content, options) {
1475
- const placement = options?.placement ?? "aboveEditor";
1476
- const removeExisting = (map) => {
1477
- const existing = map.get(key);
1478
- if (existing?.dispose)
1479
- existing.dispose();
1480
- map.delete(key);
1481
- };
1482
- removeExisting(this.extensionWidgetsAbove);
1483
- removeExisting(this.extensionWidgetsBelow);
1484
- if (content === undefined) {
1485
- this.renderWidgets();
1486
- return;
1487
- }
1488
- let component;
1489
- if (Array.isArray(content)) {
1490
- // Wrap string array in a Container with Text components
1491
- const container = new Container();
1492
- for (const line of content.slice(0, InteractiveMode.MAX_WIDGET_LINES)) {
1493
- container.addChild(new Text(line, 1, 0));
1494
- }
1495
- if (content.length > InteractiveMode.MAX_WIDGET_LINES) {
1496
- container.addChild(new Text(theme.fg("muted", "... (widget truncated)"), 1, 0));
1497
- }
1498
- component = container;
1499
- }
1500
- else {
1501
- // Factory function - create component
1502
- component = content(this.ui, theme);
1503
- }
1504
- const targetMap = placement === "belowEditor" ? this.extensionWidgetsBelow : this.extensionWidgetsAbove;
1505
- targetMap.set(key, component);
1506
- this.renderWidgets();
1507
- }
1508
- clearExtensionWidgets() {
1509
- for (const widget of this.extensionWidgetsAbove.values()) {
1510
- widget.dispose?.();
1511
- }
1512
- for (const widget of this.extensionWidgetsBelow.values()) {
1513
- widget.dispose?.();
1514
- }
1515
- this.extensionWidgetsAbove.clear();
1516
- this.extensionWidgetsBelow.clear();
1517
- this.renderWidgets();
1518
- }
1519
914
  resetExtensionUI() {
1520
- if (this.extensionSelector) {
1521
- this.hideExtensionSelector();
1522
- }
1523
- if (this.extensionInput) {
1524
- this.hideExtensionInput();
1525
- }
1526
- if (this.askOptions) {
1527
- this.hideAskOptions();
1528
- }
1529
- if (this.extensionEditor) {
1530
- this.hideExtensionEditor();
1531
- }
915
+ this.dialogs.reset();
1532
916
  this.ui.hideOverlay();
1533
917
  this.clearExtensionTerminalInputListeners();
1534
- this.setExtensionFooter(undefined);
1535
- this.setExtensionHeader(undefined);
1536
- this.clearExtensionWidgets();
918
+ this.chrome.reset();
1537
919
  this.footerDataProvider.clearExtensionStatuses();
1538
920
  this.footer.invalidate();
1539
921
  this.autocompleteProviderWrappers = [];
@@ -1549,101 +931,6 @@ export class InteractiveMode {
1549
931
  }
1550
932
  this.setHiddenThinkingLabel();
1551
933
  }
1552
- // Maximum total widget lines to prevent viewport overflow
1553
- static MAX_WIDGET_LINES = 10;
1554
- /**
1555
- * Render all extension widgets to the widget container.
1556
- */
1557
- renderWidgets() {
1558
- if (!this.widgetContainerAbove || !this.widgetContainerBelow)
1559
- return;
1560
- this.renderWidgetContainer(this.widgetContainerAbove, this.extensionWidgetsAbove, true, true);
1561
- this.renderWidgetContainer(this.widgetContainerBelow, this.extensionWidgetsBelow, false, false);
1562
- this.ui.requestRender();
1563
- }
1564
- renderWidgetContainer(container, widgets, spacerWhenEmpty, leadingSpacer) {
1565
- container.clear();
1566
- if (widgets.size === 0) {
1567
- if (spacerWhenEmpty) {
1568
- container.addChild(new Spacer(1));
1569
- }
1570
- return;
1571
- }
1572
- if (leadingSpacer) {
1573
- container.addChild(new Spacer(1));
1574
- }
1575
- for (const component of widgets.values()) {
1576
- container.addChild(component);
1577
- }
1578
- }
1579
- /**
1580
- * Set a custom footer component, or restore the built-in footer.
1581
- */
1582
- setExtensionFooter(factory) {
1583
- // Dispose existing custom footer
1584
- if (this.customFooter?.dispose) {
1585
- this.customFooter.dispose();
1586
- }
1587
- // Remove current footer from UI
1588
- if (this.customFooter) {
1589
- this.ui.removeChild(this.customFooter);
1590
- }
1591
- else {
1592
- this.ui.removeChild(this.footer);
1593
- }
1594
- if (factory) {
1595
- // Create and add custom footer, passing the data provider
1596
- this.customFooter = factory(this.ui, theme, this.footerDataProvider);
1597
- this.ui.addChild(this.customFooter);
1598
- }
1599
- else {
1600
- // Restore built-in footer
1601
- this.customFooter = undefined;
1602
- this.ui.addChild(this.footer);
1603
- }
1604
- this.ui.requestRender();
1605
- }
1606
- /**
1607
- * Set a custom header component, or restore the built-in header.
1608
- */
1609
- setExtensionHeader(factory) {
1610
- // Header may not be initialized yet if called during early initialization
1611
- if (!this.builtInHeader) {
1612
- return;
1613
- }
1614
- // Dispose existing custom header
1615
- if (this.customHeader?.dispose) {
1616
- this.customHeader.dispose();
1617
- }
1618
- // Find the index of the current header in the header container
1619
- const currentHeader = this.customHeader || this.builtInHeader;
1620
- const index = this.headerContainer.children.indexOf(currentHeader);
1621
- if (factory) {
1622
- // Create and add custom header
1623
- this.customHeader = factory(this.ui, theme);
1624
- if (isExpandable(this.customHeader)) {
1625
- this.customHeader.setExpanded(this.toolOutputExpanded);
1626
- }
1627
- if (index !== -1) {
1628
- this.headerContainer.children[index] = this.customHeader;
1629
- }
1630
- else {
1631
- // If not found (e.g. builtInHeader was never added), add at the top
1632
- this.headerContainer.children.unshift(this.customHeader);
1633
- }
1634
- }
1635
- else {
1636
- // Restore built-in header
1637
- this.customHeader = undefined;
1638
- if (isExpandable(this.builtInHeader)) {
1639
- this.builtInHeader.setExpanded(this.toolOutputExpanded);
1640
- }
1641
- if (index !== -1) {
1642
- this.headerContainer.children[index] = this.builtInHeader;
1643
- }
1644
- }
1645
- this.ui.requestRender();
1646
- }
1647
934
  addExtensionTerminalInputListener(handler) {
1648
935
  const unsubscribe = this.ui.addInputListener(handler);
1649
936
  this.extensionTerminalInputUnsubscribers.add(unsubscribe);
@@ -1663,10 +950,10 @@ export class InteractiveMode {
1663
950
  */
1664
951
  createExtensionUIContext() {
1665
952
  return {
1666
- select: (title, options, opts) => this.showExtensionSelector(title, options, opts),
1667
- confirm: (title, message, opts) => this.showExtensionConfirm(title, message, opts),
1668
- input: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),
1669
- askOptions: (questions, opts) => this.showAskOptions(questions, opts),
953
+ select: (title, options, opts) => this.dialogs.showSelector(title, options, opts),
954
+ confirm: (title, message, opts) => this.dialogs.confirm(title, message, opts),
955
+ input: (title, placeholder, opts) => this.dialogs.showInput(title, placeholder, opts),
956
+ askOptions: (questions, opts) => this.dialogs.showAskOptions(questions, opts),
1670
957
  notify: (message, type) => this.showExtensionNotify(message, type),
1671
958
  onTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),
1672
959
  setStatus: (key, text) => this.setExtensionStatus(key, text),
@@ -1684,15 +971,15 @@ export class InteractiveMode {
1684
971
  setWorkingVisible: (visible) => this.setWorkingVisible(visible),
1685
972
  setWorkingIndicator: (options) => this.setWorkingIndicator(options),
1686
973
  setHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label),
1687
- setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
1688
- setFooter: (factory) => this.setExtensionFooter(factory),
1689
- setHeader: (factory) => this.setExtensionHeader(factory),
974
+ setWidget: (key, content, options) => this.chrome.setWidget(key, content, options),
975
+ setFooter: (factory) => this.chrome.setFooter(factory),
976
+ setHeader: (factory) => this.chrome.setHeader(factory),
1690
977
  setTitle: (title) => this.ui.terminal.setTitle(title),
1691
- custom: (factory, options) => this.showExtensionCustom(factory, options),
978
+ custom: (factory, options) => this.dialogs.showCustom(factory, options),
1692
979
  pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
1693
980
  setEditorText: (text) => this.editor.setText(text),
1694
981
  getEditorText: () => this.editor.getExpandedText?.() ?? this.editor.getText(),
1695
- editor: (title, prefill) => this.showExtensionEditor(title, prefill),
982
+ editor: (title, prefill) => this.dialogs.showEditor(title, prefill),
1696
983
  addAutocompleteProvider: (factory) => {
1697
984
  this.autocompleteProviderWrappers.push(factory);
1698
985
  this.setupAutocompleteProvider();
@@ -1719,170 +1006,13 @@ export class InteractiveMode {
1719
1006
  }
1720
1007
  return result;
1721
1008
  },
1722
- getToolsExpanded: () => this.toolOutputExpanded,
1723
- setToolsExpanded: (expanded) => this.setToolsExpanded(expanded),
1724
- };
1725
- }
1726
- /**
1727
- * Show a selector for extensions.
1728
- */
1729
- showExtensionSelector(title, options, opts) {
1730
- return new Promise((resolve) => {
1731
- if (opts?.signal?.aborted) {
1732
- resolve(undefined);
1733
- return;
1734
- }
1735
- const onAbort = () => {
1736
- this.hideExtensionSelector();
1737
- resolve(undefined);
1738
- };
1739
- opts?.signal?.addEventListener("abort", onAbort, { once: true });
1740
- this.extensionSelector = new ExtensionSelectorComponent(title, options, (option) => {
1741
- opts?.signal?.removeEventListener("abort", onAbort);
1742
- this.hideExtensionSelector();
1743
- resolve(option);
1744
- }, () => {
1745
- opts?.signal?.removeEventListener("abort", onAbort);
1746
- this.hideExtensionSelector();
1747
- resolve(undefined);
1748
- }, { tui: this.ui, timeout: opts?.timeout });
1749
- this.editorContainer.clear();
1750
- this.editorContainer.addChild(this.extensionSelector);
1751
- this.ui.setFocus(this.extensionSelector);
1752
- this.ui.requestRender();
1753
- });
1754
- }
1755
- /**
1756
- * Hide the extension selector.
1757
- */
1758
- hideExtensionSelector() {
1759
- this.extensionSelector?.dispose();
1760
- this.editorContainer.clear();
1761
- this.editorContainer.addChild(this.editor);
1762
- this.extensionSelector = undefined;
1763
- this.ui.setFocus(this.editor);
1764
- this.ui.requestRender();
1765
- }
1766
- /**
1767
- * Show the options pane — the agent asking the user one or more questions.
1768
- * Resolves with one answer per question, or undefined if skipped/aborted.
1769
- */
1770
- showAskOptions(questions, opts) {
1771
- return new Promise((resolve) => {
1772
- if (!questions.length || opts?.signal?.aborted) {
1773
- resolve(undefined);
1774
- return;
1775
- }
1776
- const onAbort = () => {
1777
- this.hideAskOptions();
1778
- resolve(undefined);
1779
- };
1780
- opts?.signal?.addEventListener("abort", onAbort, { once: true });
1781
- this.askOptions = new AskOptionsComponent(questions, (answers) => {
1782
- opts?.signal?.removeEventListener("abort", onAbort);
1783
- this.hideAskOptions();
1784
- resolve(answers);
1785
- }, () => {
1786
- opts?.signal?.removeEventListener("abort", onAbort);
1787
- this.hideAskOptions();
1788
- resolve(undefined);
1789
- });
1790
- this.editorContainer.clear();
1791
- this.editorContainer.addChild(this.askOptions);
1792
- this.ui.setFocus(this.askOptions);
1793
- this.ui.requestRender();
1794
- });
1795
- }
1796
- /**
1797
- * Hide the options pane and restore the editor.
1798
- */
1799
- hideAskOptions() {
1800
- if (!this.askOptions)
1801
- return;
1802
- this.editorContainer.clear();
1803
- this.editorContainer.addChild(this.editor);
1804
- this.askOptions = undefined;
1805
- this.ui.setFocus(this.editor);
1806
- this.ui.requestRender();
1807
- }
1808
- /**
1809
- * Show a confirmation dialog for extensions.
1810
- */
1811
- async showExtensionConfirm(title, message, opts) {
1812
- const result = await this.showExtensionSelector(`${title}\n${message}`, ["Yes", "No"], opts);
1813
- return result === "Yes";
1814
- }
1815
- async promptForMissingSessionCwd(error) {
1816
- const confirmed = await this.showExtensionConfirm("Session cwd not found", formatMissingSessionCwdPrompt(error.issue));
1817
- return confirmed ? error.issue.fallbackCwd : undefined;
1818
- }
1819
- /**
1820
- * Show a text input for extensions.
1821
- */
1822
- showExtensionInput(title, placeholder, opts) {
1823
- return new Promise((resolve) => {
1824
- if (opts?.signal?.aborted) {
1825
- resolve(undefined);
1826
- return;
1827
- }
1828
- const onAbort = () => {
1829
- this.hideExtensionInput();
1830
- resolve(undefined);
1831
- };
1832
- opts?.signal?.addEventListener("abort", onAbort, { once: true });
1833
- this.extensionInput = new ExtensionInputComponent(title, placeholder, (value) => {
1834
- opts?.signal?.removeEventListener("abort", onAbort);
1835
- this.hideExtensionInput();
1836
- resolve(value);
1837
- }, () => {
1838
- opts?.signal?.removeEventListener("abort", onAbort);
1839
- this.hideExtensionInput();
1840
- resolve(undefined);
1841
- }, { tui: this.ui, timeout: opts?.timeout });
1842
- this.editorContainer.clear();
1843
- this.editorContainer.addChild(this.extensionInput);
1844
- this.ui.setFocus(this.extensionInput);
1845
- this.ui.requestRender();
1846
- });
1847
- }
1848
- /**
1849
- * Hide the extension input.
1850
- */
1851
- hideExtensionInput() {
1852
- this.extensionInput?.dispose();
1853
- this.editorContainer.clear();
1854
- this.editorContainer.addChild(this.editor);
1855
- this.extensionInput = undefined;
1856
- this.ui.setFocus(this.editor);
1857
- this.ui.requestRender();
1858
- }
1859
- /**
1860
- * Show a multi-line editor for extensions (with Ctrl+G support).
1861
- */
1862
- showExtensionEditor(title, prefill) {
1863
- return new Promise((resolve) => {
1864
- this.extensionEditor = new ExtensionEditorComponent(this.ui, this.keybindings, title, prefill, (value) => {
1865
- this.hideExtensionEditor();
1866
- resolve(value);
1867
- }, () => {
1868
- this.hideExtensionEditor();
1869
- resolve(undefined);
1870
- });
1871
- this.editorContainer.clear();
1872
- this.editorContainer.addChild(this.extensionEditor);
1873
- this.ui.setFocus(this.extensionEditor);
1874
- this.ui.requestRender();
1875
- });
1876
- }
1877
- /**
1878
- * Hide the extension editor.
1879
- */
1880
- hideExtensionEditor() {
1881
- this.editorContainer.clear();
1882
- this.editorContainer.addChild(this.editor);
1883
- this.extensionEditor = undefined;
1884
- this.ui.setFocus(this.editor);
1885
- this.ui.requestRender();
1009
+ getToolsExpanded: () => this.toolOutputExpanded,
1010
+ setToolsExpanded: (expanded) => this.setToolsExpanded(expanded),
1011
+ };
1012
+ }
1013
+ async promptForMissingSessionCwd(error) {
1014
+ const confirmed = await this.dialogs.confirm("Session cwd not found", formatMissingSessionCwdPrompt(error.issue));
1015
+ return confirmed ? error.issue.fallbackCwd : undefined;
1886
1016
  }
1887
1017
  /**
1888
1018
  * Set a custom editor component from an extension.
@@ -1958,75 +1088,6 @@ export class InteractiveMode {
1958
1088
  this.showStatus(message);
1959
1089
  }
1960
1090
  }
1961
- /** Show a custom component with keyboard focus. Overlay mode renders on top of existing content. */
1962
- async showExtensionCustom(factory, options) {
1963
- const savedText = this.editor.getText();
1964
- const isOverlay = options?.overlay ?? false;
1965
- const restoreEditor = () => {
1966
- this.editorContainer.clear();
1967
- this.editorContainer.addChild(this.editor);
1968
- this.editor.setText(savedText);
1969
- this.ui.setFocus(this.editor);
1970
- this.ui.requestRender();
1971
- };
1972
- return new Promise((resolve, reject) => {
1973
- let component;
1974
- let closed = false;
1975
- const close = (result) => {
1976
- if (closed)
1977
- return;
1978
- closed = true;
1979
- if (isOverlay)
1980
- this.ui.hideOverlay();
1981
- else
1982
- restoreEditor();
1983
- // Note: both branches above already call requestRender
1984
- resolve(result);
1985
- try {
1986
- component?.dispose?.();
1987
- }
1988
- catch {
1989
- /* ignore dispose errors */
1990
- }
1991
- };
1992
- Promise.resolve(factory(this.ui, theme, this.keybindings, close))
1993
- .then((c) => {
1994
- if (closed)
1995
- return;
1996
- component = c;
1997
- if (isOverlay) {
1998
- // Resolve overlay options - can be static or dynamic function
1999
- const resolveOptions = () => {
2000
- if (options?.overlayOptions) {
2001
- const opts = typeof options.overlayOptions === "function"
2002
- ? options.overlayOptions()
2003
- : options.overlayOptions;
2004
- return opts;
2005
- }
2006
- // Fallback: use component's width property if available
2007
- const w = component.width;
2008
- return w ? { width: w } : undefined;
2009
- };
2010
- const handle = this.ui.showOverlay(component, resolveOptions());
2011
- // Expose handle to caller for visibility control
2012
- options?.onHandle?.(handle);
2013
- }
2014
- else {
2015
- this.editorContainer.clear();
2016
- this.editorContainer.addChild(component);
2017
- this.ui.setFocus(component);
2018
- this.ui.requestRender();
2019
- }
2020
- })
2021
- .catch((err) => {
2022
- if (closed)
2023
- return;
2024
- if (!isOverlay)
2025
- restoreEditor();
2026
- reject(err);
2027
- });
2028
- });
2029
- }
2030
1091
  /**
2031
1092
  * Show an extension error in the UI.
2032
1093
  */
@@ -2100,9 +1161,9 @@ export class InteractiveMode {
2100
1161
  this.defaultEditor.onAction("app.tasks.cycleView", () => {
2101
1162
  this.taskPanel.cycleView();
2102
1163
  });
2103
- this.defaultEditor.onAction("app.team.focus", () => this.enterTeamFocus());
1164
+ this.defaultEditor.onAction("app.team.focus", () => this.teamFocus.enterFocus());
2104
1165
  this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
2105
- this.defaultEditor.onAction("app.input.voiceTranscribe", () => this.toggleVoiceTranscribe());
1166
+ this.defaultEditor.onAction("app.input.voiceTranscribe", () => this.voice.toggle());
2106
1167
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
2107
1168
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
2108
1169
  this.defaultEditor.onAction("app.session.new", () => this.commandExecutor.handleClear());
@@ -2147,278 +1208,183 @@ export class InteractiveMode {
2147
1208
  // =========================================================================
2148
1209
  /**
2149
1210
  * Wire a hooteams connection into the TUI. Called by main.ts when `--team`
2150
- * is set, before run(). Enables team focus (app.team.focus), nudging (n),
2151
- * the attach side panel (a) on the task panel's teams view, and approval
2152
- * gates: task_paused events (and gates already pending on the server)
2153
- * surface inline in the attach panel when it shows the paused role,
2154
- * otherwise in the options pane; the answer goes back over
2155
- * POST /tasks/:id/resume.
1211
+ * is set, before run(). See TeamFocusController for the feature itself.
2156
1212
  */
2157
1213
  attachTeamClient(client) {
2158
- this.teamClient = client;
2159
- const approvals = new TeamApprovalCoordinator({
2160
- present: (approval, signal) => this.presentTeamApproval(approval, signal),
2161
- resume: (taskId, option) => client.resume(taskId, option),
2162
- info: (message) => this.showStatus(message),
2163
- warn: (message) => this.showWarning(message),
2164
- });
2165
- client.subscribe((event) => approvals.handleEvent(event));
2166
- // Gates that opened before we attached don't replay as task_paused.
2167
- void client.pendingApprovals().then((pending) => {
2168
- for (const gate of pending)
2169
- approvals.enqueuePending(gate);
2170
- }, () => {
2171
- // Best-effort like the rest of the bridge; live gates still arrive via SSE.
2172
- });
1214
+ this.teamFocus.attachClient(client);
2173
1215
  }
2174
1216
  /**
2175
- * Show one team approval gate and resolve with the chosen (or free-form)
2176
- * answer, undefined when skipped. When the attach side panel is open on the
2177
- * role that paused, the gate renders inline in the panel — right where its
2178
- * stream stopped; otherwise it goes to the options pane, waiting politely
2179
- * while another ask is on screen. Either way the signal (gate answered
2180
- * elsewhere) dismisses the prompt.
1217
+ * Built-in slash commands dispatched by the editor submit handler.
1218
+ * `withArgs` commands also match "/name <args>" and receive the full text.
1219
+ * Each handler keeps its own editor-clearing order (before vs after the
1220
+ * await) some commands must show their UI before the prompt is wiped.
2181
1221
  */
2182
- async presentTeamApproval(approval, signal) {
2183
- const panel = this.teamAttachPanel;
2184
- if (panel && approval.role === panel.role) {
2185
- this.teamAttachHandle?.focus();
2186
- const answer = await panel.presentApproval(approval, signal);
2187
- // Detaching mid-gate settles as skipped — fall through to the options
2188
- // pane so the question isn't silently lost. A skip with the panel
2189
- // still open is a real skip.
2190
- if (answer !== undefined || signal.aborted || this.teamAttachPanel === panel)
2191
- return answer;
2192
- }
2193
- while (this.askOptions && !signal.aborted) {
2194
- await new Promise((resolve) => setTimeout(resolve, 200));
2195
- }
2196
- if (signal.aborted)
2197
- return undefined;
2198
- const answers = await this.showAskOptions([
2199
- {
2200
- question: approval.question,
2201
- short: approval.taskId,
2202
- detail: `team task "${approval.taskId}"${approval.role ? ` (${approval.role})` : ""} is paused until answered`,
2203
- options: approval.options.map((label) => ({ label })),
2204
- allowCustom: true,
1222
+ createBuiltInSlashCommands() {
1223
+ const clearEditor = () => this.editor.setText("");
1224
+ return new Map(Object.entries({
1225
+ "/settings": {
1226
+ run: () => {
1227
+ this.showSettingsSelector();
1228
+ clearEditor();
1229
+ },
2205
1230
  },
2206
- ], { signal });
2207
- return answers?.[0];
2208
- }
2209
- /** Move keyboard focus to the task panel's team roster. */
2210
- enterTeamFocus() {
2211
- if (!this.teamClient) {
2212
- this.showStatus("No team connected. Start with --team <url> to mirror a hooteams server.");
2213
- return;
2214
- }
2215
- if (!taskStore.agents().some((a) => a.kind === "role")) {
2216
- this.showStatus("Team roster is empty — waiting for roles from the team server.");
2217
- return;
2218
- }
2219
- this.taskPanel.setView("teams");
2220
- this.ui.setFocus(this.taskPanel);
2221
- this.ui.requestRender();
2222
- }
2223
- /** Leave team focus: detach any side panel and return focus to the editor. */
2224
- exitTeamFocus() {
2225
- this.closeTeamAttach();
2226
- this.ui.setFocus(this.editor);
2227
- this.ui.requestRender();
2228
- }
2229
- /**
2230
- * Inline nudge editor for a role (n in team focus or while attached).
2231
- * Swaps the prompt editor for a one-line input; submit fires POST /steer in
2232
- * the background (the REPL and team focus are never blocked on the network).
2233
- */
2234
- showTeamNudgeInput(role) {
2235
- const client = this.teamClient;
2236
- if (!client)
2237
- return;
2238
- const restoreFocus = () => {
2239
- this.editorContainer.clear();
2240
- this.editorContainer.addChild(this.editor);
2241
- // Return focus where the nudge came from: the attach panel if one is
2242
- // open, otherwise the team roster.
2243
- if (this.teamAttachHandle)
2244
- this.teamAttachHandle.focus();
2245
- else
2246
- this.ui.setFocus(this.taskPanel);
2247
- this.ui.requestRender();
2248
- };
2249
- const input = new ExtensionInputComponent(`Nudge ${role}`, undefined, (value) => {
2250
- input.dispose();
2251
- restoreFocus();
2252
- const message = value.trim();
2253
- if (!message)
2254
- return;
2255
- void client.steer(role, message).then(() => this.showStatus(`Nudged ${role}`), (error) => this.showWarning(`Failed to nudge ${role}: ${String(error)}`));
2256
- }, () => {
2257
- input.dispose();
2258
- restoreFocus();
2259
- }, { tui: this.ui });
2260
- this.editorContainer.clear();
2261
- this.editorContainer.addChild(input);
2262
- this.ui.setFocus(input);
2263
- this.ui.requestRender();
2264
- }
2265
- /** Open the attach side panel for a role (a in team focus). */
2266
- showTeamAttach(role) {
2267
- const client = this.teamClient;
2268
- if (!client)
2269
- return;
2270
- // One attached role at a time: re-attaching swaps the panel.
2271
- this.closeTeamAttach();
2272
- const panel = new TeamAttachPanelComponent(role, client, {
2273
- onDetach: () => this.closeTeamAttach(),
2274
- onNudge: (attachedRole) => this.showTeamNudgeInput(attachedRole),
2275
- }, this.ui);
2276
- this.teamAttachPanel = panel;
2277
- // preFocus is the task panel (attach is triggered from team focus), so
2278
- // hiding the overlay drops the user back on the role roster.
2279
- this.teamAttachHandle = this.ui.showOverlay(panel, {
2280
- anchor: "top-right",
2281
- width: "45%",
2282
- minWidth: 36,
2283
- margin: { top: 1, right: 1 },
2284
- });
2285
- }
2286
- /** Detach the side panel; the role keeps running. Safe to call when closed. */
2287
- closeTeamAttach() {
2288
- this.teamAttachPanel?.dispose();
2289
- this.teamAttachHandle?.hide();
2290
- this.teamAttachPanel = undefined;
2291
- this.teamAttachHandle = undefined;
1231
+ "/scoped-models": {
1232
+ run: async () => {
1233
+ clearEditor();
1234
+ await this.showModelsSelector();
1235
+ },
1236
+ },
1237
+ "/model": {
1238
+ withArgs: true,
1239
+ run: async (text) => {
1240
+ const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
1241
+ clearEditor();
1242
+ await this.commandExecutor.handleModel(searchTerm);
1243
+ },
1244
+ },
1245
+ "/export": {
1246
+ withArgs: true,
1247
+ run: async (text) => {
1248
+ await this.commandExecutor.handleExport(text);
1249
+ clearEditor();
1250
+ },
1251
+ },
1252
+ "/import": {
1253
+ withArgs: true,
1254
+ run: async (text) => {
1255
+ await this.commandExecutor.handleImport(text);
1256
+ clearEditor();
1257
+ },
1258
+ },
1259
+ "/share": {
1260
+ run: async () => {
1261
+ await this.commandExecutor.handleShare();
1262
+ clearEditor();
1263
+ },
1264
+ },
1265
+ "/copy": {
1266
+ run: async () => {
1267
+ await this.commandExecutor.handleCopy();
1268
+ clearEditor();
1269
+ },
1270
+ },
1271
+ "/name": {
1272
+ withArgs: true,
1273
+ run: (text) => {
1274
+ this.commandExecutor.handleName(text);
1275
+ clearEditor();
1276
+ },
1277
+ },
1278
+ "/session": {
1279
+ run: () => {
1280
+ this.commandExecutor.handleSession();
1281
+ clearEditor();
1282
+ },
1283
+ },
1284
+ "/changelog": {
1285
+ run: () => {
1286
+ this.commandExecutor.handleChangelog();
1287
+ clearEditor();
1288
+ },
1289
+ },
1290
+ "/hotkeys": {
1291
+ run: () => {
1292
+ this.commandExecutor.handleHotkeys();
1293
+ clearEditor();
1294
+ },
1295
+ },
1296
+ "/fork": {
1297
+ run: () => {
1298
+ this.showUserMessageSelector();
1299
+ clearEditor();
1300
+ },
1301
+ },
1302
+ "/clone": {
1303
+ run: async () => {
1304
+ clearEditor();
1305
+ await this.commandExecutor.handleClone();
1306
+ },
1307
+ },
1308
+ "/tree": {
1309
+ run: () => {
1310
+ this.showTreeSelector();
1311
+ clearEditor();
1312
+ },
1313
+ },
1314
+ "/login": {
1315
+ run: () => {
1316
+ this.showOAuthSelector("login");
1317
+ clearEditor();
1318
+ },
1319
+ },
1320
+ "/logout": {
1321
+ run: () => {
1322
+ this.showOAuthSelector("logout");
1323
+ clearEditor();
1324
+ },
1325
+ },
1326
+ "/new": {
1327
+ run: async () => {
1328
+ clearEditor();
1329
+ await this.commandExecutor.handleClear();
1330
+ },
1331
+ },
1332
+ "/compact": {
1333
+ withArgs: true,
1334
+ run: async (text) => {
1335
+ const prefix = "/compact ";
1336
+ const customInstructions = text.startsWith(prefix)
1337
+ ? text.slice(prefix.length).trim() || undefined
1338
+ : undefined;
1339
+ clearEditor();
1340
+ await this.handleCompactCommand(customInstructions);
1341
+ },
1342
+ },
1343
+ "/reload": {
1344
+ run: async () => {
1345
+ clearEditor();
1346
+ await this.handleReloadCommand();
1347
+ },
1348
+ },
1349
+ "/debug": {
1350
+ run: () => {
1351
+ this.commandExecutor.handleDebug();
1352
+ clearEditor();
1353
+ },
1354
+ },
1355
+ "/resume": {
1356
+ run: () => {
1357
+ this.showSessionSelector();
1358
+ clearEditor();
1359
+ },
1360
+ },
1361
+ "/quit": {
1362
+ run: async () => {
1363
+ clearEditor();
1364
+ await this.shutdown();
1365
+ },
1366
+ },
1367
+ "/subagent": {
1368
+ withArgs: true,
1369
+ run: async (text) => {
1370
+ clearEditor();
1371
+ await this.commandExecutor.handleSubagent(text);
1372
+ },
1373
+ },
1374
+ }));
2292
1375
  }
2293
1376
  setupEditorSubmitHandler() {
1377
+ const slashCommands = this.createBuiltInSlashCommands();
2294
1378
  this.defaultEditor.onSubmit = async (text) => {
2295
1379
  text = text.trim();
2296
1380
  if (!text)
2297
1381
  return;
2298
- // Handle commands
2299
- if (text === "/settings") {
2300
- this.showSettingsSelector();
2301
- this.editor.setText("");
2302
- return;
2303
- }
2304
- if (text === "/scoped-models") {
2305
- this.editor.setText("");
2306
- await this.showModelsSelector();
2307
- return;
2308
- }
2309
- if (text === "/model" || text.startsWith("/model ")) {
2310
- const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
2311
- this.editor.setText("");
2312
- await this.commandExecutor.handleModel(searchTerm);
2313
- return;
2314
- }
2315
- if (text === "/export" || text.startsWith("/export ")) {
2316
- await this.commandExecutor.handleExport(text);
2317
- this.editor.setText("");
2318
- return;
2319
- }
2320
- if (text === "/import" || text.startsWith("/import ")) {
2321
- await this.commandExecutor.handleImport(text);
2322
- this.editor.setText("");
2323
- return;
2324
- }
2325
- if (text === "/share") {
2326
- await this.commandExecutor.handleShare();
2327
- this.editor.setText("");
2328
- return;
2329
- }
2330
- if (text === "/copy") {
2331
- await this.commandExecutor.handleCopy();
2332
- this.editor.setText("");
2333
- return;
2334
- }
2335
- if (text === "/name" || text.startsWith("/name ")) {
2336
- this.commandExecutor.handleName(text);
2337
- this.editor.setText("");
2338
- return;
2339
- }
2340
- if (text === "/session") {
2341
- this.commandExecutor.handleSession();
2342
- this.editor.setText("");
2343
- return;
2344
- }
2345
- if (text === "/changelog") {
2346
- this.commandExecutor.handleChangelog();
2347
- this.editor.setText("");
2348
- return;
2349
- }
2350
- if (text === "/hotkeys") {
2351
- this.commandExecutor.handleHotkeys();
2352
- this.editor.setText("");
2353
- return;
2354
- }
2355
- if (text === "/fork") {
2356
- this.showUserMessageSelector();
2357
- this.editor.setText("");
2358
- return;
2359
- }
2360
- if (text === "/clone") {
2361
- this.editor.setText("");
2362
- await this.commandExecutor.handleClone();
2363
- return;
2364
- }
2365
- if (text === "/tree") {
2366
- this.showTreeSelector();
2367
- this.editor.setText("");
2368
- return;
2369
- }
2370
- if (text === "/login") {
2371
- this.showOAuthSelector("login");
2372
- this.editor.setText("");
2373
- return;
2374
- }
2375
- if (text === "/logout") {
2376
- this.showOAuthSelector("logout");
2377
- this.editor.setText("");
2378
- return;
2379
- }
2380
- if (text === "/new") {
2381
- this.editor.setText("");
2382
- await this.commandExecutor.handleClear();
2383
- return;
2384
- }
2385
- if (text === "/compact" || text.startsWith("/compact ")) {
2386
- const prefix = "/compact ";
2387
- const customInstructions = text.startsWith(prefix)
2388
- ? text.slice(prefix.length).trim() || undefined
2389
- : undefined;
2390
- this.editor.setText("");
2391
- await this.handleCompactCommand(customInstructions);
2392
- return;
2393
- }
2394
- if (text === "/reload") {
2395
- this.editor.setText("");
2396
- await this.handleReloadCommand();
2397
- return;
2398
- }
2399
- if (text === "/debug") {
2400
- this.commandExecutor.handleDebug();
2401
- this.editor.setText("");
2402
- return;
2403
- }
2404
- if (text === "/arminsayshi") {
2405
- this.commandExecutor.handleArminSaysHi();
2406
- this.editor.setText("");
2407
- return;
2408
- }
2409
- if (text === "/resume") {
2410
- this.showSessionSelector();
2411
- this.editor.setText("");
2412
- return;
2413
- }
2414
- if (text === "/quit") {
2415
- this.editor.setText("");
2416
- await this.shutdown();
2417
- return;
2418
- }
2419
- if (text === "/subagent" || text.startsWith("/subagent ")) {
2420
- this.editor.setText("");
2421
- await this.commandExecutor.handleSubagent(text);
1382
+ // Handle built-in slash commands
1383
+ const spaceIdx = text.indexOf(" ");
1384
+ const commandName = spaceIdx === -1 ? text : text.slice(0, spaceIdx);
1385
+ const command = slashCommands.get(commandName);
1386
+ if (command && (spaceIdx === -1 || command.withArgs)) {
1387
+ await command.run(text);
2422
1388
  return;
2423
1389
  }
2424
1390
  // Handle bash command (! for normal, !! for excluded from context)
@@ -2778,257 +1744,6 @@ export class InteractiveMode {
2778
1744
  * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
2779
1745
  * we update the previous status line instead of appending new ones to avoid log spam.
2780
1746
  */
2781
- /**
2782
- * Toggle voice-to-text capture.
2783
- *
2784
- * Prefers a persistent `voicetools serve` daemon: the first press probes for
2785
- * support and (if found) loads models once, showing a "warming up" spinner;
2786
- * every later press reuses the already-warm daemon and jumps straight to
2787
- * Listening. Binaries without `serve` support fall back to spawning
2788
- * `voicetools transcribe` per press, same as before. Pressing the shortcut
2789
- * again while listening (or while still warming up) cancels.
2790
- */
2791
- toggleVoiceTranscribe() {
2792
- if (this.voiceActive) {
2793
- if (this.voiceDaemon?.isReady) {
2794
- this.voiceDaemon.cancel();
2795
- }
2796
- else {
2797
- this.voiceSession?.stop();
2798
- }
2799
- this.voiceSession = undefined;
2800
- this.voiceActive = false;
2801
- this.resetVoiceUI();
2802
- return;
2803
- }
2804
- if (this.voiceStarting) {
2805
- // A second press while resolving/warming up: honour the cancel. Any
2806
- // daemon that finishes loading afterwards is kept warm for next time.
2807
- this.voiceStarting = false;
2808
- this.resetVoiceUI();
2809
- return;
2810
- }
2811
- if (this.voiceDaemonUnsupported) {
2812
- this.voiceStarting = true;
2813
- this.showVoiceWarming("Starting voice input...");
2814
- void this.resolveVoiceBin()
2815
- .then((bin) => {
2816
- if (!this.voiceStarting)
2817
- return;
2818
- this.voiceStarting = false;
2819
- if (!bin) {
2820
- this.resetVoiceUI();
2821
- this.showError(VOICE_UNAVAILABLE_MESSAGE);
2822
- return;
2823
- }
2824
- this.beginLegacyVoiceCapture(bin);
2825
- })
2826
- .catch((err) => {
2827
- this.voiceStarting = false;
2828
- this.resetVoiceUI();
2829
- this.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);
2830
- });
2831
- return;
2832
- }
2833
- if (this.voiceDaemon?.isReady) {
2834
- this.beginDaemonVoiceCapture();
2835
- return;
2836
- }
2837
- // No daemon yet: resolve the binary, then probe for `serve` support by
2838
- // spawning it. `VoiceDaemon.spawn` doubles as the probe: it resolves to a
2839
- // live daemon once READY arrives, to "unsupported" if the process exits
2840
- // with no output at all (an old binary rejecting the unrecognized `serve`
2841
- // subcommand), or to "error" if it printed a real ERROR first (e.g. no
2842
- // model installed yet) — already surfaced via onError, so that case skips
2843
- // the legacy fallback (it would just hit the same error) but leaves
2844
- // daemon mode available to retry on the next press.
2845
- this.voiceStarting = true;
2846
- this.showVoiceWarming("Warming up voice input...");
2847
- void this.resolveVoiceBin()
2848
- .then(async (bin) => {
2849
- if (!bin) {
2850
- this.voiceStarting = false;
2851
- this.resetVoiceUI();
2852
- this.showError(VOICE_UNAVAILABLE_MESSAGE);
2853
- return;
2854
- }
2855
- const result = await VoiceDaemon.spawn(bin, this.buildVoiceDaemonHandlers());
2856
- if (!result.ok) {
2857
- if (result.reason === "unsupported") {
2858
- this.voiceDaemonUnsupported = true;
2859
- if (!this.voiceStarting) {
2860
- this.resetVoiceUI();
2861
- return;
2862
- }
2863
- this.voiceStarting = false;
2864
- this.beginLegacyVoiceCapture(bin);
2865
- return;
2866
- }
2867
- this.voiceStarting = false;
2868
- this.resetVoiceUI();
2869
- return;
2870
- }
2871
- this.voiceDaemon = result.daemon;
2872
- if (!this.voiceStarting) {
2873
- // Cancelled while warming up: keep the loaded daemon warm for next time.
2874
- this.resetVoiceUI();
2875
- return;
2876
- }
2877
- this.voiceStarting = false;
2878
- this.beginDaemonVoiceCapture();
2879
- })
2880
- .catch((err) => {
2881
- this.voiceStarting = false;
2882
- this.resetVoiceUI();
2883
- this.showError(`Voice input failed: ${err instanceof Error ? err.message : String(err)}`);
2884
- });
2885
- }
2886
- /**
2887
- * Build the (stable, reused-across-captures) handlers for the daemon.
2888
- *
2889
- * Every handler bails out once `voiceActive` is false: CANCEL is a soft
2890
- * request (unlike the legacy path's `proc.kill()`, it doesn't sever the
2891
- * pipe), so the daemon can still have a trailing PARTIAL/FINAL/DONE for the
2892
- * just-cancelled capture in flight when the user presses cancel. Without
2893
- * this guard that stale text would land in the editor after the panel had
2894
- * already collapsed.
2895
- *
2896
- * v0.1.4 serve streams `PARTIAL <full growing hypothesis>` (live preview,
2897
- * never committed) and ends with a single `FINAL <complete text>` (the one
2898
- * commit to the editor). `SEGMENT` is still handled for the legacy
2899
- * transcribe path and any binary that streams committed chunks directly.
2900
- */
2901
- buildVoiceDaemonHandlers() {
2902
- return {
2903
- onSegment: (text) => {
2904
- if (!this.voiceActive)
2905
- return;
2906
- this.commitVoiceText(text);
2907
- },
2908
- onPartial: (text) => {
2909
- if (!this.voiceActive)
2910
- return;
2911
- this.voicePanel?.setPartial(text);
2912
- },
2913
- onFinal: (text) => {
2914
- if (!this.voiceActive)
2915
- return;
2916
- this.commitVoiceText(text);
2917
- },
2918
- onStatus: (status) => {
2919
- if (!this.voiceActive)
2920
- return;
2921
- if (status === "done") {
2922
- this.voiceActive = false;
2923
- this.resetVoiceUI();
2924
- return;
2925
- }
2926
- if (status === "transcribing") {
2927
- this.voicePanel?.setTranscribing();
2928
- }
2929
- else if (status === "listening") {
2930
- this.voicePanel?.startListening();
2931
- }
2932
- },
2933
- onLevel: (rms) => {
2934
- if (!this.voiceActive)
2935
- return;
2936
- this.voicePanel?.pushLevel(rms);
2937
- },
2938
- onPhase: (phase) => {
2939
- if (!this.voiceActive)
2940
- return;
2941
- if (phase === "silence") {
2942
- this.voicePanel?.beginSilence(VOICE_SILENCE_MS);
2943
- }
2944
- else {
2945
- this.voicePanel?.endSilence();
2946
- }
2947
- },
2948
- onError: (message) => {
2949
- this.voiceActive = false;
2950
- this.resetVoiceUI();
2951
- this.showError(`Voice input failed: ${message}`);
2952
- },
2953
- onCrash: (message) => {
2954
- this.voiceActive = false;
2955
- this.voiceDaemon = undefined;
2956
- this.resetVoiceUI();
2957
- this.showError(`Voice input daemon crashed: ${message}. It will restart on next use.`);
2958
- },
2959
- };
2960
- }
2961
- /** Inject decoded text into the editor via bracketed paste (with a trailing space). */
2962
- commitVoiceText(text) {
2963
- this.editor.handleInput(`\x1b[200~${text} \x1b[201~`);
2964
- }
2965
- beginDaemonVoiceCapture() {
2966
- if (!this.voiceDaemon?.isReady)
2967
- return;
2968
- this.voiceActive = true;
2969
- this.showVoicePanel().startListening();
2970
- this.voiceDaemon.startCapture();
2971
- }
2972
- beginLegacyVoiceCapture(bin) {
2973
- this.voiceActive = true;
2974
- const panel = this.showVoicePanel();
2975
- panel.startListening();
2976
- this.voiceSession = startVoiceTranscribe(bin, {
2977
- onStatus: (status) => {
2978
- if (status === "done") {
2979
- this.voiceActive = false;
2980
- this.voiceSession = undefined;
2981
- this.resetVoiceUI();
2982
- return;
2983
- }
2984
- // Old binaries emit no LEVEL/PARTIAL, so the panel shows a spinner
2985
- // for the batch phases; committed words still stream into the editor.
2986
- if (status === "transcribing")
2987
- panel.setTranscribing();
2988
- },
2989
- onSegment: (text) => {
2990
- this.commitVoiceText(text);
2991
- },
2992
- onError: (message) => {
2993
- this.voiceActive = false;
2994
- this.voiceSession = undefined;
2995
- this.resetVoiceUI();
2996
- this.showError(`Voice input failed: ${message}`);
2997
- },
2998
- });
2999
- }
3000
- /** Resolve the `voicetools` binary path. Prefers an explicit VOICETOOLS_BIN
3001
- * override, otherwise resolves via the managed tools manager (bin dir / PATH /
3002
- * download from the published release). Returns undefined when unavailable.
3003
- */
3004
- async resolveVoiceBin() {
3005
- const override = process.env.VOICETOOLS_BIN?.trim();
3006
- if (override)
3007
- return override;
3008
- return ensureTool("voicetools", true);
3009
- }
3010
- /** Create (or reuse) the voice panel and mount it in the status container. */
3011
- showVoicePanel() {
3012
- if (!this.voicePanel) {
3013
- this.statusContainer.clear();
3014
- this.voicePanel = new VoicePanel(this.ui, keyHint("app.input.voiceTranscribe", "cancel"));
3015
- this.statusContainer.addChild(this.voicePanel);
3016
- }
3017
- this.ui.requestRender();
3018
- return this.voicePanel;
3019
- }
3020
- showVoiceWarming(message) {
3021
- this.showVoicePanel().setWarming(message);
3022
- }
3023
- /** Collapse the voice panel back to nothing (idle) and stop its animation. */
3024
- resetVoiceUI() {
3025
- if (this.voicePanel) {
3026
- this.voicePanel.dispose();
3027
- this.voicePanel = undefined;
3028
- }
3029
- this.statusContainer.clear();
3030
- this.ui.requestRender();
3031
- }
3032
1747
  showStatus(message) {
3033
1748
  const children = this.chatContainer.children;
3034
1749
  const last = children.length > 0 ? children[children.length - 1] : undefined;
@@ -3246,7 +1961,7 @@ export class InteractiveMode {
3246
1961
  // This prevents escape sequences from leaking to the parent shell over slow SSH.
3247
1962
  await this.ui.terminal.drainInput(1000);
3248
1963
  this.taskStoreUnsubscribe?.();
3249
- this.closeTeamAttach();
1964
+ this.teamFocus.closeAttach();
3250
1965
  this.taskPanel.dispose();
3251
1966
  this.stop();
3252
1967
  await this.runtimeHost.dispose();
@@ -3430,7 +2145,7 @@ export class InteractiveMode {
3430
2145
  }
3431
2146
  setToolsExpanded(expanded) {
3432
2147
  this.toolOutputExpanded = expanded;
3433
- const activeHeader = this.customHeader ?? this.builtInHeader;
2148
+ const activeHeader = this.chrome.customHeader ?? this.builtInHeader;
3434
2149
  if (isExpandable(activeHeader)) {
3435
2150
  activeHeader.setExpanded(expanded);
3436
2151
  }
@@ -3931,7 +2646,6 @@ export class InteractiveMode {
3931
2646
  done();
3932
2647
  this.showStatus(`Model: ${model.id}`);
3933
2648
  void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
3934
- this.checkDaxnutsEasterEgg(model);
3935
2649
  }
3936
2650
  catch (error) {
3937
2651
  done();
@@ -4066,7 +2780,7 @@ export class InteractiveMode {
4066
2780
  // Check if we should skip the prompt (user preference to always default to no summary)
4067
2781
  if (!this.settingsManager.getBranchSummarySkipPrompt()) {
4068
2782
  while (true) {
4069
- const summaryChoice = await this.showExtensionSelector("Summarize branch?", [
2783
+ const summaryChoice = await this.dialogs.showSelector("Summarize branch?", [
4070
2784
  "No summary",
4071
2785
  "Summarize",
4072
2786
  "Summarize with custom prompt",
@@ -4078,7 +2792,7 @@ export class InteractiveMode {
4078
2792
  }
4079
2793
  wantsSummary = summaryChoice !== "No summary";
4080
2794
  if (summaryChoice === "Summarize with custom prompt") {
4081
- customInstructions = await this.showExtensionEditor("Custom summarization instructions");
2795
+ customInstructions = await this.dialogs.showEditor("Custom summarization instructions");
4082
2796
  if (customInstructions === undefined) {
4083
2797
  // User cancelled - loop back to summary selector
4084
2798
  continue;
@@ -4363,7 +3077,6 @@ export class InteractiveMode {
4363
3077
  if (selectedModel) {
4364
3078
  this.showStatus(`${actionLabel}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`);
4365
3079
  void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
4366
- this.checkDaxnutsEasterEgg(selectedModel);
4367
3080
  }
4368
3081
  else {
4369
3082
  this.showStatus(`${actionLabel}. Credentials saved to ${getAuthPath()}`);
@@ -4562,7 +3275,7 @@ export class InteractiveMode {
4562
3275
  try {
4563
3276
  await this.session.reload();
4564
3277
  this.keybindings.reload();
4565
- const activeHeader = this.customHeader ?? this.builtInHeader;
3278
+ const activeHeader = this.chrome.customHeader ?? this.builtInHeader;
4566
3279
  if (isExpandable(activeHeader)) {
4567
3280
  activeHeader.setExpanded(this.toolOutputExpanded);
4568
3281
  }
@@ -4609,16 +3322,6 @@ export class InteractiveMode {
4609
3322
  getAppKeyDisplay(action) {
4610
3323
  return keyDisplayText(action);
4611
3324
  }
4612
- handleDaxnuts() {
4613
- this.chatContainer.addChild(new Spacer(1));
4614
- this.chatContainer.addChild(new DaxnutsComponent(this.ui));
4615
- this.ui.requestRender();
4616
- }
4617
- checkDaxnutsEasterEgg(model) {
4618
- if (model.provider === "opencode" && model.id.toLowerCase().includes("kimi-k2.5")) {
4619
- this.handleDaxnuts();
4620
- }
4621
- }
4622
3325
  async handleBashCommand(command, excludeFromContext = false) {
4623
3326
  const extensionRunner = this.session.extensionRunner;
4624
3327
  // Emit user_bash event to let extensions intercept
@@ -4703,18 +3406,7 @@ export class InteractiveMode {
4703
3406
  }
4704
3407
  stop() {
4705
3408
  this.unregisterSignalHandlers();
4706
- if (this.voiceSession?.running) {
4707
- this.voiceSession.stop();
4708
- this.voiceSession = undefined;
4709
- }
4710
- this.voiceDaemon?.shutdown();
4711
- this.voiceDaemon = undefined;
4712
- if (this.voicePanel) {
4713
- this.voicePanel.dispose();
4714
- this.voicePanel = undefined;
4715
- }
4716
- this.voiceActive = false;
4717
- this.voiceStarting = false;
3409
+ this.voice.dispose();
4718
3410
  if (this.settingsManager.getShowTerminalProgress()) {
4719
3411
  this.ui.terminal.setProgress(false);
4720
3412
  }