@agimon-ai/doompi-web 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +196 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +1 -0
  4. package/dist/bin/serve.cjs +3 -0
  5. package/dist/bin/serve.cjs.map +1 -0
  6. package/dist/bin/serve.d.cts +1 -0
  7. package/dist/bin/serve.d.mts +1 -0
  8. package/dist/bin/serve.mjs +3 -0
  9. package/dist/bin/serve.mjs.map +1 -0
  10. package/dist/bundler.cjs +1 -0
  11. package/dist/bundler.d.cts +2 -0
  12. package/dist/bundler.d.mts +2 -0
  13. package/dist/bundler.mjs +1 -0
  14. package/dist/index.cjs +1 -0
  15. package/dist/index.d.cts +7 -0
  16. package/dist/index.d.mts +7 -0
  17. package/dist/index.mjs +1 -0
  18. package/dist/src/adapters/authRoutes.cjs +2 -0
  19. package/dist/src/adapters/authRoutes.cjs.map +1 -0
  20. package/dist/src/adapters/authRoutes.mjs +2 -0
  21. package/dist/src/adapters/authRoutes.mjs.map +1 -0
  22. package/dist/src/adapters/bundledServer.cjs +2 -0
  23. package/dist/src/adapters/bundledServer.cjs.map +1 -0
  24. package/dist/src/adapters/bundledServer.mjs +2 -0
  25. package/dist/src/adapters/bundledServer.mjs.map +1 -0
  26. package/dist/src/adapters/directoryListing.cjs +2 -0
  27. package/dist/src/adapters/directoryListing.cjs.map +1 -0
  28. package/dist/src/adapters/directoryListing.mjs +2 -0
  29. package/dist/src/adapters/directoryListing.mjs.map +1 -0
  30. package/dist/src/adapters/directorySearch.cjs +3 -0
  31. package/dist/src/adapters/directorySearch.cjs.map +1 -0
  32. package/dist/src/adapters/directorySearch.mjs +3 -0
  33. package/dist/src/adapters/directorySearch.mjs.map +1 -0
  34. package/dist/src/adapters/gitStatus.cjs +2 -0
  35. package/dist/src/adapters/gitStatus.cjs.map +1 -0
  36. package/dist/src/adapters/gitStatus.mjs +2 -0
  37. package/dist/src/adapters/gitStatus.mjs.map +1 -0
  38. package/dist/src/adapters/httpServer.cjs +2 -0
  39. package/dist/src/adapters/httpServer.cjs.map +1 -0
  40. package/dist/src/adapters/httpServer.d.cts +14 -0
  41. package/dist/src/adapters/httpServer.d.cts.map +1 -0
  42. package/dist/src/adapters/httpServer.d.mts +14 -0
  43. package/dist/src/adapters/httpServer.d.mts.map +1 -0
  44. package/dist/src/adapters/httpServer.mjs +2 -0
  45. package/dist/src/adapters/httpServer.mjs.map +1 -0
  46. package/dist/src/adapters/hubProbe.cjs +2 -0
  47. package/dist/src/adapters/hubProbe.cjs.map +1 -0
  48. package/dist/src/adapters/hubProbe.mjs +2 -0
  49. package/dist/src/adapters/hubProbe.mjs.map +1 -0
  50. package/dist/src/adapters/packageApiProxy.cjs +2 -0
  51. package/dist/src/adapters/packageApiProxy.cjs.map +1 -0
  52. package/dist/src/adapters/packageApiProxy.mjs +2 -0
  53. package/dist/src/adapters/packageApiProxy.mjs.map +1 -0
  54. package/dist/src/adapters/piHubService.cjs +2 -0
  55. package/dist/src/adapters/piHubService.cjs.map +1 -0
  56. package/dist/src/adapters/piHubService.mjs +2 -0
  57. package/dist/src/adapters/piHubService.mjs.map +1 -0
  58. package/dist/src/adapters/piWebSocketListener.cjs +2 -0
  59. package/dist/src/adapters/piWebSocketListener.cjs.map +1 -0
  60. package/dist/src/adapters/piWebSocketListener.mjs +2 -0
  61. package/dist/src/adapters/piWebSocketListener.mjs.map +1 -0
  62. package/dist/src/adapters/providerAuth.cjs +2 -0
  63. package/dist/src/adapters/providerAuth.cjs.map +1 -0
  64. package/dist/src/adapters/providerAuth.mjs +2 -0
  65. package/dist/src/adapters/providerAuth.mjs.map +1 -0
  66. package/dist/src/adapters/registryWatcher.cjs +2 -0
  67. package/dist/src/adapters/registryWatcher.cjs.map +1 -0
  68. package/dist/src/adapters/registryWatcher.mjs +2 -0
  69. package/dist/src/adapters/registryWatcher.mjs.map +1 -0
  70. package/dist/src/adapters/serverSpawner.cjs +2 -0
  71. package/dist/src/adapters/serverSpawner.cjs.map +1 -0
  72. package/dist/src/adapters/serverSpawner.mjs +2 -0
  73. package/dist/src/adapters/serverSpawner.mjs.map +1 -0
  74. package/dist/src/adapters/sessionFiles.cjs +3 -0
  75. package/dist/src/adapters/sessionFiles.cjs.map +1 -0
  76. package/dist/src/adapters/sessionFiles.mjs +3 -0
  77. package/dist/src/adapters/sessionFiles.mjs.map +1 -0
  78. package/dist/src/adapters/sessionHub.cjs +2 -0
  79. package/dist/src/adapters/sessionHub.cjs.map +1 -0
  80. package/dist/src/adapters/sessionHub.mjs +2 -0
  81. package/dist/src/adapters/sessionHub.mjs.map +1 -0
  82. package/dist/src/adapters/sessionSocketClient.cjs +2 -0
  83. package/dist/src/adapters/sessionSocketClient.cjs.map +1 -0
  84. package/dist/src/adapters/sessionSocketClient.mjs +2 -0
  85. package/dist/src/adapters/sessionSocketClient.mjs.map +1 -0
  86. package/dist/src/adapters/syncGuard.cjs +2 -0
  87. package/dist/src/adapters/syncGuard.cjs.map +1 -0
  88. package/dist/src/adapters/syncGuard.mjs +2 -0
  89. package/dist/src/adapters/syncGuard.mjs.map +1 -0
  90. package/dist/src/adapters/threadJournals.cjs +2 -0
  91. package/dist/src/adapters/threadJournals.cjs.map +1 -0
  92. package/dist/src/adapters/threadJournals.mjs +2 -0
  93. package/dist/src/adapters/threadJournals.mjs.map +1 -0
  94. package/dist/src/adapters/webBundler.cjs +2 -0
  95. package/dist/src/adapters/webBundler.cjs.map +1 -0
  96. package/dist/src/adapters/webBundler.d.cts +28 -0
  97. package/dist/src/adapters/webBundler.d.cts.map +1 -0
  98. package/dist/src/adapters/webBundler.d.mts +28 -0
  99. package/dist/src/adapters/webBundler.d.mts.map +1 -0
  100. package/dist/src/adapters/webBundler.mjs +2 -0
  101. package/dist/src/adapters/webBundler.mjs.map +1 -0
  102. package/dist/src/adapters/webHubPluginLoader.cjs +2 -0
  103. package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -0
  104. package/dist/src/adapters/webHubPluginLoader.mjs +2 -0
  105. package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -0
  106. package/dist/src/adapters/webHubPlugins.generated.cjs +2 -0
  107. package/dist/src/adapters/webHubPlugins.generated.cjs.map +1 -0
  108. package/dist/src/adapters/webHubPlugins.generated.mjs +2 -0
  109. package/dist/src/adapters/webHubPlugins.generated.mjs.map +1 -0
  110. package/dist/src/adapters/webPluginGenerate.cjs +4 -0
  111. package/dist/src/adapters/webPluginGenerate.cjs.map +1 -0
  112. package/dist/src/adapters/webPluginGenerate.mjs +4 -0
  113. package/dist/src/adapters/webPluginGenerate.mjs.map +1 -0
  114. package/dist/src/adapters/webPluginScan.cjs +2 -0
  115. package/dist/src/adapters/webPluginScan.cjs.map +1 -0
  116. package/dist/src/adapters/webPluginScan.mjs +2 -0
  117. package/dist/src/adapters/webPluginScan.mjs.map +1 -0
  118. package/dist/src/adapters/webPluginVite.cjs +2 -0
  119. package/dist/src/adapters/webPluginVite.cjs.map +1 -0
  120. package/dist/src/adapters/webPluginVite.mjs +2 -0
  121. package/dist/src/adapters/webPluginVite.mjs.map +1 -0
  122. package/dist/src/services/directoryMatch.cjs +2 -0
  123. package/dist/src/services/directoryMatch.cjs.map +1 -0
  124. package/dist/src/services/directoryMatch.mjs +2 -0
  125. package/dist/src/services/directoryMatch.mjs.map +1 -0
  126. package/dist/src/services/fileMatch.cjs +2 -0
  127. package/dist/src/services/fileMatch.cjs.map +1 -0
  128. package/dist/src/services/fileMatch.mjs +2 -0
  129. package/dist/src/services/fileMatch.mjs.map +1 -0
  130. package/dist/src/services/fileMedia.cjs +2 -0
  131. package/dist/src/services/fileMedia.cjs.map +1 -0
  132. package/dist/src/services/fileMedia.mjs +2 -0
  133. package/dist/src/services/fileMedia.mjs.map +1 -0
  134. package/dist/src/services/frameRing.cjs +2 -0
  135. package/dist/src/services/frameRing.cjs.map +1 -0
  136. package/dist/src/services/frameRing.mjs +2 -0
  137. package/dist/src/services/frameRing.mjs.map +1 -0
  138. package/dist/src/services/journalTail.cjs +3 -0
  139. package/dist/src/services/journalTail.cjs.map +1 -0
  140. package/dist/src/services/journalTail.mjs +3 -0
  141. package/dist/src/services/journalTail.mjs.map +1 -0
  142. package/dist/src/services/loginFlow.cjs +2 -0
  143. package/dist/src/services/loginFlow.cjs.map +1 -0
  144. package/dist/src/services/loginFlow.mjs +2 -0
  145. package/dist/src/services/loginFlow.mjs.map +1 -0
  146. package/dist/src/services/registryStore.cjs +2 -0
  147. package/dist/src/services/registryStore.cjs.map +1 -0
  148. package/dist/src/services/registryStore.mjs +2 -0
  149. package/dist/src/services/registryStore.mjs.map +1 -0
  150. package/dist/src/services/retryPolicy.cjs +2 -0
  151. package/dist/src/services/retryPolicy.cjs.map +1 -0
  152. package/dist/src/services/retryPolicy.mjs +2 -0
  153. package/dist/src/services/retryPolicy.mjs.map +1 -0
  154. package/dist/src/services/serveOptions.cjs +2 -0
  155. package/dist/src/services/serveOptions.cjs.map +1 -0
  156. package/dist/src/services/serveOptions.d.cts +21 -0
  157. package/dist/src/services/serveOptions.d.cts.map +1 -0
  158. package/dist/src/services/serveOptions.d.mts +21 -0
  159. package/dist/src/services/serveOptions.d.mts.map +1 -0
  160. package/dist/src/services/serveOptions.mjs +2 -0
  161. package/dist/src/services/serveOptions.mjs.map +1 -0
  162. package/dist/src/services/sessionFraming.cjs +4 -0
  163. package/dist/src/services/sessionFraming.cjs.map +1 -0
  164. package/dist/src/services/sessionFraming.mjs +4 -0
  165. package/dist/src/services/sessionFraming.mjs.map +1 -0
  166. package/dist/src/services/sessionPresence.cjs +2 -0
  167. package/dist/src/services/sessionPresence.cjs.map +1 -0
  168. package/dist/src/services/sessionPresence.mjs +2 -0
  169. package/dist/src/services/sessionPresence.mjs.map +1 -0
  170. package/dist/src/services/staticAssets.cjs +2 -0
  171. package/dist/src/services/staticAssets.cjs.map +1 -0
  172. package/dist/src/services/staticAssets.mjs +2 -0
  173. package/dist/src/services/staticAssets.mjs.map +1 -0
  174. package/dist/src/services/webDevRoots.cjs +2 -0
  175. package/dist/src/services/webDevRoots.cjs.map +1 -0
  176. package/dist/src/services/webDevRoots.mjs +2 -0
  177. package/dist/src/services/webDevRoots.mjs.map +1 -0
  178. package/dist/src/services/webPluginManifest.cjs +2 -0
  179. package/dist/src/services/webPluginManifest.cjs.map +1 -0
  180. package/dist/src/services/webPluginManifest.mjs +2 -0
  181. package/dist/src/services/webPluginManifest.mjs.map +1 -0
  182. package/dist/src/types/auth.cjs +2 -0
  183. package/dist/src/types/auth.cjs.map +1 -0
  184. package/dist/src/types/auth.d.cts +94 -0
  185. package/dist/src/types/auth.d.cts.map +1 -0
  186. package/dist/src/types/auth.d.mts +94 -0
  187. package/dist/src/types/auth.d.mts.map +1 -0
  188. package/dist/src/types/auth.mjs +2 -0
  189. package/dist/src/types/auth.mjs.map +1 -0
  190. package/dist/src/types/bridge.d.cts +31 -0
  191. package/dist/src/types/bridge.d.cts.map +1 -0
  192. package/dist/src/types/bridge.d.mts +31 -0
  193. package/dist/src/types/bridge.d.mts.map +1 -0
  194. package/dist/src/types/hub.cjs +2 -0
  195. package/dist/src/types/hub.cjs.map +1 -0
  196. package/dist/src/types/hub.d.cts +58 -0
  197. package/dist/src/types/hub.d.cts.map +1 -0
  198. package/dist/src/types/hub.d.mts +58 -0
  199. package/dist/src/types/hub.d.mts.map +1 -0
  200. package/dist/src/types/hub.mjs +2 -0
  201. package/dist/src/types/hub.mjs.map +1 -0
  202. package/dist/src/types/media.cjs +2 -0
  203. package/dist/src/types/media.cjs.map +1 -0
  204. package/dist/src/types/media.mjs +2 -0
  205. package/dist/src/types/media.mjs.map +1 -0
  206. package/dist/src/types/registry.cjs +1 -0
  207. package/dist/src/types/registry.d.cts +2 -0
  208. package/dist/src/types/registry.d.mts +2 -0
  209. package/dist/src/types/registry.mjs +1 -0
  210. package/dist/src/types/session.cjs +2 -0
  211. package/dist/src/types/session.cjs.map +1 -0
  212. package/dist/src/types/session.d.cts +24 -0
  213. package/dist/src/types/session.d.cts.map +1 -0
  214. package/dist/src/types/session.d.mts +24 -0
  215. package/dist/src/types/session.d.mts.map +1 -0
  216. package/dist/src/types/session.mjs +2 -0
  217. package/dist/src/types/session.mjs.map +1 -0
  218. package/dist/web/apple-touch-icon.png +0 -0
  219. package/dist/web/assets/index-Cg52ngKv.css +2 -0
  220. package/dist/web/assets/index-DQO7NTIP.js +90 -0
  221. package/dist/web/assets/index-DQO7NTIP.js.map +1 -0
  222. package/dist/web/favicon-16.png +0 -0
  223. package/dist/web/favicon-32.png +0 -0
  224. package/dist/web/favicon.ico +0 -0
  225. package/dist/web/favicon.svg +16 -0
  226. package/dist/web/icon-512.png +0 -0
  227. package/dist/web/index.html +17 -0
  228. package/llms.txt +8 -0
  229. package/package.json +106 -0
  230. package/src/adapters/authRoutes.ts +95 -0
  231. package/src/adapters/bundledServer.ts +64 -0
  232. package/src/adapters/directoryListing.ts +127 -0
  233. package/src/adapters/directorySearch.ts +124 -0
  234. package/src/adapters/gitStatus.ts +27 -0
  235. package/src/adapters/httpServer.ts +556 -0
  236. package/src/adapters/hubProbe.ts +27 -0
  237. package/src/adapters/packageApiProxy.ts +60 -0
  238. package/src/adapters/piHubService.ts +129 -0
  239. package/src/adapters/piWebSocketListener.ts +98 -0
  240. package/src/adapters/providerAuth.ts +181 -0
  241. package/src/adapters/registryWatcher.ts +112 -0
  242. package/src/adapters/serverSpawner.ts +181 -0
  243. package/src/adapters/sessionFiles.ts +99 -0
  244. package/src/adapters/sessionHub.ts +567 -0
  245. package/src/adapters/sessionSocketClient.ts +105 -0
  246. package/src/adapters/syncGuard.ts +119 -0
  247. package/src/adapters/threadJournals.ts +184 -0
  248. package/src/adapters/webBundler.ts +83 -0
  249. package/src/adapters/webHubPluginLoader.ts +67 -0
  250. package/src/adapters/webHubPlugins.generated.ts +4 -0
  251. package/src/adapters/webPluginGenerate.ts +172 -0
  252. package/src/adapters/webPluginScan.ts +62 -0
  253. package/src/adapters/webPluginVite.ts +51 -0
  254. package/src/bin/serve.ts +64 -0
  255. package/src/exports/bundler.ts +1 -0
  256. package/src/exports/index.ts +9 -0
  257. package/src/services/directoryMatch.ts +71 -0
  258. package/src/services/fileMatch.ts +37 -0
  259. package/src/services/fileMedia.ts +27 -0
  260. package/src/services/frameRing.ts +33 -0
  261. package/src/services/journalTail.ts +47 -0
  262. package/src/services/loginFlow.ts +144 -0
  263. package/src/services/registryStore.ts +83 -0
  264. package/src/services/retryPolicy.ts +15 -0
  265. package/src/services/serveOptions.ts +63 -0
  266. package/src/services/sessionFraming.ts +32 -0
  267. package/src/services/sessionPresence.ts +131 -0
  268. package/src/services/staticAssets.ts +37 -0
  269. package/src/services/webDevRoots.ts +36 -0
  270. package/src/services/webPluginManifest.ts +149 -0
  271. package/src/types/auth.ts +118 -0
  272. package/src/types/bridge.ts +41 -0
  273. package/src/types/hub.ts +290 -0
  274. package/src/types/media.ts +38 -0
  275. package/src/types/registry.ts +2 -0
  276. package/src/types/session.ts +36 -0
  277. package/src/web/app/Providers.tsx +46 -0
  278. package/src/web/app/protocolRuntime.ts +106 -0
  279. package/src/web/app/sessionRuntime.ts +189 -0
  280. package/src/web/app/webPlugins.generated.ts +4 -0
  281. package/src/web/components/MascotMark.tsx +46 -0
  282. package/src/web/components/PluginSurface.tsx +10 -0
  283. package/src/web/components/ToolRendererBoundary.tsx +32 -0
  284. package/src/web/features/activity/ActivityDock.tsx +125 -0
  285. package/src/web/features/connection/RefusedCard.tsx +63 -0
  286. package/src/web/features/dialogs/DialogOverlay.tsx +176 -0
  287. package/src/web/features/leader/CommandPalette.tsx +386 -0
  288. package/src/web/features/selection/SelectionBar.tsx +529 -0
  289. package/src/web/features/session/Composer.tsx +368 -0
  290. package/src/web/features/session/MentionPreviews.tsx +58 -0
  291. package/src/web/features/session/ThreadView.tsx +35 -0
  292. package/src/web/features/session/Timeline.tsx +382 -0
  293. package/src/web/features/session/ToolCard.tsx +79 -0
  294. package/src/web/features/sessions/NewSessionDialog.tsx +222 -0
  295. package/src/web/features/sessions/SessionRail.tsx +411 -0
  296. package/src/web/features/settings/AppearanceSettings.tsx +88 -0
  297. package/src/web/features/settings/LoginFlowDialog.tsx +221 -0
  298. package/src/web/features/settings/PluginSettings.tsx +92 -0
  299. package/src/web/features/settings/ProviderSettings.tsx +238 -0
  300. package/src/web/features/settings/SettingsMenu.tsx +32 -0
  301. package/src/web/features/status/TopBar.tsx +208 -0
  302. package/src/web/index.html +16 -0
  303. package/src/web/lib/authApi.ts +90 -0
  304. package/src/web/lib/commands.ts +61 -0
  305. package/src/web/lib/composition.ts +225 -0
  306. package/src/web/lib/fileMentions.ts +32 -0
  307. package/src/web/lib/hubApi.ts +86 -0
  308. package/src/web/lib/leaderTree.ts +159 -0
  309. package/src/web/lib/piTransport.ts +61 -0
  310. package/src/web/lib/pluginRegistry.ts +488 -0
  311. package/src/web/lib/pluginSlotProps.ts +50 -0
  312. package/src/web/lib/promptFocus.ts +25 -0
  313. package/src/web/lib/protocolTimeline.ts +73 -0
  314. package/src/web/lib/sessionModel.ts +673 -0
  315. package/src/web/lib/sessionSummary.ts +58 -0
  316. package/src/web/lib/settingsSections.ts +21 -0
  317. package/src/web/lib/statusLine.ts +90 -0
  318. package/src/web/lib/threadRenderer.ts +25 -0
  319. package/src/web/lib/toolMessageProps.ts +21 -0
  320. package/src/web/lib/transport.ts +30 -0
  321. package/src/web/lib/wsClient.ts +73 -0
  322. package/src/web/main.tsx +17 -0
  323. package/src/web/public/apple-touch-icon.png +0 -0
  324. package/src/web/public/favicon-16.png +0 -0
  325. package/src/web/public/favicon-32.png +0 -0
  326. package/src/web/public/favicon.ico +0 -0
  327. package/src/web/public/favicon.svg +16 -0
  328. package/src/web/public/icon-512.png +0 -0
  329. package/src/web/routes/CockpitPage.tsx +98 -0
  330. package/src/web/routes/SettingsPage.tsx +60 -0
  331. package/src/web/routes/routeTree.tsx +49 -0
  332. package/src/web/stores/menuStore.ts +78 -0
  333. package/src/web/stores/paletteStore.ts +25 -0
  334. package/src/web/stores/sessionStore.ts +303 -0
  335. package/src/web/stores/sessionsStore.ts +155 -0
  336. package/src/web/stores/themeStore.ts +52 -0
  337. package/src/web/stores/threadStore.ts +68 -0
  338. package/src/web/stores/transientTabsStore.ts +63 -0
  339. package/src/web/stores/uiStore.ts +28 -0
  340. package/src/web/stores/useOpenTab.ts +19 -0
  341. package/src/web/stores/usePluginSlotProps.ts +22 -0
  342. package/src/web/styles/app.css +15 -0
  343. package/src/web/styles/webPluginSources.generated.css +1 -0
  344. package/src/web/vite-env.d.ts +1 -0
