mulmoclaude 0.1.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 (408) hide show
  1. package/README.md +44 -0
  2. package/bin/mulmoclaude.js +202 -0
  3. package/bin/prepare-dist.js +93 -0
  4. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
  5. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
  6. package/client/assets/index-D8rhwXLq.js +4906 -0
  7. package/client/assets/index-KNLBjwuh.css +1 -0
  8. package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
  9. package/client/assets/material-icons-Dr0goTwe.woff +0 -0
  10. package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
  11. package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
  12. package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
  13. package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
  14. package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
  15. package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
  16. package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
  17. package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
  18. package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
  19. package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
  20. package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
  21. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
  22. package/client/index.html +28 -0
  23. package/package.json +66 -0
  24. package/server/agent/attachmentConverter.ts +270 -0
  25. package/server/agent/config.ts +414 -0
  26. package/server/agent/index.ts +260 -0
  27. package/server/agent/mcp-server.ts +412 -0
  28. package/server/agent/mcp-tools/index.ts +63 -0
  29. package/server/agent/mcp-tools/x.ts +188 -0
  30. package/server/agent/plugin-names.ts +75 -0
  31. package/server/agent/prompt.ts +349 -0
  32. package/server/agent/resumeFailover.ts +129 -0
  33. package/server/agent/sandboxMounts.ts +329 -0
  34. package/server/agent/stream.ts +194 -0
  35. package/server/api/auth/bearerAuth.ts +61 -0
  36. package/server/api/auth/token.ts +98 -0
  37. package/server/api/csrfGuard.ts +85 -0
  38. package/server/api/routes/agent.ts +478 -0
  39. package/server/api/routes/chart.ts +98 -0
  40. package/server/api/routes/chat-index.ts +46 -0
  41. package/server/api/routes/config.ts +258 -0
  42. package/server/api/routes/dispatchResponse.ts +79 -0
  43. package/server/api/routes/files.ts +812 -0
  44. package/server/api/routes/html.ts +101 -0
  45. package/server/api/routes/image.ts +169 -0
  46. package/server/api/routes/mulmo-script.ts +712 -0
  47. package/server/api/routes/mulmoScriptValidate.ts +101 -0
  48. package/server/api/routes/notifications.ts +69 -0
  49. package/server/api/routes/pdf.ts +163 -0
  50. package/server/api/routes/plugins.ts +276 -0
  51. package/server/api/routes/presentHtml.ts +48 -0
  52. package/server/api/routes/roles.ts +125 -0
  53. package/server/api/routes/scheduler.ts +153 -0
  54. package/server/api/routes/schedulerHandlers.ts +151 -0
  55. package/server/api/routes/schedulerTasks.ts +163 -0
  56. package/server/api/routes/sessions.ts +294 -0
  57. package/server/api/routes/sessionsCursor.ts +59 -0
  58. package/server/api/routes/skills.ts +195 -0
  59. package/server/api/routes/sources.ts +540 -0
  60. package/server/api/routes/todos.ts +263 -0
  61. package/server/api/routes/todosColumnsHandlers.ts +347 -0
  62. package/server/api/routes/todosHandlers.ts +274 -0
  63. package/server/api/routes/todosItemsHandlers.ts +386 -0
  64. package/server/api/routes/wiki/pageIndex.ts +53 -0
  65. package/server/api/routes/wiki.ts +363 -0
  66. package/server/api/sandboxStatus.ts +64 -0
  67. package/server/events/notifications.ts +160 -0
  68. package/server/events/pub-sub/index.ts +45 -0
  69. package/server/events/relay-client.ts +288 -0
  70. package/server/events/scheduler-adapter.ts +302 -0
  71. package/server/events/session-store/index.ts +492 -0
  72. package/server/events/task-manager/index.ts +181 -0
  73. package/server/index.ts +572 -0
  74. package/server/system/config.ts +243 -0
  75. package/server/system/credentials.ts +220 -0
  76. package/server/system/docker.ts +97 -0
  77. package/server/system/env.ts +109 -0
  78. package/server/system/logger/config.ts +112 -0
  79. package/server/system/logger/formatters.ts +40 -0
  80. package/server/system/logger/index.ts +53 -0
  81. package/server/system/logger/rotation.ts +37 -0
  82. package/server/system/logger/sinks.ts +101 -0
  83. package/server/system/logger/types.ts +29 -0
  84. package/server/utils/date.ts +57 -0
  85. package/server/utils/errors.ts +7 -0
  86. package/server/utils/fetch.ts +27 -0
  87. package/server/utils/files/atomic.ts +125 -0
  88. package/server/utils/files/html-io.ts +20 -0
  89. package/server/utils/files/image-store.ts +66 -0
  90. package/server/utils/files/index.ts +45 -0
  91. package/server/utils/files/journal-io.ts +213 -0
  92. package/server/utils/files/json.ts +69 -0
  93. package/server/utils/files/markdown-store.ts +33 -0
  94. package/server/utils/files/naming.ts +50 -0
  95. package/server/utils/files/reference-dirs-io.ts +45 -0
  96. package/server/utils/files/roles-io.ts +45 -0
  97. package/server/utils/files/safe.ts +106 -0
  98. package/server/utils/files/scheduler-io.ts +20 -0
  99. package/server/utils/files/scheduler-overrides-io.ts +64 -0
  100. package/server/utils/files/session-io.ts +136 -0
  101. package/server/utils/files/spreadsheet-store.ts +63 -0
  102. package/server/utils/files/todos-io.ts +29 -0
  103. package/server/utils/files/user-tasks-io.ts +25 -0
  104. package/server/utils/files/workspace-io.ts +221 -0
  105. package/server/utils/gemini.ts +59 -0
  106. package/server/utils/gitignore.ts +69 -0
  107. package/server/utils/http.ts +15 -0
  108. package/server/utils/httpError.ts +61 -0
  109. package/server/utils/id.ts +16 -0
  110. package/server/utils/json.ts +83 -0
  111. package/server/utils/logBackgroundError.ts +22 -0
  112. package/server/utils/markdown.ts +82 -0
  113. package/server/utils/request.ts +29 -0
  114. package/server/utils/slug.ts +50 -0
  115. package/server/utils/spawn.ts +62 -0
  116. package/server/utils/time.ts +34 -0
  117. package/server/utils/types.ts +47 -0
  118. package/server/workspace/chat-index/index.ts +153 -0
  119. package/server/workspace/chat-index/indexer.ts +209 -0
  120. package/server/workspace/chat-index/paths.ts +34 -0
  121. package/server/workspace/chat-index/summarizer.ts +247 -0
  122. package/server/workspace/chat-index/types.ts +38 -0
  123. package/server/workspace/custom-dirs.ts +220 -0
  124. package/server/workspace/helps/business.md +104 -0
  125. package/server/workspace/helps/github.md +23 -0
  126. package/server/workspace/helps/index.md +60 -0
  127. package/server/workspace/helps/mulmoscript.md +249 -0
  128. package/server/workspace/helps/sandbox.md +90 -0
  129. package/server/workspace/helps/spreadsheet.md +43 -0
  130. package/server/workspace/helps/telegram.md +135 -0
  131. package/server/workspace/helps/wiki.md +131 -0
  132. package/server/workspace/journal/archivist.ts +386 -0
  133. package/server/workspace/journal/dailyPass.ts +743 -0
  134. package/server/workspace/journal/diff.ts +71 -0
  135. package/server/workspace/journal/index.ts +185 -0
  136. package/server/workspace/journal/indexFile.ts +136 -0
  137. package/server/workspace/journal/linkRewrite.ts +4 -0
  138. package/server/workspace/journal/memoryExtractor.ts +130 -0
  139. package/server/workspace/journal/optimizationPass.ts +160 -0
  140. package/server/workspace/journal/paths.ts +76 -0
  141. package/server/workspace/journal/state.ts +125 -0
  142. package/server/workspace/paths.ts +158 -0
  143. package/server/workspace/reference-dirs.ts +252 -0
  144. package/server/workspace/roles.ts +37 -0
  145. package/server/workspace/skills/discovery.ts +125 -0
  146. package/server/workspace/skills/index.ts +10 -0
  147. package/server/workspace/skills/parser.ts +144 -0
  148. package/server/workspace/skills/paths.ts +41 -0
  149. package/server/workspace/skills/scheduler.ts +149 -0
  150. package/server/workspace/skills/types.ts +30 -0
  151. package/server/workspace/skills/user-tasks.ts +257 -0
  152. package/server/workspace/skills/writer.ts +189 -0
  153. package/server/workspace/sources/arxivDiscovery.ts +182 -0
  154. package/server/workspace/sources/classifier.ts +268 -0
  155. package/server/workspace/sources/fetchers/arxiv.ts +170 -0
  156. package/server/workspace/sources/fetchers/github.ts +106 -0
  157. package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
  158. package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
  159. package/server/workspace/sources/fetchers/index.ts +71 -0
  160. package/server/workspace/sources/fetchers/registerAll.ts +15 -0
  161. package/server/workspace/sources/fetchers/rss.ts +141 -0
  162. package/server/workspace/sources/fetchers/rssParser.ts +295 -0
  163. package/server/workspace/sources/httpFetcher.ts +230 -0
  164. package/server/workspace/sources/interests.ts +120 -0
  165. package/server/workspace/sources/paths.ts +110 -0
  166. package/server/workspace/sources/pipeline/dedup.ts +60 -0
  167. package/server/workspace/sources/pipeline/fetch.ts +136 -0
  168. package/server/workspace/sources/pipeline/index.ts +249 -0
  169. package/server/workspace/sources/pipeline/notify.ts +72 -0
  170. package/server/workspace/sources/pipeline/plan.ts +66 -0
  171. package/server/workspace/sources/pipeline/summarize.ts +189 -0
  172. package/server/workspace/sources/pipeline/write.ts +185 -0
  173. package/server/workspace/sources/rateLimiter.ts +148 -0
  174. package/server/workspace/sources/registry.ts +326 -0
  175. package/server/workspace/sources/robots.ts +271 -0
  176. package/server/workspace/sources/sourceState.ts +135 -0
  177. package/server/workspace/sources/taxonomy.ts +74 -0
  178. package/server/workspace/sources/types.ts +144 -0
  179. package/server/workspace/sources/urls.ts +112 -0
  180. package/server/workspace/tool-trace/classify.ts +114 -0
  181. package/server/workspace/tool-trace/index.ts +250 -0
  182. package/server/workspace/tool-trace/writeSearch.ts +98 -0
  183. package/server/workspace/wiki-backlinks/index.ts +107 -0
  184. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
  185. package/server/workspace/workspace.ts +66 -0
  186. package/src/App.vue +720 -0
  187. package/src/assets/mulmo_bw.png +0 -0
  188. package/src/components/CanvasViewToggle.vue +27 -0
  189. package/src/components/ChatAttachmentPreview.vue +45 -0
  190. package/src/components/ChatImagePreview.vue +17 -0
  191. package/src/components/ChatInput.vue +208 -0
  192. package/src/components/FileContentHeader.vue +49 -0
  193. package/src/components/FileContentRenderer.vue +162 -0
  194. package/src/components/FileTree.vue +115 -0
  195. package/src/components/FileTreePane.vue +85 -0
  196. package/src/components/FilesView.vue +206 -0
  197. package/src/components/LockStatusPopup.vue +111 -0
  198. package/src/components/NotificationBell.vue +131 -0
  199. package/src/components/NotificationToast.vue +72 -0
  200. package/src/components/PluginLauncher.vue +138 -0
  201. package/src/components/RightSidebar.vue +113 -0
  202. package/src/components/RoleSelector.vue +64 -0
  203. package/src/components/SessionHistoryPanel.vue +176 -0
  204. package/src/components/SessionTabBar.vue +81 -0
  205. package/src/components/SettingsMcpTab.vue +350 -0
  206. package/src/components/SettingsModal.vue +275 -0
  207. package/src/components/SettingsReferenceDirsTab.vue +173 -0
  208. package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
  209. package/src/components/SidebarHeader.vue +69 -0
  210. package/src/components/StackView.vue +360 -0
  211. package/src/components/SuggestionsPanel.vue +65 -0
  212. package/src/components/TodoExplorer.vue +358 -0
  213. package/src/components/ToolResultsPanel.vue +77 -0
  214. package/src/components/todo/TodoAddDialog.vue +131 -0
  215. package/src/components/todo/TodoEditDialog.vue +47 -0
  216. package/src/components/todo/TodoEditPanel.vue +113 -0
  217. package/src/components/todo/TodoKanbanView.vue +249 -0
  218. package/src/components/todo/TodoListView.vue +79 -0
  219. package/src/components/todo/TodoTableView.vue +177 -0
  220. package/src/composables/useActiveSession.ts +40 -0
  221. package/src/composables/useAppApi.ts +45 -0
  222. package/src/composables/useCanvasViewMode.ts +121 -0
  223. package/src/composables/useChatScroll.ts +47 -0
  224. package/src/composables/useClickOutside.ts +26 -0
  225. package/src/composables/useClipboardCopy.ts +44 -0
  226. package/src/composables/useContentDisplay.ts +52 -0
  227. package/src/composables/useDebugBeat.ts +23 -0
  228. package/src/composables/useDynamicFavicon.ts +115 -0
  229. package/src/composables/useEventListeners.ts +42 -0
  230. package/src/composables/useExpandedDirs.ts +64 -0
  231. package/src/composables/useFaviconState.ts +30 -0
  232. package/src/composables/useFileSelection.ts +115 -0
  233. package/src/composables/useFileSortMode.ts +24 -0
  234. package/src/composables/useFileTree.ts +85 -0
  235. package/src/composables/useFreshPluginData.ts +89 -0
  236. package/src/composables/useHealth.ts +38 -0
  237. package/src/composables/useImeAwareEnter.ts +57 -0
  238. package/src/composables/useKeyNavigation.ts +60 -0
  239. package/src/composables/useMarkdownLinkHandler.ts +46 -0
  240. package/src/composables/useMarkdownMode.ts +17 -0
  241. package/src/composables/useMcpTools.ts +71 -0
  242. package/src/composables/useMergedSessions.ts +27 -0
  243. package/src/composables/useNotifications.ts +90 -0
  244. package/src/composables/usePdfDownload.ts +60 -0
  245. package/src/composables/usePendingCalls.ts +77 -0
  246. package/src/composables/usePubSub.ts +85 -0
  247. package/src/composables/useRightSidebar.ts +23 -0
  248. package/src/composables/useRoles.ts +34 -0
  249. package/src/composables/useSandboxStatus.ts +67 -0
  250. package/src/composables/useSelectedResult.ts +49 -0
  251. package/src/composables/useSessionDerived.ts +51 -0
  252. package/src/composables/useSessionHistory.ts +81 -0
  253. package/src/composables/useSessionSync.ts +57 -0
  254. package/src/composables/useViewLayout.ts +55 -0
  255. package/src/config/apiRoutes.ts +173 -0
  256. package/src/config/pubsubChannels.ts +45 -0
  257. package/src/config/roles.ts +335 -0
  258. package/src/config/schedulerActions.ts +25 -0
  259. package/src/config/toolNames.ts +71 -0
  260. package/src/config/workspacePaths.ts +24 -0
  261. package/src/index.css +107 -0
  262. package/src/main.ts +25 -0
  263. package/src/plugins/canvas/Preview.vue +13 -0
  264. package/src/plugins/canvas/View.vue +333 -0
  265. package/src/plugins/canvas/definition.ts +38 -0
  266. package/src/plugins/canvas/index.ts +36 -0
  267. package/src/plugins/chart/Preview.vue +49 -0
  268. package/src/plugins/chart/View.vue +143 -0
  269. package/src/plugins/chart/definition.ts +58 -0
  270. package/src/plugins/chart/index.ts +52 -0
  271. package/src/plugins/editImage/Preview.vue +13 -0
  272. package/src/plugins/editImage/View.vue +13 -0
  273. package/src/plugins/editImage/definition.ts +27 -0
  274. package/src/plugins/editImage/index.ts +36 -0
  275. package/src/plugins/generateImage/Preview.vue +13 -0
  276. package/src/plugins/generateImage/View.vue +33 -0
  277. package/src/plugins/generateImage/definition.ts +32 -0
  278. package/src/plugins/generateImage/index.ts +56 -0
  279. package/src/plugins/manageRoles/Preview.vue +49 -0
  280. package/src/plugins/manageRoles/View.vue +525 -0
  281. package/src/plugins/manageRoles/definition.ts +43 -0
  282. package/src/plugins/manageRoles/index.ts +47 -0
  283. package/src/plugins/manageSkills/Preview.vue +21 -0
  284. package/src/plugins/manageSkills/View.vue +321 -0
  285. package/src/plugins/manageSkills/definition.ts +49 -0
  286. package/src/plugins/manageSkills/index.ts +49 -0
  287. package/src/plugins/manageSource/Preview.vue +33 -0
  288. package/src/plugins/manageSource/View.vue +697 -0
  289. package/src/plugins/manageSource/definition.ts +63 -0
  290. package/src/plugins/manageSource/index.ts +66 -0
  291. package/src/plugins/markdown/Preview.vue +77 -0
  292. package/src/plugins/markdown/View.vue +476 -0
  293. package/src/plugins/markdown/definition.ts +50 -0
  294. package/src/plugins/markdown/index.ts +36 -0
  295. package/src/plugins/presentHtml/Preview.vue +25 -0
  296. package/src/plugins/presentHtml/View.vue +52 -0
  297. package/src/plugins/presentHtml/definition.ts +27 -0
  298. package/src/plugins/presentHtml/helpers.ts +72 -0
  299. package/src/plugins/presentHtml/index.ts +41 -0
  300. package/src/plugins/presentMulmoScript/Preview.vue +23 -0
  301. package/src/plugins/presentMulmoScript/View.vue +1166 -0
  302. package/src/plugins/presentMulmoScript/definition.ts +95 -0
  303. package/src/plugins/presentMulmoScript/helpers.ts +162 -0
  304. package/src/plugins/presentMulmoScript/index.ts +40 -0
  305. package/src/plugins/scheduler/Preview.vue +67 -0
  306. package/src/plugins/scheduler/TasksTab.vue +205 -0
  307. package/src/plugins/scheduler/View.vue +565 -0
  308. package/src/plugins/scheduler/definition.ts +57 -0
  309. package/src/plugins/scheduler/index.ts +45 -0
  310. package/src/plugins/scheduler/viewModes.ts +26 -0
  311. package/src/plugins/spreadsheet/Preview.vue +29 -0
  312. package/src/plugins/spreadsheet/View.vue +997 -0
  313. package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
  314. package/src/plugins/spreadsheet/definition.ts +121 -0
  315. package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
  316. package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
  317. package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
  318. package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
  319. package/src/plugins/spreadsheet/engine/engine.ts +176 -0
  320. package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
  321. package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
  322. package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
  323. package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
  324. package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
  325. package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
  326. package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
  327. package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
  328. package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
  329. package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
  330. package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
  331. package/src/plugins/spreadsheet/engine/index.ts +27 -0
  332. package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
  333. package/src/plugins/spreadsheet/engine/parser.ts +143 -0
  334. package/src/plugins/spreadsheet/engine/registry.ts +150 -0
  335. package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
  336. package/src/plugins/spreadsheet/engine/types.ts +64 -0
  337. package/src/plugins/spreadsheet/index.ts +36 -0
  338. package/src/plugins/textResponse/Preview.vue +94 -0
  339. package/src/plugins/textResponse/View.vue +503 -0
  340. package/src/plugins/textResponse/definition.ts +34 -0
  341. package/src/plugins/textResponse/index.ts +27 -0
  342. package/src/plugins/textResponse/plugin.ts +29 -0
  343. package/src/plugins/textResponse/samples.ts +97 -0
  344. package/src/plugins/textResponse/types.ts +11 -0
  345. package/src/plugins/todo/Preview.vue +63 -0
  346. package/src/plugins/todo/View.vue +364 -0
  347. package/src/plugins/todo/composables/useTodos.ts +177 -0
  348. package/src/plugins/todo/definition.ts +45 -0
  349. package/src/plugins/todo/index.ts +61 -0
  350. package/src/plugins/todo/labels.ts +163 -0
  351. package/src/plugins/todo/priority.ts +98 -0
  352. package/src/plugins/todo/viewModes.ts +19 -0
  353. package/src/plugins/ui-image/ImagePreview.vue +23 -0
  354. package/src/plugins/ui-image/ImageView.vue +34 -0
  355. package/src/plugins/ui-image/index.ts +3 -0
  356. package/src/plugins/ui-image/types.ts +4 -0
  357. package/src/plugins/wiki/Preview.vue +65 -0
  358. package/src/plugins/wiki/View.vue +342 -0
  359. package/src/plugins/wiki/definition.ts +25 -0
  360. package/src/plugins/wiki/helpers.ts +59 -0
  361. package/src/plugins/wiki/index.ts +52 -0
  362. package/src/router/guards.ts +61 -0
  363. package/src/router/index.ts +50 -0
  364. package/src/tools/index.ts +52 -0
  365. package/src/tools/types.ts +27 -0
  366. package/src/types/events.ts +16 -0
  367. package/src/types/fileTree.ts +13 -0
  368. package/src/types/notification.ts +67 -0
  369. package/src/types/session.ts +116 -0
  370. package/src/types/sse.ts +90 -0
  371. package/src/types/toolCallHistory.ts +13 -0
  372. package/src/utils/agent/eventDispatch.ts +74 -0
  373. package/src/utils/agent/request.ts +55 -0
  374. package/src/utils/agent/toolCalls.ts +62 -0
  375. package/src/utils/api.ts +218 -0
  376. package/src/utils/canvas/viewMode.ts +46 -0
  377. package/src/utils/dom/authTokenMeta.ts +20 -0
  378. package/src/utils/dom/clickOutside.ts +11 -0
  379. package/src/utils/dom/externalLink.ts +57 -0
  380. package/src/utils/dom/scrollable.ts +24 -0
  381. package/src/utils/errors.ts +11 -0
  382. package/src/utils/files/expandedDirs.ts +25 -0
  383. package/src/utils/files/filename.ts +12 -0
  384. package/src/utils/files/sortChildren.ts +20 -0
  385. package/src/utils/filesPreview/schedulerPreview.ts +38 -0
  386. package/src/utils/filesPreview/todoPreview.ts +40 -0
  387. package/src/utils/format/date.ts +85 -0
  388. package/src/utils/format/frontmatter.ts +80 -0
  389. package/src/utils/format/jsonSyntax.ts +109 -0
  390. package/src/utils/html/previewCsp.ts +65 -0
  391. package/src/utils/image/resolve.ts +8 -0
  392. package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
  393. package/src/utils/markdown/extractFirstH1.ts +39 -0
  394. package/src/utils/notification/dispatch.ts +22 -0
  395. package/src/utils/path/relativeLink.ts +130 -0
  396. package/src/utils/role/icon.ts +20 -0
  397. package/src/utils/role/merge.ts +10 -0
  398. package/src/utils/role/plugins.ts +12 -0
  399. package/src/utils/session/mergeSessions.ts +103 -0
  400. package/src/utils/session/seedRoleDefault.ts +35 -0
  401. package/src/utils/session/sessionEntries.ts +121 -0
  402. package/src/utils/session/sessionFactory.ts +22 -0
  403. package/src/utils/session/sessionHelpers.ts +99 -0
  404. package/src/utils/tools/dedup.ts +17 -0
  405. package/src/utils/tools/mcp.ts +33 -0
  406. package/src/utils/tools/pendingCalls.ts +16 -0
  407. package/src/utils/tools/result.ts +40 -0
  408. package/src/utils/types.ts +44 -0
