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