@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
Binary file
Binary file
Binary file
@@ -0,0 +1,16 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="DoomPi">
2
+ <rect width="32" height="32" rx="7" fill="#1c1f24"/>
3
+ <g fill="#ecbe7b">
4
+ <path d="M7.0 10.0 Q6.2 4.2 9.3 3.0 Q10.4 6.3 12.1 9.6 Z"/>
5
+ <path d="M25.0 10.0 Q25.8 4.2 22.7 3.0 Q21.6 6.3 19.9 9.6 Z"/>
6
+ </g>
7
+ <g fill="#c678dd">
8
+ <rect x="3.2" y="8.6" width="25.6" height="7.6" rx="3.4"/>
9
+ <rect x="6.9" y="14.0" width="5.5" height="13.0" rx="2.5"/>
10
+ <rect x="19.6" y="14.0" width="5.5" height="13.0" rx="2.5"/>
11
+ </g>
12
+ <g fill="#1c1f24">
13
+ <circle cx="10.9" cy="12.3" r="1.9"/>
14
+ <circle cx="21.1" cy="12.3" r="1.9"/>
15
+ </g>
16
+ </svg>
Binary file
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
7
+ <link rel="alternate icon" href="/favicon.ico" sizes="48x48 32x32 16x16" />
8
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
9
+ <meta name="theme-color" content="#1c1f24" />
10
+ <title>DoomPi cockpit</title>
11
+ <script type="module" crossorigin src="/assets/index-DQO7NTIP.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-Cg52ngKv.css">
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+ </body>
17
+ </html>
package/llms.txt ADDED
@@ -0,0 +1,8 @@
1
+ # @agimon-ai/doompi-web
2
+
3
+ Package-owned guidance for the DoomPi web cockpit.
4
+
5
+ ## Resources
6
+
7
+ - [Package README](./README.md): Running the cockpit, the bridge's security stance, what each
8
+ surface renders, and the limits of what Pi RPC exposes.
package/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "@agimon-ai/doompi-web",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "Web cockpit for DoomPi: serves a browser session console and bridges it to an authenticated doompi-server socket",
5
+ "keywords": [
6
+ "cockpit",
7
+ "coding-agent",
8
+ "doompi",
9
+ "pi-coding-agent",
10
+ "session-console",
11
+ "web"
12
+ ],
13
+ "homepage": "https://agimon.ai",
14
+ "bugs": {
15
+ "url": "https://github.com/AgiFlow/doompi/issues"
16
+ },
17
+ "license": "SEE LICENSE IN LICENSE",
18
+ "author": "Vuong Ngo",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/AgiFlow/doompi.git",
22
+ "directory": "packages/clients/doompi-web"
23
+ },
24
+ "bin": {
25
+ "doompi-web": "./dist/bin/serve.mjs"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src",
30
+ "llms.txt",
31
+ "README.md",
32
+ "LICENSE",
33
+ "package.json"
34
+ ],
35
+ "type": "module",
36
+ "main": "./dist/index.cjs",
37
+ "types": "./dist/index.d.mts",
38
+ "jsnext:main": "./dist/index.mjs",
39
+ "exports": {
40
+ ".": {
41
+ "types": "./dist/index.d.mts",
42
+ "import": "./dist/index.mjs",
43
+ "require": "./dist/index.cjs"
44
+ },
45
+ "./bundler": {
46
+ "types": "./dist/bundler.d.mts",
47
+ "import": "./dist/bundler.mjs",
48
+ "require": "./dist/bundler.cjs"
49
+ },
50
+ "./package.json": "./package.json"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "dependencies": {
56
+ "@earendil-works/pi-client": "0.84.3",
57
+ "@earendil-works/pi-coding-agent": "0.84.3",
58
+ "@earendil-works/pi-protocol": "0.84.3",
59
+ "@earendil-works/pi-server": "0.84.3",
60
+ "@hono/node-server": "2.1.1",
61
+ "@hono/node-ws": "1.3.1",
62
+ "@tailwindcss/vite": "4.3.3",
63
+ "@tanstack/react-router": "1.170.32",
64
+ "@tanstack/react-store": "0.11.1",
65
+ "@tanstack/store": "0.11.1",
66
+ "@vitejs/plugin-react": "6.1.0",
67
+ "hono": "4.13.3",
68
+ "react": "19.2.8",
69
+ "react-dom": "19.2.8",
70
+ "react-markdown": "10.1.0",
71
+ "remark-gfm": "4.0.1",
72
+ "tailwindcss": "4.3.3",
73
+ "vite": "8.2.2",
74
+ "@agimon-ai/doompi": "0.0.1-alpha.39",
75
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.39",
76
+ "@agimon-ai/doompi-server": "0.0.1-alpha.2",
77
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.0",
78
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.2"
79
+ },
80
+ "devDependencies": {
81
+ "@playwright/test": "1.62.1",
82
+ "@types/node": "26.2.0",
83
+ "@types/react": "19.2.7",
84
+ "@types/react-dom": "19.2.4",
85
+ "@types/ws": "8.18.1",
86
+ "@vitest/coverage-v8": "4.1.11",
87
+ "tsdown": "0.22.14",
88
+ "typescript": "7.0.2",
89
+ "vitest": "4.1.11",
90
+ "ws": "8.21.3",
91
+ "@agimon-ai/vibe-lint-plugin-doom-web": "0.0.1-alpha.36"
92
+ },
93
+ "engines": {
94
+ "node": ">=22.19.0"
95
+ },
96
+ "doompiWeb": [],
97
+ "scripts": {
98
+ "build": "tsdown && vite build",
99
+ "dev": "vite",
100
+ "test": "vitest --run",
101
+ "test:e2e": "playwright test",
102
+ "typecheck": "tsc --noEmit",
103
+ "lint": "oxlint . && oxfmt . --check",
104
+ "fixcode": "oxlint . --fix && oxfmt ."
105
+ }
106
+ }
@@ -0,0 +1,95 @@
1
+ import type { Hono } from 'hono';
2
+ import { AUTH_LOGINS_API_ROUTE, AUTH_PROVIDERS_API_ROUTE, type AuthMethodType } from '../types/auth.ts';
3
+ import type { ProviderAuth } from './providerAuth.ts';
4
+
5
+ const METHOD_TYPES: readonly AuthMethodType[] = ['api_key', 'oauth'];
6
+
7
+ function isRecord(value: unknown): value is Record<string, unknown> {
8
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
9
+ }
10
+
11
+ function isMethodType(value: unknown): value is AuthMethodType {
12
+ return typeof value === 'string' && (METHOD_TYPES as readonly string[]).includes(value);
13
+ }
14
+
15
+ function describe(error: unknown): string {
16
+ return error instanceof Error ? error.message : String(error);
17
+ }
18
+
19
+ /**
20
+ * The provider-auth REST surface. Every handler that reaches the Pi runtime
21
+ * turns a load failure into 502 with the cause, so a machine without a usable
22
+ * Pi install explains itself on the providers page instead of hanging it.
23
+ */
24
+ export function registerAuthRoutes(app: Hono, auth: ProviderAuth): void {
25
+ app.get(AUTH_PROVIDERS_API_ROUTE, async (context) => {
26
+ try {
27
+ return context.json({ providers: await auth.listProviders() });
28
+ } catch (error) {
29
+ return context.json({ error: `Provider auth is unavailable: ${describe(error)}` }, 502);
30
+ }
31
+ });
32
+
33
+ app.delete(`${AUTH_PROVIDERS_API_ROUTE}/:providerId`, async (context) => {
34
+ const providerId = context.req.param('providerId');
35
+ try {
36
+ const outcome = await auth.logout(providerId);
37
+ if (outcome.ok) return context.json({ providerId });
38
+ return context.json({ error: outcome.error }, outcome.code === 'unknown_provider' ? 404 : 502);
39
+ } catch (error) {
40
+ return context.json({ error: `Provider auth is unavailable: ${describe(error)}` }, 502);
41
+ }
42
+ });
43
+
44
+ app.post(AUTH_LOGINS_API_ROUTE, async (context) => {
45
+ let body: unknown;
46
+ try {
47
+ body = await context.req.json();
48
+ } catch {
49
+ return context.json({ error: 'The request body must be JSON.' }, 400);
50
+ }
51
+ if (!isRecord(body) || typeof body.providerId !== 'string' || body.providerId === '') {
52
+ return context.json({ error: 'A providerId string is required.' }, 400);
53
+ }
54
+ if (!isMethodType(body.type)) {
55
+ return context.json({ error: `type must be one of ${METHOD_TYPES.join(', ')}.` }, 400);
56
+ }
57
+ try {
58
+ const outcome = await auth.startLogin(body.providerId, body.type);
59
+ if (outcome.ok) return context.json({ flow: outcome.flow }, 201);
60
+ const status = outcome.code === 'unknown_provider' ? 404 : outcome.code === 'busy' ? 409 : 400;
61
+ return context.json({ error: outcome.error }, status);
62
+ } catch (error) {
63
+ return context.json({ error: `Provider auth is unavailable: ${describe(error)}` }, 502);
64
+ }
65
+ });
66
+
67
+ app.get(`${AUTH_LOGINS_API_ROUTE}/:flowId`, (context) => {
68
+ const flow = auth.getLogin(context.req.param('flowId'));
69
+ if (!flow) return context.json({ error: 'No such login flow.' }, 404);
70
+ return context.json({ flow });
71
+ });
72
+
73
+ app.post(`${AUTH_LOGINS_API_ROUTE}/:flowId/answer`, async (context) => {
74
+ const flowId = context.req.param('flowId');
75
+ let body: unknown;
76
+ try {
77
+ body = await context.req.json();
78
+ } catch {
79
+ return context.json({ error: 'The request body must be JSON.' }, 400);
80
+ }
81
+ if (!isRecord(body) || typeof body.promptId !== 'string' || typeof body.value !== 'string') {
82
+ return context.json({ error: 'promptId and value strings are required.' }, 400);
83
+ }
84
+ const outcome = auth.answerLogin(flowId, body.promptId, body.value);
85
+ if (outcome === 'unknown_flow') return context.json({ error: 'No such login flow.' }, 404);
86
+ if (outcome === 'not_waiting') return context.json({ error: 'The flow is not waiting on that prompt.' }, 409);
87
+ return context.json({ flow: auth.getLogin(flowId) });
88
+ });
89
+
90
+ app.delete(`${AUTH_LOGINS_API_ROUTE}/:flowId`, (context) => {
91
+ const flow = auth.cancelLogin(context.req.param('flowId'));
92
+ if (!flow) return context.json({ error: 'No such login flow.' }, 404);
93
+ return context.json({ flow }, 202);
94
+ });
95
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ const AGENT_COMMAND_ENV = 'DOOMPI_AGENT_COMMAND';
7
+ const WEB_MODULE_ENV = 'DOOMPI_WEB_MODULE';
8
+ const REPOSITORY_CLI = ['node_modules', '@agimon-ai', 'doompi', 'dist', 'bin', 'cli.mjs'];
9
+
10
+ export interface BundledServerLaunch {
11
+ readonly command: string;
12
+ /** Leading arguments the command needs; a caller appends its own after these. */
13
+ readonly args: readonly string[];
14
+ readonly environment: NodeJS.ProcessEnv;
15
+ }
16
+
17
+ type FileExists = (file: string) => boolean;
18
+
19
+ function packageFile(packageName: string, segments: readonly string[], parentUrl: string): string {
20
+ const manifest = createRequire(parentUrl).resolve(`${packageName}/package.json`);
21
+ return path.join(path.dirname(manifest), ...segments);
22
+ }
23
+
24
+ /** Finds the DoomPi CLI pinned by the repository containing the requested session. */
25
+ export function repositoryDoomPiCli(cwd: string, exists: FileExists = fs.existsSync): string | undefined {
26
+ for (let directory = path.resolve(cwd); ; directory = path.dirname(directory)) {
27
+ const cli = path.join(directory, ...REPOSITORY_CLI);
28
+ if (exists(cli)) return cli;
29
+ if (directory === path.dirname(directory)) return undefined;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Resolves the Server and fallback agent shipped by a DoomPi Web installation.
35
+ *
36
+ * This is what makes `doompi-web` need no flags: the hub launches the
37
+ * doompi-server sitting in its own dependency tree rather than whichever one
38
+ * PATH happens to name, and points it at the agent from the same install, so a
39
+ * checkout runs its own build. A repository that pins its own DoomPi still
40
+ * wins, since a session created there should run that repository's agent.
41
+ */
42
+ export function defaultServerLaunch(
43
+ cwd: string,
44
+ environment: NodeJS.ProcessEnv,
45
+ parentUrl: string = import.meta.url,
46
+ exists: FileExists = fs.existsSync,
47
+ ): BundledServerLaunch {
48
+ const server = packageFile('@agimon-ai/doompi-server', ['dist', 'bin', 'serve.mjs'], parentUrl);
49
+ const bundledAgent = packageFile('@agimon-ai/doompi', ['dist', 'bin', 'cli.mjs'], parentUrl);
50
+ const bundledWeb = packageFile('@agimon-ai/doompi-web', ['dist', 'index.mjs'], parentUrl);
51
+ const configuredAgent = environment[AGENT_COMMAND_ENV];
52
+ const localAgent = repositoryDoomPiCli(cwd, exists);
53
+ const agentEnvironment =
54
+ configuredAgent || localAgent ? { ...environment } : { ...environment, [AGENT_COMMAND_ENV]: bundledAgent };
55
+
56
+ return {
57
+ command: process.execPath,
58
+ args: [server],
59
+ environment: {
60
+ ...agentEnvironment,
61
+ [WEB_MODULE_ENV]: environment[WEB_MODULE_ENV] || pathToFileURL(bundledWeb).href,
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,127 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { rankDirectories, searchTermFor } from '../services/directoryMatch.ts';
5
+ import { searchDirectoryTree } from './directorySearch.ts';
6
+
7
+ const PATH_SEPARATOR = '/';
8
+ const HIDDEN_PREFIX = '.';
9
+ /** A pattern that starts with a dot, literal or escaped, anchored or not, is asking for dotdirs. */
10
+ const ASKS_FOR_HIDDEN = /^\^?\\?\./;
11
+ /** The picker shows a handful; a longer list is a sign to type more. */
12
+ const DEFAULT_LIMIT = 12;
13
+
14
+ export interface DirectoryQuery {
15
+ /** The directory whose children are listed. */
16
+ parent: string;
17
+ /** The typed trailing segment, applied to child names as a regular expression. */
18
+ pattern: string;
19
+ }
20
+
21
+ /**
22
+ * Splits a typed path into the directory to list and the filter for its
23
+ * children: everything up to the last slash is the parent, the rest is the
24
+ * pattern. Only absolute paths qualify, matching what a session accepts.
25
+ */
26
+ export function splitDirectoryQuery(typed: string): DirectoryQuery | undefined {
27
+ if (!path.isAbsolute(typed)) return undefined;
28
+ const cut = typed.lastIndexOf(PATH_SEPARATOR);
29
+ return { parent: cut === 0 ? PATH_SEPARATOR : typed.slice(0, cut), pattern: typed.slice(cut + 1) };
30
+ }
31
+
32
+ function nameMatcher(pattern: string): (name: string) => boolean {
33
+ if (pattern === '') return () => true;
34
+ try {
35
+ const regex = new RegExp(pattern, 'i');
36
+ return (name) => regex.test(name);
37
+ } catch {
38
+ // A pattern still being typed ("src/[") is not a regex yet; substring
39
+ // matching keeps the list narrowing instead of going blank.
40
+ const lowered = pattern.toLowerCase();
41
+ return (name) => name.toLowerCase().includes(lowered);
42
+ }
43
+ }
44
+
45
+ async function isDirectory(parent: string, entry: fs.Dirent): Promise<boolean> {
46
+ if (!entry.isSymbolicLink()) return entry.isDirectory();
47
+ try {
48
+ return (await fs.promises.stat(path.join(parent, entry.name))).isDirectory();
49
+ } catch {
50
+ return false; // A dangling link is not somewhere a session can start.
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Absolute paths of the directories the typed path could complete to, for
56
+ * the new-session picker. Hidden directories stay out of the way unless the
57
+ * pattern itself begins with a dot; an unreadable or missing parent yields
58
+ * nothing rather than an error, since the person is still typing.
59
+ */
60
+ export async function listDirectories(typed: string, limit = DEFAULT_LIMIT): Promise<string[]> {
61
+ const query = splitDirectoryQuery(typed);
62
+ if (query === undefined) return [];
63
+ let entries: fs.Dirent[];
64
+ try {
65
+ entries = await fs.promises.readdir(query.parent, { withFileTypes: true });
66
+ } catch {
67
+ return [];
68
+ }
69
+ const showHidden = ASKS_FOR_HIDDEN.test(query.pattern);
70
+ const matches = nameMatcher(query.pattern);
71
+ const names: string[] = [];
72
+ for (const entry of entries) {
73
+ if (entry.name.startsWith(HIDDEN_PREFIX) && !showHidden) continue;
74
+ if (!matches(entry.name)) continue;
75
+ if (await isDirectory(query.parent, entry)) names.push(entry.name);
76
+ }
77
+ names.sort((left, right) => left.localeCompare(right));
78
+ return names.slice(0, limit).map((name) => path.join(query.parent, name));
79
+ }
80
+
81
+ /**
82
+ * Where a fuzzy search starts when the typed value names no place that exists.
83
+ * Home is where projects live; a path outside it is still reachable by typing
84
+ * it out, which is what the completion above is for.
85
+ */
86
+ function searchRoots(homeDirectory: string): string[] {
87
+ return [homeDirectory];
88
+ }
89
+
90
+ export interface SuggestDirectoriesOptions {
91
+ limit?: number;
92
+ /** Injectable so a test does not search the machine it runs on. */
93
+ homeDirectory?: string;
94
+ /** Injectable seam over the tree search. */
95
+ search?: (root: string, query: string) => Promise<string[]>;
96
+ }
97
+
98
+ /**
99
+ * The directories to offer for a typed value, however it was typed.
100
+ *
101
+ * Completion comes first: an absolute path whose parent exists is someone
102
+ * drilling in, and listing that parent's children is exactly right. Anything
103
+ * else is a name, not a location. A bare "agirepo", or a path remembered from
104
+ * another machine where the folder sat somewhere else, both mean the same
105
+ * thing, and answering "No such directory" to either is a dead end when the
106
+ * folder is sitting in the reader's home directory under a different parent.
107
+ */
108
+ export async function suggestDirectories(typed: string, options: SuggestDirectoriesOptions = {}): Promise<string[]> {
109
+ const limit = options.limit ?? DEFAULT_LIMIT;
110
+ const completions = await listDirectories(typed, limit);
111
+ if (completions.length > 0) return completions;
112
+
113
+ const query = searchTermFor(typed);
114
+ if (query === '') return [];
115
+ const home = options.homeDirectory ?? os.homedir();
116
+ const search = options.search ?? searchDirectoryTree;
117
+
118
+ const found: string[] = [];
119
+ for (const root of searchRoots(home)) {
120
+ try {
121
+ found.push(...(await search(root, query)));
122
+ } catch {
123
+ // A root that cannot be searched simply contributes nothing.
124
+ }
125
+ }
126
+ return rankDirectories(found, query, limit);
127
+ }
@@ -0,0 +1,124 @@
1
+ import { execFile } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ /** How deep a search descends; a project someone starts a session in is never far from home. */
6
+ const MAX_DEPTH = 6;
7
+ /** A ceiling on the walk so a pathological tree cannot hold the request open. */
8
+ const MAX_VISITED = 20_000;
9
+ /** `fd` is fast; if it has not answered by now the fallback will beat waiting for it. */
10
+ const FD_TIMEOUT_MS = 1_500;
11
+ const FD_MAX_BUFFER = 4 * 1024 * 1024;
12
+
13
+ /**
14
+ * Directories nobody starts a session in, and which dwarf everything else in
15
+ * a tree. Pruning them is the difference between a walk that answers now and
16
+ * one that reads a million files.
17
+ */
18
+ const PRUNED = new Set([
19
+ 'node_modules',
20
+ 'Library',
21
+ 'Applications',
22
+ 'dist',
23
+ 'build',
24
+ 'out',
25
+ 'target',
26
+ 'vendor',
27
+ 'coverage',
28
+ '.git',
29
+ '.nx',
30
+ '.next',
31
+ '.cache',
32
+ '.venv',
33
+ '__pycache__',
34
+ ]);
35
+
36
+ const HIDDEN_PREFIX = '.';
37
+
38
+ function skips(name: string, wantsHidden: boolean): boolean {
39
+ if (PRUNED.has(name)) return true;
40
+ return name.startsWith(HIDDEN_PREFIX) && !wantsHidden;
41
+ }
42
+
43
+ /**
44
+ * `fd` if this machine has it: it is built for exactly this, prunes ignored
45
+ * paths itself, and is far quicker than walking from Node. A missing binary,
46
+ * a non-zero exit, or a slow answer all fall through to the walk rather than
47
+ * failing the request, because a suggestion list is a convenience.
48
+ */
49
+ async function searchWithFd(root: string, query: string, wantsHidden: boolean): Promise<string[] | undefined> {
50
+ const args = [
51
+ '--type',
52
+ 'd',
53
+ '--absolute-path',
54
+ '--fixed-strings',
55
+ '--color',
56
+ 'never',
57
+ '--max-depth',
58
+ String(MAX_DEPTH),
59
+ // The same prune list the walk uses, so which engine answered is invisible
60
+ // to the reader: no caches, no build output, no OS folders nobody codes in.
61
+ ...[...PRUNED].flatMap((name) => ['--exclude', name]),
62
+ ...(wantsHidden ? ['--hidden'] : []),
63
+ '--',
64
+ query,
65
+ root,
66
+ ];
67
+ return new Promise((resolve) => {
68
+ execFile('fd', args, { timeout: FD_TIMEOUT_MS, maxBuffer: FD_MAX_BUFFER }, (error, stdout) => {
69
+ // fd exits non-zero when it matches nothing, which is an answer, not a
70
+ // failure; only a missing binary or a timeout sends us to the walk.
71
+ if (error && stdout.length === 0 && error.code !== 1) {
72
+ resolve(undefined);
73
+ return;
74
+ }
75
+ resolve(
76
+ stdout
77
+ .split('\n')
78
+ .map((line) => line.trimEnd().replace(/\/$/u, ''))
79
+ .filter((line) => line !== ''),
80
+ );
81
+ });
82
+ });
83
+ }
84
+
85
+ /** Every directory under a root, breadth first, bounded in depth and in total work. */
86
+ async function walkDirectories(root: string, wantsHidden: boolean): Promise<string[]> {
87
+ const found: string[] = [];
88
+ let frontier = [root];
89
+ let visited = 0;
90
+
91
+ for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0 && visited < MAX_VISITED; depth += 1) {
92
+ const next: string[] = [];
93
+ for (const directory of frontier) {
94
+ if (visited >= MAX_VISITED) break;
95
+ let entries: fs.Dirent[];
96
+ try {
97
+ entries = await fs.promises.readdir(directory, { withFileTypes: true });
98
+ } catch {
99
+ continue; // Unreadable is not an error here, just nothing to offer.
100
+ }
101
+ for (const entry of entries) {
102
+ if (!entry.isDirectory() || skips(entry.name, wantsHidden)) continue;
103
+ visited += 1;
104
+ const child = path.join(directory, entry.name);
105
+ found.push(child);
106
+ next.push(child);
107
+ }
108
+ }
109
+ frontier = next;
110
+ }
111
+ return found;
112
+ }
113
+
114
+ /**
115
+ * Directories under a root whose path could answer the query.
116
+ *
117
+ * Matching happens in the caller, against the whole candidate set, so the
118
+ * ranking is the same whichever engine produced it.
119
+ */
120
+ export async function searchDirectoryTree(root: string, query: string): Promise<string[]> {
121
+ const wantsHidden = query.startsWith(HIDDEN_PREFIX);
122
+ const viaFd = await searchWithFd(root, query, wantsHidden);
123
+ return viaFd ?? (await walkDirectories(root, wantsHidden));
124
+ }
@@ -0,0 +1,27 @@
1
+ import { execFile } from 'node:child_process';
2
+ import type { SessionGitStatus } from '../types/hub.ts';
3
+
4
+ const GIT_TIMEOUT_MS = 2000;
5
+
6
+ function git(cwd: string, args: string[]): Promise<string | undefined> {
7
+ return new Promise((resolve) => {
8
+ execFile('git', args, { cwd, timeout: GIT_TIMEOUT_MS }, (error, stdout) => {
9
+ // A non-repo cwd, missing git, or timeout all mean the same thing to the
10
+ // rail: no branch chip.
11
+ resolve(error ? undefined : stdout);
12
+ });
13
+ });
14
+ }
15
+
16
+ /**
17
+ * Reads the branch chip for one session's working directory.
18
+ *
19
+ * Asks git itself rather than parsing .git/HEAD: worktrees (where .git is a
20
+ * file) come free, and only git can answer whether the tree is dirty.
21
+ */
22
+ export async function readGitStatus(cwd: string): Promise<SessionGitStatus | undefined> {
23
+ const branch = (await git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']))?.trim();
24
+ if (!branch) return undefined;
25
+ const status = await git(cwd, ['status', '--porcelain']);
26
+ return { branch, dirty: status !== undefined && status.trim().length > 0 };
27
+ }