@@ -0,0 +1,90 @@
1
+ // Web-side subscriber for the `notifications` pub-sub channel.
2
+ // Stores incoming NotificationPayloads for the bell badge + panel.
3
+ //
4
+ // Uses a singleton subscription pattern: the first component that
5
+ // calls useNotifications() subscribes to the pub-sub channel; the
6
+ // last one to unmount unsubscribes. All consumers share the same
7
+ // module-level state (notifications + readAt).
8
+
9
+ import { onUnmounted, ref, computed, type Ref, type ComputedRef } from "vue";
10
+ import { PUBSUB_CHANNELS } from "../config/pubsubChannels";
11
+ import { usePubSub } from "./usePubSub";
12
+ import { NOTIFICATION_KINDS } from "../types/notification";
13
+ import type { NotificationPayload } from "../types/notification";
14
+ import { isRecord } from "../utils/types";
15
+
16
+ const MAX_RECENT = 50;
17
+
18
+ const VALID_KINDS = new Set<string>(Object.values(NOTIFICATION_KINDS));
19
+
20
+ function isNotificationPayload(value: unknown): value is NotificationPayload {
21
+ if (!isRecord(value)) return false;
22
+ if (typeof value.id !== "string") return false;
23
+ if (typeof value.kind !== "string" || !VALID_KINDS.has(value.kind)) return false;
24
+ if (typeof value.title !== "string") return false;
25
+ if (typeof value.firedAt !== "string") return false;
26
+ if (!isValidAction(value.action)) return false;
27
+ return true;
28
+ }
29
+
30
+ function isValidAction(action: unknown): boolean {
31
+ if (!isRecord(action)) return false;
32
+ return typeof action.type === "string";
33
+ }
34
+
35
+ // Module-level state so all components share the same list.
36
+ const notifications = ref<NotificationPayload[]>([]);
37
+ const readAt = ref<string | null>(null);
38
+
39
+ // Singleton subscription — ref-counted across consumers.
40
+ let subscriberCount = 0;
41
+ let unsubscribeFn: (() => void) | null = null;
42
+
43
+ function ensureSubscribed(subscribe: ReturnType<typeof usePubSub>["subscribe"]): void {
44
+ subscriberCount++;
45
+ if (unsubscribeFn) return; // already listening
46
+ unsubscribeFn = subscribe(PUBSUB_CHANNELS.notifications, (data) => {
47
+ if (!isNotificationPayload(data)) return;
48
+ notifications.value = [data, ...notifications.value].slice(0, MAX_RECENT);
49
+ });
50
+ }
51
+
52
+ function releaseSubscription(): void {
53
+ subscriberCount--;
54
+ if (subscriberCount <= 0 && unsubscribeFn) {
55
+ unsubscribeFn();
56
+ unsubscribeFn = null;
57
+ subscriberCount = 0;
58
+ }
59
+ }
60
+
61
+ export function useNotifications(): {
62
+ notifications: Ref<NotificationPayload[]>;
63
+ latest: ComputedRef<NotificationPayload | null>;
64
+ unreadCount: ComputedRef<number>;
65
+ markAllRead: () => void;
66
+ dismiss: (id: string) => void;
67
+ } {
68
+ const { subscribe } = usePubSub();
69
+ ensureSubscribed(subscribe);
70
+ onUnmounted(releaseSubscription);
71
+
72
+ const latest = computed(() => notifications.value[0] ?? null);
73
+
74
+ const unreadCount = computed(() => {
75
+ if (!readAt.value) return notifications.value.length;
76
+ return notifications.value.filter((n) => n.firedAt > readAt.value!).length;
77
+ });
78
+
79
+ function markAllRead(): void {
80
+ if (notifications.value.length > 0) {
81
+ readAt.value = notifications.value[0].firedAt;
82
+ }
83
+ }
84
+
85
+ function dismiss(id: string): void {
86
+ notifications.value = notifications.value.filter((n) => n.id !== id);
87
+ }
88
+
89
+ return { notifications, latest, unreadCount, markAllRead, dismiss };
90
+ }
@@ -0,0 +1,60 @@
1
+ // Shared PDF download logic used by the markdown and textResponse
2
+ // plugin views. Encapsulates the POST /api/pdf/markdown call, the
3
+ // in-flight `pdfDownloading` flag, the `pdfError` state, and the
4
+ // blob-to-download dance (createObjectURL → click → revoke).
5
+ //
6
+ // Error handling contract: any failure path (network, non-OK HTTP,
7
+ // malformed blob) sets pdfDownloading back to false and populates
8
+ // pdfError with a user-facing message. Callers can render pdfError
9
+ // below the download button.
10
+
11
+ import { ref, type Ref } from "vue";
12
+ import { API_ROUTES } from "../config/apiRoutes";
13
+ import { apiFetchRaw } from "../utils/api";
14
+ import { errorMessage } from "../utils/errors";
15
+
16
+ export interface UsePdfDownloadHandle {
17
+ pdfDownloading: Ref<boolean>;
18
+ pdfError: Ref<string | null>;
19
+ downloadPdf: (markdown: string, filename: string) => Promise<void>;
20
+ }
21
+
22
+ export function usePdfDownload(): UsePdfDownloadHandle {
23
+ const pdfDownloading = ref(false);
24
+ const pdfError = ref<string | null>(null);
25
+
26
+ async function downloadPdf(markdown: string, filename: string): Promise<void> {
27
+ pdfError.value = null;
28
+ pdfDownloading.value = true;
29
+ let url: string | null = null;
30
+ try {
31
+ // PDF endpoint returns a binary blob, not JSON — use the raw
32
+ // Response escape hatch so we can call `.blob()` ourselves.
33
+ const response = await apiFetchRaw(API_ROUTES.pdf.markdown, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify({ markdown, filename }),
37
+ });
38
+ if (!response.ok) {
39
+ const errText = await response.text().catch(() => "");
40
+ pdfError.value = `PDF error ${response.status}: ${errText}`;
41
+ return;
42
+ }
43
+ const blob = await response.blob();
44
+ url = URL.createObjectURL(blob);
45
+ const a = document.createElement("a");
46
+ a.href = url;
47
+ a.download = filename;
48
+ a.click();
49
+ } catch (err) {
50
+ pdfError.value = errorMessage(err);
51
+ } finally {
52
+ // Always clean up the object URL and release the in-flight flag
53
+ // so the button is never left disabled forever.
54
+ if (url) URL.revokeObjectURL(url);
55
+ pdfDownloading.value = false;
56
+ }
57
+ }
58
+
59
+ return { pdfDownloading, pdfError, downloadPdf };
60
+ }
@@ -0,0 +1,77 @@
1
+ // Composable that bundles the "minimum visible duration" trick for
2
+ // pending tool call rows: while the agent is running, tick a counter
3
+ // every 50ms so the `pendingCalls` computed re-evaluates and any
4
+ // freshly-resolved call stays visible for at least PENDING_MIN_MS
5
+ // before disappearing. After the run ends, schedule one final tick
6
+ // so the computed clears the lingering rows.
7
+
8
+ import { computed, ref, watch, type ComputedRef, type Ref } from "vue";
9
+ import type { ToolCallHistoryItem } from "../types/toolCallHistory";
10
+ import { isCallStillPending, PENDING_MIN_MS } from "../utils/tools/pendingCalls";
11
+
12
+ interface UsePendingCallsOptions {
13
+ isRunning: ComputedRef<boolean> | Ref<boolean>;
14
+ toolCallHistory: ComputedRef<ToolCallHistoryItem[]> | Ref<ToolCallHistoryItem[]>;
15
+ }
16
+
17
+ export function usePendingCalls(opts: UsePendingCallsOptions) {
18
+ const displayTick = ref(0);
19
+ let tickInterval: ReturnType<typeof setInterval> | null = null;
20
+ // Tracked so teardown can cancel the lingering "final tick" and we
21
+ // never mutate displayTick after the composable's owner unmounts.
22
+ let delayedTickTimeout: ReturnType<typeof setTimeout> | null = null;
23
+
24
+ watch(
25
+ opts.isRunning,
26
+ (running) => {
27
+ if (running) {
28
+ // Guard against double-start: if the watcher fires twice with
29
+ // running=true (e.g. immediate + a synchronous flip), don't
30
+ // stack a second interval.
31
+ if (tickInterval !== null) return;
32
+ tickInterval = setInterval(() => {
33
+ displayTick.value++;
34
+ }, 50);
35
+ } else if (tickInterval !== null) {
36
+ clearInterval(tickInterval);
37
+ tickInterval = null;
38
+ // One final tick so the computed clears after the minimum
39
+ // duration has elapsed. Cancel any previous pending one first
40
+ // so back-to-back start/stop runs do not stack timeouts.
41
+ if (delayedTickTimeout !== null) clearTimeout(delayedTickTimeout);
42
+ delayedTickTimeout = setTimeout(() => {
43
+ displayTick.value++;
44
+ delayedTickTimeout = null;
45
+ }, PENDING_MIN_MS);
46
+ }
47
+ },
48
+ // immediate so a composable created while a run is already in
49
+ // flight (e.g. mounted mid-stream) starts ticking right away
50
+ // instead of waiting for the next isRunning flip.
51
+ { immediate: true },
52
+ );
53
+
54
+ const pendingCalls = computed(() => {
55
+ // Read displayTick to register the computed as a reactive
56
+ // dependency on it — that is how a freshly-resolved row stays
57
+ // visible for the minimum window. The `__` prefix tells ESLint
58
+ // (varsIgnorePattern: "^__") that the variable is intentionally
59
+ // unused.
60
+ const __tickDep = displayTick.value;
61
+ const now = Date.now();
62
+ return opts.toolCallHistory.value.filter((c) => __tickDep >= 0 && isCallStillPending(c, now));
63
+ });
64
+
65
+ function teardown(): void {
66
+ if (tickInterval !== null) {
67
+ clearInterval(tickInterval);
68
+ tickInterval = null;
69
+ }
70
+ if (delayedTickTimeout !== null) {
71
+ clearTimeout(delayedTickTimeout);
72
+ delayedTickTimeout = null;
73
+ }
74
+ }
75
+
76
+ return { pendingCalls, teardown };
77
+ }
@@ -0,0 +1,85 @@
1
+ import { io, type Socket } from "socket.io-client";
2
+
3
+ interface PubSubMessage {
4
+ channel: string;
5
+ data: unknown;
6
+ }
7
+
8
+ type Callback = (data: unknown) => void;
9
+ type Unsubscribe = () => void;
10
+
11
+ // Socket.IO replaces the raw WebSocket + hand-rolled reconnect
12
+ // state machine. One multiplexed connection; channels map to
13
+ // socket.io rooms via `subscribe` / `unsubscribe` events.
14
+ //
15
+ // Reconnect / backoff / heartbeat are all handled by socket.io,
16
+ // so there's no reconnectTimer / reconnectDelay here anymore. On
17
+ // reconnect, `connect` fires again and we re-send every
18
+ // subscription the client still cares about.
19
+
20
+ let socket: Socket | null = null;
21
+
22
+ const listeners = new Map<string, Set<Callback>>();
23
+
24
+ function resendSubscriptions(s: Socket): void {
25
+ for (const channel of listeners.keys()) {
26
+ s.emit("subscribe", channel);
27
+ }
28
+ }
29
+
30
+ function connect(): Socket {
31
+ if (socket) return socket;
32
+
33
+ const s = io({
34
+ path: "/ws/pubsub",
35
+ // Match the server. Long-polling is fine as a fallback but
36
+ // the server refuses it, so don't negotiate it here either —
37
+ // fail fast if the WS upgrade doesn't go through.
38
+ transports: ["websocket"],
39
+ });
40
+
41
+ s.on("connect", () => resendSubscriptions(s));
42
+
43
+ s.on("data", (msg: PubSubMessage) => {
44
+ const cbs = listeners.get(msg.channel);
45
+ if (cbs) {
46
+ for (const cb of cbs) cb(msg.data);
47
+ }
48
+ });
49
+
50
+ socket = s;
51
+ return s;
52
+ }
53
+
54
+ function maybeDisconnect(): void {
55
+ if (listeners.size > 0) return;
56
+ if (!socket) return;
57
+ socket.disconnect();
58
+ socket = null;
59
+ }
60
+
61
+ export function usePubSub() {
62
+ function subscribe(channel: string, callback: Callback): Unsubscribe {
63
+ if (!listeners.has(channel)) listeners.set(channel, new Set());
64
+ listeners.get(channel)!.add(callback);
65
+
66
+ const s = connect();
67
+ if (s.connected) s.emit("subscribe", channel);
68
+ // If not yet connected, the "connect" handler replays every
69
+ // listener's subscription, so newly-added channels are
70
+ // covered without extra bookkeeping.
71
+
72
+ return () => {
73
+ const cbs = listeners.get(channel);
74
+ if (!cbs) return;
75
+ cbs.delete(callback);
76
+ if (cbs.size === 0) {
77
+ listeners.delete(channel);
78
+ if (socket?.connected) socket.emit("unsubscribe", channel);
79
+ }
80
+ maybeDisconnect();
81
+ };
82
+ }
83
+
84
+ return { subscribe };
85
+ }
@@ -0,0 +1,23 @@
1
+ // Composable for the right-sidebar (tool-history) visibility toggle.
2
+ //
3
+ // Persists the open/closed state to localStorage so user preference
4
+ // survives reloads. Kept out of the URL — this is pure "my personal
5
+ // UI choice" rather than a shareable view of the session.
6
+
7
+ import { ref, type Ref } from "vue";
8
+
9
+ const STORAGE_KEY = "right_sidebar_visible";
10
+
11
+ export function useRightSidebar(): {
12
+ showRightSidebar: Ref<boolean>;
13
+ toggleRightSidebar: () => void;
14
+ } {
15
+ const showRightSidebar = ref(localStorage.getItem(STORAGE_KEY) === "true");
16
+
17
+ function toggleRightSidebar(): void {
18
+ showRightSidebar.value = !showRightSidebar.value;
19
+ localStorage.setItem(STORAGE_KEY, String(showRightSidebar.value));
20
+ }
21
+
22
+ return { showRightSidebar, toggleRightSidebar };
23
+ }
@@ -0,0 +1,34 @@
1
+ // Composable that owns the active role list, the currently
2
+ // selected role id, and the refresh-from-server fetch. The merge
3
+ // rule lives in src/utils/roleMerge so it can be unit-tested
4
+ // independently.
5
+
6
+ import { computed, ref, type ComputedRef, type Ref } from "vue";
7
+ import { API_ROUTES } from "../config/apiRoutes";
8
+ import { ROLES, type Role } from "../config/roles";
9
+ import { mergeRoles } from "../utils/role/merge";
10
+ import { apiGet } from "../utils/api";
11
+
12
+ export function useRoles(): {
13
+ roles: Ref<Role[]>;
14
+ currentRoleId: Ref<string>;
15
+ currentRole: ComputedRef<Role>;
16
+ refreshRoles: () => Promise<void>;
17
+ } {
18
+ const roles = ref<Role[]>(ROLES);
19
+ const currentRoleId = ref(ROLES[0].id);
20
+ const currentRole = computed(() => roles.value.find((r) => r.id === currentRoleId.value) ?? roles.value[0]);
21
+
22
+ async function refreshRoles(): Promise<void> {
23
+ const result = await apiGet<Role[]>(API_ROUTES.roles.list);
24
+ if (!result.ok) {
25
+ // Keep the current role list on failure — losing custom roles
26
+ // is preferable to crashing the UI on a transient API hiccup.
27
+ console.warn(`[useRoles] refreshRoles failed: ${result.status} ${result.error}`);
28
+ return;
29
+ }
30
+ roles.value = mergeRoles(ROLES, result.data);
31
+ }
32
+
33
+ return { roles, currentRoleId, currentRole, refreshRoles };
34
+ }
@@ -0,0 +1,67 @@
1
+ // Lazy fetcher for `GET /api/sandbox` (#329).
2
+ //
3
+ // The lock popup consumes this to show which host credentials are
4
+ // attached to the Docker sandbox. Deliberately lazy: the popup is
5
+ // hidden most of the time, and env-var changes only take effect on
6
+ // server restart anyway, so a page-lifetime cache populated on first
7
+ // open is enough.
8
+ //
9
+ // Paired with `useHealth` (which loads `sandboxEnabled` at bootstrap)
10
+ // — when the sandbox is disabled this composable is never called.
11
+
12
+ import { ref, type Ref } from "vue";
13
+ import { API_ROUTES } from "../config/apiRoutes";
14
+ import { apiGet } from "../utils/api";
15
+
16
+ export interface SandboxStatus {
17
+ sshAgent: boolean;
18
+ mounts: string[];
19
+ }
20
+
21
+ interface RawResponse {
22
+ sshAgent?: unknown;
23
+ mounts?: unknown;
24
+ }
25
+
26
+ function isSandboxStatus(raw: RawResponse): raw is {
27
+ sshAgent: boolean;
28
+ mounts: string[];
29
+ } {
30
+ if (typeof raw.sshAgent !== "boolean") return false;
31
+ if (!Array.isArray(raw.mounts)) return false;
32
+ return raw.mounts.every((m) => typeof m === "string");
33
+ }
34
+
35
+ export interface UseSandboxStatusHandle {
36
+ /** Parsed status, or null while not yet loaded / sandbox disabled
37
+ * / fetch failed. UI renders a placeholder in all three cases. */
38
+ status: Ref<SandboxStatus | null>;
39
+ /** One-shot loader. Safe to call repeatedly — short-circuits once a
40
+ * non-null value is cached. Designed to be triggered from a
41
+ * `watch(() => props.open, …)` on the popup. */
42
+ ensureLoaded: () => Promise<void>;
43
+ }
44
+
45
+ export function useSandboxStatus(): UseSandboxStatusHandle {
46
+ const status = ref<SandboxStatus | null>(null);
47
+ let loaded = false;
48
+
49
+ async function ensureLoaded(): Promise<void> {
50
+ if (loaded) return;
51
+ loaded = true;
52
+ const result = await apiGet<RawResponse>(API_ROUTES.sandbox);
53
+ if (!result.ok) {
54
+ // Leave `status` null — popup shows a neutral "state unavailable"
55
+ // line. Allow a retry on next open by flipping `loaded` back.
56
+ loaded = false;
57
+ return;
58
+ }
59
+ // Server returns `{}` (empty object) when the sandbox is disabled.
60
+ // The popup shouldn't call us in that case, but double-guard here
61
+ // so a stale render doesn't blow up on shape validation.
62
+ if (!isSandboxStatus(result.data)) return;
63
+ status.value = result.data;
64
+ }
65
+
66
+ return { status, ensureLoaded };
67
+ }
@@ -0,0 +1,49 @@
1
+ // Writable computed that bridges activeSession.selectedResultUuid
2
+ // with the URL's ?result= query parameter.
3
+
4
+ import { computed, watch, type ComputedRef, type WritableComputedRef } from "vue";
5
+ import { useRoute, useRouter, isNavigationFailure } from "vue-router";
6
+ import type { ActiveSession } from "../types/session";
7
+
8
+ export function useSelectedResult(opts: {
9
+ activeSession: ComputedRef<ActiveSession | undefined>;
10
+ sessionMap: Map<string, ActiveSession>;
11
+ currentSessionId: { readonly value: string };
12
+ }): {
13
+ selectedResultUuid: WritableComputedRef<string | null>;
14
+ } {
15
+ const { activeSession } = opts;
16
+ const route = useRoute();
17
+ const router = useRouter();
18
+
19
+ const selectedResultUuid = computed({
20
+ get: () => activeSession.value?.selectedResultUuid ?? null,
21
+ set: (val: string | null) => {
22
+ if (activeSession.value) activeSession.value.selectedResultUuid = val;
23
+ const { result: __result, ...restQuery } = route.query;
24
+ const nextQuery = val ? { ...restQuery, result: val } : restQuery;
25
+ router.replace({ query: nextQuery }).catch((err: unknown) => {
26
+ if (!isNavigationFailure(err)) {
27
+ console.error("[selectedResultUuid] navigation failed:", err);
28
+ }
29
+ });
30
+ },
31
+ });
32
+
33
+ // External URL changes for ?result= → sync into the session.
34
+ watch(
35
+ () => route.query.result,
36
+ (newResult) => {
37
+ const session = opts.sessionMap.get(opts.currentSessionId.value);
38
+ if (!session) return;
39
+ // Ignore malformed (array) values rather than clobbering state.
40
+ if (Array.isArray(newResult)) return;
41
+ const resultId = typeof newResult === "string" ? newResult : null;
42
+ if (resultId !== session.selectedResultUuid) {
43
+ session.selectedResultUuid = resultId;
44
+ }
45
+ },
46
+ );
47
+
48
+ return { selectedResultUuid };
49
+ }
@@ -0,0 +1,51 @@
1
+ // Computed properties derived from sessionMap + sessions list.
2
+ // Extracted from App.vue to reduce the component's reactive surface.
3
+
4
+ import { computed, type Ref } from "vue";
5
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
6
+ import type { ActiveSession, SessionSummary } from "../types/session";
7
+ import type { ToolCallHistoryItem } from "../types/toolCallHistory";
8
+ import { deduplicateResults } from "../utils/tools/dedup";
9
+
10
+ export function useSessionDerived(opts: { sessionMap: Map<string, ActiveSession>; currentSessionId: Ref<string>; sessions: Ref<SessionSummary[]> }) {
11
+ const { sessionMap, currentSessionId, sessions } = opts;
12
+
13
+ const activeSession = computed(() => sessionMap.get(currentSessionId.value));
14
+
15
+ const toolResults = computed<ToolResultComplete[]>(() => activeSession.value?.toolResults ?? []);
16
+
17
+ const sidebarResults = computed(() => deduplicateResults(toolResults.value));
18
+
19
+ const currentSummary = computed(() => sessions.value.find((s) => s.id === currentSessionId.value));
20
+
21
+ // The server-side summary already merges pendingGenerations into
22
+ // `isRunning` (see server/api/routes/sessions.ts), but pub/sub events
23
+ // for background generations arrive faster than the next sessions
24
+ // refetch — fold the in-memory map in so ChatInput reflects the new
25
+ // state immediately.
26
+ const isRunning = computed(() => {
27
+ const active = activeSession.value;
28
+ const pending = active ? Object.keys(active.pendingGenerations).length > 0 : false;
29
+ return currentSummary.value?.isRunning || active?.isRunning || pending || false;
30
+ });
31
+
32
+ const statusMessage = computed(() => currentSummary.value?.statusMessage ?? activeSession.value?.statusMessage ?? "");
33
+
34
+ const toolCallHistory = computed<ToolCallHistoryItem[]>(() => activeSession.value?.toolCallHistory ?? []);
35
+
36
+ const activeSessionCount = computed(() => sessions.value.filter((s) => s.isRunning).length);
37
+
38
+ const unreadCount = computed(() => sessions.value.filter((s) => s.hasUnread).length);
39
+
40
+ return {
41
+ activeSession,
42
+ toolResults,
43
+ sidebarResults,
44
+ currentSummary,
45
+ isRunning,
46
+ statusMessage,
47
+ toolCallHistory,
48
+ activeSessionCount,
49
+ unreadCount,
50
+ };
51
+ }
@@ -0,0 +1,81 @@
1
+ // Composable for the session-history dropdown in the header.
2
+ //
3
+ // Owns the `sessions` list (what the server knows about) and the
4
+ // `showHistory` open/closed flag, plus the fetch + toggle helpers.
5
+ // The dropdown lazy-loads the list only when opened, and callers
6
+ // can invoke `fetchSessions()` directly after an end-of-run so the
7
+ // sidebar title cache stays fresh.
8
+ //
9
+ // Since #205, `fetchSessions()` sends the server's last-issued
10
+ // cursor back as `?since=<cursor>` so the server can reply with
11
+ // only the rows that changed. The first call has no cursor (full
12
+ // fetch); subsequent calls receive a diff that we merge into the
13
+ // existing cache via `applySessionDiff`.
14
+
15
+ import { ref, type Ref } from "vue";
16
+ import { API_ROUTES } from "../config/apiRoutes";
17
+ import type { SessionSummary } from "../types/session";
18
+ import { apiGet } from "../utils/api";
19
+ import { applySessionDiff } from "../utils/session/mergeSessions";
20
+
21
+ interface SessionsResponse {
22
+ sessions: SessionSummary[];
23
+ cursor: string;
24
+ deletedIds: string[];
25
+ }
26
+
27
+ export function useSessionHistory(): {
28
+ sessions: Ref<SessionSummary[]>;
29
+ showHistory: Ref<boolean>;
30
+ historyError: Ref<string | null>;
31
+ fetchSessions: () => Promise<SessionSummary[]>;
32
+ toggleHistory: () => Promise<void>;
33
+ } {
34
+ const sessions = ref<SessionSummary[]>([]);
35
+ const showHistory = ref(false);
36
+ // Surfaces the most recent fetch failure. Kept alongside the (stale)
37
+ // sessions list rather than wiping it — a dropdown that goes blank
38
+ // the moment the network hiccups is worse UX than one that shows
39
+ // "⚠ using cached list" with the last-known good entries.
40
+ const historyError = ref<string | null>(null);
41
+ // Opaque cursor the server hands back on every successful call.
42
+ // Tab-scoped — issue #205 calls out cross-tab sharing via
43
+ // localStorage as out of scope.
44
+ let cursor: string | null = null;
45
+
46
+ async function fetchSessions(): Promise<SessionSummary[]> {
47
+ const query: Record<string, string> = {};
48
+ if (cursor !== null) query.since = cursor;
49
+ const result = await apiGet<SessionsResponse>(API_ROUTES.sessions.list, query);
50
+ if (!result.ok) {
51
+ historyError.value = result.error;
52
+ // Intentionally preserve `sessions.value` — callers keep showing
53
+ // whatever list was last known to work.
54
+ return sessions.value;
55
+ }
56
+ historyError.value = null;
57
+ const body = result.data;
58
+ if (cursor === null) {
59
+ // First call in this composable instance — server returned the
60
+ // full list; seed the cache directly.
61
+ sessions.value = body.sessions;
62
+ } else {
63
+ sessions.value = applySessionDiff(sessions.value, body.sessions, body.deletedIds);
64
+ }
65
+ cursor = body.cursor;
66
+ return sessions.value;
67
+ }
68
+
69
+ async function toggleHistory(): Promise<void> {
70
+ showHistory.value = !showHistory.value;
71
+ if (showHistory.value) await fetchSessions();
72
+ }
73
+
74
+ return {
75
+ sessions,
76
+ showHistory,
77
+ historyError,
78
+ fetchSessions,
79
+ toggleHistory,
80
+ };
81
+ }
@@ -0,0 +1,57 @@
1
+ // Keep in-memory session state in sync with the server via pub/sub.
2
+ // Subscribes to the global `sessions` channel and refetches summaries
3
+ // whenever any session's state changes. Also provides markSessionRead
4
+ // for clearing the unread flag on the server.
5
+
6
+ import { onScopeDispose } from "vue";
7
+ import type { Ref } from "vue";
8
+ import type { ActiveSession, SessionSummary } from "../types/session";
9
+ import { usePubSub } from "./usePubSub";
10
+ import { PUBSUB_CHANNELS } from "../config/pubsubChannels";
11
+ import { apiPost } from "../utils/api";
12
+ import { API_ROUTES } from "../config/apiRoutes";
13
+
14
+ export function useSessionSync(opts: {
15
+ sessionMap: Map<string, ActiveSession>;
16
+ currentSessionId: Ref<string>;
17
+ fetchSessions: () => Promise<SessionSummary[]>;
18
+ }) {
19
+ const { sessionMap, currentSessionId, fetchSessions } = opts;
20
+ const { subscribe } = usePubSub();
21
+
22
+ async function refreshSessionStates(): Promise<void> {
23
+ let summaries: SessionSummary[];
24
+ try {
25
+ summaries = await fetchSessions();
26
+ } catch (err) {
27
+ // Network / HTTP failure — log and bail so the pub/sub
28
+ // callback doesn't produce an unhandled rejection.
29
+ console.warn("[session-sync] failed to fetch sessions:", err);
30
+ return;
31
+ }
32
+ for (const s of summaries) {
33
+ const live = sessionMap.get(s.id);
34
+ if (!live) continue;
35
+ live.isRunning = s.isRunning ?? false;
36
+ live.statusMessage = s.statusMessage ?? "";
37
+ const unread = s.hasUnread ?? false;
38
+ if (!(unread && s.id === currentSessionId.value)) {
39
+ live.hasUnread = unread;
40
+ }
41
+ }
42
+ }
43
+
44
+ async function markSessionRead(id: string): Promise<void> {
45
+ const result = await apiPost<{ ok: boolean }>(API_ROUTES.sessions.markRead.replace(":id", encodeURIComponent(id)));
46
+ if (!result.ok || result.data.ok === false) {
47
+ await refreshSessionStates();
48
+ }
49
+ }
50
+
51
+ const unsub = subscribe(PUBSUB_CHANNELS.sessions, () => {
52
+ void refreshSessionStates();
53
+ });
54
+ if (typeof unsub === "function") onScopeDispose(unsub);
55
+
56
+ return { refreshSessionStates, markSessionRead };
57
+ }