@draht/coding-agent 2026.3.14 → 2026.3.25-1

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 (345) hide show
  1. package/README.md +45 -30
  2. package/dist/bun/cli.d.ts +3 -0
  3. package/dist/bun/cli.d.ts.map +1 -0
  4. package/dist/bun/cli.js +7 -0
  5. package/dist/bun/cli.js.map +1 -0
  6. package/dist/bun/register-bedrock.d.ts +2 -0
  7. package/dist/bun/register-bedrock.d.ts.map +1 -0
  8. package/dist/bun/register-bedrock.js +4 -0
  9. package/dist/bun/register-bedrock.js.map +1 -0
  10. package/dist/cli/args.d.ts +1 -0
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +11 -6
  13. package/dist/cli/args.js.map +1 -1
  14. package/dist/cli/file-processor.d.ts.map +1 -1
  15. package/dist/cli/file-processor.js +4 -0
  16. package/dist/cli/file-processor.js.map +1 -1
  17. package/dist/cli/initial-message.d.ts +18 -0
  18. package/dist/cli/initial-message.d.ts.map +1 -0
  19. package/dist/cli/initial-message.js +22 -0
  20. package/dist/cli/initial-message.js.map +1 -0
  21. package/dist/cli/session-picker.d.ts.map +1 -1
  22. package/dist/cli/session-picker.js +2 -1
  23. package/dist/cli/session-picker.js.map +1 -1
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +1 -3
  26. package/dist/cli.js.map +1 -1
  27. package/dist/core/agent-session.d.ts +38 -5
  28. package/dist/core/agent-session.d.ts.map +1 -1
  29. package/dist/core/agent-session.js +201 -73
  30. package/dist/core/agent-session.js.map +1 -1
  31. package/dist/core/bash-executor.d.ts +6 -7
  32. package/dist/core/bash-executor.d.ts.map +1 -1
  33. package/dist/core/bash-executor.js +8 -107
  34. package/dist/core/bash-executor.js.map +1 -1
  35. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  36. package/dist/core/compaction/branch-summarization.js +1 -0
  37. package/dist/core/compaction/branch-summarization.js.map +1 -1
  38. package/dist/core/compaction/compaction.d.ts.map +1 -1
  39. package/dist/core/compaction/compaction.js +2 -0
  40. package/dist/core/compaction/compaction.js.map +1 -1
  41. package/dist/core/exec.d.ts.map +1 -1
  42. package/dist/core/exec.js +7 -3
  43. package/dist/core/exec.js.map +1 -1
  44. package/dist/core/export-html/index.d.ts +2 -2
  45. package/dist/core/export-html/index.d.ts.map +1 -1
  46. package/dist/core/export-html/index.js +7 -6
  47. package/dist/core/export-html/index.js.map +1 -1
  48. package/dist/core/export-html/template.css +43 -13
  49. package/dist/core/export-html/template.html +1 -0
  50. package/dist/core/export-html/template.js +107 -0
  51. package/dist/core/export-html/tool-renderer.d.ts +2 -2
  52. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  53. package/dist/core/export-html/tool-renderer.js +41 -16
  54. package/dist/core/export-html/tool-renderer.js.map +1 -1
  55. package/dist/core/extensions/index.d.ts +4 -3
  56. package/dist/core/extensions/index.d.ts.map +1 -1
  57. package/dist/core/extensions/index.js +1 -1
  58. package/dist/core/extensions/index.js.map +1 -1
  59. package/dist/core/extensions/loader.d.ts.map +1 -1
  60. package/dist/core/extensions/loader.js +16 -6
  61. package/dist/core/extensions/loader.js.map +1 -1
  62. package/dist/core/extensions/runner.d.ts +9 -9
  63. package/dist/core/extensions/runner.d.ts.map +1 -1
  64. package/dist/core/extensions/runner.js +89 -71
  65. package/dist/core/extensions/runner.js.map +1 -1
  66. package/dist/core/extensions/types.d.ts +49 -13
  67. package/dist/core/extensions/types.d.ts.map +1 -1
  68. package/dist/core/extensions/types.js.map +1 -1
  69. package/dist/core/extensions/wrapper.d.ts +4 -11
  70. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  71. package/dist/core/extensions/wrapper.js +6 -86
  72. package/dist/core/extensions/wrapper.js.map +1 -1
  73. package/dist/core/footer-data-provider.d.ts +13 -1
  74. package/dist/core/footer-data-provider.d.ts.map +1 -1
  75. package/dist/core/footer-data-provider.js +155 -37
  76. package/dist/core/footer-data-provider.js.map +1 -1
  77. package/dist/core/index.d.ts +2 -1
  78. package/dist/core/index.d.ts.map +1 -1
  79. package/dist/core/index.js +2 -1
  80. package/dist/core/index.js.map +1 -1
  81. package/dist/core/keybindings.d.ts +270 -50
  82. package/dist/core/keybindings.d.ts.map +1 -1
  83. package/dist/core/keybindings.js +222 -134
  84. package/dist/core/keybindings.js.map +1 -1
  85. package/dist/core/model-registry.d.ts +1 -0
  86. package/dist/core/model-registry.d.ts.map +1 -1
  87. package/dist/core/model-registry.js +49 -23
  88. package/dist/core/model-registry.js.map +1 -1
  89. package/dist/core/model-resolver.d.ts +6 -0
  90. package/dist/core/model-resolver.d.ts.map +1 -1
  91. package/dist/core/model-resolver.js +41 -17
  92. package/dist/core/model-resolver.js.map +1 -1
  93. package/dist/core/output-guard.d.ts +6 -0
  94. package/dist/core/output-guard.d.ts.map +1 -0
  95. package/dist/core/output-guard.js +59 -0
  96. package/dist/core/output-guard.js.map +1 -0
  97. package/dist/core/package-manager.d.ts +22 -1
  98. package/dist/core/package-manager.d.ts.map +1 -1
  99. package/dist/core/package-manager.js +373 -53
  100. package/dist/core/package-manager.js.map +1 -1
  101. package/dist/core/prompt-templates.d.ts +2 -1
  102. package/dist/core/prompt-templates.d.ts.map +1 -1
  103. package/dist/core/prompt-templates.js +39 -39
  104. package/dist/core/prompt-templates.js.map +1 -1
  105. package/dist/core/resolve-config-value.d.ts.map +1 -1
  106. package/dist/core/resolve-config-value.js +43 -8
  107. package/dist/core/resolve-config-value.js.map +1 -1
  108. package/dist/core/resource-loader.d.ts +6 -7
  109. package/dist/core/resource-loader.d.ts.map +1 -1
  110. package/dist/core/resource-loader.js +141 -118
  111. package/dist/core/resource-loader.js.map +1 -1
  112. package/dist/core/sdk.d.ts +3 -3
  113. package/dist/core/sdk.d.ts.map +1 -1
  114. package/dist/core/sdk.js +4 -4
  115. package/dist/core/sdk.js.map +1 -1
  116. package/dist/core/session-manager.d.ts +6 -0
  117. package/dist/core/session-manager.d.ts.map +1 -1
  118. package/dist/core/session-manager.js +9 -10
  119. package/dist/core/session-manager.js.map +1 -1
  120. package/dist/core/settings-manager.d.ts +3 -0
  121. package/dist/core/settings-manager.d.ts.map +1 -1
  122. package/dist/core/settings-manager.js +8 -0
  123. package/dist/core/settings-manager.js.map +1 -1
  124. package/dist/core/skills.d.ts +5 -3
  125. package/dist/core/skills.d.ts.map +1 -1
  126. package/dist/core/skills.js +54 -9
  127. package/dist/core/skills.js.map +1 -1
  128. package/dist/core/slash-commands.d.ts +2 -3
  129. package/dist/core/slash-commands.d.ts.map +1 -1
  130. package/dist/core/slash-commands.js +3 -2
  131. package/dist/core/slash-commands.js.map +1 -1
  132. package/dist/core/source-info.d.ts +18 -0
  133. package/dist/core/source-info.d.ts.map +1 -0
  134. package/dist/core/source-info.js +19 -0
  135. package/dist/core/source-info.js.map +1 -0
  136. package/dist/core/system-prompt.d.ts.map +1 -1
  137. package/dist/core/system-prompt.js +17 -60
  138. package/dist/core/system-prompt.js.map +1 -1
  139. package/dist/core/tools/bash.d.ts +24 -6
  140. package/dist/core/tools/bash.d.ts.map +1 -1
  141. package/dist/core/tools/bash.js +210 -110
  142. package/dist/core/tools/bash.js.map +1 -1
  143. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  144. package/dist/core/tools/edit-diff.js +1 -0
  145. package/dist/core/tools/edit-diff.js.map +1 -1
  146. package/dist/core/tools/edit.d.ts +14 -2
  147. package/dist/core/tools/edit.d.ts.map +1 -1
  148. package/dist/core/tools/edit.js +95 -23
  149. package/dist/core/tools/edit.js.map +1 -1
  150. package/dist/core/tools/file-mutation-queue.d.ts +6 -0
  151. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -0
  152. package/dist/core/tools/file-mutation-queue.js +37 -0
  153. package/dist/core/tools/file-mutation-queue.js.map +1 -0
  154. package/dist/core/tools/find.d.ts +11 -4
  155. package/dist/core/tools/find.d.ts.map +1 -1
  156. package/dist/core/tools/find.js +82 -30
  157. package/dist/core/tools/find.js.map +1 -1
  158. package/dist/core/tools/grep.d.ts +15 -4
  159. package/dist/core/tools/grep.d.ts.map +1 -1
  160. package/dist/core/tools/grep.js +83 -29
  161. package/dist/core/tools/grep.js.map +1 -1
  162. package/dist/core/tools/index.d.ts +58 -19
  163. package/dist/core/tools/index.d.ts.map +1 -1
  164. package/dist/core/tools/index.js +51 -26
  165. package/dist/core/tools/index.js.map +1 -1
  166. package/dist/core/tools/ls.d.ts +9 -3
  167. package/dist/core/tools/ls.d.ts.map +1 -1
  168. package/dist/core/tools/ls.js +67 -13
  169. package/dist/core/tools/ls.js.map +1 -1
  170. package/dist/core/tools/read.d.ts +10 -3
  171. package/dist/core/tools/read.d.ts.map +1 -1
  172. package/dist/core/tools/read.js +110 -51
  173. package/dist/core/tools/read.js.map +1 -1
  174. package/dist/core/tools/render-utils.d.ts +21 -0
  175. package/dist/core/tools/render-utils.d.ts.map +1 -0
  176. package/dist/core/tools/render-utils.js +49 -0
  177. package/dist/core/tools/render-utils.js.map +1 -0
  178. package/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
  179. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -0
  180. package/dist/core/tools/tool-definition-wrapper.js +30 -0
  181. package/dist/core/tools/tool-definition-wrapper.js.map +1 -0
  182. package/dist/core/tools/write.d.ts +9 -3
  183. package/dist/core/tools/write.d.ts.map +1 -1
  184. package/dist/core/tools/write.js +168 -30
  185. package/dist/core/tools/write.js.map +1 -1
  186. package/dist/index.d.ts +5 -4
  187. package/dist/index.d.ts.map +1 -1
  188. package/dist/index.js +4 -3
  189. package/dist/index.js.map +1 -1
  190. package/dist/main.d.ts.map +1 -1
  191. package/dist/main.js +105 -226
  192. package/dist/main.js.map +1 -1
  193. package/dist/modes/interactive/components/bash-execution.d.ts +0 -1
  194. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  195. package/dist/modes/interactive/components/bash-execution.js +22 -9
  196. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  197. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  198. package/dist/modes/interactive/components/bordered-loader.js +1 -1
  199. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  200. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  201. package/dist/modes/interactive/components/branch-summary-message.js +2 -2
  202. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  203. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  204. package/dist/modes/interactive/components/compaction-summary-message.js +2 -2
  205. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  206. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  207. package/dist/modes/interactive/components/config-selector.js +8 -8
  208. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  209. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  210. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  211. package/dist/modes/interactive/components/custom-editor.js +6 -6
  212. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  213. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  214. package/dist/modes/interactive/components/extension-editor.js +9 -9
  215. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  216. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  217. package/dist/modes/interactive/components/extension-input.js +5 -5
  218. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  219. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  220. package/dist/modes/interactive/components/extension-selector.js +8 -8
  221. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  222. package/dist/modes/interactive/components/index.d.ts +1 -1
  223. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  224. package/dist/modes/interactive/components/index.js +1 -1
  225. package/dist/modes/interactive/components/index.js.map +1 -1
  226. package/dist/modes/interactive/components/keybinding-hints.d.ts +3 -36
  227. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  228. package/dist/modes/interactive/components/keybinding-hints.js +5 -44
  229. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  230. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  231. package/dist/modes/interactive/components/login-dialog.js +6 -6
  232. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  233. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  234. package/dist/modes/interactive/components/model-selector.js +13 -9
  235. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  236. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  237. package/dist/modes/interactive/components/oauth-selector.js +6 -6
  238. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  239. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  240. package/dist/modes/interactive/components/scoped-models-selector.js +4 -4
  241. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  242. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  243. package/dist/modes/interactive/components/session-selector.js +32 -35
  244. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  245. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  246. package/dist/modes/interactive/components/settings-selector.js +5 -1
  247. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  248. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  249. package/dist/modes/interactive/components/show-images-selector.js +5 -1
  250. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  251. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  252. package/dist/modes/interactive/components/skill-invocation-message.js +2 -2
  253. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  254. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  255. package/dist/modes/interactive/components/theme-selector.js +5 -1
  256. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  257. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  258. package/dist/modes/interactive/components/thinking-selector.js +5 -1
  259. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  260. package/dist/modes/interactive/components/tool-execution.d.ts +16 -34
  261. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  262. package/dist/modes/interactive/components/tool-execution.js +128 -636
  263. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  264. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  265. package/dist/modes/interactive/components/tree-selector.js +27 -16
  266. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  267. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  268. package/dist/modes/interactive/components/user-message-selector.js +6 -6
  269. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  270. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  271. package/dist/modes/interactive/components/user-message.js +2 -1
  272. package/dist/modes/interactive/components/user-message.js.map +1 -1
  273. package/dist/modes/interactive/interactive-mode.d.ts +7 -11
  274. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  275. package/dist/modes/interactive/interactive-mode.js +353 -214
  276. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  277. package/dist/modes/interactive/theme/theme.d.ts +3 -0
  278. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  279. package/dist/modes/interactive/theme/theme.js +63 -37
  280. package/dist/modes/interactive/theme/theme.js.map +1 -1
  281. package/dist/modes/print-mode.d.ts.map +1 -1
  282. package/dist/modes/print-mode.js +5 -11
  283. package/dist/modes/print-mode.js.map +1 -1
  284. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  285. package/dist/modes/rpc/rpc-mode.js +27 -17
  286. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  287. package/dist/modes/rpc/rpc-types.d.ts +3 -4
  288. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  289. package/dist/modes/rpc/rpc-types.js.map +1 -1
  290. package/dist/utils/child-process.d.ts +11 -0
  291. package/dist/utils/child-process.d.ts.map +1 -0
  292. package/dist/utils/child-process.js +78 -0
  293. package/dist/utils/child-process.js.map +1 -0
  294. package/dist/utils/clipboard-image.d.ts.map +1 -1
  295. package/dist/utils/clipboard-image.js +94 -11
  296. package/dist/utils/clipboard-image.js.map +1 -1
  297. package/dist/utils/clipboard-native.d.ts +1 -0
  298. package/dist/utils/clipboard-native.d.ts.map +1 -1
  299. package/dist/utils/clipboard-native.js.map +1 -1
  300. package/dist/utils/clipboard.d.ts +1 -1
  301. package/dist/utils/clipboard.d.ts.map +1 -1
  302. package/dist/utils/clipboard.js +27 -16
  303. package/dist/utils/clipboard.js.map +1 -1
  304. package/dist/utils/exif-orientation.d.ts +5 -0
  305. package/dist/utils/exif-orientation.d.ts.map +1 -0
  306. package/dist/utils/exif-orientation.js +158 -0
  307. package/dist/utils/exif-orientation.js.map +1 -0
  308. package/dist/utils/image-convert.d.ts.map +1 -1
  309. package/dist/utils/image-convert.js +5 -1
  310. package/dist/utils/image-convert.js.map +1 -1
  311. package/dist/utils/image-resize.d.ts +5 -5
  312. package/dist/utils/image-resize.d.ts.map +1 -1
  313. package/dist/utils/image-resize.js +51 -95
  314. package/dist/utils/image-resize.js.map +1 -1
  315. package/dist/utils/tools-manager.d.ts.map +1 -1
  316. package/dist/utils/tools-manager.js +5 -4
  317. package/dist/utils/tools-manager.js.map +1 -1
  318. package/docs/custom-provider.md +6 -2
  319. package/docs/extensions.md +108 -21
  320. package/docs/keybindings.md +103 -112
  321. package/docs/models.md +39 -1
  322. package/docs/packages.md +9 -0
  323. package/docs/providers.md +7 -0
  324. package/docs/rpc.md +15 -6
  325. package/docs/sdk.md +2 -2
  326. package/docs/settings.md +9 -0
  327. package/docs/terminal-setup.md +11 -0
  328. package/docs/tui.md +2 -2
  329. package/examples/extensions/README.md +2 -2
  330. package/examples/extensions/antigravity-image-gen.ts +5 -3
  331. package/examples/extensions/built-in-tool-renderer.ts +8 -8
  332. package/examples/extensions/commands.ts +3 -3
  333. package/examples/extensions/minimal-mode.ts +14 -14
  334. package/examples/extensions/question.ts +2 -2
  335. package/examples/extensions/questionnaire.ts +2 -2
  336. package/examples/extensions/subagent/index.ts +30 -8
  337. package/examples/extensions/titlebar-spinner.ts +2 -2
  338. package/examples/extensions/todo.ts +2 -2
  339. package/examples/extensions/tool-override.ts +9 -7
  340. package/examples/extensions/truncated-tool.ts +8 -5
  341. package/examples/sdk/04-skills.ts +8 -2
  342. package/examples/sdk/08-prompt-templates.ts +8 -2
  343. package/examples/sdk/12-full-control.ts +0 -1
  344. package/examples/sdk/README.md +1 -1
  345. package/package.json +4 -4
