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,115 @@
1
+ // Composable: file selection, content loading with abort, URL sync.
2
+ // Extracted from FilesView.vue (#507 step 2).
3
+
4
+ import { ref } from "vue";
5
+ import { useRoute, useRouter, isNavigationFailure } from "vue-router";
6
+ import { apiGet } from "../utils/api";
7
+ import { API_ROUTES } from "../config/apiRoutes";
8
+
9
+ interface TextContent {
10
+ kind: "text";
11
+ path: string;
12
+ content: string;
13
+ size: number;
14
+ modifiedMs: number;
15
+ }
16
+
17
+ interface MetaContent {
18
+ kind: "image" | "pdf" | "audio" | "video" | "binary" | "too-large";
19
+ path: string;
20
+ size: number;
21
+ modifiedMs: number;
22
+ message?: string;
23
+ }
24
+
25
+ export type FileContent = TextContent | MetaContent;
26
+
27
+ /** Segment-wise traversal check: rejects `../` path components
28
+ * but allows legitimate filenames like `my..notes.txt`. */
29
+ export function isValidFilePath(value: unknown): value is string {
30
+ if (typeof value !== "string" || value.length === 0) return false;
31
+ if (value.startsWith("/")) return false;
32
+ return !value.split("/").some((seg) => seg === "..");
33
+ }
34
+
35
+ export function useFileSelection() {
36
+ const route = useRoute();
37
+ const router = useRouter();
38
+
39
+ const urlPath = route.query.path;
40
+ const selectedPath = ref<string | null>(isValidFilePath(urlPath) ? urlPath : null);
41
+ const content = ref<FileContent | null>(null);
42
+ const contentLoading = ref(false);
43
+ const contentError = ref<string | null>(null);
44
+
45
+ let contentAbort: AbortController | null = null;
46
+
47
+ async function loadContent(filePath: string): Promise<void> {
48
+ contentAbort?.abort();
49
+ const controller = new AbortController();
50
+ contentAbort = controller;
51
+
52
+ contentLoading.value = true;
53
+ contentError.value = null;
54
+ content.value = null;
55
+ try {
56
+ const result = await apiGet<FileContent>(API_ROUTES.files.content, { path: filePath }, { signal: controller.signal });
57
+ if (controller.signal.aborted) return;
58
+ if (!result.ok) {
59
+ contentError.value = result.error;
60
+ } else {
61
+ content.value = result.data;
62
+ }
63
+ } finally {
64
+ if (contentAbort === controller) {
65
+ contentLoading.value = false;
66
+ contentAbort = null;
67
+ }
68
+ }
69
+ }
70
+
71
+ function selectFile(filePath: string): void {
72
+ selectedPath.value = filePath;
73
+ loadContent(filePath);
74
+ const { path: __path, ...restQuery } = route.query;
75
+ router.push({ query: { ...restQuery, path: filePath } }).catch((err: unknown) => {
76
+ if (!isNavigationFailure(err)) {
77
+ // Frontend composable — server logger not available.
78
+ // console.error is the standard pattern in Vue composables.
79
+ console.error("[selectFile] navigation failed:", err);
80
+ }
81
+ });
82
+ }
83
+
84
+ function deselectFile(): void {
85
+ contentAbort?.abort();
86
+ contentAbort = null;
87
+ selectedPath.value = null;
88
+ content.value = null;
89
+ contentLoading.value = false;
90
+ contentError.value = null;
91
+ const { path: __path, ...restQuery } = route.query;
92
+ router.replace({ query: restQuery }).catch((err: unknown) => {
93
+ if (!isNavigationFailure(err)) {
94
+ console.error("[deselectFile] navigation failed:", err);
95
+ }
96
+ });
97
+ }
98
+
99
+ function abortContent(): void {
100
+ contentAbort?.abort();
101
+ contentAbort = null;
102
+ contentLoading.value = false;
103
+ }
104
+
105
+ return {
106
+ selectedPath,
107
+ content,
108
+ contentLoading,
109
+ contentError,
110
+ loadContent,
111
+ selectFile,
112
+ deselectFile,
113
+ abortContent,
114
+ };
115
+ }
@@ -0,0 +1,24 @@
1
+ // Composable: file-tree sort mode ("name" | "recent") persisted to
2
+ // localStorage. Applied globally across every directory in the Files
3
+ // view; only affects within-group order (dirs-first grouping stays).
4
+
5
+ import { ref } from "vue";
6
+
7
+ const SORT_MODE_STORAGE_KEY = "files_sort_mode";
8
+
9
+ export type FileSortMode = "name" | "recent";
10
+
11
+ function readStoredMode(): FileSortMode {
12
+ return localStorage.getItem(SORT_MODE_STORAGE_KEY) === "recent" ? "recent" : "name";
13
+ }
14
+
15
+ export function useFileSortMode() {
16
+ const sortMode = ref<FileSortMode>(readStoredMode());
17
+
18
+ function setSortMode(mode: FileSortMode): void {
19
+ sortMode.value = mode;
20
+ localStorage.setItem(SORT_MODE_STORAGE_KEY, mode);
21
+ }
22
+
23
+ return { sortMode, setSortMode };
24
+ }
@@ -0,0 +1,85 @@
1
+ // Composable: workspace file tree state + lazy loading.
2
+ // Extracted from FilesView.vue (#507 step 1).
3
+
4
+ import { ref } from "vue";
5
+ import type { TreeNode } from "../types/fileTree";
6
+ import { useExpandedDirs } from "./useExpandedDirs";
7
+ import { apiGet } from "../utils/api";
8
+ import { API_ROUTES } from "../config/apiRoutes";
9
+
10
+ export function useFileTree() {
11
+ const { expand } = useExpandedDirs();
12
+
13
+ const rootNode = ref<TreeNode | null>(null);
14
+ const refRoots = ref<TreeNode[]>([]);
15
+ const childrenByPath = ref<Map<string, TreeNode[] | null>>(new Map());
16
+ const treeError = ref<string | null>(null);
17
+
18
+ // Generation counter — incremented on reloadRoot so in-flight
19
+ // requests from a prior generation don't write stale data.
20
+ let generation = 0;
21
+
22
+ async function loadDirChildren(path: string): Promise<void> {
23
+ if (childrenByPath.value.has(path)) return;
24
+
25
+ const gen = generation;
26
+ const next = new Map(childrenByPath.value);
27
+ next.set(path, null);
28
+ childrenByPath.value = next;
29
+
30
+ const result = await apiGet<TreeNode>(API_ROUTES.files.dir, { path });
31
+ // Bail if reloadRoot was called while we were awaiting.
32
+ if (gen !== generation) return;
33
+ if (!result.ok) {
34
+ const rollback = new Map(childrenByPath.value);
35
+ rollback.delete(path);
36
+ childrenByPath.value = rollback;
37
+ treeError.value = result.error || `dir: ${result.status}`;
38
+ return;
39
+ }
40
+ const node = result.data;
41
+ const updated = new Map(childrenByPath.value);
42
+ updated.set(path, node.children ?? []);
43
+ childrenByPath.value = updated;
44
+ if (path === "") rootNode.value = { ...node, children: [] };
45
+ }
46
+
47
+ async function ensureAncestorsLoaded(filePath: string): Promise<void> {
48
+ const parts = filePath.split("/").filter(Boolean);
49
+ if (parts.length <= 1) return;
50
+ const ancestors: string[] = [];
51
+ for (let i = 1; i < parts.length; i++) {
52
+ ancestors.push(parts.slice(0, i).join("/"));
53
+ }
54
+ for (const dir of ancestors) {
55
+ expand(dir);
56
+ await loadDirChildren(dir);
57
+ }
58
+ }
59
+
60
+ async function reloadRoot(): Promise<void> {
61
+ generation++;
62
+ rootNode.value = null;
63
+ childrenByPath.value = new Map();
64
+ treeError.value = null;
65
+ await loadDirChildren("");
66
+ }
67
+
68
+ async function loadRefRoots(): Promise<void> {
69
+ const result = await apiGet<TreeNode[]>(API_ROUTES.files.refRoots);
70
+ if (result.ok && Array.isArray(result.data)) {
71
+ refRoots.value = result.data;
72
+ }
73
+ }
74
+
75
+ return {
76
+ rootNode,
77
+ refRoots,
78
+ childrenByPath,
79
+ treeError,
80
+ loadDirChildren,
81
+ ensureAncestorsLoaded,
82
+ reloadRoot,
83
+ loadRefRoots,
84
+ };
85
+ }
@@ -0,0 +1,89 @@
1
+ // Shared fetch-fresh-data-on-mount pattern for plugin view / preview
2
+ // components. Each affected plugin (todo, scheduler, wiki, manageRoles)
3
+ // used to duplicate the same onMounted + AbortController + JSON parse
4
+ // + apply routine in 8 files. This composable factors it out and
5
+ // drives 3 distinct flavours via callbacks:
6
+ //
7
+ // endpoint(): returns the URL to fetch. A function (not a string)
8
+ // so callers can derive it from local refs — wiki/View picks
9
+ // between /api/wiki and /api/wiki?slug=... depending on the
10
+ // current action.
11
+ //
12
+ // extract(json): pulls the data off the response envelope. Three
13
+ // shapes in practice:
14
+ // - json.data.items (todo / scheduler)
15
+ // - json.data (wiki — the whole WikiData object)
16
+ // - bare array (manageRoles — /api/roles returns an
17
+ // unwrapped CustomRole[])
18
+ // Return `null` to skip the apply step (e.g. response malformed
19
+ // or the caller wants to ignore a particular payload).
20
+ //
21
+ // apply(data): writes into the caller's local refs. Callers can
22
+ // guard here — wiki/Preview only applies the index payload when
23
+ // it's currently showing the index view, because the preview is
24
+ // reused for page / log / lint_report previews.
25
+ //
26
+ // Lifecycle: `onMounted` fires one initial refresh. `onUnmounted`
27
+ // aborts any in-flight request. Callers can also call `refresh()`
28
+ // explicitly (e.g. from a `watch(() => props.selectedResult.uuid)`
29
+ // to re-fetch when switching between tool results).
30
+
31
+ import { onMounted, onUnmounted } from "vue";
32
+ import { apiGet } from "../utils/api";
33
+
34
+ export interface UseFreshPluginDataOptions<T> {
35
+ endpoint: () => string;
36
+ extract: (json: unknown) => T | null;
37
+ apply: (data: T) => void;
38
+ }
39
+
40
+ export interface UseFreshPluginDataHandle {
41
+ // Abort any in-flight fetch and fire a new one. Returns true if
42
+ // the response was applied, false on abort / non-OK / malformed /
43
+ // apply-skipped. Callers can await this when they want to know
44
+ // whether the refresh succeeded (e.g. for a manual retry flow).
45
+ refresh: () => Promise<boolean>;
46
+
47
+ // Cancel any in-flight fetch without firing a new one. Called
48
+ // automatically by the composable on component unmount.
49
+ abort: () => void;
50
+ }
51
+
52
+ // Pure core of the refresh logic, exported separately so tests can
53
+ // exercise it without spinning up a Vue component lifecycle. The
54
+ // composable below wraps this with AbortController / onMounted /
55
+ // onUnmounted management.
56
+ export async function refreshOnce<T>(opts: UseFreshPluginDataOptions<T>, signal: AbortSignal): Promise<boolean> {
57
+ const result = await apiGet<unknown>(opts.endpoint(), undefined, { signal });
58
+ // AbortError / network error / non-OK HTTP / malformed JSON all land
59
+ // as { ok: false }. The caller still has prop-initialised state as a
60
+ // fallback, so a failed refresh is a silent no-op.
61
+ if (signal.aborted || !result.ok) return false;
62
+ const extracted = opts.extract(result.data);
63
+ if (extracted === null) return false;
64
+ opts.apply(extracted);
65
+ return true;
66
+ }
67
+
68
+ export function useFreshPluginData<T>(opts: UseFreshPluginDataOptions<T>): UseFreshPluginDataHandle {
69
+ let controller: AbortController | null = null;
70
+
71
+ async function refresh(): Promise<boolean> {
72
+ controller?.abort();
73
+ const c = new AbortController();
74
+ controller = c;
75
+ return refreshOnce(opts, c.signal);
76
+ }
77
+
78
+ function abort(): void {
79
+ controller?.abort();
80
+ controller = null;
81
+ }
82
+
83
+ onMounted(() => {
84
+ void refresh();
85
+ });
86
+ onUnmounted(abort);
87
+
88
+ return { refresh, abort };
89
+ }
@@ -0,0 +1,38 @@
1
+ // Composable for the server /api/health probe.
2
+ //
3
+ // Owns two refs that the UI reads (gemini key availability + sandbox
4
+ // toggle) plus the one-shot fetch that populates them on mount. On
5
+ // fetch failure we assume Gemini is unavailable so dependent UI
6
+ // (e.g. the "generate image" plugin buttons) falls back gracefully
7
+ // — the sandbox flag keeps its initial `true` so the lock indicator
8
+ // doesn't momentarily flash "sandbox disabled" on a transient error.
9
+
10
+ import { ref, type Ref } from "vue";
11
+ import { API_ROUTES } from "../config/apiRoutes";
12
+ import { apiGet } from "../utils/api";
13
+
14
+ interface HealthResponse {
15
+ geminiAvailable?: unknown;
16
+ sandboxEnabled?: unknown;
17
+ }
18
+
19
+ export function useHealth(): {
20
+ geminiAvailable: Ref<boolean>;
21
+ sandboxEnabled: Ref<boolean>;
22
+ fetchHealth: () => Promise<void>;
23
+ } {
24
+ const geminiAvailable = ref(true);
25
+ const sandboxEnabled = ref(true);
26
+
27
+ async function fetchHealth(): Promise<void> {
28
+ const result = await apiGet<HealthResponse>(API_ROUTES.health);
29
+ if (!result.ok) {
30
+ geminiAvailable.value = false;
31
+ return;
32
+ }
33
+ geminiAvailable.value = !!result.data.geminiAvailable;
34
+ sandboxEnabled.value = !!result.data.sandboxEnabled;
35
+ }
36
+
37
+ return { geminiAvailable, sandboxEnabled, fetchHealth };
38
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Unify IME (Japanese, Chinese, Korean) Enter-confirmation behavior
3
+ * across Safari and Chrome / Firefox so only a user-intended Enter
4
+ * sends a message.
5
+ *
6
+ * Safari fires `compositionend` BEFORE the confirming Enter's
7
+ * `keydown`, so `event.isComposing` is already `false` by the time
8
+ * the keydown handler runs — the standard `!isComposing` guard lets
9
+ * the message send on IME confirmation. Chrome / Firefox fire
10
+ * `compositionend` AFTER the keydown and keep `isComposing` true, so
11
+ * they handle confirmation correctly on their own.
12
+ *
13
+ * We use a tight time window after `compositionend` to suppress only
14
+ * the immediately-following keydown. Safari's sequence is synchronous
15
+ * (microseconds); a human follow-up Enter takes >= 100 ms and never
16
+ * falls inside.
17
+ */
18
+ export const SAFARI_IME_RACE_WINDOW_MS = 30;
19
+
20
+ export interface ImeAwareEnterHandlers {
21
+ onCompositionStart: () => void;
22
+ onCompositionEnd: () => void;
23
+ onKeydown: (event: KeyboardEvent) => void;
24
+ onBlur: () => void;
25
+ }
26
+
27
+ export function useImeAwareEnter(onSend: () => void, now: () => number = () => performance.now()): ImeAwareEnterHandlers {
28
+ let isImeComposing = false;
29
+ let lastCompositionEndAt = 0;
30
+
31
+ return {
32
+ onCompositionStart() {
33
+ isImeComposing = true;
34
+ },
35
+ onCompositionEnd() {
36
+ isImeComposing = false;
37
+ lastCompositionEndAt = now();
38
+ },
39
+ onBlur() {
40
+ isImeComposing = false;
41
+ lastCompositionEndAt = 0;
42
+ },
43
+ onKeydown(event: KeyboardEvent) {
44
+ if (event.key !== "Enter" || event.shiftKey) return;
45
+ if (event.isComposing || isImeComposing) {
46
+ event.preventDefault();
47
+ return;
48
+ }
49
+ if (now() - lastCompositionEndAt < SAFARI_IME_RACE_WINDOW_MS) {
50
+ event.preventDefault();
51
+ return;
52
+ }
53
+ event.preventDefault();
54
+ onSend();
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,60 @@
1
+ // Keyboard navigation extracted from App.vue.
2
+ // Arrow keys scroll the canvas (main pane) or navigate the sidebar
3
+ // result list depending on which pane is active.
4
+
5
+ import type { Ref, ComputedRef } from "vue";
6
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
7
+ import { findScrollableChild } from "../utils/dom/scrollable";
8
+
9
+ const SCROLL_AMOUNT = 60;
10
+
11
+ function isEditableTarget(target: EventTarget | null): boolean {
12
+ return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
13
+ }
14
+
15
+ function isVerticalArrow(key: string): key is "ArrowUp" | "ArrowDown" {
16
+ return key === "ArrowUp" || key === "ArrowDown";
17
+ }
18
+
19
+ function resolveNextUuid(results: ToolResultComplete[], currentUuid: string | null, direction: "ArrowUp" | "ArrowDown"): string | null {
20
+ if (results.length === 0) return null;
21
+ const idx = results.findIndex((r) => r.uuid === currentUuid);
22
+ if (idx === -1) {
23
+ return direction === "ArrowDown" ? results[0].uuid : results[results.length - 1].uuid;
24
+ }
25
+ const next = direction === "ArrowUp" ? Math.max(0, idx - 1) : Math.min(results.length - 1, idx + 1);
26
+ return results[next].uuid;
27
+ }
28
+
29
+ export function useKeyNavigation(opts: {
30
+ canvasRef: Ref<HTMLDivElement | null>;
31
+ activePane: Ref<"sidebar" | "main">;
32
+ sidebarResults: ComputedRef<ToolResultComplete[]>;
33
+ selectedResultUuid: ComputedRef<string | null> & {
34
+ value: string | null;
35
+ };
36
+ }) {
37
+ const { canvasRef, activePane, sidebarResults, selectedResultUuid } = opts;
38
+
39
+ function handleCanvasKeydown(e: KeyboardEvent): void {
40
+ if (!isVerticalArrow(e.key)) return;
41
+ if (isEditableTarget(e.target)) return;
42
+ if (!canvasRef.value) return;
43
+ const scrollable = findScrollableChild(canvasRef.value);
44
+ if (!scrollable) return;
45
+ e.preventDefault();
46
+ const delta = e.key === "ArrowDown" ? SCROLL_AMOUNT : -SCROLL_AMOUNT;
47
+ scrollable.scrollBy({ top: delta, behavior: "smooth" });
48
+ }
49
+
50
+ function handleKeyNavigation(e: KeyboardEvent): void {
51
+ if (activePane.value !== "sidebar") return;
52
+ if (isEditableTarget(e.target)) return;
53
+ if (!isVerticalArrow(e.key)) return;
54
+ e.preventDefault();
55
+ const nextUuid = resolveNextUuid(sidebarResults.value, selectedResultUuid.value, e.key);
56
+ if (nextUuid) selectedResultUuid.value = nextUuid;
57
+ }
58
+
59
+ return { handleCanvasKeydown, handleKeyNavigation };
60
+ }
@@ -0,0 +1,46 @@
1
+ // Composable: intercept clicks on <a> links inside rendered markdown
2
+ // bodies and route workspace-internal refs back into FilesView / the
3
+ // chat session loader. Extracted from FilesView.vue (#507 step 8).
4
+
5
+ import type { Ref } from "vue";
6
+ import { isExternalHref, resolveWorkspaceLink, extractSessionIdFromPath } from "../utils/path/relativeLink";
7
+
8
+ interface MarkdownLinkHandlers {
9
+ /** Invoked when a link resolves to a workspace file path. */
10
+ onNavigate: (path: string) => void;
11
+ /** Invoked when a link resolves to a chat session jsonl. */
12
+ onLoadSession: (sessionId: string) => void;
13
+ }
14
+
15
+ export function useMarkdownLinkHandler(selectedPath: Ref<string | null>, handlers: MarkdownLinkHandlers) {
16
+ function handleMarkdownLinkClick(event: MouseEvent): void {
17
+ if (event.button !== 0) return;
18
+ if (event.ctrlKey || event.metaKey || event.shiftKey) return;
19
+ const target = event.target;
20
+ if (!(target instanceof Element)) return;
21
+ const anchor = target.closest("a");
22
+ if (!anchor) return;
23
+ const href = anchor.getAttribute("href");
24
+ if (!href) return;
25
+ // External URLs and mailto/tel: let TextResponseView's existing
26
+ // handler open them in a new tab.
27
+ if (isExternalHref(href)) return;
28
+ // Anchor-only (#section): let the browser handle in-page scroll.
29
+ if (href.startsWith("#")) return;
30
+ if (!selectedPath.value) return;
31
+ const resolved = resolveWorkspaceLink(selectedPath.value, href);
32
+ if (!resolved) return;
33
+ event.preventDefault();
34
+ event.stopPropagation();
35
+ // Chat session link: hand off so the sidebar chat switches to that
36
+ // session instead of opening the raw jsonl as a file.
37
+ const sessionId = extractSessionIdFromPath(resolved);
38
+ if (sessionId !== null) {
39
+ handlers.onLoadSession(sessionId);
40
+ return;
41
+ }
42
+ handlers.onNavigate(resolved);
43
+ }
44
+
45
+ return { handleMarkdownLinkClick };
46
+ }
@@ -0,0 +1,17 @@
1
+ // Composable: markdown raw/rendered toggle persisted to localStorage.
2
+ // Extracted from FilesView.vue (#507 step 5).
3
+
4
+ import { ref } from "vue";
5
+
6
+ const MD_RAW_STORAGE_KEY = "files_md_raw_mode";
7
+
8
+ export function useMarkdownMode() {
9
+ const mdRawMode = ref(localStorage.getItem(MD_RAW_STORAGE_KEY) === "true");
10
+
11
+ function toggleMdRaw(): void {
12
+ mdRawMode.value = !mdRawMode.value;
13
+ localStorage.setItem(MD_RAW_STORAGE_KEY, String(mdRawMode.value));
14
+ }
15
+
16
+ return { mdRawMode, toggleMdRaw };
17
+ }
@@ -0,0 +1,71 @@
1
+ // Composable that owns the MCP tool state used by the sidebar:
2
+ // which tools are currently disabled, their per-tool prompts, and
3
+ // the derived `availableTools` / `toolDescriptions` computeds. The
4
+ // pure rules live in src/utils/mcpTools so they are unit-testable
5
+ // independently of fetch / Vue.
6
+
7
+ import { computed, ref, type ComputedRef } from "vue";
8
+ import { API_ROUTES } from "../config/apiRoutes";
9
+ import type { Role } from "../config/roles";
10
+ import { availableToolsFor, toolDescriptionsFor, type ToolDefinitionMetadata } from "../utils/tools/mcp";
11
+ import { apiGet } from "../utils/api";
12
+
13
+ interface UseMcpToolsOptions {
14
+ currentRole: ComputedRef<Role>;
15
+ // Injection point for the in-app plugin registry lookup. Real
16
+ // callers pass `(name) => getPlugin(name)?.toolDefinition ?? null`,
17
+ // tests can stub it.
18
+ getDefinition: (name: string) => ToolDefinitionMetadata | null;
19
+ }
20
+
21
+ export function useMcpTools(opts: UseMcpToolsOptions) {
22
+ const disabledMcpTools = ref(new Set<string>());
23
+ const mcpToolDescriptions = ref<Record<string, string>>({});
24
+ // Surfaces the most recent GET /api/mcp-tools failure so consumers
25
+ // (e.g. the Settings modal's MCP tab) can render a small warning.
26
+ // We intentionally keep the "all tools visible" fallback below so
27
+ // the UI stays usable; this ref lets the UI tell the user *why* the
28
+ // list looks incomplete / unfiltered.
29
+ const mcpToolsError = ref<string | null>(null);
30
+
31
+ const availableTools = computed(() => availableToolsFor(opts.currentRole.value.availablePlugins, disabledMcpTools.value));
32
+
33
+ const toolDescriptions = computed(() => toolDescriptionsFor(opts.currentRole.value.availablePlugins, opts.getDefinition, mcpToolDescriptions.value));
34
+
35
+ interface McpToolStatus {
36
+ name: string;
37
+ enabled: boolean;
38
+ prompt?: string;
39
+ }
40
+
41
+ function hasPrompt(tool: McpToolStatus): tool is McpToolStatus & { prompt: string } {
42
+ return typeof tool.prompt === "string" && tool.prompt.length > 0;
43
+ }
44
+
45
+ async function fetchMcpToolsStatus(): Promise<void> {
46
+ const result = await apiGet<McpToolStatus[]>(API_ROUTES.mcpTools.list);
47
+ if (!result.ok) {
48
+ mcpToolsError.value = result.error;
49
+ // Keep the "all tools visible" fallback — not clearing
50
+ // disabledMcpTools or descriptions means the UI remains usable.
51
+ return;
52
+ }
53
+ if (!Array.isArray(result.data)) {
54
+ mcpToolsError.value = "Unexpected response shape from /api/mcp-tools";
55
+ return;
56
+ }
57
+ mcpToolsError.value = null;
58
+ const tools = result.data;
59
+ disabledMcpTools.value = new Set(tools.filter((t) => !t.enabled).map((t) => t.name));
60
+ mcpToolDescriptions.value = Object.fromEntries(tools.filter(hasPrompt).map((t) => [t.name, t.prompt]));
61
+ }
62
+
63
+ return {
64
+ disabledMcpTools,
65
+ mcpToolDescriptions,
66
+ mcpToolsError,
67
+ availableTools,
68
+ toolDescriptions,
69
+ fetchMcpToolsStatus,
70
+ };
71
+ }
@@ -0,0 +1,27 @@
1
+ // Merged session list for the history pane + tab bar.
2
+ // Live sessions in sessionMap are merged with server-only sessions
3
+ // (from the chat indexer), sorted newest-first by updatedAt.
4
+
5
+ import { computed, type Ref } from "vue";
6
+ import type { ActiveSession, SessionSummary } from "../types/session";
7
+ import { mergeSessionLists } from "../utils/session/mergeSessions";
8
+
9
+ const MAX_TABS = 6;
10
+
11
+ // A freshly-created live session (plus-button / role-change) has no
12
+ // toolResults yet. Hide it from the tab bar + history until the user
13
+ // sends the first message — otherwise the UI shows an empty shell row
14
+ // that the user didn't ask for.
15
+ function hasContent(session: ActiveSession): boolean {
16
+ return session.toolResults.length > 0;
17
+ }
18
+
19
+ export function useMergedSessions(opts: { sessionMap: Map<string, ActiveSession>; sessions: Ref<SessionSummary[]> }) {
20
+ const { sessionMap, sessions } = opts;
21
+
22
+ const mergedSessions = computed((): SessionSummary[] => mergeSessionLists([...sessionMap.values()].filter(hasContent), sessions.value));
23
+
24
+ const tabSessions = computed(() => mergedSessions.value.slice(0, MAX_TABS));
25
+
26
+ return { mergedSessions, tabSessions };
27
+ }