@@ -0,0 +1,386 @@
1
+ import {
2
+ cn,
3
+ CommandEmpty,
4
+ CommandFooter,
5
+ CommandGroupLabel,
6
+ CommandHeader,
7
+ CommandInput,
8
+ CommandItem,
9
+ CommandItemLabel,
10
+ CommandList,
11
+ Dialog,
12
+ DialogContent,
13
+ DialogTitle,
14
+ Kbd,
15
+ OptionLabel,
16
+ OptionRow,
17
+ StatusBadge,
18
+ } from '@agimon-ai/doompi-web-components';
19
+ import type { LeaderBindingContribution } from '@agimon-ai/doompi-web-contracts';
20
+ import { useStore } from '@tanstack/react-store';
21
+ import { type KeyboardEvent as ReactKeyboardEvent, useEffect, useMemo, useRef, useState } from 'react';
22
+ import { type LeaderOption, leaderGroup } from '../../lib/leaderTree.ts';
23
+ import { paletteCommands, pluginLeaderBindings } from '../../lib/pluginRegistry.ts';
24
+ import { focusPrompt } from '../../lib/promptFocus.ts';
25
+ import { sendFrame } from '../../lib/transport.ts';
26
+ import { useOpenTab } from '../../stores/useOpenTab.ts';
27
+ import { closePalette, paletteStore, setPalettePath, togglePalette } from '../../stores/paletteStore.ts';
28
+ import { runCommand, useActiveSession } from '../../stores/sessionStore.ts';
29
+ import { sessionsStore } from '../../stores/sessionsStore.ts';
30
+
31
+ const LEADER_PREFIX = 'SPC';
32
+ const SEARCH_KEY = '/';
33
+ const LEADER_KEY = /^[a-z0-9]$/;
34
+
35
+ function pathLabel(keys: readonly string[]): string {
36
+ return [LEADER_PREFIX, ...keys].join(' ');
37
+ }
38
+
39
+ /**
40
+ * Leader Space, as much of it as a browser can honestly show.
41
+ *
42
+ * The session's own leader tree never reaches an RPC client, so the tree here
43
+ * is what the installed web plugins declared: each package's cockpit half of
44
+ * the key paths its TUI documents. Keys walk the tree the way the TUI does,
45
+ * backspace climbs, and the slash search is the road to the commands no
46
+ * package has put on a key. The surface is a modal dialog, so focus comes
47
+ * back to wherever it was (usually the composer) when it closes.
48
+ */
49
+ export function CommandPalette() {
50
+ const commands = useActiveSession((state) => state.commands);
51
+ const { open, path } = useStore(paletteStore);
52
+ const activeId = useStore(sessionsStore, (state) => state.activeId);
53
+ const openTab = useOpenTab();
54
+ const [search, setSearch] = useState('');
55
+ const [selected, setSelected] = useState(0);
56
+ const surface = useRef<HTMLDivElement>(null);
57
+ const searchInput = useRef<HTMLInputElement>(null);
58
+
59
+ useEffect(() => {
60
+ const onKey = (event: KeyboardEvent): void => {
61
+ // ctrl+k is the browser-friendly binding; ctrl+space mirrors the TUI's
62
+ // global leader key. Space in an empty composer opens it too.
63
+ const leaderKey = event.key === 'k' || event.key === ' ';
64
+ if (leaderKey && (event.ctrlKey || event.metaKey)) {
65
+ event.preventDefault();
66
+ togglePalette();
67
+ }
68
+ };
69
+ window.addEventListener('keydown', onKey);
70
+ return () => window.removeEventListener('keydown', onKey);
71
+ }, []);
72
+
73
+ // Every opening starts at the root with an empty search.
74
+ useEffect(() => {
75
+ if (open) {
76
+ setSearch('');
77
+ setSelected(0);
78
+ }
79
+ }, [open]);
80
+
81
+ const keys = useMemo(() => path.split(''), [path]);
82
+ const bindings = pluginLeaderBindings();
83
+ const group = useMemo(() => leaderGroup(bindings, keys) ?? leaderGroup(bindings, []), [bindings, keys]);
84
+ const options = group?.options ?? [];
85
+
86
+ const needle = search.trim().toLowerCase();
87
+ const searching = needle.length > 0;
88
+ const matches = useMemo(
89
+ () => (searching ? commands.filter((entry) => entry.name.toLowerCase().includes(needle)) : []),
90
+ [commands, needle, searching],
91
+ );
92
+ const pluginEntries = keys.length === 0 && !searching ? paletteCommands() : [];
93
+
94
+ const rowCount = searching ? matches.length : options.length;
95
+ const cursor = Math.min(selected, Math.max(rowCount - 1, 0));
96
+ const current = searching ? undefined : options[cursor];
97
+ const currentMatch = searching ? matches[cursor] : undefined;
98
+
99
+ const fire = (binding: LeaderBindingContribution): void => {
100
+ closePalette();
101
+ if ('command' in binding) {
102
+ runCommand(binding.command);
103
+ return;
104
+ }
105
+ binding.run({ sessionId: activeId, openTab, sendSessionFrame: sendFrame });
106
+ };
107
+
108
+ const descend = (option: LeaderOption, from: readonly string[] = keys): void => {
109
+ if (option.binding) {
110
+ fire(option.binding);
111
+ return;
112
+ }
113
+ setPalettePath([...from, option.key].join(''));
114
+ setSelected(0);
115
+ };
116
+
117
+ const climb = (): void => {
118
+ if (keys.length === 0) {
119
+ closePalette();
120
+ return;
121
+ }
122
+ setPalettePath(keys.slice(0, -1).join(''));
123
+ setSelected(0);
124
+ };
125
+
126
+ const invoke = (name: string): void => {
127
+ closePalette();
128
+ runCommand(name);
129
+ };
130
+
131
+ const runPluginCommand = (id: string): void => {
132
+ const command = paletteCommands().find((candidate) => candidate.id === id);
133
+ if (!command) return;
134
+ closePalette();
135
+ command.run({ sessionId: activeId, openTab, sendSessionFrame: sendFrame });
136
+ };
137
+
138
+ const onSurfaceKey = (event: ReactKeyboardEvent<HTMLDivElement>): void => {
139
+ // The search box handles its own keys; letters typed there are text.
140
+ if (event.target === searchInput.current) return;
141
+ if (event.key === 'Backspace') {
142
+ event.preventDefault();
143
+ climb();
144
+ return;
145
+ }
146
+ if (event.key === 'ArrowDown') {
147
+ event.preventDefault();
148
+ setSelected(Math.min(cursor + 1, Math.max(rowCount - 1, 0)));
149
+ return;
150
+ }
151
+ if (event.key === 'ArrowUp') {
152
+ event.preventDefault();
153
+ setSelected(Math.max(cursor - 1, 0));
154
+ return;
155
+ }
156
+ if (event.key === 'Enter') {
157
+ event.preventDefault();
158
+ if (current) descend(current);
159
+ return;
160
+ }
161
+ if (event.key === SEARCH_KEY) {
162
+ event.preventDefault();
163
+ searchInput.current?.focus();
164
+ return;
165
+ }
166
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
167
+ const key = event.key.toLowerCase();
168
+ if (!LEADER_KEY.test(key)) return;
169
+ const option = options.find((candidate) => candidate.key === key);
170
+ if (!option) return;
171
+ event.preventDefault();
172
+ descend(option);
173
+ };
174
+
175
+ const onSearchKey = (event: ReactKeyboardEvent<HTMLInputElement>): void => {
176
+ if (event.key === 'ArrowDown') {
177
+ event.preventDefault();
178
+ setSelected(Math.min(cursor + 1, Math.max(rowCount - 1, 0)));
179
+ return;
180
+ }
181
+ if (event.key === 'ArrowUp') {
182
+ event.preventDefault();
183
+ setSelected(Math.max(cursor - 1, 0));
184
+ return;
185
+ }
186
+ if (event.key === 'Enter' && currentMatch) {
187
+ event.preventDefault();
188
+ invoke(currentMatch.name);
189
+ }
190
+ };
191
+
192
+ return (
193
+ <Dialog
194
+ open={open}
195
+ onOpenChange={(next) => {
196
+ if (!next) closePalette();
197
+ }}
198
+ >
199
+ <DialogContent
200
+ ref={surface}
201
+ width="lg"
202
+ data-testid="palette"
203
+ data-path={path}
204
+ aria-describedby={undefined}
205
+ onKeyDown={onSurfaceKey}
206
+ // The palette owns the keyboard: focus lands on the surface, not the
207
+ // search box, so a typed letter is a leader key rather than filter text.
208
+ onOpenAutoFocus={(event) => {
209
+ event.preventDefault();
210
+ surface.current?.focus();
211
+ }}
212
+ // Opened by a shortcut, so there is no trigger to restore focus to.
213
+ onCloseAutoFocus={(event) => {
214
+ event.preventDefault();
215
+ focusPrompt();
216
+ }}
217
+ // Escape in a live search clears the search; only an empty one closes.
218
+ onEscapeKeyDown={(event) => {
219
+ if (searching && document.activeElement === searchInput.current) {
220
+ event.preventDefault();
221
+ setSearch('');
222
+ setSelected(0);
223
+ surface.current?.focus();
224
+ }
225
+ }}
226
+ className="top-[12vh] w-[720px] max-w-[92vw] translate-y-0"
227
+ >
228
+ <DialogTitle className="sr-only">Leader Space</DialogTitle>
229
+ <CommandHeader className="gap-3 px-4 py-3">
230
+ <StatusBadge tone="accent" size="xs" className="tracking-widest">
231
+ {LEADER_PREFIX}
232
+ </StatusBadge>
233
+ <span data-testid="palette-path" className="text-[12px] font-bold text-doom-hi">
234
+ {keys.length === 0 ? 'Leader Space' : `${keys.join(' ')} · ${group?.label ?? ''}`}
235
+ </span>
236
+ <CommandInput
237
+ ref={searchInput}
238
+ data-testid="palette-filter"
239
+ value={search}
240
+ autoFocus={false}
241
+ placeholder="/ search commands…"
242
+ onChange={(event) => {
243
+ setSearch(event.target.value);
244
+ setSelected(0);
245
+ }}
246
+ onKeyDown={onSearchKey}
247
+ className="text-right"
248
+ />
249
+ <span data-testid="palette-count" className="text-[10px] text-doom-faint">
250
+ {rowCount}
251
+ </span>
252
+ </CommandHeader>
253
+
254
+ <div className="flex min-h-[260px]">
255
+ <CommandList data-testid="palette-keys" className="w-[280px] shrink-0 gap-0 border-r border-doom-border p-1">
256
+ {searching ? (
257
+ matches.length === 0 ? (
258
+ <CommandEmpty data-testid="palette-no-match">no command matches</CommandEmpty>
259
+ ) : (
260
+ matches.map((command, index) => (
261
+ <CommandItem
262
+ key={command.name}
263
+ data-testid={`palette-sub-${command.name}`}
264
+ active={index === cursor}
265
+ onMouseEnter={() => setSelected(index)}
266
+ onClick={() => invoke(command.name)}
267
+ className={cn('w-full gap-2.5 px-3 py-1.5', index === cursor && 'bg-doom-magenta/10')}
268
+ >
269
+ <span className="text-[11px] font-bold text-doom-blue">/{command.name}</span>
270
+ <CommandItemLabel className="text-[10px] text-doom-faint">{command.description}</CommandItemLabel>
271
+ </CommandItem>
272
+ ))
273
+ )
274
+ ) : options.length === 0 ? (
275
+ <CommandEmpty data-testid="palette-empty">
276
+ {keys.length === 0
277
+ ? 'no package in this bundle registered leader keys · / searches the session’s commands'
278
+ : 'nothing bound under this key'}
279
+ </CommandEmpty>
280
+ ) : (
281
+ options.map((option, index) => (
282
+ <CommandItem
283
+ key={option.key}
284
+ data-testid={`palette-item-${index}`}
285
+ data-key={option.key}
286
+ active={index === cursor}
287
+ onMouseEnter={() => setSelected(index)}
288
+ onClick={() => descend(option)}
289
+ className={cn('w-full gap-2.5 px-3 py-1.5', index === cursor && 'bg-doom-magenta/10')}
290
+ >
291
+ <Kbd
292
+ className={cn(
293
+ 'w-5 text-center',
294
+ index === cursor ? 'bg-doom-magenta/25 text-doom-magenta' : 'bg-doom-deep text-doom-dim',
295
+ )}
296
+ >
297
+ {option.key}
298
+ </Kbd>
299
+ <CommandItemLabel className="text-[11px] text-doom-text">{option.label}</CommandItemLabel>
300
+ {option.binding ? null : (
301
+ <span className="text-[9px] text-doom-faint">
302
+ {option.children.length} {option.children.length === 1 ? 'key' : 'keys'}
303
+ </span>
304
+ )}
305
+ </CommandItem>
306
+ ))
307
+ )}
308
+ {pluginEntries.length > 0 ? (
309
+ <>
310
+ <CommandGroupLabel>plugins</CommandGroupLabel>
311
+ {pluginEntries.map((command) => (
312
+ <CommandItem
313
+ key={command.id}
314
+ data-testid={`palette-plugin-${command.id}`}
315
+ onClick={() => runPluginCommand(command.id)}
316
+ className="w-full gap-2.5 px-3 py-1.5 hover:bg-doom-magenta/10"
317
+ >
318
+ <CommandItemLabel className="text-[11px] text-doom-text">{command.title}</CommandItemLabel>
319
+ {command.description ? (
320
+ <span className="truncate text-[9px] text-doom-faint">{command.description}</span>
321
+ ) : null}
322
+ </CommandItem>
323
+ ))}
324
+ </>
325
+ ) : null}
326
+ </CommandList>
327
+
328
+ <div data-testid="palette-detail" className="flex min-w-0 flex-1 flex-col gap-2 px-4 py-3">
329
+ {currentMatch ? (
330
+ <>
331
+ <span className="text-[12px] font-bold text-doom-blue">/{currentMatch.name}</span>
332
+ <span className="text-[11px] text-doom-faint">{currentMatch.description}</span>
333
+ </>
334
+ ) : null}
335
+ {current ? (
336
+ <>
337
+ <div className="flex items-center gap-2">
338
+ <span className="text-[12px] font-bold text-doom-magenta">{pathLabel([...keys, current.key])}</span>
339
+ <span className="text-[12px] text-doom-hi">{current.label}</span>
340
+ </div>
341
+ {current.detail ? <span className="text-[10px] text-doom-faint">{current.detail}</span> : null}
342
+ {current.binding ? (
343
+ <span data-testid="palette-target" className="text-[11px] font-bold text-doom-blue">
344
+ {'command' in current.binding ? `/${current.binding.command}` : 'in the cockpit'}
345
+ </span>
346
+ ) : (
347
+ <div className="flex flex-col gap-1">
348
+ {current.children.map((child) => (
349
+ <OptionRow
350
+ key={child.key}
351
+ data-testid={`palette-child-${child.key}`}
352
+ onClick={() => descend(child, [...keys, current.key])}
353
+ className="min-h-0 gap-3 border-doom-border-soft bg-doom-deep px-3 py-1.5 hover:border-doom-blue/50"
354
+ >
355
+ <span className="w-5 text-center text-[10px] font-bold text-doom-magenta">{child.key}</span>
356
+ <span className="text-[11px] font-bold text-doom-hi">{child.label}</span>
357
+ <OptionLabel className="truncate text-[10px] text-doom-faint">{child.detail}</OptionLabel>
358
+ </OptionRow>
359
+ ))}
360
+ </div>
361
+ )}
362
+ </>
363
+ ) : null}
364
+ </div>
365
+ </div>
366
+
367
+ <CommandFooter className="px-4 py-2.5">
368
+ <span className="flex items-center gap-1.5 text-[10px] text-doom-faint">
369
+ {searching ? (
370
+ <>
371
+ <Kbd>↑↓</Kbd> move · <Kbd>enter</Kbd> run · <Kbd>esc</Kbd> back to keys
372
+ </>
373
+ ) : (
374
+ <>
375
+ keys walk · <Kbd>⌫</Kbd> up · <Kbd>enter</Kbd> select · <Kbd>/</Kbd> search · <Kbd>esc</Kbd> close
376
+ </>
377
+ )}
378
+ </span>
379
+ <span className="text-[10px] text-doom-dim">
380
+ {bindings.length} keys · {commands.length} commands
381
+ </span>
382
+ </CommandFooter>
383
+ </DialogContent>
384
+ </Dialog>
385
+ );
386
+ }