@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,189 @@
1
+ import {
2
+ HISTORY_PAGE_TYPE,
3
+ HUB_RESYNCED_TYPE,
4
+ SESSION_BACKLOG_TYPE,
5
+ SESSION_FRAME_TYPE,
6
+ SESSION_REMOVED_TYPE,
7
+ SESSION_UPSERT_TYPE,
8
+ SESSIONS_SNAPSHOT_TYPE,
9
+ subscribeFrame,
10
+ THREAD_BACKLOG_TYPE,
11
+ THREAD_FRAME_TYPE,
12
+ unsubscribeFrame,
13
+ } from '../../types/hub.ts';
14
+ import { dispatchChannelFrame, dropPluginSessionData } from '../lib/pluginRegistry.ts';
15
+ import { startProtocolRuntime } from './protocolRuntime.ts';
16
+ import { bindTransport, releaseTransport, sendHubFrame } from '../lib/transport.ts';
17
+ import { createSessionSocket, sessionSocketUrl } from '../lib/wsClient.ts';
18
+ import { claimDialogMenu, clearPendingMenu } from '../stores/menuStore.ts';
19
+ import {
20
+ applyHistoryPage,
21
+ applySessionFrame,
22
+ applyThreadFrame,
23
+ dropSessionStore,
24
+ refreshSessionFacts,
25
+ resetSessionStore,
26
+ seedHistoryCursor,
27
+ } from '../stores/sessionStore.ts';
28
+ import { dropThreads, resubscribeThreads, threadStoreKey } from '../stores/threadStore.ts';
29
+ import { dropTransientTabs } from '../stores/transientTabsStore.ts';
30
+ import {
31
+ applySessionBacklog,
32
+ applySessionRemoved,
33
+ applySessionsSnapshot,
34
+ applySessionUpsert,
35
+ markSocketClosed,
36
+ sessionsStore,
37
+ } from '../stores/sessionsStore.ts';
38
+
39
+ function isRecord(value: unknown): value is Record<string, unknown> {
40
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
41
+ }
42
+
43
+ /**
44
+ * Wires the hub socket to the stores.
45
+ *
46
+ * One socket carries every session. The rail runs on hub summaries; the
47
+ * timeline of the focused session is hydrated by subscribing, which replays
48
+ * the hub's ring and then streams live frames. Session facts are pulled after
49
+ * every replay and again once a run settles, because Pi reports model, stats
50
+ * and commands on request rather than pushing them as events.
51
+ */
52
+ /** The journal id of the oldest restored entry, which is where paging back resumes. */
53
+ function oldestEntryId(frames: readonly Record<string, unknown>[]): string | null {
54
+ for (const frame of frames) {
55
+ if (frame.type !== 'entry_appended') continue;
56
+ const entry = frame.entry;
57
+ if (isRecord(entry) && typeof entry.id === 'string') return entry.id;
58
+ }
59
+ return null;
60
+ }
61
+
62
+ export function startSessionRuntime(): () => void {
63
+ // The hub-side subscription this page currently holds; it dies with the
64
+ // socket, which is why the snapshot handler re-subscribes.
65
+ let subscribed: string | null = null;
66
+ // The transcript comes from Pi's protocol; this socket keeps carrying what
67
+ // the protocol has no shape for.
68
+ const protocol = startProtocolRuntime();
69
+
70
+ const syncSubscription = (force = false): void => {
71
+ const { activeId, byId } = sessionsStore.state;
72
+ const target = activeId !== null && activeId in byId ? activeId : null;
73
+ protocol.focus(target);
74
+ if (!force && target === subscribed) return;
75
+ if (subscribed !== null && subscribed !== target) sendHubFrame(unsubscribeFrame(subscribed));
76
+ subscribed = target;
77
+ if (target !== null) sendHubFrame(subscribeFrame(target));
78
+ };
79
+
80
+ const socket = createSessionSocket(sessionSocketUrl(window.location), {
81
+ onFrame(frame) {
82
+ switch (frame.type) {
83
+ // Sync rebuilt the bundle this page is running. Nothing in a loaded
84
+ // bundle notices its source moved, so the page picks the new one up.
85
+ case HUB_RESYNCED_TYPE:
86
+ window.location.reload();
87
+ return;
88
+
89
+ case SESSIONS_SNAPSHOT_TYPE:
90
+ applySessionsSnapshot(frame);
91
+ // A snapshot means a fresh socket; any prior subscription died with
92
+ // the old one.
93
+ syncSubscription(true);
94
+ resubscribeThreads();
95
+ return;
96
+ case SESSION_UPSERT_TYPE:
97
+ applySessionUpsert(frame);
98
+ syncSubscription();
99
+ return;
100
+ case SESSION_REMOVED_TYPE: {
101
+ if (typeof frame.sessionId !== 'string') return;
102
+ applySessionRemoved(frame);
103
+ dropSessionStore(frame.sessionId);
104
+ dropPluginSessionData(frame.sessionId);
105
+ dropThreads(frame.sessionId);
106
+ dropTransientTabs(frame.sessionId);
107
+ if (subscribed === frame.sessionId) subscribed = null;
108
+ return;
109
+ }
110
+ case SESSION_BACKLOG_TYPE: {
111
+ if (typeof frame.sessionId !== 'string' || !Array.isArray(frame.frames)) return;
112
+ const sessionId = frame.sessionId;
113
+ const frames = frame.frames.filter(isRecord);
114
+ const dropped = typeof frame.dropped === 'number' ? frame.dropped : 0;
115
+ applySessionBacklog(sessionId, frames.length, dropped);
116
+ resetSessionStore(sessionId);
117
+ for (const replayed of frames) applySessionFrame(sessionId, replayed);
118
+ // Where the backlog starts is where paging back has to continue
119
+ // from, so the oldest journal id it carried becomes the cursor.
120
+ seedHistoryCursor(sessionId, oldestEntryId(frames));
121
+ refreshSessionFacts(sessionId);
122
+ return;
123
+ }
124
+ case HISTORY_PAGE_TYPE: {
125
+ if (typeof frame.sessionId !== 'string' || !Array.isArray(frame.frames)) return;
126
+ applyHistoryPage(frame.sessionId, frame.frames.filter(isRecord), {
127
+ cursor: typeof frame.cursor === 'string' ? frame.cursor : null,
128
+ hasMore: frame.hasMore === true,
129
+ });
130
+ return;
131
+ }
132
+ case SESSION_FRAME_TYPE: {
133
+ if (typeof frame.sessionId !== 'string' || !isRecord(frame.frame)) return;
134
+ applySessionFrame(frame.sessionId, frame.frame);
135
+ // A select the bar asked for becomes the bar's popover; the claim is
136
+ // settled here, at frame time, so no surface renders it twice.
137
+ if (frame.frame.type === 'extension_ui_request' && frame.frame.method === 'select') {
138
+ claimDialogMenu(typeof frame.frame.id === 'string' ? frame.frame.id : '');
139
+ }
140
+ if (frame.frame.type === 'agent_settled') {
141
+ clearPendingMenu();
142
+ refreshSessionFacts(frame.sessionId);
143
+ }
144
+ return;
145
+ }
146
+ // A thread folds like a session of its own, under a key of its own;
147
+ // the backlog replaces what the page had, the same as a session's.
148
+ case THREAD_BACKLOG_TYPE: {
149
+ if (typeof frame.sessionId !== 'string' || typeof frame.threadId !== 'string') return;
150
+ if (!Array.isArray(frame.frames)) return;
151
+ const key = threadStoreKey(frame.sessionId, frame.threadId);
152
+ resetSessionStore(key);
153
+ for (const replayed of frame.frames.filter(isRecord)) applyThreadFrame(key, replayed);
154
+ return;
155
+ }
156
+ case THREAD_FRAME_TYPE: {
157
+ if (typeof frame.sessionId !== 'string' || typeof frame.threadId !== 'string') return;
158
+ if (!isRecord(frame.frame)) return;
159
+ applyThreadFrame(threadStoreKey(frame.sessionId, frame.threadId), frame.frame);
160
+ return;
161
+ }
162
+ default:
163
+ // Any other frame type may be a plugin channel; unclaimed types are
164
+ // dropped silently the way unknown frames always have been.
165
+ dispatchChannelFrame(frame);
166
+ return;
167
+ }
168
+ },
169
+ onOpen() {
170
+ // The snapshot that follows the hub's hello is the real "connected".
171
+ },
172
+ onClose() {
173
+ subscribed = null;
174
+ markSocketClosed();
175
+ },
176
+ });
177
+
178
+ bindTransport((frame) => socket.send(frame));
179
+ // Focus changes come from routing; the runtime follows them with
180
+ // subscribe/unsubscribe so features never touch the wire protocol.
181
+ const subscription = sessionsStore.subscribe(() => syncSubscription());
182
+
183
+ return () => {
184
+ subscription.unsubscribe();
185
+ protocol.stop();
186
+ releaseTransport();
187
+ socket.close();
188
+ };
189
+ }
@@ -0,0 +1,4 @@
1
+ // Generated by src/adapters/webPluginGenerate.ts. Do not edit by hand.
2
+ import type { WebPluginDefinition } from '@agimon-ai/doompi-web-contracts';
3
+
4
+ export const webPlugins: readonly WebPluginDefinition[] = [];
@@ -0,0 +1,46 @@
1
+ /** The glyph's intrinsic box, tight to the horns and the paws. */
2
+ const VIEW_BOX = '3 2.5 26 25';
3
+ const ASPECT = 26 / 25;
4
+
5
+ export interface MascotMarkProps {
6
+ /** Rendered height in pixels; the width follows the glyph's aspect ratio. */
7
+ size?: number;
8
+ className?: string;
9
+ }
10
+
11
+ /**
12
+ * The DoomPi mascot reduced to a wordmark glyph, so the pi it is built from
13
+ * can stand in for the "Pi" in DoomPi and the lockup reads DOOM + mark.
14
+ *
15
+ * The traced mascot carries over a thousand points and thins to hairlines
16
+ * below thirty pixels, which is smaller than every place a wordmark sits, so
17
+ * this is the favicon's geometry without its badge: the same silhouette in
18
+ * four flat shapes that still reads at twenty. Decorative by itself, because
19
+ * the lockup around it is what carries the accessible name.
20
+ */
21
+ export function MascotMark({ size = 24, className }: MascotMarkProps) {
22
+ return (
23
+ <svg
24
+ viewBox={VIEW_BOX}
25
+ height={size}
26
+ width={size * ASPECT}
27
+ className={className}
28
+ aria-hidden="true"
29
+ focusable="false"
30
+ >
31
+ <g className="fill-doom-yellow">
32
+ <path d="M7 10Q6.2 4.2 9.3 3q1.1 3.3 2.8 6.6Z" />
33
+ <path d="M25 10q.8-5.8-2.3-7-1.1 3.3-2.8 6.6Z" />
34
+ </g>
35
+ <g className="fill-doom-magenta">
36
+ <rect x="3.2" y="8.6" width="25.6" height="7.6" rx="3.4" />
37
+ <rect x="6.9" y="14" width="5.5" height="13" rx="2.5" />
38
+ <rect x="19.6" y="14" width="5.5" height="13" rx="2.5" />
39
+ </g>
40
+ <g className="fill-doom-deep">
41
+ <circle cx="10.9" cy="12.3" r="1.9" />
42
+ <circle cx="21.1" cy="12.3" r="1.9" />
43
+ </g>
44
+ </svg>
45
+ );
46
+ }
@@ -0,0 +1,10 @@
1
+ import { usePluginSlotProps } from '../stores/usePluginSlotProps.ts';
2
+
3
+ /**
4
+ * Renders every fill placed into one of the host's slots. Host surfaces put
5
+ * this where their layout wants plugin content; a slot nobody filled renders
6
+ * nothing.
7
+ */
8
+ export function PluginSurface({ slot, sessionId }: { slot: string; sessionId: string | null }) {
9
+ return <>{usePluginSlotProps(sessionId).renderSlot(slot)}</>;
10
+ }
@@ -0,0 +1,32 @@
1
+ import { Component, type ReactNode } from 'react';
2
+
3
+ interface ToolRendererBoundaryProps {
4
+ toolName: string;
5
+ /** Renders the item; `failed` is true once the plugin renderer threw, so the host item stands in. */
6
+ children: (failed: boolean) => ReactNode;
7
+ }
8
+
9
+ interface ToolRendererBoundaryState {
10
+ failed: boolean;
11
+ }
12
+
13
+ /**
14
+ * Keeps one plugin's tool renderer from taking the timeline down with it: a
15
+ * throw during render is logged once and the call falls back to the host's
16
+ * own item, while every other entry keeps rendering.
17
+ */
18
+ export class ToolRendererBoundary extends Component<ToolRendererBoundaryProps, ToolRendererBoundaryState> {
19
+ override state: ToolRendererBoundaryState = { failed: false };
20
+
21
+ static getDerivedStateFromError(): ToolRendererBoundaryState {
22
+ return { failed: true };
23
+ }
24
+
25
+ override componentDidCatch(error: unknown): void {
26
+ console.error(`web plugin tool renderer for '${this.props.toolName}' threw; showing the host item instead`, error);
27
+ }
28
+
29
+ override render(): ReactNode {
30
+ return this.props.children(this.state.failed);
31
+ }
32
+ }
@@ -0,0 +1,125 @@
1
+ import { Button, EmptyState, Kbd, SectionLabel, StatusBadge } from '@agimon-ai/doompi-web-components';
2
+ import { useStore } from '@tanstack/react-store';
3
+ import { PluginSurface } from '../../components/PluginSurface.tsx';
4
+ import { activityGroups } from '../../lib/composition.ts';
5
+ import { activityGroupSlot, HOST_SLOTS, slotFills } from '../../lib/pluginRegistry.ts';
6
+ import { usePluginSlotProps } from '../../stores/usePluginSlotProps.ts';
7
+ import { useActiveSession } from '../../stores/sessionStore.ts';
8
+ import { sessionsStore } from '../../stores/sessionsStore.ts';
9
+
10
+ /**
11
+ * Asynchronous work that outlives the turn that started it.
12
+ *
13
+ * Agents, runners, and workflows deliberately live outside the transcript, so
14
+ * they get a surface that does not scroll away. The host owns only the frame:
15
+ * which groups exist, their key chips, and what they render come from the
16
+ * packages that declare them. Each declared group opens the keyed slot
17
+ * activity.<name>; the sections any plugin registers under that name render
18
+ * there, in slot order, and a group nobody fills shows the one-line summary
19
+ * the session's footer publishes.
20
+ */
21
+ export function ActivityDock({ onClose }: { onClose: () => void }) {
22
+ const activeId = useStore(sessionsStore, (state) => state.activeId);
23
+ const statuses = useActiveSession((state) => state.statuses);
24
+ const widgets = useActiveSession((state) => state.widgets);
25
+ const slotProps = usePluginSlotProps(activeId);
26
+ const groups = activityGroups(statuses, widgets);
27
+ const busy = groups.filter((group) => group.active).length;
28
+
29
+ return (
30
+ <aside
31
+ data-testid="activity-dock"
32
+ className="flex w-[300px] shrink-0 flex-col overflow-hidden border-l border-doom-border bg-doom-rail"
33
+ >
34
+ <div className="flex h-12 shrink-0 items-center justify-between border-b border-doom-border px-4">
35
+ <div className="flex items-center gap-2">
36
+ <SectionLabel className="text-doom-hi">activity</SectionLabel>
37
+ {busy > 0 ? (
38
+ <StatusBadge tone="running" size="xs" data-testid="activity-busy" className="normal-case">
39
+ {busy} running
40
+ </StatusBadge>
41
+ ) : null}
42
+ </div>
43
+ <Button variant="ghost" size="xs" data-testid="activity-close" onClick={onClose} className="text-[10px]">
44
+ hide
45
+ </Button>
46
+ </div>
47
+
48
+ <div className="min-h-0 flex-1 overflow-y-auto">
49
+ {groups.length === 0 ? (
50
+ <EmptyState
51
+ data-testid="activity-empty"
52
+ className="px-4 py-5"
53
+ title="nothing supervised yet"
54
+ description="a package's group appears here once its extension reports in."
55
+ />
56
+ ) : (
57
+ <div className="flex flex-col">
58
+ {groups.map((group) => (
59
+ <div
60
+ key={group.name}
61
+ data-testid={`activity-${group.name}`}
62
+ data-active={group.active}
63
+ className="flex flex-col gap-2 border-b border-doom-border-soft px-3 py-3"
64
+ >
65
+ <div className="flex items-center gap-2 px-1">
66
+ {/* One marker per head. The glyph says "section", so a group
67
+ never reads as one more row among the items beneath it,
68
+ and it carries the busy colour the status dot used to:
69
+ two marks for one fact is one mark too many. A glyph
70
+ rather than an icon because every group is declared by a
71
+ plugin the host knows nothing about. */}
72
+ <span
73
+ aria-hidden
74
+ data-active={group.active}
75
+ className={`text-[11px] font-bold ${
76
+ group.active ? 'animate-pulse text-doom-yellow' : 'text-doom-faint'
77
+ }`}
78
+ >
79
+ #
80
+ </span>
81
+ <span className="flex-1 text-[11px] font-bold text-doom-text">{group.name}</span>
82
+ {group.tab === undefined ? (
83
+ <Kbd data-testid={`activity-keys-${group.name}`} className="bg-doom-panel">
84
+ {group.keys}
85
+ </Kbd>
86
+ ) : (
87
+ <Button
88
+ variant="subtle"
89
+ size="xs"
90
+ data-testid={`activity-open-${group.name}`}
91
+ title={`open the ${group.tab} tab`}
92
+ onClick={() => slotProps.openTab(group.tab ?? null)}
93
+ className="h-auto px-1.5 py-0.5 text-[8px] font-bold text-doom-violet hover:text-doom-magenta"
94
+ >
95
+ {group.keys}
96
+ </Button>
97
+ )}
98
+ </div>
99
+ {slotFills(activityGroupSlot(group.name)).length > 0 ? (
100
+ slotProps.renderSlot(activityGroupSlot(group.name))
101
+ ) : (
102
+ <p
103
+ data-testid={`activity-summary-${group.name}`}
104
+ className={`px-1 text-[10px] ${group.active ? 'text-doom-yellow' : 'text-doom-faint'}`}
105
+ >
106
+ {group.summary || 'idle'}
107
+ </p>
108
+ )}
109
+ </div>
110
+ ))}
111
+ </div>
112
+ )}
113
+
114
+ <PluginSurface slot={HOST_SLOTS.activity} sessionId={activeId} />
115
+ </div>
116
+
117
+ <div className="border-t border-doom-border px-4 py-3">
118
+ <span className="text-[9px] leading-relaxed text-doom-faint">
119
+ each group is rendered by the package that owns it; the session's summary line stands in until that package
120
+ publishes per-run detail
121
+ </span>
122
+ </div>
123
+ </aside>
124
+ );
125
+ }
@@ -0,0 +1,63 @@
1
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, Spinner } from '@agimon-ai/doompi-web-components';
2
+ import { abbreviateCwd } from '../../lib/sessionSummary.ts';
3
+ import { useActiveSessionMeta } from '../../stores/sessionsStore.ts';
4
+
5
+ function Detail({ label, value }: { label: string; value: string }) {
6
+ return (
7
+ <div className="flex items-center justify-between gap-3">
8
+ <span className="shrink-0 text-[10px] text-doom-faint">{label}</span>
9
+ <span className="truncate text-[10px] text-doom-text">{value}</span>
10
+ </div>
11
+ );
12
+ }
13
+
14
+ /**
15
+ * Shown when the focused session will not take this cockpit.
16
+ *
17
+ * doompi-server holds one client at a time so two views cannot fight over the
18
+ * same agent. The hub keeps retrying underneath, so this explains the wait
19
+ * rather than offering a button that would do the same thing again. It is not
20
+ * dismissible for the same reason: nothing behind it would work. Other
21
+ * sessions' refusals stay in their rail cards; only the focused one earns the
22
+ * overlay.
23
+ */
24
+ export function RefusedCard() {
25
+ const meta = useActiveSessionMeta();
26
+ if (meta === null || meta.attach !== 'refused') return null;
27
+
28
+ return (
29
+ <Dialog open>
30
+ <DialogContent
31
+ width="lg"
32
+ data-testid="refused-card"
33
+ aria-describedby={undefined}
34
+ className="w-[520px] border-doom-red/50"
35
+ onEscapeKeyDown={(event) => event.preventDefault()}
36
+ onPointerDownOutside={(event) => event.preventDefault()}
37
+ onInteractOutside={(event) => event.preventDefault()}
38
+ >
39
+ <DialogHeader className="border-doom-red/40 bg-doom-red/10">
40
+ <DialogTitle data-testid="refused-title" className="text-doom-red">
41
+ session already attached
42
+ </DialogTitle>
43
+ <Spinner className="text-doom-red/70" />
44
+ </DialogHeader>
45
+ <div className="flex flex-col gap-3 px-4 py-4">
46
+ <p className="text-[12px] leading-relaxed text-doom-text">
47
+ doompi-server holds one client at a time so two views cannot fight over the same agent. Another cockpit is
48
+ attached to this socket right now.
49
+ </p>
50
+ <div className="flex flex-col gap-1.5 rounded border border-doom-border bg-doom-deep px-3 py-2.5">
51
+ <Detail label="reason" value={meta.reason || 'attach_error'} />
52
+ <Detail label="socket" value={meta.summary.socketPath} />
53
+ <Detail label="cwd" value={abbreviateCwd(meta.summary.cwd)} />
54
+ <Detail label="retry" value="automatic, with backoff" />
55
+ </div>
56
+ <p data-testid="refused-hint" className="text-[10px] text-doom-faint">
57
+ close the other cockpit and this one takes over on the next attempt.
58
+ </p>
59
+ </div>
60
+ </DialogContent>
61
+ </Dialog>
62
+ );
63
+ }
@@ -0,0 +1,176 @@
1
+ import {
2
+ Button,
3
+ Dialog,
4
+ DialogBody,
5
+ DialogContent,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ handleOptionListKey,
10
+ Kbd,
11
+ OptionList,
12
+ optionListHint,
13
+ Panel,
14
+ ShieldIcon,
15
+ Textarea,
16
+ } from '@agimon-ai/doompi-web-components';
17
+ import { useStore } from '@tanstack/react-store';
18
+ import { useEffect, useState } from 'react';
19
+ import { focusPrompt } from '../../lib/promptFocus.ts';
20
+ import { menuStore } from '../../stores/menuStore.ts';
21
+ import { answerDialogConfirm, answerDialogValue, cancelDialog, useActiveSession } from '../../stores/sessionStore.ts';
22
+
23
+ /**
24
+ * Renders the extension UI sub-protocol as a modal.
25
+ *
26
+ * The agent blocks on this answer, so the surface is modal and always offers a
27
+ * way out: an unanswered request would strand the run. A select the user asked
28
+ * for from the selection bar is that bar's popover instead (the claim is
29
+ * settled when the frame arrives, see stores/menuStore.ts), so this overlay
30
+ * leaves it alone.
31
+ */
32
+ export function DialogOverlay() {
33
+ const dialog = useActiveSession((state) => state.dialog);
34
+ const claimed = useStore(menuStore, (state) => state.claimed);
35
+ const [value, setValue] = useState('');
36
+ const [cursor, setCursor] = useState(0);
37
+
38
+ useEffect(() => {
39
+ setValue(dialog?.prefill ?? '');
40
+ setCursor(0);
41
+ }, [dialog?.id, dialog?.prefill]);
42
+
43
+ if (!dialog) return null;
44
+ if (dialog.method === 'select' && claimed !== null && claimed.dialogId === dialog.id) return null;
45
+
46
+ return (
47
+ <Dialog
48
+ open
49
+ onOpenChange={(next) => {
50
+ if (!next) cancelDialog(dialog.id);
51
+ }}
52
+ >
53
+ <DialogContent
54
+ width="lg"
55
+ data-testid="dialog"
56
+ data-dialog-method={dialog.method}
57
+ aria-describedby={undefined}
58
+ // The dialog content is what the overlay focuses, so the option
59
+ // keyboard is handled here rather than on the list itself.
60
+ onKeyDown={(event) => {
61
+ if (dialog.method !== 'select') return;
62
+ handleOptionListKey(event, {
63
+ options: dialog.options,
64
+ cursor,
65
+ onCursorChange: setCursor,
66
+ onSelect: (option) => answerDialogValue(dialog.id, option),
67
+ });
68
+ }}
69
+ // The agent pushed this dialog; nothing on the page opened it, so the
70
+ // keyboard goes back to the composer rather than to the body.
71
+ onCloseAutoFocus={(event) => {
72
+ event.preventDefault();
73
+ focusPrompt();
74
+ }}
75
+ className="rounded-[10px] border-doom-edge-magenta"
76
+ >
77
+ <DialogHeader className="h-[42px] border-doom-edge-magenta bg-doom-tint-magenta py-0">
78
+ <DialogTitle data-testid="dialog-title" className="flex items-center gap-2 text-doom-magenta">
79
+ <ShieldIcon className="h-[13px] w-[13px] shrink-0" />
80
+ {dialog.title}
81
+ </DialogTitle>
82
+ <span className="text-[9px] text-doom-faint">extension · {dialog.method}</span>
83
+ </DialogHeader>
84
+
85
+ <DialogBody>
86
+ {dialog.message && dialog.method === 'select' ? (
87
+ <Panel data-testid="dialog-command" className="bg-doom-deep px-3 py-2.5">
88
+ <div className="flex gap-2">
89
+ <span className="text-[12px] text-doom-green">$</span>
90
+ <pre
91
+ data-testid="dialog-message"
92
+ className="min-w-0 flex-1 whitespace-pre-wrap break-words text-[12px] leading-relaxed text-doom-hi"
93
+ >
94
+ {dialog.message}
95
+ </pre>
96
+ </div>
97
+ </Panel>
98
+ ) : dialog.message ? (
99
+ <p data-testid="dialog-message" className="text-[12px] leading-relaxed text-doom-text">
100
+ {dialog.message}
101
+ </p>
102
+ ) : null}
103
+
104
+ {dialog.method === 'select' ? (
105
+ <OptionList
106
+ options={dialog.options}
107
+ cursor={cursor}
108
+ onCursorChange={setCursor}
109
+ testIdPrefix="dialog-option"
110
+ onSelect={(option) => answerDialogValue(dialog.id, option)}
111
+ />
112
+ ) : null}
113
+
114
+ {dialog.method === 'input' || dialog.method === 'editor' ? (
115
+ <Textarea
116
+ data-testid="dialog-input"
117
+ value={value}
118
+ autoFocus
119
+ rows={dialog.method === 'editor' ? 5 : 1}
120
+ placeholder={dialog.placeholder}
121
+ onChange={(event) => setValue(event.target.value)}
122
+ onKeyDown={(event) => {
123
+ if (event.key === 'Enter' && !event.shiftKey && dialog.method === 'input') {
124
+ event.preventDefault();
125
+ answerDialogValue(dialog.id, value);
126
+ }
127
+ }}
128
+ />
129
+ ) : null}
130
+ </DialogBody>
131
+
132
+ <DialogFooter variant="bar" className="h-[34px]">
133
+ <span data-testid="dialog-hints" className="flex items-center gap-1.5 text-[10px] text-doom-faint">
134
+ {dialog.method === 'select' ? optionListHint(dialog.options.length) : 'enter confirm'} · <Kbd>esc</Kbd>{' '}
135
+ cancels and tells the agent
136
+ </span>
137
+ <div className="flex items-center gap-2">
138
+ <Button variant="outline" size="sm" data-testid="dialog-cancel" onClick={() => cancelDialog(dialog.id)}>
139
+ cancel
140
+ </Button>
141
+ {dialog.method === 'confirm' ? (
142
+ <>
143
+ <Button
144
+ variant="danger-outline"
145
+ size="sm"
146
+ data-testid="dialog-deny"
147
+ onClick={() => answerDialogConfirm(dialog.id, false)}
148
+ >
149
+ no
150
+ </Button>
151
+ <Button
152
+ variant="success"
153
+ size="sm"
154
+ data-testid="dialog-confirm"
155
+ onClick={() => answerDialogConfirm(dialog.id, true)}
156
+ >
157
+ yes
158
+ </Button>
159
+ </>
160
+ ) : null}
161
+ {dialog.method === 'input' || dialog.method === 'editor' ? (
162
+ <Button
163
+ variant="primary"
164
+ size="sm"
165
+ data-testid="dialog-submit"
166
+ onClick={() => answerDialogValue(dialog.id, value)}
167
+ >
168
+ submit
169
+ </Button>
170
+ ) : null}
171
+ </div>
172
+ </DialogFooter>
173
+ </DialogContent>
174
+ </Dialog>
175
+ );
176
+ }