@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,221 @@
1
+ import {
2
+ Button,
3
+ Dialog,
4
+ DialogBody,
5
+ DialogContent,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ Input,
10
+ Spinner,
11
+ } from '@agimon-ai/doompi-web-components';
12
+ import { useEffect, useState } from 'react';
13
+ import type { AuthMethodType, LoginEvent, LoginFlowSnapshot, LoginPromptView } from '../../../types/auth.ts';
14
+
15
+ export const METHOD_LABEL: Readonly<Record<AuthMethodType, string>> = { api_key: 'api key', oauth: 'oauth' };
16
+
17
+ /** Button's link variant plus the wrapping a full auth URL needs. */
18
+ const linkClass = 'inline break-all decoration-doom-blue/40 hover:decoration-doom-blue';
19
+
20
+ function FlowEvent({ event }: { event: LoginEvent }) {
21
+ if (event.type === 'auth_url') {
22
+ return (
23
+ <div className="flex flex-col gap-1 text-[11px] text-doom-text">
24
+ <span>open this link to authorize:</span>
25
+ <Button asChild variant="link" size="xs" className={linkClass}>
26
+ <a data-testid="login-flow-auth-url" href={event.url} target="_blank" rel="noreferrer">
27
+ {event.url}
28
+ </a>
29
+ </Button>
30
+ {event.instructions ? <span className="text-doom-dim">{event.instructions}</span> : null}
31
+ </div>
32
+ );
33
+ }
34
+ if (event.type === 'device_code') {
35
+ return (
36
+ <div className="flex flex-col gap-1 text-[11px] text-doom-text">
37
+ <span>
38
+ enter code{' '}
39
+ <span data-testid="login-flow-device-code" className="font-bold text-doom-hi">
40
+ {event.userCode}
41
+ </span>{' '}
42
+ at{' '}
43
+ <a href={event.verificationUri} target="_blank" rel="noreferrer" className={linkClass}>
44
+ {event.verificationUri}
45
+ </a>
46
+ </span>
47
+ </div>
48
+ );
49
+ }
50
+ if (event.type === 'info') {
51
+ return (
52
+ <div className="flex flex-col gap-1 text-[11px] text-doom-text">
53
+ <span>{event.message}</span>
54
+ {event.links?.map((link) => (
55
+ <a key={link.url} href={link.url} target="_blank" rel="noreferrer" className={linkClass}>
56
+ {link.label ?? link.url}
57
+ </a>
58
+ ))}
59
+ </div>
60
+ );
61
+ }
62
+ return <span className="text-[11px] text-doom-dim">{event.message}</span>;
63
+ }
64
+
65
+ function PromptField({
66
+ prompt,
67
+ value,
68
+ onChange,
69
+ onSubmit,
70
+ onSelect,
71
+ }: {
72
+ prompt: LoginPromptView;
73
+ value: string;
74
+ onChange: (value: string) => void;
75
+ onSubmit: () => void;
76
+ onSelect: (optionId: string) => void;
77
+ }) {
78
+ if (prompt.type === 'select') {
79
+ return (
80
+ <div data-testid="login-prompt" data-prompt-type={prompt.type} className="flex flex-col gap-2">
81
+ <span className="text-[11px] text-doom-text">{prompt.message}</span>
82
+ <div className="flex flex-col gap-1">
83
+ {(prompt.options ?? []).map((option) => (
84
+ <Button
85
+ key={option.id}
86
+ variant="outline"
87
+ data-testid={`login-prompt-option-${option.id}`}
88
+ onClick={() => onSelect(option.id)}
89
+ className="h-auto flex-col items-start gap-0.5 px-2.5 py-1.5 text-left whitespace-normal focus-visible:border-doom-blue/50 focus-visible:ring-0"
90
+ >
91
+ <span className="text-[11px] text-doom-hi">{option.label}</span>
92
+ {option.description ? <span className="text-[10px] text-doom-faint">{option.description}</span> : null}
93
+ </Button>
94
+ ))}
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+ return (
100
+ <label data-testid="login-prompt" data-prompt-type={prompt.type} className="flex flex-col gap-1">
101
+ <span className="text-[11px] text-doom-text">{prompt.message}</span>
102
+ <div className="flex items-center gap-2">
103
+ <Input
104
+ data-testid="login-prompt-input"
105
+ type={prompt.type === 'secret' ? 'password' : 'text'}
106
+ value={value}
107
+ autoFocus
108
+ autoComplete="off"
109
+ placeholder={prompt.placeholder}
110
+ onChange={(event) => onChange(event.target.value)}
111
+ onKeyDown={(event) => {
112
+ if (event.key === 'Enter') onSubmit();
113
+ }}
114
+ className="flex-1"
115
+ />
116
+ <Button variant="primary" data-testid="login-prompt-submit" disabled={value.trim() === ''} onClick={onSubmit}>
117
+ continue
118
+ </Button>
119
+ </div>
120
+ </label>
121
+ );
122
+ }
123
+
124
+ /**
125
+ * A provider's login, rendered from the hub's snapshots: the links and codes
126
+ * it announced, the one question it is waiting on, and how it ended.
127
+ */
128
+ export function LoginFlowDialog({
129
+ flow,
130
+ onAnswer,
131
+ onCancel,
132
+ onClose,
133
+ }: {
134
+ flow: LoginFlowSnapshot;
135
+ onAnswer: (promptId: string, value: string) => void;
136
+ onCancel: () => void;
137
+ onClose: () => void;
138
+ }) {
139
+ const [value, setValue] = useState('');
140
+ const prompt = flow.prompt;
141
+ const promptId = prompt?.id;
142
+ const running = flow.status === 'running';
143
+
144
+ // A new question starts from an empty field.
145
+ useEffect(() => {
146
+ setValue('');
147
+ }, [promptId]);
148
+
149
+ const submit = (): void => {
150
+ if (!prompt || value.trim() === '') return;
151
+ onAnswer(prompt.id, value);
152
+ };
153
+
154
+ let outcome: { text: string; className: string } | undefined;
155
+ if (flow.status === 'succeeded')
156
+ outcome = { text: `authenticated with ${flow.providerName}.`, className: 'text-doom-green' };
157
+ else if (flow.status === 'failed') outcome = { text: flow.error ?? 'sign-in failed.', className: 'text-doom-red' };
158
+ else if (flow.status === 'cancelled') outcome = { text: 'sign-in cancelled.', className: 'text-doom-dim' };
159
+
160
+ return (
161
+ <Dialog
162
+ open
163
+ onOpenChange={(next) => {
164
+ if (next) return;
165
+ // Escape or an outside click abandons a live sign-in, and simply
166
+ // dismisses a finished one.
167
+ if (running) onCancel();
168
+ else onClose();
169
+ }}
170
+ >
171
+ <DialogContent width="md" data-testid="login-flow" data-status={flow.status} aria-describedby={undefined}>
172
+ <DialogHeader>
173
+ <DialogTitle>sign in to {flow.providerName}</DialogTitle>
174
+ <span className="text-[10px] text-doom-faint">{METHOD_LABEL[flow.type]}</span>
175
+ </DialogHeader>
176
+ <DialogBody>
177
+ {flow.events.length > 0 ? (
178
+ <ol data-testid="login-flow-events" className="flex flex-col gap-2">
179
+ {flow.events.map((event, index) => (
180
+ <li key={index}>
181
+ <FlowEvent event={event} />
182
+ </li>
183
+ ))}
184
+ </ol>
185
+ ) : null}
186
+ {running && prompt ? (
187
+ <PromptField
188
+ prompt={prompt}
189
+ value={value}
190
+ onChange={setValue}
191
+ onSubmit={submit}
192
+ onSelect={(optionId) => onAnswer(prompt.id, optionId)}
193
+ />
194
+ ) : null}
195
+ {running && !prompt ? (
196
+ <p data-testid="login-flow-waiting" className="flex items-center gap-2 text-[11px] text-doom-dim">
197
+ <Spinner />
198
+ {flow.events.length === 0 ? 'starting…' : `waiting for ${flow.providerName}…`}
199
+ </p>
200
+ ) : null}
201
+ {outcome ? (
202
+ <p data-testid="login-flow-result" className={`text-[11px] ${outcome.className}`}>
203
+ {outcome.text}
204
+ </p>
205
+ ) : null}
206
+ <DialogFooter>
207
+ {running ? (
208
+ <Button variant="outline" data-testid="login-flow-cancel" onClick={onCancel}>
209
+ cancel
210
+ </Button>
211
+ ) : (
212
+ <Button variant="outline" data-testid="login-flow-close" autoFocus onClick={onClose}>
213
+ close
214
+ </Button>
215
+ )}
216
+ </DialogFooter>
217
+ </DialogBody>
218
+ </DialogContent>
219
+ </Dialog>
220
+ );
221
+ }
@@ -0,0 +1,92 @@
1
+ import { Panel, SectionLabel } from '@agimon-ai/doompi-web-components';
2
+ import type { WebPluginDefinition } from '@agimon-ai/doompi-web-contracts';
3
+ import { installedWebPlugins, webPluginDiagnostics } from '../../lib/pluginRegistry.ts';
4
+
5
+ /** What one plugin contributes, as counts: enough to see it landed without opening its code. */
6
+ function contributions(plugin: WebPluginDefinition): string[] {
7
+ const parts: string[] = [];
8
+ const count = (label: string, list: readonly unknown[] | undefined): void => {
9
+ if (list !== undefined && list.length > 0) parts.push(`${String(list.length)} ${label}`);
10
+ };
11
+ count('tabs', plugin.tabs);
12
+ count('channels', plugin.channels);
13
+ count('tool renderers', plugin.toolRenderers);
14
+ count('activity groups', plugin.activityGroups);
15
+ count('activity sections', plugin.activitySections);
16
+ count('minor modes', plugin.minorModes);
17
+ count('selection axes', plugin.selectionAxes);
18
+ count('leader bindings', plugin.leaderBindings);
19
+ count('slots', plugin.slots);
20
+ count('fills', plugin.fills);
21
+ return parts;
22
+ }
23
+
24
+ /**
25
+ * The plugins page: the web plugins this bundle carries and what the install
26
+ * had to resolve between them. Both are read once from the registry, which
27
+ * is complete before the first render; a plugin comes or goes with the next
28
+ * doompi sync, never at runtime.
29
+ */
30
+ export function PluginSettings() {
31
+ const plugins = installedWebPlugins();
32
+ const diagnostics = webPluginDiagnostics();
33
+ return (
34
+ <div data-testid="settings-plugins" className="flex max-w-[640px] flex-col gap-5">
35
+ <div className="flex flex-col gap-2">
36
+ <SectionLabel>installed</SectionLabel>
37
+ <p className="text-[11px] leading-relaxed text-doom-faint">
38
+ the web plugins compiled into this bundle; doompi sync rebuilds it from the packages installed in the
39
+ composition.
40
+ </p>
41
+ {plugins.length === 0 ? (
42
+ <p data-testid="settings-plugins-empty" className="text-[11px] text-doom-dim">
43
+ no web plugins in this bundle. run doompi sync to bundle the installed packages.
44
+ </p>
45
+ ) : (
46
+ <ul className="flex flex-col gap-1">
47
+ {plugins.map((plugin) => (
48
+ <Panel asChild key={plugin.id} className="flex items-baseline gap-3 bg-transparent px-3 py-2">
49
+ <li data-testid={`settings-plugin-${plugin.id}`}>
50
+ <span className="text-[12px] font-bold text-doom-hi">{plugin.id}</span>
51
+ <span className="min-w-0 flex-1 truncate text-[10px] text-doom-faint">
52
+ {contributions(plugin).join(' · ') || 'no contributions'}
53
+ </span>
54
+ </li>
55
+ </Panel>
56
+ ))}
57
+ </ul>
58
+ )}
59
+ </div>
60
+
61
+ <div className="flex flex-col gap-2">
62
+ <SectionLabel>install diagnostics</SectionLabel>
63
+ <p className="text-[11px] leading-relaxed text-doom-faint">
64
+ plugins are independent, so when two want the same tab, tool, group, or key the install keeps one and records
65
+ the other here rather than failing the page.
66
+ </p>
67
+ {diagnostics.length === 0 ? (
68
+ <p data-testid="settings-plugin-diagnostics-empty" className="text-[11px] text-doom-dim">
69
+ nothing to resolve: no two plugins wanted the same name.
70
+ </p>
71
+ ) : (
72
+ <ul className="flex flex-col gap-1">
73
+ {diagnostics.map((diagnostic, index) => (
74
+ <Panel
75
+ asChild
76
+ key={`${diagnostic.pluginId}:${diagnostic.kind}:${String(index)}`}
77
+ className="flex flex-col gap-0.5 bg-transparent px-3 py-2"
78
+ >
79
+ <li data-testid={`settings-plugin-diagnostic-${diagnostic.pluginId}`} data-kind={diagnostic.kind}>
80
+ <span className="text-[10px] font-bold text-doom-yellow">
81
+ {diagnostic.pluginId} · {diagnostic.kind}
82
+ </span>
83
+ <span className="text-[10px] leading-snug text-doom-dim">{diagnostic.message}</span>
84
+ </li>
85
+ </Panel>
86
+ ))}
87
+ </ul>
88
+ )}
89
+ </div>
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,238 @@
1
+ import { Badge, Button, Dot, EmptyState, Input, Panel, Spinner } from '@agimon-ai/doompi-web-components';
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import type { AuthMethodType, LoginFlowSnapshot, ProviderAuthSummary } from '../../../types/auth.ts';
4
+ import { answerLogin, cancelLogin, listProviders, logoutProvider, readLogin, startLogin } from '../../lib/authApi.ts';
5
+ import { LoginFlowDialog, METHOD_LABEL } from './LoginFlowDialog.tsx';
6
+
7
+ const FLOW_POLL_MS = 500;
8
+ /** The auth source Pi reports for a credential its own /login stored; the only kind /logout removes. */
9
+ const STORED_SOURCE = 'stored';
10
+
11
+ function statusText(provider: ProviderAuthSummary): string {
12
+ if (!provider.authenticated) return 'not authenticated';
13
+ const { type, source } = provider.authenticated;
14
+ const via = source === STORED_SOURCE ? 'stored' : source;
15
+ return `authenticated · ${METHOD_LABEL[type]}${via ? ` · ${via}` : ''}`;
16
+ }
17
+
18
+ function ProviderRow({
19
+ provider,
20
+ busy,
21
+ onLogin,
22
+ onLogout,
23
+ }: {
24
+ provider: ProviderAuthSummary;
25
+ busy: boolean;
26
+ onLogin: (type: AuthMethodType) => void;
27
+ onLogout: () => void;
28
+ }) {
29
+ const authenticated = provider.authenticated !== undefined;
30
+ return (
31
+ <Panel asChild className="flex items-center gap-3 px-3.5 py-2.5 transition-colors hover:border-doom-blue/30">
32
+ <li data-testid={`provider-${provider.id}`} data-authenticated={authenticated}>
33
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5">
34
+ <span className="truncate text-[12px] font-bold text-doom-hi">{provider.name}</span>
35
+ <span className="truncate text-[10px] text-doom-faint">{provider.id}</span>
36
+ </div>
37
+ <Badge tone={authenticated ? 'green' : 'neutral'} data-testid={`provider-status-${provider.id}`}>
38
+ <Dot tone={authenticated ? 'green' : 'neutral'} />
39
+ {statusText(provider)}
40
+ </Badge>
41
+ {provider.methods.map((method) => (
42
+ <Button
43
+ key={method.type}
44
+ variant="outline"
45
+ size="sm"
46
+ data-testid={`provider-login-${method.type}-${provider.id}`}
47
+ title={method.label}
48
+ disabled={busy}
49
+ onClick={() => onLogin(method.type)}
50
+ >
51
+ sign in · {METHOD_LABEL[method.type]}
52
+ </Button>
53
+ ))}
54
+ {provider.authenticated?.source === STORED_SOURCE ? (
55
+ <Button
56
+ variant="outline"
57
+ size="sm"
58
+ data-testid={`provider-logout-${provider.id}`}
59
+ disabled={busy}
60
+ onClick={onLogout}
61
+ className="hover:border-doom-red/50 hover:text-doom-red"
62
+ >
63
+ sign out
64
+ </Button>
65
+ ) : null}
66
+ {provider.methods.length === 0 ? (
67
+ <span className="text-[10px] text-doom-faint">ambient credentials only</span>
68
+ ) : null}
69
+ </li>
70
+ </Panel>
71
+ );
72
+ }
73
+
74
+ /**
75
+ * The providers page: what Pi can sign in to on the hub's machine, and
76
+ * whether it has. A login runs on the hub; this page polls its flow and
77
+ * relays the one question it may ask.
78
+ */
79
+ export function ProviderSettings() {
80
+ const [providers, setProviders] = useState<ProviderAuthSummary[] | null>(null);
81
+ const [error, setError] = useState('');
82
+ const [flow, setFlow] = useState<LoginFlowSnapshot | null>(null);
83
+ /** The provider with a request in flight, so its buttons cannot double-fire. */
84
+ const [busyId, setBusyId] = useState<string | null>(null);
85
+ const [filter, setFilter] = useState('');
86
+
87
+ const reload = useCallback(async (): Promise<void> => {
88
+ const result = await listProviders();
89
+ if ('providers' in result) {
90
+ setProviders(result.providers);
91
+ setError('');
92
+ } else {
93
+ setError(result.error);
94
+ }
95
+ }, []);
96
+
97
+ useEffect(() => {
98
+ void reload();
99
+ }, [reload]);
100
+
101
+ // Poll the running flow one request at a time; once it settles, the list
102
+ // is re-read so the provider's chip reflects the outcome.
103
+ const flowId = flow?.id;
104
+ const running = flow?.status === 'running';
105
+ useEffect(() => {
106
+ if (flowId === undefined || !running) return;
107
+ let stopped = false;
108
+ let timer: ReturnType<typeof setTimeout> | undefined;
109
+ const poll = async (): Promise<void> => {
110
+ const result = await readLogin(flowId);
111
+ if (stopped) return;
112
+ if ('error' in result) {
113
+ setFlow((current) =>
114
+ current && current.id === flowId ? { ...current, status: 'failed', error: result.error } : current,
115
+ );
116
+ return;
117
+ }
118
+ setFlow(result.flow);
119
+ if (result.flow.status !== 'running') {
120
+ void reload();
121
+ return;
122
+ }
123
+ timer = setTimeout(() => void poll(), FLOW_POLL_MS);
124
+ };
125
+ timer = setTimeout(() => void poll(), FLOW_POLL_MS);
126
+ return () => {
127
+ stopped = true;
128
+ if (timer !== undefined) clearTimeout(timer);
129
+ };
130
+ }, [flowId, running, reload]);
131
+
132
+ const begin = async (providerId: string, type: AuthMethodType): Promise<void> => {
133
+ setBusyId(providerId);
134
+ const result = await startLogin(providerId, type);
135
+ setBusyId(null);
136
+ if ('flow' in result) setFlow(result.flow);
137
+ else setError(result.error);
138
+ };
139
+
140
+ const answer = async (promptId: string, value: string): Promise<void> => {
141
+ if (!flow) return;
142
+ const result = await answerLogin(flow.id, promptId, value);
143
+ if ('flow' in result) setFlow(result.flow);
144
+ else setError(result.error);
145
+ };
146
+
147
+ const cancel = async (): Promise<void> => {
148
+ if (!flow) return;
149
+ const result = await cancelLogin(flow.id);
150
+ setFlow('flow' in result ? result.flow : null);
151
+ void reload();
152
+ };
153
+
154
+ const signOut = async (providerId: string): Promise<void> => {
155
+ setBusyId(providerId);
156
+ const result = await logoutProvider(providerId);
157
+ if ('error' in result) setError(result.error);
158
+ else await reload();
159
+ setBusyId(null);
160
+ };
161
+
162
+ // The list is long enough that finding a provider by eye is work; signed-in
163
+ // ones lead because they are the ones a reader came to check.
164
+ const needle = filter.trim().toLowerCase();
165
+ const shown = (providers ?? [])
166
+ .filter((provider) => `${provider.name} ${provider.id}`.toLowerCase().includes(needle))
167
+ .sort((left, right) => Number(right.authenticated !== undefined) - Number(left.authenticated !== undefined));
168
+ const authenticatedCount = (providers ?? []).filter((provider) => provider.authenticated !== undefined).length;
169
+
170
+ return (
171
+ <div data-testid="provider-settings" className="flex max-w-[780px] flex-col gap-4">
172
+ <div className="flex flex-col gap-1">
173
+ <h2 className="text-[13px] font-bold text-doom-hi">providers</h2>
174
+ <p className="text-[11px] leading-relaxed text-doom-dim">
175
+ sign in to the model providers Pi can use. credentials land in Pi&apos;s auth.json on this machine, so every
176
+ session shares them.
177
+ </p>
178
+ </div>
179
+ {error ? (
180
+ <p data-testid="provider-settings-error" className="text-[11px] leading-relaxed text-doom-red">
181
+ {error}
182
+ </p>
183
+ ) : null}
184
+ {providers === null && !error ? (
185
+ <p className="flex items-center gap-2 text-[11px] text-doom-faint">
186
+ <Spinner label="reading providers" />
187
+ reading providers…
188
+ </p>
189
+ ) : null}
190
+ {providers ? (
191
+ <>
192
+ <div className="flex items-center gap-3">
193
+ <Input
194
+ data-testid="provider-filter"
195
+ value={filter}
196
+ placeholder="filter providers…"
197
+ onChange={(event) => setFilter(event.target.value)}
198
+ className="flex-1"
199
+ />
200
+ <span className="shrink-0 text-[10px] text-doom-faint">
201
+ {authenticatedCount} of {providers.length} signed in
202
+ </span>
203
+ </div>
204
+ <ul data-testid="provider-list" className="flex flex-col gap-1.5">
205
+ {shown.map((provider) => (
206
+ <ProviderRow
207
+ key={provider.id}
208
+ provider={provider}
209
+ busy={busyId === provider.id}
210
+ onLogin={(type) => void begin(provider.id, type)}
211
+ onLogout={() => void signOut(provider.id)}
212
+ />
213
+ ))}
214
+ {shown.length === 0 ? (
215
+ <EmptyState
216
+ asChild
217
+ data-testid="provider-no-match"
218
+ className="py-6"
219
+ title="no provider matches"
220
+ description={`nothing here answers to “${filter.trim()}”.`}
221
+ >
222
+ <li />
223
+ </EmptyState>
224
+ ) : null}
225
+ </ul>
226
+ </>
227
+ ) : null}
228
+ {flow ? (
229
+ <LoginFlowDialog
230
+ flow={flow}
231
+ onAnswer={(promptId, value) => void answer(promptId, value)}
232
+ onCancel={() => void cancel()}
233
+ onClose={() => setFlow(null)}
234
+ />
235
+ ) : null}
236
+ </div>
237
+ );
238
+ }
@@ -0,0 +1,32 @@
1
+ import { Link } from '@tanstack/react-router';
2
+ import { SETTINGS_SECTIONS } from '../../lib/settingsSections.ts';
3
+
4
+ export function SettingsMenu({ active }: { active: string | undefined }) {
5
+ return (
6
+ <nav
7
+ data-testid="settings-menu"
8
+ className="flex w-[220px] shrink-0 flex-col gap-0.5 border-r border-doom-border px-2.5 py-3"
9
+ >
10
+ {SETTINGS_SECTIONS.map((section) => {
11
+ const current = section.id === active;
12
+ return (
13
+ <Link
14
+ key={section.id}
15
+ to="/settings/$section"
16
+ params={{ section: section.id }}
17
+ data-testid={`settings-section-${section.id}`}
18
+ data-active={current}
19
+ className={`flex flex-col gap-0.5 rounded-md px-[11px] py-2 transition-colors ${
20
+ current ? 'bg-doom-tint-blue' : 'hover:bg-doom-panel'
21
+ }`}
22
+ >
23
+ <span className={`text-[12px] font-bold ${current ? 'text-doom-blue' : 'text-doom-hi'}`}>
24
+ {section.label}
25
+ </span>
26
+ <span className="text-[10px] leading-snug text-doom-faint">{section.detail}</span>
27
+ </Link>
28
+ );
29
+ })}
30
+ </nav>
31
+ );
32
+ }