@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,529 @@
1
+ import {
2
+ Button,
3
+ buttonVariants,
4
+ ChevronDownIcon,
5
+ cn,
6
+ Dot,
7
+ EmptyState,
8
+ handleOptionListKey,
9
+ Input,
10
+ Kbd,
11
+ OptionLabel,
12
+ OptionList,
13
+ optionListHint,
14
+ OptionRow,
15
+ Popover,
16
+ PopoverAnchor,
17
+ PopoverContent,
18
+ PopoverFooter,
19
+ PopoverHeader,
20
+ PopoverTrigger,
21
+ SectionLabel,
22
+ Spinner,
23
+ } from '@agimon-ai/doompi-web-components';
24
+ import { useStore } from '@tanstack/react-store';
25
+ import { useEffect, useState } from 'react';
26
+ import { PluginSurface } from '../../components/PluginSurface.tsx';
27
+ import { HOST_SLOTS } from '../../lib/pluginRegistry.ts';
28
+ import { minorModes, type MinorMode, selectionAxes } from '../../lib/composition.ts';
29
+ import type { AgentInfo, DialogRequest, ModelChoice } from '../../lib/sessionModel.ts';
30
+ import { focusPrompt } from '../../lib/promptFocus.ts';
31
+ import { parseSelection } from '../../lib/statusLine.ts';
32
+ import { menuStore, setPendingMenu } from '../../stores/menuStore.ts';
33
+ import {
34
+ answerDialogValue,
35
+ cancelDialog,
36
+ loadModelChoices,
37
+ runCommand,
38
+ selectModel,
39
+ selectThinkingLevel,
40
+ useActiveSession,
41
+ } from '../../stores/sessionStore.ts';
42
+ import { sessionsStore } from '../../stores/sessionsStore.ts';
43
+
44
+ // The known axes keep their mockup styling; a plugin-declared axis the host
45
+ // has no entry for reads in the neutral tone.
46
+ const AXIS_TONE: Readonly<Record<string, string>> = {
47
+ profile: 'text-doom-green',
48
+ domains: 'text-doom-violet',
49
+ };
50
+
51
+ const MENU_TITLE: Readonly<Record<string, string>> = {
52
+ mode: 'MAJOR MODE',
53
+ profile: 'PROFILE',
54
+ domains: 'DOMAINS',
55
+ minor: 'MINOR MODES',
56
+ };
57
+
58
+ const MENU_ACCENT: Readonly<Record<string, { border: string; title: string }>> = {
59
+ mode: { border: 'border-doom-edge-blue', title: 'text-doom-blue' },
60
+ profile: { border: 'border-doom-edge-green', title: 'text-doom-green' },
61
+ domains: { border: 'border-doom-edge-violet', title: 'text-doom-violet' },
62
+ minor: { border: 'border-doom-edge-magenta', title: 'text-doom-magenta' },
63
+ };
64
+
65
+ const DEFAULT_ACCENT = { border: 'border-doom-border', title: 'text-doom-hi' };
66
+
67
+ const AVAILABILITY_TONE: Readonly<Record<MinorMode['availability'], string>> = {
68
+ on: 'text-doom-hi',
69
+ off: 'text-doom-dim',
70
+ unavailable: 'text-doom-faint/60',
71
+ };
72
+
73
+ /**
74
+ * A select the user asked for from this bar, rendered as the button's own
75
+ * popover rather than a centered modal: the question came from here, so the
76
+ * answer belongs here. It is the same dialog and the same replies; only the
77
+ * frame differs.
78
+ */
79
+ function AxisMenu({ menu, dialog }: { menu: string; dialog: DialogRequest }) {
80
+ const accent = MENU_ACCENT[menu] ?? DEFAULT_ACCENT;
81
+ const [cursor, setCursor] = useState(0);
82
+ return (
83
+ <PopoverContent
84
+ side="top"
85
+ align="start"
86
+ data-testid="dialog"
87
+ data-dialog-method={dialog.method}
88
+ data-dialog-menu={menu}
89
+ onCloseAutoFocus={(event) => {
90
+ event.preventDefault();
91
+ focusPrompt();
92
+ }}
93
+ onKeyDown={(event) =>
94
+ handleOptionListKey(event, {
95
+ options: dialog.options,
96
+ cursor,
97
+ onCursorChange: setCursor,
98
+ onSelect: (option) => answerDialogValue(dialog.id, option),
99
+ })
100
+ }
101
+ className={`w-[420px] ${accent.border}`}
102
+ >
103
+ <PopoverHeader>
104
+ <span data-testid="dialog-title" className={`text-[10px] font-bold tracking-wide ${accent.title}`}>
105
+ {MENU_TITLE[menu] ?? menu.toUpperCase()}
106
+ </span>
107
+ <span className="truncate text-[9px] text-doom-faint">{dialog.title}</span>
108
+ </PopoverHeader>
109
+ <div className="flex max-h-[320px] min-h-0 flex-col">
110
+ <OptionList
111
+ options={dialog.options}
112
+ cursor={cursor}
113
+ onCursorChange={setCursor}
114
+ density="compact"
115
+ testIdPrefix="dialog-option"
116
+ onSelect={(option) => answerDialogValue(dialog.id, option)}
117
+ />
118
+ </div>
119
+ <PopoverFooter>
120
+ <span data-testid="dialog-hints" className="flex items-center gap-1.5">
121
+ {optionListHint(dialog.options.length)} · <Kbd>esc</Kbd> closes
122
+ </span>
123
+ <Button variant="ghost" size="xs" data-testid="dialog-cancel" onClick={() => cancelDialog(dialog.id)}>
124
+ cancel
125
+ </Button>
126
+ </PopoverFooter>
127
+ </PopoverContent>
128
+ );
129
+ }
130
+
131
+ /**
132
+ * One selection axis: a button that asks the agent, and the menu the agent
133
+ * answers with. While the question is in flight the button says so, because
134
+ * the round trip runs through the session and can take a moment.
135
+ */
136
+ function AxisButton({
137
+ name,
138
+ command,
139
+ pending,
140
+ claimedDialog,
141
+ children,
142
+ className,
143
+ }: {
144
+ name: string;
145
+ command: string;
146
+ pending: boolean;
147
+ claimedDialog: DialogRequest | null;
148
+ children: React.ReactNode;
149
+ className: string;
150
+ }) {
151
+ const open = claimedDialog !== null;
152
+ return (
153
+ <Popover
154
+ open={open}
155
+ onOpenChange={(next) => {
156
+ if (!next && claimedDialog) cancelDialog(claimedDialog.id);
157
+ }}
158
+ >
159
+ <PopoverAnchor asChild>
160
+ <Button
161
+ data-testid={`axis-${name}`}
162
+ data-pending={pending}
163
+ aria-haspopup="menu"
164
+ aria-expanded={open}
165
+ title={pending ? `asking the session for ${name}…` : `change ${name}`}
166
+ onClick={() => {
167
+ setPendingMenu(name);
168
+ runCommand(command);
169
+ }}
170
+ className={className}
171
+ >
172
+ {children}
173
+ {pending ? <Spinner className="h-[10px] w-[10px]" /> : <ChevronDownIcon className="h-[10px] w-[10px]" />}
174
+ </Button>
175
+ </PopoverAnchor>
176
+ {claimedDialog ? <AxisMenu menu={name} dialog={claimedDialog} /> : null}
177
+ </Popover>
178
+ );
179
+ }
180
+
181
+ /**
182
+ * The minor-modes popup: state is derived from the statuses the session
183
+ * already published, and every row is a button that hands the mode to the
184
+ * runtime's /minor command. The runtime owns what happens next: a mode with
185
+ * one opt-in toggles outright, one with several opens its action picker as a
186
+ * dialog, and modes compose, so any number can be on at once.
187
+ */
188
+ function MinorModesPopup({ modes, onClose }: { modes: MinorMode[]; onClose: () => void }) {
189
+ const on = modes.filter((mode) => mode.availability === 'on').length;
190
+ return (
191
+ <PopoverContent side="top" align="start" data-testid="minor-popup" className="w-[430px] border-doom-edge-magenta">
192
+ <PopoverHeader>
193
+ <SectionLabel className="tracking-wide text-doom-magenta">minor modes</SectionLabel>
194
+ <span className="text-[9px] text-doom-faint">{on} on</span>
195
+ </PopoverHeader>
196
+ <div
197
+ role="listbox"
198
+ aria-label="minor modes"
199
+ className="flex max-h-[320px] flex-col gap-0.5 overflow-y-auto p-1.5"
200
+ >
201
+ {modes.map((mode) => (
202
+ <OptionRow
203
+ key={mode.name}
204
+ density="compact"
205
+ active={mode.availability === 'on'}
206
+ data-testid={`minor-${mode.name}`}
207
+ data-availability={mode.availability}
208
+ // A mode that cannot run here says so on hover instead of taking a
209
+ // click and answering with a notice after the round trip.
210
+ disabled={mode.availability === 'unavailable'}
211
+ title={mode.availability === 'unavailable' ? mode.unavailableReason : `drive ${mode.name}`}
212
+ onClick={() => {
213
+ // The runtime may answer with an opt-in picker; claiming the menu
214
+ // keeps that question on this chip, where it was asked, instead
215
+ // of throwing it to the middle of the screen.
216
+ setPendingMenu('minor');
217
+ runCommand(`/minor ${mode.id}`);
218
+ onClose();
219
+ }}
220
+ className={cn(
221
+ 'w-full gap-2.5 py-1.5 transition-colors hover:bg-doom-deep focus-visible:bg-doom-deep disabled:cursor-not-allowed',
222
+ mode.availability === 'on' && 'bg-doom-tint-magenta hover:brightness-125',
223
+ )}
224
+ >
225
+ <Kbd
226
+ className={`w-8 justify-center ${
227
+ mode.availability === 'on' ? 'bg-doom-magenta/25 text-doom-magenta' : ''
228
+ }`}
229
+ >
230
+ {mode.keys}
231
+ </Kbd>
232
+ <OptionLabel density="compact" className={AVAILABILITY_TONE[mode.availability]}>
233
+ {mode.name}
234
+ </OptionLabel>
235
+ {mode.detail ? (
236
+ <span data-testid={`minor-detail-${mode.name}`} className="truncate text-[9px] text-doom-magenta">
237
+ {mode.detail}
238
+ </span>
239
+ ) : null}
240
+ {mode.availability === 'unavailable' && mode.unavailableReason ? (
241
+ <span
242
+ data-testid={`minor-reason-${mode.name}`}
243
+ className="min-w-0 flex-1 truncate text-right text-[9px] text-doom-faint/70"
244
+ >
245
+ {mode.unavailableReason}
246
+ </span>
247
+ ) : null}
248
+ {mode.availability === 'unavailable' ? (
249
+ <span className="shrink-0 text-[8px] text-doom-faint/60">n/a</span>
250
+ ) : (
251
+ <Dot tone={mode.availability === 'on' ? 'magenta' : 'muted'} />
252
+ )}
253
+ </OptionRow>
254
+ ))}
255
+ </div>
256
+ <PopoverFooter>
257
+ <span>click drives a mode · one with several opt-ins asks · esc closes</span>
258
+ </PopoverFooter>
259
+ </PopoverContent>
260
+ );
261
+ }
262
+
263
+ /**
264
+ * The model popup: the models Pi can switch to and the thinking levels the
265
+ * current model accepts, both asked for on open because Pi only reports them
266
+ * on request. A pick goes straight to the RPC verbs; the chip updates from the
267
+ * get_state that follows, so it never shows a choice the agent refused.
268
+ */
269
+ function ModelPopup({
270
+ agent,
271
+ models,
272
+ levels,
273
+ onClose,
274
+ }: {
275
+ agent: AgentInfo | null;
276
+ models: ModelChoice[];
277
+ levels: string[];
278
+ onClose: () => void;
279
+ }) {
280
+ const [filter, setFilter] = useState('');
281
+
282
+ useEffect(() => {
283
+ loadModelChoices();
284
+ }, []);
285
+
286
+ const needle = filter.trim().toLowerCase();
287
+ const shown = needle
288
+ ? models.filter((model) => `${model.provider} ${model.id} ${model.name}`.toLowerCase().includes(needle))
289
+ : models;
290
+
291
+ return (
292
+ <PopoverContent side="top" align="end" data-testid="model-popup" className="w-[400px] border-doom-edge-yellow">
293
+ <PopoverHeader>
294
+ <SectionLabel className="tracking-wide text-doom-yellow">model</SectionLabel>
295
+ <span className="truncate text-[9px] text-doom-faint">{agent ? `${agent.provider}/${agent.model}` : ''}</span>
296
+ </PopoverHeader>
297
+ <div className="border-b border-doom-border-soft p-1.5">
298
+ <Input
299
+ data-testid="model-filter"
300
+ value={filter}
301
+ autoFocus
302
+ placeholder="filter models…"
303
+ onChange={(event) => setFilter(event.target.value)}
304
+ className="w-full px-2 py-1 text-[11px]"
305
+ />
306
+ </div>
307
+ <div role="listbox" aria-label="models" className="flex max-h-[260px] flex-col gap-0.5 overflow-y-auto p-1.5">
308
+ {models.length === 0 ? (
309
+ <span className="flex items-center gap-2 px-2 py-1.5 text-[10px] text-doom-faint">
310
+ <Spinner label="asking the session for its models" />
311
+ asking the session for its models…
312
+ </span>
313
+ ) : shown.length === 0 ? (
314
+ <EmptyState className="py-4" title="no model matches" />
315
+ ) : null}
316
+ {shown.map((model) => {
317
+ const current = agent?.model === model.id && agent.provider === model.provider;
318
+ return (
319
+ <OptionRow
320
+ key={`${model.provider}/${model.id}`}
321
+ density="compact"
322
+ active={current}
323
+ data-testid={`model-${model.provider}-${model.id}`}
324
+ data-current={current}
325
+ onClick={() => {
326
+ selectModel(model.provider, model.id);
327
+ onClose();
328
+ }}
329
+ className={cn(
330
+ 'w-full gap-2.5 py-1.5 transition-colors hover:bg-doom-deep focus-visible:bg-doom-deep',
331
+ current && 'bg-doom-tint-yellow hover:brightness-125',
332
+ )}
333
+ >
334
+ <span className="w-20 shrink-0 truncate text-[9px] text-doom-faint">{model.provider}</span>
335
+ <OptionLabel density="compact" className={current ? 'text-doom-yellow' : 'text-doom-hi'}>
336
+ {model.id}
337
+ </OptionLabel>
338
+ {model.reasoning ? <span className="text-[8px] text-doom-faint">thinks</span> : null}
339
+ <Dot tone={current ? 'yellow' : 'muted'} />
340
+ </OptionRow>
341
+ );
342
+ })}
343
+ </div>
344
+ <PopoverFooter className="flex-wrap justify-start gap-1 py-1.5">
345
+ <SectionLabel className="mr-1 tracking-wide">thinking</SectionLabel>
346
+ {levels.length === 0 ? <span className="text-[9px] text-doom-faint">…</span> : null}
347
+ {levels.map((level) => {
348
+ const current = agent?.thinkingLevel === level;
349
+ return (
350
+ <Button
351
+ key={level}
352
+ variant="ghost"
353
+ size="xs"
354
+ data-testid={`thinking-${level}`}
355
+ data-current={current}
356
+ onClick={() => {
357
+ selectThinkingLevel(level);
358
+ onClose();
359
+ }}
360
+ className={cn(
361
+ 'text-[10px]',
362
+ current && 'bg-doom-yellow/25 font-bold text-doom-yellow hover:bg-doom-yellow/25',
363
+ )}
364
+ >
365
+ {level}
366
+ </Button>
367
+ );
368
+ })}
369
+ </PopoverFooter>
370
+ </PopoverContent>
371
+ );
372
+ }
373
+
374
+ /**
375
+ * The mockup's bottom bar: the DoomPi selection axes as buttons, the model,
376
+ * and the context gauge. Buttons route through the same slash commands the
377
+ * TUI uses; the agent's select dialog then opens as that button's popover.
378
+ */
379
+ export function SelectionBar() {
380
+ const activeId = useStore(sessionsStore, (state) => state.activeId);
381
+ const raw = useActiveSession((state) => state.statuses['doom-major-mode'] ?? '');
382
+ const statuses = useActiveSession((state) => state.statuses);
383
+ const widgets = useActiveSession((state) => state.widgets);
384
+ const catalog = useActiveSession((state) => state.minorModes);
385
+ const agent = useActiveSession((state) => state.agent);
386
+ const stats = useActiveSession((state) => state.stats);
387
+ const models = useActiveSession((state) => state.models);
388
+ const thinkingLevels = useActiveSession((state) => state.thinkingLevels);
389
+ const dialog = useActiveSession((state) => state.dialog);
390
+ const pendingMenu = useStore(menuStore, (state) => state.pending);
391
+ const claimed = useStore(menuStore, (state) => state.claimed);
392
+ const [minorOpen, setMinorOpen] = useState(false);
393
+ const [modelOpen, setModelOpen] = useState(false);
394
+
395
+ const selection = parseSelection(raw);
396
+ const axes = selectionAxes(statuses);
397
+ const modes = minorModes(statuses, widgets, catalog);
398
+ const activeMinors = modes.filter((mode) => mode.availability === 'on');
399
+
400
+ /** The dialog this axis asked for, when the claim named it and it is still open. */
401
+ const dialogFor = (name: string): DialogRequest | null =>
402
+ dialog !== null && claimed !== null && claimed.dialogId === dialog.id && claimed.menu === name ? dialog : null;
403
+ const minorDialog = dialogFor('minor');
404
+
405
+ const modeClass = cn(
406
+ buttonVariants({ variant: 'primary', size: 'sm' }),
407
+ 'h-[21px] rounded-[3px] px-2 text-doom-rail',
408
+ selection.pending && 'bg-doom-yellow',
409
+ );
410
+ const axisClass = cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'h-[21px] min-w-0 rounded-[3px] px-2');
411
+
412
+ return (
413
+ <footer
414
+ data-testid="selection-bar"
415
+ data-pending={selection.pending}
416
+ className="relative flex h-[34px] shrink-0 items-center gap-2 border-t border-doom-border bg-doom-rail px-3.5"
417
+ >
418
+ <AxisButton
419
+ name="mode"
420
+ command="mode"
421
+ pending={pendingMenu === 'mode'}
422
+ claimedDialog={dialogFor('mode')}
423
+ className={modeClass}
424
+ >
425
+ <span data-testid="selection-mode" className="text-[10px] font-bold tracking-[0.08em]">
426
+ {(selection.majorMode || 'mode').toUpperCase()}
427
+ </span>
428
+ </AxisButton>
429
+
430
+ {axes.map((axis) => (
431
+ <AxisButton
432
+ key={axis.name}
433
+ name={axis.name}
434
+ command={axis.command}
435
+ pending={pendingMenu === axis.name}
436
+ claimedDialog={dialogFor(axis.name)}
437
+ className={axisClass}
438
+ >
439
+ <span
440
+ data-testid={`selection-${axis.name}`}
441
+ className={`truncate text-[10px] ${axis.multi ? '' : 'font-bold'} ${
442
+ axis.values.length > 0 ? (AXIS_TONE[axis.name] ?? 'text-doom-hi') : 'text-doom-faint'
443
+ }`}
444
+ >
445
+ {axis.values.length === 0 ? axis.emptyLabel : axis.multi ? axis.values.join(', ') : `*${axis.values[0]}*`}
446
+ </span>
447
+ </AxisButton>
448
+ ))}
449
+
450
+ <Popover
451
+ open={minorOpen || minorDialog !== null}
452
+ onOpenChange={(next) => {
453
+ if (next) {
454
+ setMinorOpen(true);
455
+ return;
456
+ }
457
+ if (minorDialog) cancelDialog(minorDialog.id);
458
+ setMinorOpen(false);
459
+ }}
460
+ >
461
+ <PopoverTrigger asChild>
462
+ <Button
463
+ data-testid="axis-minor"
464
+ title="minor modes"
465
+ variant={activeMinors.length > 0 ? 'subtle' : 'outline'}
466
+ size="sm"
467
+ className={cn(
468
+ 'h-[21px] rounded-[3px] px-2',
469
+ activeMinors.length > 0
470
+ ? 'bg-doom-tint-magenta text-doom-magenta hover:bg-doom-tint-magenta hover:brightness-125'
471
+ : 'text-doom-faint',
472
+ )}
473
+ >
474
+ <span data-testid="minor-summary" className="text-[10px] font-bold">
475
+ {activeMinors[0]?.name ?? 'minor'}
476
+ </span>
477
+ {activeMinors.length > 1 ? <span className="text-[9px]">+{activeMinors.length - 1}</span> : null}
478
+ <ChevronDownIcon className="h-[10px] w-[10px]" />
479
+ </Button>
480
+ </PopoverTrigger>
481
+ {minorDialog ? (
482
+ <AxisMenu menu="minor" dialog={minorDialog} />
483
+ ) : minorOpen ? (
484
+ <MinorModesPopup modes={modes} onClose={() => setMinorOpen(false)} />
485
+ ) : null}
486
+ </Popover>
487
+
488
+ <PluginSurface slot={HOST_SLOTS.selectionBar} sessionId={activeId} />
489
+
490
+ <div className="min-w-0 flex-1" />
491
+
492
+ <Popover open={modelOpen} onOpenChange={setModelOpen}>
493
+ <PopoverTrigger asChild>
494
+ <Button data-testid="axis-model" title="model and thinking level" className={axisClass}>
495
+ <span data-testid="agent-model" className="text-[10px] text-doom-hi">
496
+ {agent?.model ?? '—'}
497
+ </span>
498
+ <span data-testid="agent-thinking" className="text-[10px] text-doom-yellow">
499
+ {agent?.thinkingLevel ?? ''}
500
+ </span>
501
+ <ChevronDownIcon className="h-[10px] w-[10px] text-doom-faint" />
502
+ </Button>
503
+ </PopoverTrigger>
504
+ {modelOpen ? (
505
+ <ModelPopup agent={agent} models={models} levels={thinkingLevels} onClose={() => setModelOpen(false)} />
506
+ ) : null}
507
+ </Popover>
508
+
509
+ <span
510
+ data-testid="top-context"
511
+ title={
512
+ stats?.contextTokens != null && stats.contextWindow != null
513
+ ? `${stats.contextTokens.toLocaleString()} of ${stats.contextWindow.toLocaleString()} tokens`
514
+ : 'context usage, once the session reports it'
515
+ }
516
+ className="text-[10px] text-doom-dim"
517
+ >
518
+ {stats?.contextPercent == null ? 'ctx —' : `ctx ${Math.round(stats.contextPercent)}%`}
519
+ </span>
520
+ <span
521
+ data-testid="top-cost"
522
+ title={stats ? `${stats.totalTokens.toLocaleString()} tokens this session` : ''}
523
+ className="text-[10px] text-doom-dim"
524
+ >
525
+ {stats ? `$${stats.cost.toFixed(2)}` : ''}
526
+ </span>
527
+ </footer>
528
+ );
529
+ }