@@ -6,20 +6,21 @@ 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 { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@draht/tui";
9
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@draht/tui";
10
10
  import { spawn, spawnSync } from "child_process";
11
- import { APP_NAME, getAuthPath, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
11
+ import { APP_NAME, getAgentDir, getAuthPath, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
12
12
  import { parseSkillBlock } from "../../core/agent-session.js";
13
13
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
14
14
  import { KeybindingsManager } from "../../core/keybindings.js";
15
15
  import { createCompactionSummaryMessage } from "../../core/messages.js";
16
- import { resolveModelScope } from "../../core/model-resolver.js";
16
+ import { findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
17
+ import { DefaultPackageManager } from "../../core/package-manager.js";
17
18
  import { SessionManager } from "../../core/session-manager.js";
18
19
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
19
20
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
20
21
  import { copyToClipboard } from "../../utils/clipboard.js";
21
22
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
22
- import { sendTerminalNotification } from "../../utils/notify.js";
23
+ import { parseGitUrl } from "../../utils/git.js";
23
24
  import { ensureTool } from "../../utils/tools-manager.js";
24
25
  import { ArminComponent } from "./components/armin.js";
25
26
  import { AssistantMessageComponent } from "./components/assistant-message.js";
@@ -35,7 +36,7 @@ import { ExtensionEditorComponent } from "./components/extension-editor.js";
35
36
  import { ExtensionInputComponent } from "./components/extension-input.js";
36
37
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
37
38
  import { FooterComponent } from "./components/footer.js";
38
- import { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./components/keybinding-hints.js";
39
+ import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
39
40
  import { LoginDialogComponent } from "./components/login-dialog.js";
40
41
  import { ModelSelectorComponent } from "./components/model-selector.js";
41
42
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
@@ -65,6 +66,7 @@ export class InteractiveMode {
65
66
  editorContainer;
66
67
  footer;
67
68
  footerDataProvider;
69
+ // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
68
70
  keybindings;
69
71
  version;
70
72
  isInitialized = false;
@@ -148,6 +150,7 @@ export class InteractiveMode {
148
150
  this.widgetContainerAbove = new Container();
149
151
  this.widgetContainerBelow = new Container();
150
152
  this.keybindings = KeybindingsManager.create();
153
+ setKeybindings(this.keybindings);
151
154
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
152
155
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
153
156
  this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
@@ -166,6 +169,48 @@ export class InteractiveMode {
166
169
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
167
170
  initTheme(this.settingsManager.getTheme(), true);
168
171
  }
172
+ getAutocompleteSourceTag(sourceInfo) {
173
+ if (!sourceInfo) {
174
+ return undefined;
175
+ }
176
+ const scopePrefix = sourceInfo.scope === "user" ? "u" : sourceInfo.scope === "project" ? "p" : "t";
177
+ const source = sourceInfo.source.trim();
178
+ if (source === "auto" || source === "local" || source === "cli") {
179
+ return scopePrefix;
180
+ }
181
+ if (source.startsWith("npm:")) {
182
+ return `${scopePrefix}:${source}`;
183
+ }
184
+ const gitSource = parseGitUrl(source);
185
+ if (gitSource) {
186
+ const ref = gitSource.ref ? `@${gitSource.ref}` : "";
187
+ return `${scopePrefix}:git:${gitSource.host}/${gitSource.path}${ref}`;
188
+ }
189
+ return scopePrefix;
190
+ }
191
+ prefixAutocompleteDescription(description, sourceInfo) {
192
+ const sourceTag = this.getAutocompleteSourceTag(sourceInfo);
193
+ if (!sourceTag) {
194
+ return description;
195
+ }
196
+ return description ? `[${sourceTag}] ${description}` : `[${sourceTag}]`;
197
+ }
198
+ getBuiltInCommandConflictDiagnostics(extensionRunner) {
199
+ if (!extensionRunner) {
200
+ return [];
201
+ }
202
+ const builtinNames = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
203
+ return extensionRunner
204
+ .getRegisteredCommands()
205
+ .filter((command) => builtinNames.has(command.name))
206
+ .map((command) => ({
207
+ type: "warning",
208
+ message: command.invocationName === command.name
209
+ ? `Extension command '/${command.name}' conflicts with built-in interactive command. Skipping in autocomplete.`
210
+ : `Extension command '/${command.name}' conflicts with built-in interactive command. Available as '/${command.invocationName}'.`,
211
+ path: command.sourceInfo.path,
212
+ }));
213
+ }
169
214
  setupAutocomplete(fdPath) {
170
215
  // Define commands for autocomplete
171
216
  const slashCommands = BUILTIN_SLASH_COMMANDS.map((command) => ({
@@ -201,13 +246,13 @@ export class InteractiveMode {
201
246
  // Convert prompt templates to SlashCommand format for autocomplete
202
247
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
203
248
  name: cmd.name,
204
- description: cmd.description,
249
+ description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
205
250
  }));
206
251
  // Convert extension commands to SlashCommand format
207
252
  const builtinCommandNames = new Set(slashCommands.map((c) => c.name));
208
- const extensionCommands = (this.session.extensionRunner?.getRegisteredCommands(builtinCommandNames) ?? []).map((cmd) => ({
209
- name: cmd.name,
210
- description: cmd.description ?? "(extension command)",
253
+ const extensionCommands = (this.session.extensionRunner?.getRegisteredCommands().filter((cmd) => !builtinCommandNames.has(cmd.name)) ?? []).map((cmd) => ({
254
+ name: cmd.invocationName,
255
+ description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
211
256
  getArgumentCompletions: cmd.getArgumentCompletions,
212
257
  }));
213
258
  // Build skill commands from session.skills (if enabled)
@@ -217,7 +262,10 @@ export class InteractiveMode {
217
262
  for (const skill of this.session.resourceLoader.getSkills().skills) {
218
263
  const commandName = `skill:${skill.name}`;
219
264
  this.skillCommands.set(commandName, skill.filePath);
220
- skillCommandList.push({ name: commandName, description: skill.description });
265
+ skillCommandList.push({
266
+ name: commandName,
267
+ description: this.prefixAutocompleteDescription(skill.description, skill.sourceInfo),
268
+ });
221
269
  }
222
270
  }
223
271
  // Setup autocomplete
@@ -242,27 +290,26 @@ export class InteractiveMode {
242
290
  if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
243
291
  const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
244
292
  // Build startup instructions using keybinding hint helpers
245
- const kb = this.keybindings;
246
- const hint = (action, desc) => appKeyHint(kb, action, desc);
293
+ const hint = (keybinding, description) => keyHint(keybinding, description);
247
294
  const instructions = [
248
- hint("interrupt", "to interrupt"),
249
- hint("clear", "to clear"),
250
- rawKeyHint(`${appKey(kb, "clear")} twice`, "to exit"),
251
- hint("exit", "to exit (empty)"),
252
- hint("suspend", "to suspend"),
253
- keyHint("deleteToLineEnd", "to delete to end"),
254
- hint("cycleThinkingLevel", "to cycle thinking level"),
255
- rawKeyHint(`${appKey(kb, "cycleModelForward")}/${appKey(kb, "cycleModelBackward")}`, "to cycle models"),
256
- hint("selectModel", "to select model"),
257
- hint("expandTools", "to expand tools"),
258
- hint("toggleThinking", "to expand thinking"),
259
- hint("externalEditor", "for external editor"),
295
+ hint("app.interrupt", "to interrupt"),
296
+ hint("app.clear", "to clear"),
297
+ rawKeyHint(`${keyText("app.clear")} twice`, "to exit"),
298
+ hint("app.exit", "to exit (empty)"),
299
+ hint("app.suspend", "to suspend"),
300
+ keyHint("tui.editor.deleteToLineEnd", "to delete to end"),
301
+ hint("app.thinking.cycle", "to cycle thinking level"),
302
+ rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
303
+ hint("app.model.select", "to select model"),
304
+ hint("app.tools.expand", "to expand tools"),
305
+ hint("app.thinking.toggle", "to expand thinking"),
306
+ hint("app.editor.external", "for external editor"),
260
307
  rawKeyHint("/", "for commands"),
261
308
  rawKeyHint("!", "to run bash"),
262
309
  rawKeyHint("!!", "to run bash (no context)"),
263
- hint("followUp", "to queue follow-up"),
264
- hint("dequeue", "to edit all queued messages"),
265
- hint("pasteImage", "to paste image"),
310
+ hint("app.message.followUp", "to queue follow-up"),
311
+ hint("app.message.dequeue", "to edit all queued messages"),
312
+ hint("app.clipboard.pasteImage", "to paste image"),
266
313
  rawKeyHint("drop files", "to attach"),
267
314
  ].join("\n");
268
315
  this.builtInHeader = new Text(`${logo}\n${instructions}`, 1, 0);
@@ -312,13 +359,13 @@ export class InteractiveMode {
312
359
  this.ui.setFocus(this.editor);
313
360
  this.setupKeyHandlers();
314
361
  this.setupEditorSubmitHandler();
362
+ // Start the UI before initializing extensions so session_start handlers can use interactive dialogs
363
+ this.ui.start();
364
+ this.isInitialized = true;
315
365
  // Initialize extensions first so resources are shown before messages
316
366
  await this.initExtensions();
317
367
  // Render initial messages AFTER showing loaded resources
318
368
  this.renderInitialMessages();
319
- // Start the UI
320
- this.ui.start();
321
- this.isInitialized = true;
322
369
  // Set terminal title
323
370
  this.updateTerminalTitle();
324
371
  // Subscribe to agent events
@@ -343,10 +390,10 @@ export class InteractiveMode {
343
390
  const cwdBasename = path.basename(process.cwd());
344
391
  const sessionName = this.sessionManager.getSessionName();
345
392
  if (sessionName) {
346
- this.ui.terminal.setTitle(`𝗗 - ${sessionName} - ${cwdBasename}`);
393
+ this.ui.terminal.setTitle(`D - ${sessionName} - ${cwdBasename}`);
347
394
  }
348
395
  else {
349
- this.ui.terminal.setTitle(`𝗗 - ${cwdBasename}`);
396
+ this.ui.terminal.setTitle(`D - ${cwdBasename}`);
350
397
  }
351
398
  }
352
399
  /**
@@ -361,6 +408,12 @@ export class InteractiveMode {
361
408
  this.showNewVersionNotification(newVersion);
362
409
  }
363
410
  });
411
+ // Start package update check asynchronously
412
+ this.checkForPackageUpdates().then((updates) => {
413
+ if (updates.length > 0) {
414
+ this.showPackageUpdateNotification(updates);
415
+ }
416
+ });
364
417
  // Check tmux keyboard setup asynchronously
365
418
  this.checkTmuxKeyboardSetup().then((warning) => {
366
419
  if (warning) {
@@ -416,7 +469,7 @@ export class InteractiveMode {
416
469
  * Check npm registry for a newer version.
417
470
  */
418
471
  async checkForNewVersion() {
419
- if (process.env.DRAHT_SKIP_VERSION_CHECK || process.env.DRAHT_OFFLINE)
472
+ if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
420
473
  return undefined;
421
474
  try {
422
475
  const response = await fetch("https://registry.npmjs.org/@draht/coding-agent/latest", {
@@ -435,6 +488,23 @@ export class InteractiveMode {
435
488
  return undefined;
436
489
  }
437
490
  }
491
+ async checkForPackageUpdates() {
492
+ if (process.env.PI_OFFLINE) {
493
+ return [];
494
+ }
495
+ try {
496
+ const packageManager = new DefaultPackageManager({
497
+ cwd: process.cwd(),
498
+ agentDir: getAgentDir(),
499
+ settingsManager: this.settingsManager,
500
+ });
501
+ const updates = await packageManager.checkForAvailableUpdates();
502
+ return updates.map((update) => update.displayName);
503
+ }
504
+ catch {
505
+ return [];
506
+ }
507
+ }
438
508
  async checkTmuxKeyboardSetup() {
439
509
  if (!process.env.TMUX)
440
510
  return undefined;
@@ -465,6 +535,9 @@ export class InteractiveMode {
465
535
  runTmuxShow("extended-keys"),
466
536
  runTmuxShow("extended-keys-format"),
467
537
  ]);
538
+ // If we couldn't query tmux (timeout, sandbox, etc.), don't warn
539
+ if (extendedKeys === undefined)
540
+ return undefined;
468
541
  if (extendedKeys !== "on" && extendedKeys !== "always") {
469
542
  return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
470
543
  }
@@ -520,21 +593,21 @@ export class InteractiveMode {
520
593
  /**
521
594
  * Get a short path relative to the package root for display.
522
595
  */
523
- getShortPath(fullPath, source) {
524
- // For npm packages, show path relative to node_modules/pkg/
596
+ getShortPath(fullPath, sourceInfo) {
597
+ const source = sourceInfo?.source ?? "";
525
598
  const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
526
599
  if (npmMatch && source.startsWith("npm:")) {
527
600
  return npmMatch[2];
528
601
  }
529
- // For git packages, show path relative to repo root
530
602
  const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
531
603
  if (gitMatch && source.startsWith("git:")) {
532
604
  return gitMatch[1];
533
605
  }
534
- // For local/auto, just use formatDisplayPath
535
606
  return this.formatDisplayPath(fullPath);
536
607
  }
537
- getDisplaySourceInfo(source, scope) {
608
+ getDisplaySourceInfo(sourceInfo) {
609
+ const source = sourceInfo?.source ?? "local";
610
+ const scope = sourceInfo?.scope ?? "project";
538
611
  if (source === "local") {
539
612
  if (scope === "user") {
540
613
  return { label: "user", color: "muted" };
@@ -553,7 +626,9 @@ export class InteractiveMode {
553
626
  const scopeLabel = scope === "user" ? "user" : scope === "project" ? "project" : scope === "temporary" ? "temp" : undefined;
554
627
  return { label: source, scopeLabel, color: "accent" };
555
628
  }
556
- getScopeGroup(source, scope) {
629
+ getScopeGroup(sourceInfo) {
630
+ const source = sourceInfo?.source ?? "local";
631
+ const scope = sourceInfo?.scope ?? "project";
557
632
  if (source === "cli" || scope === "temporary")
558
633
  return "path";
559
634
  if (scope === "user")
@@ -562,28 +637,27 @@ export class InteractiveMode {
562
637
  return "project";
563
638
  return "path";
564
639
  }
565
- isPackageSource(source) {
640
+ isPackageSource(sourceInfo) {
641
+ const source = sourceInfo?.source ?? "";
566
642
  return source.startsWith("npm:") || source.startsWith("git:");
567
643
  }
568
- buildScopeGroups(paths, metadata) {
644
+ buildScopeGroups(items) {
569
645
  const groups = {
570
646
  user: { scope: "user", paths: [], packages: new Map() },
571
647
  project: { scope: "project", paths: [], packages: new Map() },
572
648
  path: { scope: "path", paths: [], packages: new Map() },
573
649
  };
574
- for (const p of paths) {
575
- const meta = this.findMetadata(p, metadata);
576
- const source = meta?.source ?? "local";
577
- const scope = meta?.scope ?? "project";
578
- const groupKey = this.getScopeGroup(source, scope);
650
+ for (const item of items) {
651
+ const groupKey = this.getScopeGroup(item.sourceInfo);
579
652
  const group = groups[groupKey];
580
- if (this.isPackageSource(source)) {
653
+ const source = item.sourceInfo?.source ?? "local";
654
+ if (this.isPackageSource(item.sourceInfo)) {
581
655
  const list = group.packages.get(source) ?? [];
582
- list.push(p);
656
+ list.push(item);
583
657
  group.packages.set(source, list);
584
658
  }
585
659
  else {
586
- group.paths.push(p);
660
+ group.paths.push(item);
587
661
  }
588
662
  }
589
663
  return [groups.project, groups.user, groups.path].filter((group) => group.paths.length > 0 || group.packages.size > 0);
@@ -592,57 +666,44 @@ export class InteractiveMode {
592
666
  const lines = [];
593
667
  for (const group of groups) {
594
668
  lines.push(` ${theme.fg("accent", group.scope)}`);
595
- const sortedPaths = [...group.paths].sort((a, b) => a.localeCompare(b));
596
- for (const p of sortedPaths) {
597
- lines.push(theme.fg("dim", ` ${options.formatPath(p)}`));
669
+ const sortedPaths = [...group.paths].sort((a, b) => a.path.localeCompare(b.path));
670
+ for (const item of sortedPaths) {
671
+ lines.push(theme.fg("dim", ` ${options.formatPath(item)}`));
598
672
  }
599
673
  const sortedPackages = Array.from(group.packages.entries()).sort(([a], [b]) => a.localeCompare(b));
600
- for (const [source, paths] of sortedPackages) {
674
+ for (const [source, items] of sortedPackages) {
601
675
  lines.push(` ${theme.fg("mdLink", source)}`);
602
- const sortedPackagePaths = [...paths].sort((a, b) => a.localeCompare(b));
603
- for (const p of sortedPackagePaths) {
604
- lines.push(theme.fg("dim", ` ${options.formatPackagePath(p, source)}`));
676
+ const sortedPackagePaths = [...items].sort((a, b) => a.path.localeCompare(b.path));
677
+ for (const item of sortedPackagePaths) {
678
+ lines.push(theme.fg("dim", ` ${options.formatPackagePath(item, source)}`));
605
679
  }
606
680
  }
607
681
  }
608
682
  return lines.join("\n");
609
683
  }
610
- /**
611
- * Find metadata for a path, checking parent directories if exact match fails.
612
- * Package manager stores metadata for directories, but we display file paths.
613
- */
614
- findMetadata(p, metadata) {
615
- // Try exact match first
616
- const exact = metadata.get(p);
684
+ findSourceInfoForPath(p, sourceInfos) {
685
+ const exact = sourceInfos.get(p);
617
686
  if (exact)
618
687
  return exact;
619
- // Try parent directories (package manager stores directory paths)
620
688
  let current = p;
621
689
  while (current.includes("/")) {
622
690
  current = current.substring(0, current.lastIndexOf("/"));
623
- const parent = metadata.get(current);
691
+ const parent = sourceInfos.get(current);
624
692
  if (parent)
625
693
  return parent;
626
694
  }
627
695
  return undefined;
628
696
  }
629
- /**
630
- * Format a path with its source/scope info from metadata.
631
- */
632
- formatPathWithSource(p, metadata) {
633
- const meta = this.findMetadata(p, metadata);
634
- if (meta) {
635
- const shortPath = this.getShortPath(p, meta.source);
636
- const { label, scopeLabel } = this.getDisplaySourceInfo(meta.source, meta.scope);
697
+ formatPathWithSource(p, sourceInfo) {
698
+ if (sourceInfo) {
699
+ const shortPath = this.getShortPath(p, sourceInfo);
700
+ const { label, scopeLabel } = this.getDisplaySourceInfo(sourceInfo);
637
701
  const labelText = scopeLabel ? `${label} (${scopeLabel})` : label;
638
702
  return `${labelText} ${shortPath}`;
639
703
  }
640
704
  return this.formatDisplayPath(p);
641
705
  }
642
- /**
643
- * Format resource diagnostics with nice collision display using metadata.
644
- */
645
- formatDiagnostics(diagnostics, metadata) {
706
+ formatDiagnostics(diagnostics, sourceInfos) {
646
707
  const lines = [];
647
708
  // Group collision diagnostics by name
648
709
  const collisions = new Map();
@@ -663,21 +724,17 @@ export class InteractiveMode {
663
724
  if (!first)
664
725
  continue;
665
726
  lines.push(theme.fg("warning", ` "${name}" collision:`));
666
- // Show winner
667
- lines.push(theme.fg("dim", ` ${theme.fg("success", "✓")} ${this.formatPathWithSource(first.winnerPath, metadata)}`));
668
- // Show all losers
727
+ lines.push(theme.fg("dim", ` ${theme.fg("success", "✓")} ${this.formatPathWithSource(first.winnerPath, this.findSourceInfoForPath(first.winnerPath, sourceInfos))}`));
669
728
  for (const d of collisionList) {
670
729
  if (d.collision) {
671
- lines.push(theme.fg("dim", ` ${theme.fg("warning", "✗")} ${this.formatPathWithSource(d.collision.loserPath, metadata)} (skipped)`));
730
+ lines.push(theme.fg("dim", ` ${theme.fg("warning", "✗")} ${this.formatPathWithSource(d.collision.loserPath, this.findSourceInfoForPath(d.collision.loserPath, sourceInfos))} (skipped)`));
672
731
  }
673
732
  }
674
733
  }
675
- // Format other diagnostics (skill name collisions, parse errors, etc.)
676
734
  for (const d of otherDiagnostics) {
677
735
  if (d.path) {
678
- // Use metadata-aware formatting for paths
679
- const sourceInfo = this.formatPathWithSource(d.path, metadata);
680
- lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${sourceInfo}`));
736
+ const formattedPath = this.formatPathWithSource(d.path, this.findSourceInfoForPath(d.path, sourceInfos));
737
+ lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${formattedPath}`));
681
738
  lines.push(theme.fg(d.type === "error" ? "error" : "warning", ` ${d.message}`));
682
739
  }
683
740
  else {
@@ -692,11 +749,36 @@ export class InteractiveMode {
692
749
  if (!showListing && !showDiagnostics) {
693
750
  return;
694
751
  }
695
- const metadata = this.session.resourceLoader.getPathMetadata();
696
752
  const sectionHeader = (name, color = "mdHeading") => theme.fg(color, `[${name}]`);
697
753
  const skillsResult = this.session.resourceLoader.getSkills();
698
754
  const promptsResult = this.session.resourceLoader.getPrompts();
699
755
  const themesResult = this.session.resourceLoader.getThemes();
756
+ const extensions = options?.extensions ??
757
+ this.session.resourceLoader.getExtensions().extensions.map((extension) => ({
758
+ path: extension.path,
759
+ sourceInfo: extension.sourceInfo,
760
+ }));
761
+ const sourceInfos = new Map();
762
+ for (const extension of extensions) {
763
+ if (extension.sourceInfo) {
764
+ sourceInfos.set(extension.path, extension.sourceInfo);
765
+ }
766
+ }
767
+ for (const skill of skillsResult.skills) {
768
+ if (skill.sourceInfo) {
769
+ sourceInfos.set(skill.filePath, skill.sourceInfo);
770
+ }
771
+ }
772
+ for (const prompt of promptsResult.prompts) {
773
+ if (prompt.sourceInfo) {
774
+ sourceInfos.set(prompt.filePath, prompt.sourceInfo);
775
+ }
776
+ }
777
+ for (const loadedTheme of themesResult.themes) {
778
+ if (loadedTheme.sourcePath && loadedTheme.sourceInfo) {
779
+ sourceInfos.set(loadedTheme.sourcePath, loadedTheme.sourceInfo);
780
+ }
781
+ }
700
782
  if (showListing) {
701
783
  const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
702
784
  if (contextFiles.length > 0) {
@@ -709,39 +791,36 @@ export class InteractiveMode {
709
791
  }
710
792
  const skills = skillsResult.skills;
711
793
  if (skills.length > 0) {
712
- const skillPaths = skills.map((s) => s.filePath);
713
- const groups = this.buildScopeGroups(skillPaths, metadata);
794
+ const groups = this.buildScopeGroups(skills.map((skill) => ({ path: skill.filePath, sourceInfo: skill.sourceInfo })));
714
795
  const skillList = this.formatScopeGroups(groups, {
715
- formatPath: (p) => this.formatDisplayPath(p),
716
- formatPackagePath: (p, source) => this.getShortPath(p, source),
796
+ formatPath: (item) => this.formatDisplayPath(item.path),
797
+ formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
717
798
  });
718
799
  this.chatContainer.addChild(new Text(`${sectionHeader("Skills")}\n${skillList}`, 0, 0));
719
800
  this.chatContainer.addChild(new Spacer(1));
720
801
  }
721
802
  const templates = this.session.promptTemplates;
722
803
  if (templates.length > 0) {
723
- const templatePaths = templates.map((t) => t.filePath);
724
- const groups = this.buildScopeGroups(templatePaths, metadata);
804
+ const groups = this.buildScopeGroups(templates.map((template) => ({ path: template.filePath, sourceInfo: template.sourceInfo })));
725
805
  const templateByPath = new Map(templates.map((t) => [t.filePath, t]));
726
806
  const templateList = this.formatScopeGroups(groups, {
727
- formatPath: (p) => {
728
- const template = templateByPath.get(p);
729
- return template ? `/${template.name}` : this.formatDisplayPath(p);
807
+ formatPath: (item) => {
808
+ const template = templateByPath.get(item.path);
809
+ return template ? `/${template.name}` : this.formatDisplayPath(item.path);
730
810
  },
731
- formatPackagePath: (p) => {
732
- const template = templateByPath.get(p);
733
- return template ? `/${template.name}` : this.formatDisplayPath(p);
811
+ formatPackagePath: (item) => {
812
+ const template = templateByPath.get(item.path);
813
+ return template ? `/${template.name}` : this.formatDisplayPath(item.path);
734
814
  },
735
815
  });
736
816
  this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0));
737
817
  this.chatContainer.addChild(new Spacer(1));
738
818
  }
739
- const extensionPaths = options?.extensionPaths ?? [];
740
- if (extensionPaths.length > 0) {
741
- const groups = this.buildScopeGroups(extensionPaths, metadata);
819
+ if (extensions.length > 0) {
820
+ const groups = this.buildScopeGroups(extensions);
742
821
  const extList = this.formatScopeGroups(groups, {
743
- formatPath: (p) => this.formatDisplayPath(p),
744
- formatPackagePath: (p, source) => this.getShortPath(p, source),
822
+ formatPath: (item) => this.formatDisplayPath(item.path),
823
+ formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
745
824
  });
746
825
  this.chatContainer.addChild(new Text(`${sectionHeader("Extensions", "mdHeading")}\n${extList}`, 0, 0));
747
826
  this.chatContainer.addChild(new Spacer(1));
@@ -750,11 +829,13 @@ export class InteractiveMode {
750
829
  const loadedThemes = themesResult.themes;
751
830
  const customThemes = loadedThemes.filter((t) => t.sourcePath);
752
831
  if (customThemes.length > 0) {
753
- const themePaths = customThemes.map((t) => t.sourcePath);
754
- const groups = this.buildScopeGroups(themePaths, metadata);
832
+ const groups = this.buildScopeGroups(customThemes.map((loadedTheme) => ({
833
+ path: loadedTheme.sourcePath,
834
+ sourceInfo: loadedTheme.sourceInfo,
835
+ })));
755
836
  const themeList = this.formatScopeGroups(groups, {
756
- formatPath: (p) => this.formatDisplayPath(p),
757
- formatPackagePath: (p, source) => this.getShortPath(p, source),
837
+ formatPath: (item) => this.formatDisplayPath(item.path),
838
+ formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
758
839
  });
759
840
  this.chatContainer.addChild(new Text(`${sectionHeader("Themes")}\n${themeList}`, 0, 0));
760
841
  this.chatContainer.addChild(new Spacer(1));
@@ -763,13 +844,13 @@ export class InteractiveMode {
763
844
  if (showDiagnostics) {
764
845
  const skillDiagnostics = skillsResult.diagnostics;
765
846
  if (skillDiagnostics.length > 0) {
766
- const warningLines = this.formatDiagnostics(skillDiagnostics, metadata);
847
+ const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
767
848
  this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
768
849
  this.chatContainer.addChild(new Spacer(1));
769
850
  }
770
851
  const promptDiagnostics = promptsResult.diagnostics;
771
852
  if (promptDiagnostics.length > 0) {
772
- const warningLines = this.formatDiagnostics(promptDiagnostics, metadata);
853
+ const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
773
854
  this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
774
855
  this.chatContainer.addChild(new Spacer(1));
775
856
  }
@@ -782,16 +863,17 @@ export class InteractiveMode {
782
863
  }
783
864
  const commandDiagnostics = this.session.extensionRunner?.getCommandDiagnostics() ?? [];
784
865
  extensionDiagnostics.push(...commandDiagnostics);
866
+ extensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.session.extensionRunner));
785
867
  const shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? [];
786
868
  extensionDiagnostics.push(...shortcutDiagnostics);
787
869
  if (extensionDiagnostics.length > 0) {
788
- const warningLines = this.formatDiagnostics(extensionDiagnostics, metadata);
870
+ const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
789
871
  this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
790
872
  this.chatContainer.addChild(new Spacer(1));
791
873
  }
792
874
  const themeDiagnostics = themesResult.diagnostics;
793
875
  if (themeDiagnostics.length > 0) {
794
- const warningLines = this.formatDiagnostics(themeDiagnostics, metadata);
876
+ const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
795
877
  this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
796
878
  this.chatContainer.addChild(new Spacer(1));
797
879
  }
@@ -880,19 +962,17 @@ export class InteractiveMode {
880
962
  this.setupAutocomplete(this.fdPath);
881
963
  const extensionRunner = this.session.extensionRunner;
882
964
  if (!extensionRunner) {
883
- this.showLoadedResources({ extensionPaths: [], force: false });
965
+ this.showLoadedResources({ extensions: [], force: false });
884
966
  return;
885
967
  }
886
968
  this.setupExtensionShortcuts(extensionRunner);
887
- this.showLoadedResources({ extensionPaths: extensionRunner.getExtensionPaths(), force: false });
969
+ this.showLoadedResources({ force: false });
888
970
  }
889
971
  /**
890
972
  * Get a registered tool definition by name (for custom rendering).
891
973
  */
892
974
  getRegisteredToolDefinition(toolName) {
893
- const tools = this.session.extensionRunner?.getAllRegisteredTools() ?? [];
894
- const registeredTool = tools.find((t) => t.definition.name === toolName);
895
- return registeredTool?.definition;
975
+ return this.session.getToolDefinition(toolName);
896
976
  }
897
977
  /**
898
978
  * Set up keyboard shortcuts registered by extensions.
@@ -1023,7 +1103,7 @@ export class InteractiveMode {
1023
1103
  this.defaultEditor.onExtensionShortcut = undefined;
1024
1104
  this.updateTerminalTitle();
1025
1105
  if (this.loadingAnimation) {
1026
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
1106
+ this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1027
1107
  }
1028
1108
  }
1029
1109
  // Maximum total widget lines to prevent viewport overflow
@@ -1146,7 +1226,7 @@ export class InteractiveMode {
1146
1226
  this.loadingAnimation.setMessage(message);
1147
1227
  }
1148
1228
  else {
1149
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
1229
+ this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1150
1230
  }
1151
1231
  }
1152
1232
  else {
@@ -1161,7 +1241,7 @@ export class InteractiveMode {
1161
1241
  custom: (factory, options) => this.showExtensionCustom(factory, options),
1162
1242
  pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
1163
1243
  setEditorText: (text) => this.editor.setText(text),
1164
- getEditorText: () => this.editor.getText(),
1244
+ getEditorText: () => this.editor.getExpandedText?.() ?? this.editor.getText(),
1165
1245
  editor: (title, prefill) => this.showExtensionEditor(title, prefill),
1166
1246
  setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
1167
1247
  get theme() {
@@ -1504,24 +1584,24 @@ export class InteractiveMode {
1504
1584
  }
1505
1585
  };
1506
1586
  // Register app action handlers
1507
- this.defaultEditor.onAction("clear", () => this.handleCtrlC());
1587
+ this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
1508
1588
  this.defaultEditor.onCtrlD = () => this.handleCtrlD();
1509
- this.defaultEditor.onAction("suspend", () => this.handleCtrlZ());
1510
- this.defaultEditor.onAction("cycleThinkingLevel", () => this.cycleThinkingLevel());
1511
- this.defaultEditor.onAction("cycleModelForward", () => this.cycleModel("forward"));
1512
- this.defaultEditor.onAction("cycleModelBackward", () => this.cycleModel("backward"));
1589
+ this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ());
1590
+ this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel());
1591
+ this.defaultEditor.onAction("app.model.cycleForward", () => this.cycleModel("forward"));
1592
+ this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));
1513
1593
  // Global debug handler on TUI (works regardless of focus)
1514
1594
  this.ui.onDebug = () => this.handleDebugCommand();
1515
- this.defaultEditor.onAction("selectModel", () => this.showModelSelector());
1516
- this.defaultEditor.onAction("expandTools", () => this.toggleToolOutputExpansion());
1517
- this.defaultEditor.onAction("toggleThinking", () => this.toggleThinkingBlockVisibility());
1518
- this.defaultEditor.onAction("externalEditor", () => this.openExternalEditor());
1519
- this.defaultEditor.onAction("followUp", () => this.handleFollowUp());
1520
- this.defaultEditor.onAction("dequeue", () => this.handleDequeue());
1521
- this.defaultEditor.onAction("newSession", () => this.handleClearCommand());
1522
- this.defaultEditor.onAction("tree", () => this.showTreeSelector());
1523
- this.defaultEditor.onAction("fork", () => this.showUserMessageSelector());
1524
- this.defaultEditor.onAction("resume", () => this.showSessionSelector());
1595
+ this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
1596
+ this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1597
+ this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1598
+ this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
1599
+ this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
1600
+ this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
1601
+ this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
1602
+ this.defaultEditor.onAction("app.session.tree", () => this.showTreeSelector());
1603
+ this.defaultEditor.onAction("app.session.fork", () => this.showUserMessageSelector());
1604
+ this.defaultEditor.onAction("app.session.resume", () => this.showSessionSelector());
1525
1605
  this.defaultEditor.onChange = (text) => {
1526
1606
  const wasBashMode = this.isBashMode;
1527
1607
  this.isBashMode = text.trimStart().startsWith("!");
@@ -1581,13 +1661,18 @@ export class InteractiveMode {
1581
1661
  this.editor.setText("");
1582
1662
  return;
1583
1663
  }
1664
+ if (text.startsWith("/import")) {
1665
+ await this.handleImportCommand(text);
1666
+ this.editor.setText("");
1667
+ return;
1668
+ }
1584
1669
  if (text === "/share") {
1585
1670
  await this.handleShareCommand();
1586
1671
  this.editor.setText("");
1587
1672
  return;
1588
1673
  }
1589
1674
  if (text === "/copy") {
1590
- this.handleCopyCommand();
1675
+ await this.handleCopyCommand();
1591
1676
  this.editor.setText("");
1592
1677
  return;
1593
1678
  }
@@ -1777,7 +1862,7 @@ export class InteractiveMode {
1777
1862
  for (const content of this.streamingMessage.content) {
1778
1863
  if (content.type === "toolCall") {
1779
1864
  if (!this.pendingTools.has(content.id)) {
1780
- const component = new ToolExecutionComponent(content.name, content.arguments, {
1865
+ const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
1781
1866
  showImages: this.settingsManager.getShowImages(),
1782
1867
  }, this.getRegisteredToolDefinition(content.name), this.ui);
1783
1868
  component.setExpanded(this.toolOutputExpanded);
@@ -1835,15 +1920,17 @@ export class InteractiveMode {
1835
1920
  this.ui.requestRender();
1836
1921
  break;
1837
1922
  case "tool_execution_start": {
1838
- if (!this.pendingTools.has(event.toolCallId)) {
1839
- const component = new ToolExecutionComponent(event.toolName, event.args, {
1923
+ let component = this.pendingTools.get(event.toolCallId);
1924
+ if (!component) {
1925
+ component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
1840
1926
  showImages: this.settingsManager.getShowImages(),
1841
1927
  }, this.getRegisteredToolDefinition(event.toolName), this.ui);
1842
1928
  component.setExpanded(this.toolOutputExpanded);
1843
1929
  this.chatContainer.addChild(component);
1844
1930
  this.pendingTools.set(event.toolCallId, component);
1845
- this.ui.requestRender();
1846
1931
  }
1932
+ component.markExecutionStarted();
1933
+ this.ui.requestRender();
1847
1934
  break;
1848
1935
  }
1849
1936
  case "tool_execution_update": {
@@ -1876,7 +1963,6 @@ export class InteractiveMode {
1876
1963
  }
1877
1964
  this.pendingTools.clear();
1878
1965
  await this.checkShutdownRequested();
1879
- sendTerminalNotification(APP_NAME, "Ready for input");
1880
1966
  this.ui.requestRender();
1881
1967
  break;
1882
1968
  case "auto_compaction_start": {
@@ -1889,7 +1975,7 @@ export class InteractiveMode {
1889
1975
  // Show compacting indicator with reason
1890
1976
  this.statusContainer.clear();
1891
1977
  const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
1892
- this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `${reasonText}Auto-compacting... (${appKey(this.keybindings, "interrupt")} to cancel)`);
1978
+ this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `${reasonText}Auto-compacting... (${keyText("app.interrupt")} to cancel)`);
1893
1979
  this.statusContainer.addChild(this.autoCompactionLoader);
1894
1980
  this.ui.requestRender();
1895
1981
  break;
@@ -1941,7 +2027,7 @@ export class InteractiveMode {
1941
2027
  // Show retry indicator
1942
2028
  this.statusContainer.clear();
1943
2029
  const delaySeconds = Math.round(event.delayMs / 1000);
1944
- this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${appKey(this.keybindings, "interrupt")} to cancel)`);
2030
+ this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${keyText("app.interrupt")} to cancel)`);
1945
2031
  this.statusContainer.addChild(this.retryLoader);
1946
2032
  this.ui.requestRender();
1947
2033
  break;
@@ -2092,7 +2178,7 @@ export class InteractiveMode {
2092
2178
  // Render tool call components
2093
2179
  for (const content of message.content) {
2094
2180
  if (content.type === "toolCall") {
2095
- const component = new ToolExecutionComponent(content.name, content.arguments, { showImages: this.settingsManager.getShowImages() }, this.getRegisteredToolDefinition(content.name), this.ui);
2181
+ const component = new ToolExecutionComponent(content.name, content.id, content.arguments, { showImages: this.settingsManager.getShowImages() }, this.getRegisteredToolDefinition(content.name), this.ui);
2096
2182
  component.setExpanded(this.toolOutputExpanded);
2097
2183
  this.chatContainer.addChild(component);
2098
2184
  if (message.stopReason === "aborted" || message.stopReason === "error") {
@@ -2210,20 +2296,32 @@ export class InteractiveMode {
2210
2296
  await this.shutdown();
2211
2297
  }
2212
2298
  handleCtrlZ() {
2299
+ // Keep the event loop alive while suspended. Without this, stopping the TUI
2300
+ // can leave Node with no ref'ed handles, causing the process to exit on fg
2301
+ // before the SIGCONT handler gets a chance to restore the terminal.
2302
+ const suspendKeepAlive = setInterval(() => { }, 2 ** 30);
2213
2303
  // Ignore SIGINT while suspended so Ctrl+C in the terminal does not
2214
2304
  // kill the backgrounded process. The handler is removed on resume.
2215
2305
  const ignoreSigint = () => { };
2216
2306
  process.on("SIGINT", ignoreSigint);
2217
2307
  // Set up handler to restore TUI when resumed
2218
2308
  process.once("SIGCONT", () => {
2309
+ clearInterval(suspendKeepAlive);
2219
2310
  process.removeListener("SIGINT", ignoreSigint);
2220
2311
  this.ui.start();
2221
2312
  this.ui.requestRender(true);
2222
2313
  });
2223
- // Stop the TUI (restore terminal to normal mode)
2224
- this.ui.stop();
2225
- // Send SIGTSTP to process group (pid=0 means all processes in group)
2226
- process.kill(0, "SIGTSTP");
2314
+ try {
2315
+ // Stop the TUI (restore terminal to normal mode)
2316
+ this.ui.stop();
2317
+ // Send SIGTSTP to process group (pid=0 means all processes in group)
2318
+ process.kill(0, "SIGTSTP");
2319
+ }
2320
+ catch (error) {
2321
+ clearInterval(suspendKeepAlive);
2322
+ process.removeListener("SIGINT", ignoreSigint);
2323
+ throw error;
2324
+ }
2227
2325
  }
2228
2326
  async handleFollowUp() {
2229
2327
  const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
@@ -2337,7 +2435,7 @@ export class InteractiveMode {
2337
2435
  return;
2338
2436
  }
2339
2437
  const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
2340
- const tmpFile = path.join(os.tmpdir(), `draht-editor-${Date.now()}.draht.md`);
2438
+ const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`);
2341
2439
  try {
2342
2440
  // Write current content to temp file
2343
2441
  fs.writeFileSync(tmpFile, currentText, "utf-8");
@@ -2399,6 +2497,16 @@ export class InteractiveMode {
2399
2497
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2400
2498
  this.ui.requestRender();
2401
2499
  }
2500
+ showPackageUpdateNotification(packages) {
2501
+ const action = theme.fg("accent", `${APP_NAME} update`);
2502
+ const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action;
2503
+ const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n");
2504
+ this.chatContainer.addChild(new Spacer(1));
2505
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2506
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n${updateInstruction}\n${theme.fg("muted", "Packages:")}\n${packageLines}`, 1, 0));
2507
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2508
+ this.ui.requestRender();
2509
+ }
2402
2510
  /**
2403
2511
  * Get all queued messages (read-only).
2404
2512
  * Combines session queue and compaction queue.
@@ -2446,7 +2554,7 @@ export class InteractiveMode {
2446
2554
  const text = theme.fg("dim", `Follow-up: ${message}`);
2447
2555
  this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
2448
2556
  }
2449
- const dequeueHint = this.getAppKeyDisplay("dequeue");
2557
+ const dequeueHint = this.getAppKeyDisplay("app.message.dequeue");
2450
2558
  const hintText = theme.fg("dim", `↳ ${dequeueHint} to edit all queued messages`);
2451
2559
  this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
2452
2560
  }
@@ -2736,28 +2844,8 @@ export class InteractiveMode {
2736
2844
  this.showModelSelector(searchTerm);
2737
2845
  }
2738
2846
  async findExactModelMatch(searchTerm) {
2739
- const term = searchTerm.trim();
2740
- if (!term)
2741
- return undefined;
2742
- let targetProvider;
2743
- let targetModelId = "";
2744
- if (term.includes("/")) {
2745
- const parts = term.split("/", 2);
2746
- targetProvider = parts[0]?.trim().toLowerCase();
2747
- targetModelId = parts[1]?.trim().toLowerCase() ?? "";
2748
- }
2749
- else {
2750
- targetModelId = term.toLowerCase();
2751
- }
2752
- if (!targetModelId)
2753
- return undefined;
2754
2847
  const models = await this.getModelCandidates();
2755
- const exactMatches = models.filter((item) => {
2756
- const idMatch = item.id.toLowerCase() === targetModelId;
2757
- const providerMatch = !targetProvider || item.provider.toLowerCase() === targetProvider;
2758
- return idMatch && providerMatch;
2759
- });
2760
- return exactMatches.length === 1 ? exactMatches[0] : undefined;
2848
+ return findExactModelReferenceMatch(searchTerm, models);
2761
2849
  }
2762
2850
  async getModelCandidates() {
2763
2851
  if (this.session.scopedModels.length > 0) {
@@ -2988,7 +3076,7 @@ export class InteractiveMode {
2988
3076
  this.session.abortBranchSummary();
2989
3077
  };
2990
3078
  this.chatContainer.addChild(new Spacer(1));
2991
- summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${appKey(this.keybindings, "interrupt")} to cancel)`);
3079
+ summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
2992
3080
  this.statusContainer.addChild(summaryLoader);
2993
3081
  this.ui.requestRender();
2994
3082
  }
@@ -3208,7 +3296,7 @@ export class InteractiveMode {
3208
3296
  return;
3209
3297
  }
3210
3298
  this.resetExtensionUI();
3211
- const loader = new BorderedLoader(this.ui, theme, "Reloading extensions, skills, prompts, themes...", {
3299
+ const loader = new BorderedLoader(this.ui, theme, "Reloading keybindings, extensions, skills, prompts, themes...", {
3212
3300
  cancellable: false,
3213
3301
  });
3214
3302
  const previousEditor = this.editor;
@@ -3225,6 +3313,7 @@ export class InteractiveMode {
3225
3313
  };
3226
3314
  try {
3227
3315
  await this.session.reload();
3316
+ this.keybindings.reload();
3228
3317
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
3229
3318
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
3230
3319
  const themeName = this.settingsManager.getTheme();
@@ -3250,7 +3339,6 @@ export class InteractiveMode {
3250
3339
  this.rebuildChatFromMessages();
3251
3340
  dismissLoader(this.editor);
3252
3341
  this.showLoadedResources({
3253
- extensionPaths: runner?.getExtensionPaths() ?? [],
3254
3342
  force: false,
3255
3343
  showDiagnosticsWhenQuiet: true,
3256
3344
  });
@@ -3258,7 +3346,7 @@ export class InteractiveMode {
3258
3346
  if (modelsJsonError) {
3259
3347
  this.showError(`models.json error: ${modelsJsonError}`);
3260
3348
  }
3261
- this.showStatus("Reloaded extensions, skills, prompts, themes");
3349
+ this.showStatus("Reloaded keybindings, extensions, skills, prompts, themes");
3262
3350
  }
3263
3351
  catch (error) {
3264
3352
  dismissLoader(previousEditor);
@@ -3269,13 +3357,58 @@ export class InteractiveMode {
3269
3357
  const parts = text.split(/\s+/);
3270
3358
  const outputPath = parts.length > 1 ? parts[1] : undefined;
3271
3359
  try {
3272
- const filePath = await this.session.exportToHtml(outputPath);
3273
- this.showStatus(`Session exported to: ${filePath}`);
3360
+ if (outputPath?.endsWith(".jsonl")) {
3361
+ const filePath = this.session.exportToJsonl(outputPath);
3362
+ this.showStatus(`Session exported to: ${filePath}`);
3363
+ }
3364
+ else {
3365
+ const filePath = await this.session.exportToHtml(outputPath);
3366
+ this.showStatus(`Session exported to: ${filePath}`);
3367
+ }
3274
3368
  }
3275
3369
  catch (error) {
3276
3370
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
3277
3371
  }
3278
3372
  }
3373
+ async handleImportCommand(text) {
3374
+ const parts = text.split(/\s+/);
3375
+ if (parts.length < 2 || !parts[1]) {
3376
+ this.showError("Usage: /import <path.jsonl>");
3377
+ return;
3378
+ }
3379
+ const inputPath = parts[1];
3380
+ const confirmed = await this.showExtensionConfirm("Import session", `Replace current session with ${inputPath}?`);
3381
+ if (!confirmed) {
3382
+ this.showStatus("Import cancelled");
3383
+ return;
3384
+ }
3385
+ try {
3386
+ // Stop loading animation
3387
+ if (this.loadingAnimation) {
3388
+ this.loadingAnimation.stop();
3389
+ this.loadingAnimation = undefined;
3390
+ }
3391
+ this.statusContainer.clear();
3392
+ // Clear UI state
3393
+ this.pendingMessagesContainer.clear();
3394
+ this.compactionQueuedMessages = [];
3395
+ this.streamingComponent = undefined;
3396
+ this.streamingMessage = undefined;
3397
+ this.pendingTools.clear();
3398
+ const success = await this.session.importFromJsonl(inputPath);
3399
+ if (!success) {
3400
+ this.showWarning("Import cancelled");
3401
+ return;
3402
+ }
3403
+ // Clear and re-render the chat
3404
+ this.chatContainer.clear();
3405
+ this.renderInitialMessages();
3406
+ this.showStatus(`Session imported from: ${inputPath}`);
3407
+ }
3408
+ catch (error) {
3409
+ this.showError(`Failed to import session: ${error instanceof Error ? error.message : "Unknown error"}`);
3410
+ }
3411
+ }
3279
3412
  async handleShareCommand() {
3280
3413
  // Check if gh is available and logged in
3281
3414
  try {
@@ -3363,14 +3496,14 @@ export class InteractiveMode {
3363
3496
  }
3364
3497
  }
3365
3498
  }
3366
- handleCopyCommand() {
3499
+ async handleCopyCommand() {
3367
3500
  const text = this.session.getLastAssistantText();
3368
3501
  if (!text) {
3369
3502
  this.showError("No agent messages to copy yet.");
3370
3503
  return;
3371
3504
  }
3372
3505
  try {
3373
- copyToClipboard(text);
3506
+ await copyToClipboard(text);
3374
3507
  this.showStatus("Copied last agent message to clipboard");
3375
3508
  }
3376
3509
  catch (error) {
@@ -3463,53 +3596,59 @@ export class InteractiveMode {
3463
3596
  * Get capitalized display string for an app keybinding action.
3464
3597
  */
3465
3598
  getAppKeyDisplay(action) {
3466
- return this.capitalizeKey(appKey(this.keybindings, action));
3599
+ return this.capitalizeKey(keyText(action));
3467
3600
  }
3468
3601
  /**
3469
3602
  * Get capitalized display string for an editor keybinding action.
3470
3603
  */
3471
3604
  getEditorKeyDisplay(action) {
3472
- return this.capitalizeKey(editorKey(action));
3605
+ return this.capitalizeKey(keyText(action));
3473
3606
  }
3474
3607
  handleHotkeysCommand() {
3475
3608
  // Navigation keybindings
3476
- const cursorWordLeft = this.getEditorKeyDisplay("cursorWordLeft");
3477
- const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
3478
- const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
3479
- const cursorLineEnd = this.getEditorKeyDisplay("cursorLineEnd");
3480
- const jumpForward = this.getEditorKeyDisplay("jumpForward");
3481
- const jumpBackward = this.getEditorKeyDisplay("jumpBackward");
3482
- const pageUp = this.getEditorKeyDisplay("pageUp");
3483
- const pageDown = this.getEditorKeyDisplay("pageDown");
3609
+ const cursorUp = this.getEditorKeyDisplay("tui.editor.cursorUp");
3610
+ const cursorDown = this.getEditorKeyDisplay("tui.editor.cursorDown");
3611
+ const cursorLeft = this.getEditorKeyDisplay("tui.editor.cursorLeft");
3612
+ const cursorRight = this.getEditorKeyDisplay("tui.editor.cursorRight");
3613
+ const cursorWordLeft = this.getEditorKeyDisplay("tui.editor.cursorWordLeft");
3614
+ const cursorWordRight = this.getEditorKeyDisplay("tui.editor.cursorWordRight");
3615
+ const cursorLineStart = this.getEditorKeyDisplay("tui.editor.cursorLineStart");
3616
+ const cursorLineEnd = this.getEditorKeyDisplay("tui.editor.cursorLineEnd");
3617
+ const jumpForward = this.getEditorKeyDisplay("tui.editor.jumpForward");
3618
+ const jumpBackward = this.getEditorKeyDisplay("tui.editor.jumpBackward");
3619
+ const pageUp = this.getEditorKeyDisplay("tui.editor.pageUp");
3620
+ const pageDown = this.getEditorKeyDisplay("tui.editor.pageDown");
3484
3621
  // Editing keybindings
3485
- const submit = this.getEditorKeyDisplay("submit");
3486
- const newLine = this.getEditorKeyDisplay("newLine");
3487
- const deleteWordBackward = this.getEditorKeyDisplay("deleteWordBackward");
3488
- const deleteWordForward = this.getEditorKeyDisplay("deleteWordForward");
3489
- const deleteToLineStart = this.getEditorKeyDisplay("deleteToLineStart");
3490
- const deleteToLineEnd = this.getEditorKeyDisplay("deleteToLineEnd");
3491
- const yank = this.getEditorKeyDisplay("yank");
3492
- const yankPop = this.getEditorKeyDisplay("yankPop");
3493
- const undo = this.getEditorKeyDisplay("undo");
3494
- const tab = this.getEditorKeyDisplay("tab");
3622
+ const submit = this.getEditorKeyDisplay("tui.input.submit");
3623
+ const newLine = this.getEditorKeyDisplay("tui.input.newLine");
3624
+ const deleteWordBackward = this.getEditorKeyDisplay("tui.editor.deleteWordBackward");
3625
+ const deleteWordForward = this.getEditorKeyDisplay("tui.editor.deleteWordForward");
3626
+ const deleteToLineStart = this.getEditorKeyDisplay("tui.editor.deleteToLineStart");
3627
+ const deleteToLineEnd = this.getEditorKeyDisplay("tui.editor.deleteToLineEnd");
3628
+ const yank = this.getEditorKeyDisplay("tui.editor.yank");
3629
+ const yankPop = this.getEditorKeyDisplay("tui.editor.yankPop");
3630
+ const undo = this.getEditorKeyDisplay("tui.editor.undo");
3631
+ const tab = this.getEditorKeyDisplay("tui.input.tab");
3495
3632
  // App keybindings
3496
- const interrupt = this.getAppKeyDisplay("interrupt");
3497
- const clear = this.getAppKeyDisplay("clear");
3498
- const exit = this.getAppKeyDisplay("exit");
3499
- const suspend = this.getAppKeyDisplay("suspend");
3500
- const cycleThinkingLevel = this.getAppKeyDisplay("cycleThinkingLevel");
3501
- const cycleModelForward = this.getAppKeyDisplay("cycleModelForward");
3502
- const selectModel = this.getAppKeyDisplay("selectModel");
3503
- const expandTools = this.getAppKeyDisplay("expandTools");
3504
- const toggleThinking = this.getAppKeyDisplay("toggleThinking");
3505
- const externalEditor = this.getAppKeyDisplay("externalEditor");
3506
- const followUp = this.getAppKeyDisplay("followUp");
3507
- const dequeue = this.getAppKeyDisplay("dequeue");
3633
+ const interrupt = this.getAppKeyDisplay("app.interrupt");
3634
+ const clear = this.getAppKeyDisplay("app.clear");
3635
+ const exit = this.getAppKeyDisplay("app.exit");
3636
+ const suspend = this.getAppKeyDisplay("app.suspend");
3637
+ const cycleThinkingLevel = this.getAppKeyDisplay("app.thinking.cycle");
3638
+ const cycleModelForward = this.getAppKeyDisplay("app.model.cycleForward");
3639
+ const selectModel = this.getAppKeyDisplay("app.model.select");
3640
+ const expandTools = this.getAppKeyDisplay("app.tools.expand");
3641
+ const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
3642
+ const externalEditor = this.getAppKeyDisplay("app.editor.external");
3643
+ const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
3644
+ const followUp = this.getAppKeyDisplay("app.message.followUp");
3645
+ const dequeue = this.getAppKeyDisplay("app.message.dequeue");
3646
+ const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
3508
3647
  let hotkeys = `
3509
3648
  **Navigation**
3510
3649
  | Key | Action |
3511
3650
  |-----|--------|
3512
- | \`Arrow keys\` | Move cursor / browse history (Up when empty) |
3651
+ | \`${cursorUp}\` / \`${cursorDown}\` / \`${cursorLeft}\` / \`${cursorRight}\` | Move cursor / browse history (Up when empty) |
3513
3652
  | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
3514
3653
  | \`${cursorLineStart}\` | Start of line |
3515
3654
  | \`${cursorLineEnd}\` | End of line |
@@ -3539,14 +3678,14 @@ export class InteractiveMode {
3539
3678
  | \`${exit}\` | Exit (when editor is empty) |
3540
3679
  | \`${suspend}\` | Suspend to background |
3541
3680
  | \`${cycleThinkingLevel}\` | Cycle thinking level |
3542
- | \`${cycleModelForward}\` | Cycle models |
3681
+ | \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
3543
3682
  | \`${selectModel}\` | Open model selector |
3544
3683
  | \`${expandTools}\` | Toggle tool output expansion |
3545
3684
  | \`${toggleThinking}\` | Toggle thinking block visibility |
3546
3685
  | \`${externalEditor}\` | Edit message in external editor |
3547
3686
  | \`${followUp}\` | Queue follow-up message |
3548
3687
  | \`${dequeue}\` | Restore queued messages |
3549
- | \`Ctrl+V\` | Paste image from clipboard |
3688
+ | \`${pasteImage}\` | Paste image from clipboard |
3550
3689
  | \`/\` | Slash commands |
3551
3690
  | \`!\` | Run bash command |
3552
3691
  | \`!!\` | Run bash command (excluded from context) |
@@ -3729,7 +3868,7 @@ export class InteractiveMode {
3729
3868
  };
3730
3869
  // Show compacting status
3731
3870
  this.chatContainer.addChild(new Spacer(1));
3732
- const cancelHint = `(${appKey(this.keybindings, "interrupt")} to cancel)`;
3871
+ const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
3733
3872
  const label = isAuto ? `Auto-compacting context... ${cancelHint}` : `Compacting context... ${cancelHint}`;
3734
3873
  const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
3735
3874
  this.statusContainer.addChild(compactingLoader);