@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,368 @@
1
+ import {
2
+ Badge,
3
+ Button,
4
+ Kbd,
5
+ OptionLabel,
6
+ OptionRow,
7
+ Popover,
8
+ PopoverAnchor,
9
+ PopoverContent,
10
+ PopoverFooter,
11
+ StopIcon,
12
+ Textarea,
13
+ } from '@agimon-ai/doompi-web-components';
14
+ import { useCallback, useEffect, useRef, useState } from 'react';
15
+ import { searchSessionFiles } from '../../lib/hubApi.ts';
16
+ import { registerPromptInput } from '../../lib/promptFocus.ts';
17
+ import { abortRun, queueFollowUp, submitMessage, useActiveSession } from '../../stores/sessionStore.ts';
18
+ import { activeSessionId, useActiveSessionMeta } from '../../stores/sessionsStore.ts';
19
+ import { openPalette } from '../../stores/paletteStore.ts';
20
+
21
+ /** The input grows with the draft up to this many pixels, then scrolls. */
22
+ const MAX_INPUT_HEIGHT_PX = 192;
23
+ const MAX_COMPLETION_ITEMS = 8;
24
+ const FILE_SEARCH_DEBOUNCE_MS = 150;
25
+
26
+ interface CompletionItem {
27
+ /** What replaces the trigger token when accepted. */
28
+ insert: string;
29
+ label: string;
30
+ detail?: string;
31
+ }
32
+
33
+ interface CompletionState {
34
+ kind: 'command' | 'file';
35
+ /** Index of the trigger character in the draft. */
36
+ tokenStart: number;
37
+ query: string;
38
+ items: CompletionItem[];
39
+ selected: number;
40
+ }
41
+
42
+ /**
43
+ * Whether accepting the highlighted item would leave the draft as it is. The
44
+ * popup still shows what a longer name would complete to, but the keystroke
45
+ * that would change nothing is handed back to the composer to send.
46
+ */
47
+ function isCompletionRedundant(state: CompletionState): boolean {
48
+ const item = state.items[state.selected];
49
+ return item !== undefined && item.insert.trimEnd() === `${state.kind === 'command' ? '/' : '@'}${state.query}`;
50
+ }
51
+
52
+ /** The /command or @file token the caret sits in, or null when there is none. */
53
+ function triggerTokenAt(
54
+ draft: string,
55
+ caret: number,
56
+ ): { kind: 'command' | 'file'; start: number; query: string } | null {
57
+ const before = draft.slice(0, caret);
58
+ const start = Math.max(before.lastIndexOf(' '), before.lastIndexOf('\n')) + 1;
59
+ const token = before.slice(start);
60
+ if (token.startsWith('/') && start === 0) return { kind: 'command', start, query: token.slice(1) };
61
+ if (token.startsWith('@') && token.length <= 64) return { kind: 'file', start, query: token.slice(1) };
62
+ return null;
63
+ }
64
+
65
+ export function Composer() {
66
+ const meta = useActiveSessionMeta();
67
+ const streaming = useActiveSession((state) => state.streaming);
68
+ const commands = useActiveSession((state) => state.commands);
69
+ const [draft, setDraft] = useState('');
70
+ const [caret, setCaret] = useState(0);
71
+ const [dismissedToken, setDismissedToken] = useState<number | null>(null);
72
+ const [completion, setCompletion] = useState<CompletionState | null>(null);
73
+ const inputRef = useRef<HTMLTextAreaElement>(null);
74
+ const containerRef = useRef<HTMLDivElement>(null);
75
+ const searchTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
76
+
77
+ const attached = meta?.attach === 'attached';
78
+ const queued = meta?.summary.pendingMessageCount ?? 0;
79
+
80
+ // Overlays hand the keyboard back here when they close.
81
+ useEffect(() => registerPromptInput(inputRef.current), []);
82
+
83
+ // Auto-grow: measure after every draft change so pasted stack traces are
84
+ // actually visible instead of scrolling inside one line.
85
+ const resize = useCallback((): void => {
86
+ const input = inputRef.current;
87
+ if (!input) return;
88
+ input.style.height = 'auto';
89
+ input.style.height = `${String(Math.min(input.scrollHeight, MAX_INPUT_HEIGHT_PX))}px`;
90
+ }, []);
91
+ useEffect(() => resize(), [draft, resize]);
92
+
93
+ const closeCompletion = useCallback((): void => {
94
+ if (searchTimer.current) clearTimeout(searchTimer.current);
95
+ setCompletion(null);
96
+ }, []);
97
+
98
+ /** Esc dismisses the popup for the token being typed; edits re-arm it. */
99
+ const dismissCompletion = useCallback((): void => {
100
+ const trigger = triggerTokenAt(draft, caret);
101
+ setDismissedToken(trigger?.start ?? null);
102
+ closeCompletion();
103
+ }, [draft, caret, closeCompletion]);
104
+
105
+ // The popup is derived state: any change to the draft, caret, session
106
+ // commands, or attachment recomputes it, so data arriving after a keystroke
107
+ // still opens it.
108
+ useEffect(() => {
109
+ const trigger = triggerTokenAt(draft, caret);
110
+ if (!trigger || !attached || dismissedToken === trigger.start) {
111
+ closeCompletion();
112
+ return;
113
+ }
114
+ if (trigger.kind === 'command') {
115
+ const items = commands
116
+ .filter((command) => command.name.toLowerCase().startsWith(trigger.query.toLowerCase()))
117
+ .slice(0, MAX_COMPLETION_ITEMS)
118
+ .map((command) => ({ insert: `/${command.name} `, label: `/${command.name}`, detail: command.description }));
119
+ setCompletion(
120
+ items.length > 0
121
+ ? { kind: 'command', tokenStart: trigger.start, query: trigger.query, items, selected: 0 }
122
+ : null,
123
+ );
124
+ return;
125
+ }
126
+ if (searchTimer.current) clearTimeout(searchTimer.current);
127
+ searchTimer.current = setTimeout(() => {
128
+ const sessionId = activeSessionId();
129
+ if (sessionId === null) return;
130
+ void searchSessionFiles(sessionId, trigger.query).then((files) => {
131
+ // The draft may have moved on while the lookup ran.
132
+ const input = inputRef.current;
133
+ if (!input || triggerTokenAt(input.value, input.selectionStart)?.query !== trigger.query) return;
134
+ const items = files.slice(0, MAX_COMPLETION_ITEMS).map((file) => ({ insert: `@${file} `, label: `@${file}` }));
135
+ setCompletion(
136
+ items.length > 0
137
+ ? { kind: 'file', tokenStart: trigger.start, query: trigger.query, items, selected: 0 }
138
+ : null,
139
+ );
140
+ });
141
+ }, FILE_SEARCH_DEBOUNCE_MS);
142
+ return () => {
143
+ if (searchTimer.current) clearTimeout(searchTimer.current);
144
+ };
145
+ }, [draft, caret, commands, attached, dismissedToken, closeCompletion]);
146
+
147
+ // Clicking anywhere outside the composer closes the popup.
148
+ useEffect(() => {
149
+ if (!completion) return;
150
+ const onPointerDown = (event: MouseEvent): void => {
151
+ if (containerRef.current && event.target instanceof Node && !containerRef.current.contains(event.target)) {
152
+ closeCompletion();
153
+ }
154
+ };
155
+ document.addEventListener('mousedown', onPointerDown);
156
+ return () => document.removeEventListener('mousedown', onPointerDown);
157
+ }, [completion, closeCompletion]);
158
+
159
+ const accept = (state: CompletionState, index: number): void => {
160
+ const item = state.items[index];
161
+ if (!item) return;
162
+ const next = draft.slice(0, state.tokenStart) + item.insert + draft.slice(caret);
163
+ setDraft(next);
164
+ const position = state.tokenStart + item.insert.length;
165
+ setCaret(position);
166
+ setDismissedToken(state.tokenStart);
167
+ requestAnimationFrame(() => {
168
+ inputRef.current?.focus();
169
+ inputRef.current?.setSelectionRange(position, position);
170
+ });
171
+ };
172
+
173
+ const submit = (): void => {
174
+ if (!draft.trim() || !attached) return;
175
+ submitMessage(draft);
176
+ setDraft('');
177
+ setCaret(0);
178
+ setDismissedToken(null);
179
+ closeCompletion();
180
+ };
181
+
182
+ const queue = (): void => {
183
+ if (!draft.trim() || !attached) return;
184
+ queueFollowUp(draft);
185
+ setDraft('');
186
+ setCaret(0);
187
+ setDismissedToken(null);
188
+ closeCompletion();
189
+ };
190
+
191
+ const placeholder = !attached
192
+ ? 'waiting for the session…'
193
+ : streaming
194
+ ? 'steer the run without stopping it…'
195
+ : 'ask anything · / for commands · @ for files…';
196
+
197
+ return (
198
+ <div className="shrink-0 border-t border-doom-border bg-doom-rail px-5 pt-3 pb-2.5">
199
+ <Popover
200
+ open={completion !== null}
201
+ onOpenChange={(next) => {
202
+ if (!next) dismissCompletion();
203
+ }}
204
+ >
205
+ <PopoverAnchor asChild>
206
+ <div
207
+ ref={containerRef}
208
+ className={`relative rounded-lg border bg-doom-deep transition-colors focus-within:border-doom-blue/60 ${
209
+ streaming ? 'border-doom-edge-yellow' : 'border-doom-border'
210
+ }`}
211
+ >
212
+ {completion ? (
213
+ <PopoverContent
214
+ side="top"
215
+ align="start"
216
+ sideOffset={8}
217
+ data-testid="composer-completion"
218
+ // The keys belong to the textarea, so the menu never takes
219
+ // focus; a click in the composer is a caret move, not a
220
+ // dismissal, which is why the anchor is exempt from outside.
221
+ onOpenAutoFocus={(event) => event.preventDefault()}
222
+ onCloseAutoFocus={(event) => event.preventDefault()}
223
+ onInteractOutside={(event) => {
224
+ if (containerRef.current?.contains(event.target as Node) === true) event.preventDefault();
225
+ }}
226
+ className="w-[420px] max-w-[85vw] rounded-md p-0 shadow-xl"
227
+ >
228
+ {completion.items.map((item, index) => (
229
+ <OptionRow
230
+ key={item.label}
231
+ density="compact"
232
+ active={index === completion.selected}
233
+ data-testid={`composer-completion-item-${String(index)}`}
234
+ onMouseEnter={() => setCompletion({ ...completion, selected: index })}
235
+ onMouseDown={(event) => {
236
+ event.preventDefault();
237
+ accept(completion, index);
238
+ }}
239
+ className="w-full items-baseline gap-2.5 rounded-none px-3 py-1.5"
240
+ >
241
+ <span className="shrink-0 text-[12px] font-bold text-doom-blue">{item.label}</span>
242
+ {item.detail ? (
243
+ <OptionLabel density="compact" className="text-[10px] text-doom-dim">
244
+ {item.detail}
245
+ </OptionLabel>
246
+ ) : null}
247
+ </OptionRow>
248
+ ))}
249
+ <PopoverFooter className="py-1">
250
+ <span className="flex items-center gap-1.5">
251
+ <Kbd>tab</Kbd> or <Kbd>enter</Kbd> completes · <Kbd>esc</Kbd> closes
252
+ </span>
253
+ </PopoverFooter>
254
+ </PopoverContent>
255
+ ) : null}
256
+ <div className="flex items-start gap-2.5 px-3.5 pt-3">
257
+ <span className="mt-[3px] shrink-0 select-none text-[13px] leading-none text-doom-green">&gt;</span>
258
+ <Textarea
259
+ variant="bare"
260
+ ref={inputRef}
261
+ data-testid="composer-input"
262
+ value={draft}
263
+ disabled={!attached}
264
+ onChange={(event) => {
265
+ setDraft(event.target.value);
266
+ setCaret(event.target.selectionStart);
267
+ setDismissedToken(null);
268
+ }}
269
+ onClick={(event) => setCaret(event.currentTarget.selectionStart)}
270
+ onKeyUp={(event) => setCaret(event.currentTarget.selectionStart)}
271
+ onKeyDown={(event) => {
272
+ // The TUI's leader key: space with nothing typed opens Leader Space.
273
+ if (event.key === ' ' && draft === '') {
274
+ event.preventDefault();
275
+ openPalette();
276
+ return;
277
+ }
278
+ if (completion) {
279
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
280
+ event.preventDefault();
281
+ const delta = event.key === 'ArrowDown' ? 1 : -1;
282
+ const count = completion.items.length;
283
+ setCompletion({ ...completion, selected: (completion.selected + delta + count) % count });
284
+ return;
285
+ }
286
+ if (event.key === 'Tab') {
287
+ event.preventDefault();
288
+ accept(completion, completion.selected);
289
+ return;
290
+ }
291
+ // Enter completes, unless the token is already the whole
292
+ // thing: completing then would only add a space, so the
293
+ // keystroke would read as doing nothing and the reader would
294
+ // have to press enter twice to send what they had typed.
295
+ if (event.key === 'Enter' && !isCompletionRedundant(completion)) {
296
+ event.preventDefault();
297
+ accept(completion, completion.selected);
298
+ return;
299
+ }
300
+ if (event.key === 'Escape') {
301
+ event.preventDefault();
302
+ dismissCompletion();
303
+ return;
304
+ }
305
+ }
306
+ if (event.key === 'Enter' && !event.shiftKey) {
307
+ event.preventDefault();
308
+ submit();
309
+ }
310
+ if (event.key === 'Escape' && streaming) {
311
+ event.preventDefault();
312
+ abortRun();
313
+ }
314
+ }}
315
+ rows={1}
316
+ placeholder={placeholder}
317
+ className="min-h-[20px] flex-1 text-[13px] leading-relaxed"
318
+ />
319
+ </div>
320
+ <div className="flex items-center gap-2 px-3.5 pt-2 pb-2.5">
321
+ <span data-testid="composer-hint" className="text-[10px] text-doom-faint">
322
+ {streaming
323
+ ? 'enter steers the run · esc aborts'
324
+ : 'enter sends · shift+enter for a new line · space opens leader'}
325
+ </span>
326
+ {queued > 0 ? (
327
+ <Badge
328
+ size="xs"
329
+ data-testid="composer-queued"
330
+ className="rounded-full border-transparent bg-doom-panel py-0.5 text-[9px] text-doom-dim"
331
+ >
332
+ {queued} queued
333
+ </Badge>
334
+ ) : null}
335
+ <span className="min-w-0 flex-1" />
336
+ {streaming ? (
337
+ <Button variant="danger-outline" size="md" data-testid="composer-abort" onClick={() => abortRun()}>
338
+ <StopIcon className="h-2 w-2 fill-current" />
339
+ abort
340
+ </Button>
341
+ ) : null}
342
+ <Button
343
+ variant="outline"
344
+ size="md"
345
+ data-testid="composer-queue"
346
+ onClick={queue}
347
+ disabled={!attached || !draft.trim()}
348
+ title="deliver after the current run settles"
349
+ >
350
+ queue
351
+ </Button>
352
+ <Button
353
+ variant="primary"
354
+ size="md"
355
+ data-testid="composer-send"
356
+ onClick={submit}
357
+ disabled={!attached || !draft.trim()}
358
+ className="px-3.5"
359
+ >
360
+ {streaming ? 'steer' : 'send'}
361
+ </Button>
362
+ </div>
363
+ </div>
364
+ </PopoverAnchor>
365
+ </Popover>
366
+ </div>
367
+ );
368
+ }
@@ -0,0 +1,58 @@
1
+ import { Badge, FileIcon } from '@agimon-ai/doompi-web-components';
2
+ import type { FileMention } from '../../lib/fileMentions.ts';
3
+ import { sessionFileUrl } from '../../../types/media.ts';
4
+
5
+ function Preview({ mention, url }: { mention: FileMention; url: string }) {
6
+ if (mention.kind === 'image') {
7
+ return (
8
+ <a href={url} target="_blank" rel="noreferrer">
9
+ <img src={url} alt={mention.path} className="max-h-[360px] max-w-full rounded border border-doom-border" />
10
+ </a>
11
+ );
12
+ }
13
+ if (mention.kind === 'video') {
14
+ return (
15
+ <video
16
+ src={url}
17
+ controls
18
+ preload="metadata"
19
+ className="max-h-[360px] max-w-full rounded border border-doom-border"
20
+ />
21
+ );
22
+ }
23
+ if (mention.kind === 'pdf') {
24
+ return (
25
+ <iframe
26
+ src={url}
27
+ title={mention.path}
28
+ className="h-[480px] w-full rounded border border-doom-border bg-doom-deep"
29
+ />
30
+ );
31
+ }
32
+ return (
33
+ <Badge asChild size="md" className="self-start bg-doom-panel text-doom-text hover:border-doom-blue/50">
34
+ <a href={url}>
35
+ <FileIcon className="h-3 w-3 shrink-0 text-doom-faint" />
36
+ {mention.path}
37
+ </a>
38
+ </Badge>
39
+ );
40
+ }
41
+
42
+ /**
43
+ * The files a message mentioned with @, shown under its text. The hub serves
44
+ * each from the session's working directory, so a preview is only as fresh
45
+ * as the file on disk when the timeline renders it.
46
+ */
47
+ export function MentionPreviews({ sessionId, mentions }: { sessionId: string; mentions: FileMention[] }) {
48
+ if (mentions.length === 0) return null;
49
+ return (
50
+ <div data-testid="mention-previews" className="flex flex-col gap-2">
51
+ {mentions.map((mention) => (
52
+ <div key={mention.path} data-testid="mention-preview" data-kind={mention.kind} data-path={mention.path}>
53
+ <Preview mention={mention} url={sessionFileUrl(sessionId, mention.path)} />
54
+ </div>
55
+ ))}
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,35 @@
1
+ import { EmptyState } from '@agimon-ai/doompi-web-components';
2
+ import { useEffect } from 'react';
3
+ import { sessionStoreFor } from '../../stores/sessionStore.ts';
4
+ import { subscribeThread, threadStoreKey, unsubscribeThread } from '../../stores/threadStore.ts';
5
+ import { Transcript } from './Timeline.tsx';
6
+
7
+ const THREAD_TEST_ID = 'thread-timeline';
8
+
9
+ /**
10
+ * A thread of the focused session, such as a subagent run's own journal, on
11
+ * the same transcript as the session itself and followed only while shown.
12
+ * The parent session id goes down with it so tool cards resolve renderers
13
+ * and slot props exactly as the main thread does.
14
+ */
15
+ export function ThreadView({ sessionId, threadId }: { sessionId: string; threadId: string }) {
16
+ useEffect(() => {
17
+ subscribeThread(sessionId, threadId);
18
+ return () => unsubscribeThread(sessionId, threadId);
19
+ }, [sessionId, threadId]);
20
+
21
+ return (
22
+ <Transcript
23
+ store={sessionStoreFor(threadStoreKey(sessionId, threadId))}
24
+ sessionId={sessionId}
25
+ testId={THREAD_TEST_ID}
26
+ empty={
27
+ <EmptyState
28
+ data-testid="thread-empty"
29
+ title="no messages yet"
30
+ description="the agent's journal shows here as soon as it starts writing."
31
+ />
32
+ }
33
+ />
34
+ );
35
+ }