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,697 @@
1
+ <template>
2
+ <div class="h-full flex flex-col overflow-hidden">
3
+ <div class="px-4 py-2 border-b border-gray-100 shrink-0 flex items-center justify-between gap-2">
4
+ <span class="text-sm font-medium text-gray-700 truncate"> Information sources </span>
5
+ <div class="flex items-center gap-2 shrink-0">
6
+ <span class="text-xs text-gray-500"> {{ sources.length }} source{{ sources.length === 1 ? "" : "s" }} </span>
7
+ <button
8
+ class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:opacity-50"
9
+ :disabled="adding || busy === 'rebuild'"
10
+ data-testid="sources-add-btn"
11
+ @click="startAdd"
12
+ >
13
+ <span class="material-icons text-sm align-middle">add</span>
14
+ Add
15
+ </button>
16
+ <button
17
+ class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:opacity-50"
18
+ :disabled="busy === 'rebuild'"
19
+ data-testid="sources-rebuild-btn"
20
+ @click="rebuild"
21
+ >
22
+ <span class="material-icons text-sm align-middle">refresh</span>
23
+ {{ busy === "rebuild" ? "Rebuilding…" : "Rebuild now" }}
24
+ </button>
25
+ </div>
26
+ </div>
27
+
28
+ <div v-if="adding" class="px-4 py-3 border-b border-blue-200 bg-blue-50/50 shrink-0 space-y-2" data-testid="sources-add-form">
29
+ <div class="flex flex-wrap items-center gap-2">
30
+ <label class="text-xs text-gray-700">
31
+ Type
32
+ <select v-model="draft.kind" class="ml-1 text-xs border border-gray-300 rounded px-1 py-0.5" data-testid="sources-draft-kind" @change="onKindChange">
33
+ <option value="rss">RSS</option>
34
+ <option value="github-releases">GitHub releases</option>
35
+ <option value="github-issues">GitHub issues</option>
36
+ <option value="arxiv">arXiv</option>
37
+ </select>
38
+ </label>
39
+ <input
40
+ v-model="draft.primary"
41
+ class="flex-1 min-w-[12rem] text-xs border border-gray-300 rounded px-2 py-1 font-mono"
42
+ :placeholder="primaryPlaceholder"
43
+ data-testid="sources-draft-primary"
44
+ @keydown.enter="commitAdd"
45
+ />
46
+ <input
47
+ v-model="draft.title"
48
+ class="w-40 text-xs border border-gray-300 rounded px-2 py-1"
49
+ placeholder="Title (optional)"
50
+ data-testid="sources-draft-title"
51
+ @keydown.enter="commitAdd"
52
+ />
53
+ </div>
54
+ <div class="flex items-center justify-between text-xs">
55
+ <span class="text-gray-500">
56
+ {{ primaryHint }}
57
+ </span>
58
+ <div class="flex gap-2">
59
+ <button class="px-2 py-1 rounded border border-gray-300 text-gray-600 hover:bg-gray-50" data-testid="sources-draft-cancel" @click="cancelAdd">
60
+ Cancel
61
+ </button>
62
+ <button
63
+ class="px-2 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50"
64
+ :disabled="busy === 'add' || !draft.primary.trim()"
65
+ data-testid="sources-draft-add"
66
+ @click="commitAdd"
67
+ >
68
+ {{ busy === "add" ? "Adding…" : "Add + Rebuild" }}
69
+ </button>
70
+ </div>
71
+ </div>
72
+ <div v-if="draftError" class="text-xs text-red-600" data-testid="sources-draft-error">
73
+ {{ draftError }}
74
+ </div>
75
+ </div>
76
+
77
+ <div
78
+ v-if="actionMessage"
79
+ class="px-4 py-2 text-xs border-b shrink-0"
80
+ :class="actionError ? 'bg-red-50 text-red-700 border-red-200' : 'bg-green-50 text-green-700 border-green-200'"
81
+ data-testid="sources-action-message"
82
+ >
83
+ {{ actionMessage }}
84
+ </div>
85
+
86
+ <div class="flex-1 overflow-y-auto">
87
+ <div v-if="sources.length === 0" class="flex flex-col items-center justify-center h-full p-6 gap-4" data-testid="sources-empty">
88
+ <p class="text-sm text-gray-500 italic text-center max-w-md">
89
+ No sources registered yet. Pick a starter pack below, click
90
+ <strong>+ Add</strong> above, or ask Claude to register one.
91
+ </p>
92
+ <div class="w-full max-w-md space-y-2" data-testid="sources-presets">
93
+ <button
94
+ v-for="preset in PRESETS"
95
+ :key="preset.id"
96
+ class="w-full text-left border border-gray-200 rounded-lg p-3 hover:bg-blue-50 hover:border-blue-300 disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:border-gray-200"
97
+ :disabled="busy === 'preset-' + preset.id"
98
+ :data-testid="`sources-preset-${preset.id}`"
99
+ @click="installPreset(preset)"
100
+ >
101
+ <div class="flex items-baseline justify-between gap-2">
102
+ <span class="text-sm font-medium text-gray-800">
103
+ {{ preset.label }}
104
+ </span>
105
+ <span class="text-[11px] text-gray-500 shrink-0"> {{ preset.entries.length }} source{{ preset.entries.length === 1 ? "" : "s" }} </span>
106
+ </div>
107
+ <div class="text-xs text-gray-500 mt-1">
108
+ {{ preset.description }}
109
+ </div>
110
+ <div v-if="busy === 'preset-' + preset.id" class="text-xs text-blue-600 mt-1 italic">Registering + fetching…</div>
111
+ </button>
112
+ </div>
113
+ </div>
114
+ <ul v-else class="divide-y divide-gray-100 border-b border-gray-100">
115
+ <li
116
+ v-for="source in sources"
117
+ :key="source.slug"
118
+ class="px-4 py-3 flex items-start gap-3"
119
+ :class="{
120
+ 'bg-amber-50': source.slug === highlightSlug,
121
+ }"
122
+ :data-testid="`source-row-${source.slug}`"
123
+ >
124
+ <span class="text-[10px] uppercase tracking-wide rounded px-1.5 py-0.5 mt-0.5 shrink-0" :class="kindBadgeClass(source.fetcherKind)">
125
+ {{ kindLabel(source.fetcherKind) }}
126
+ </span>
127
+ <div class="min-w-0 flex-1">
128
+ <div class="flex items-baseline gap-2">
129
+ <a :href="source.url" target="_blank" rel="noopener noreferrer" class="text-sm font-medium text-blue-700 hover:underline truncate">
130
+ {{ source.title }}
131
+ </a>
132
+ <code class="text-[11px] text-gray-400 shrink-0">
133
+ {{ source.slug }}
134
+ </code>
135
+ </div>
136
+ <div class="text-xs text-gray-500 truncate">
137
+ {{ source.url }}
138
+ </div>
139
+ <div v-if="source.categories.length > 0" class="mt-1 flex flex-wrap gap-1">
140
+ <span v-for="cat in source.categories" :key="cat" class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-600">
141
+ {{ cat }}
142
+ </span>
143
+ </div>
144
+ <div v-if="source.notes" class="mt-1 text-xs text-gray-600 italic">
145
+ {{ source.notes }}
146
+ </div>
147
+ </div>
148
+ <button
149
+ class="text-xs text-red-600 hover:text-red-800 shrink-0 disabled:opacity-50"
150
+ :disabled="busy === source.slug"
151
+ :data-testid="`source-remove-${source.slug}`"
152
+ @click="remove(source.slug)"
153
+ >
154
+ {{ busy === source.slug ? "Removing…" : "Remove" }}
155
+ </button>
156
+ </li>
157
+ </ul>
158
+
159
+ <!-- Today's brief. Auto-fetched on mount and refreshed after
160
+ every Rebuild. Rendered as markdown so lists / headings
161
+ feel like a document, not a dump. -->
162
+ <div v-if="sources.length > 0 && (briefLoading || briefHtml || briefError)" class="p-4" data-testid="sources-brief">
163
+ <div class="flex items-baseline justify-between mb-2">
164
+ <h3 class="text-sm font-semibold text-gray-800">
165
+ Today's brief
166
+ <span v-if="briefDate" class="text-xs text-gray-400 font-normal"> ({{ briefDate }}) </span>
167
+ </h3>
168
+ <button v-if="briefFilePath" class="text-[11px] text-gray-500 hover:text-gray-700" :title="briefFilePath">
169
+ {{ briefFilePath }}
170
+ </button>
171
+ </div>
172
+ <div v-if="briefLoading" class="text-xs text-gray-500 italic">Loading today's brief…</div>
173
+ <div v-else-if="briefError" class="text-xs text-gray-500 italic" data-testid="sources-brief-empty">
174
+ {{ briefError }}
175
+ </div>
176
+ <!-- eslint-disable-next-line vue/no-v-html -->
177
+ <div v-else class="markdown-content" v-html="briefHtml" />
178
+ </div>
179
+ </div>
180
+
181
+ <div v-if="lastRebuild" class="px-4 py-2 border-t border-gray-100 shrink-0 text-xs text-gray-600" data-testid="sources-rebuild-summary">
182
+ Last rebuild ({{ lastRebuild.isoDate }}): <strong>{{ lastRebuild.itemCount }}</strong> items from <strong>{{ lastRebuild.plannedCount }}</strong> sources,
183
+ <strong>{{ lastRebuild.duplicateCount }}</strong> duplicates dropped.
184
+ <span v-if="lastRebuild.archiveErrors.length > 0" class="text-red-600"> ({{ lastRebuild.archiveErrors.length }} archive errors) </span>
185
+ </div>
186
+ </div>
187
+ </template>
188
+
189
+ <script setup lang="ts">
190
+ import { computed, onMounted, ref, watch } from "vue";
191
+ import { marked } from "marked";
192
+ import DOMPurify from "dompurify";
193
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
194
+ import type { ManageSourceData, RebuildSummary, Source } from "./index";
195
+ import { apiGet, apiPost, apiDelete } from "../../utils/api";
196
+ import { API_ROUTES } from "../../config/apiRoutes";
197
+
198
+ const props = defineProps<{
199
+ selectedResult: ToolResultComplete<ManageSourceData>;
200
+ }>();
201
+
202
+ // Local mirror of the source list that we mutate after Remove /
203
+ // Rebuild button clicks, so the UI stays responsive without the LLM
204
+ // having to re-list. Initial value comes from the tool result.
205
+ const localSources = ref<Source[] | null>(null);
206
+ const lastRebuild = ref<RebuildSummary | null>(null);
207
+ const actionMessage = ref("");
208
+ const actionError = ref(false);
209
+ // Tracks the current button-driven request: "rebuild", "add", or a
210
+ // slug (Remove). Used to disable/relabel the matching button.
211
+ const busy = ref<string | null>(null);
212
+
213
+ // --- Add source form state ---------------------------------------------
214
+
215
+ type DraftKind = "rss" | "github-releases" | "github-issues" | "arxiv";
216
+ interface DraftState {
217
+ kind: DraftKind;
218
+ primary: string; // Feed URL / repo URL / repo slug / arxiv query
219
+ title: string;
220
+ }
221
+
222
+ const adding = ref(false);
223
+ const draft = ref<DraftState>(emptyDraft());
224
+ const draftError = ref("");
225
+
226
+ function emptyDraft(): DraftState {
227
+ return { kind: "rss", primary: "", title: "" };
228
+ }
229
+
230
+ function startAdd(): void {
231
+ draft.value = emptyDraft();
232
+ draftError.value = "";
233
+ adding.value = true;
234
+ }
235
+
236
+ function cancelAdd(): void {
237
+ adding.value = false;
238
+ draftError.value = "";
239
+ }
240
+
241
+ function onKindChange(): void {
242
+ draftError.value = "";
243
+ }
244
+
245
+ const primaryPlaceholder = computed(() => {
246
+ switch (draft.value.kind) {
247
+ case "rss":
248
+ return "https://news.ycombinator.com/rss";
249
+ case "github-releases":
250
+ case "github-issues":
251
+ return "https://github.com/owner/repo (or owner/repo)";
252
+ case "arxiv":
253
+ return "cat:cs.CL";
254
+ }
255
+ return "";
256
+ });
257
+
258
+ const primaryHint = computed(() => {
259
+ switch (draft.value.kind) {
260
+ case "rss":
261
+ return "Feed URL (RSS 2.0 / Atom / RDF)";
262
+ case "github-releases":
263
+ return "GitHub repo URL or owner/repo — fetches releases";
264
+ case "github-issues":
265
+ return "GitHub repo URL or owner/repo — fetches issues";
266
+ case "arxiv":
267
+ return "arXiv search query (e.g. cat:cs.CL or au:hinton)";
268
+ }
269
+ return "";
270
+ });
271
+
272
+ // Extract owner/repo from either a full github.com URL or a bare
273
+ // "owner/repo" string. Returns null when the input doesn't look
274
+ // like a recognisable GitHub repo.
275
+ function parseRepoSlug(input: string): string | null {
276
+ const trimmed = input.trim();
277
+ if (!trimmed) return null;
278
+ const urlMatch = trimmed.match(/^https?:\/\/github\.com\/([^/\s]+)\/([^/\s?#]+)/i);
279
+ if (urlMatch) return `${urlMatch[1]}/${urlMatch[2].replace(/\.git$/, "")}`;
280
+ if (/^[^/\s]+\/[^/\s]+$/.test(trimmed)) return trimmed.replace(/\.git$/, "");
281
+ return null;
282
+ }
283
+
284
+ // Build the /api/sources body from the draft. Returns an error
285
+ // string when the input is invalid for the chosen kind.
286
+ interface RegisterPayload {
287
+ title: string;
288
+ url: string;
289
+ fetcherKind: DraftKind;
290
+ fetcherParams: Record<string, string>;
291
+ }
292
+
293
+ function buildRegisterPayload(input: DraftState): RegisterPayload | string {
294
+ const primary = input.primary.trim();
295
+ const title = input.title.trim();
296
+ if (!primary) return "Please fill in the URL / query field.";
297
+ switch (input.kind) {
298
+ case "rss": {
299
+ if (!/^https?:\/\//i.test(primary)) {
300
+ return "RSS feed URL must start with http:// or https://";
301
+ }
302
+ let hostname: string;
303
+ try {
304
+ hostname = new URL(primary).hostname;
305
+ } catch {
306
+ return "RSS feed URL is not a valid URL.";
307
+ }
308
+ if (!hostname) {
309
+ return "RSS feed URL must include a host.";
310
+ }
311
+ return {
312
+ title: title || hostname,
313
+ url: primary,
314
+ fetcherKind: "rss",
315
+ fetcherParams: { rss_url: primary },
316
+ };
317
+ }
318
+ case "github-releases":
319
+ case "github-issues": {
320
+ const slug = parseRepoSlug(primary);
321
+ if (!slug) {
322
+ return "Enter a GitHub repo URL (https://github.com/owner/repo) or owner/repo.";
323
+ }
324
+ return {
325
+ title: title || slug,
326
+ url: `https://github.com/${slug}`,
327
+ fetcherKind: input.kind,
328
+ fetcherParams: { github_repo: slug },
329
+ };
330
+ }
331
+ case "arxiv": {
332
+ const query = primary;
333
+ return {
334
+ title: title || `arXiv: ${query}`,
335
+ url: `https://export.arxiv.org/api/query?search_query=${encodeURIComponent(query)}`,
336
+ fetcherKind: "arxiv",
337
+ fetcherParams: { arxiv_query: query },
338
+ };
339
+ }
340
+ }
341
+ return "Unsupported fetcher kind.";
342
+ }
343
+
344
+ async function commitAdd(): Promise<void> {
345
+ const payload = buildRegisterPayload(draft.value);
346
+ if (typeof payload === "string") {
347
+ draftError.value = payload;
348
+ return;
349
+ }
350
+ draftError.value = "";
351
+ busy.value = "add";
352
+ const response = await apiPost<unknown>(API_ROUTES.sources.create, payload);
353
+ if (!response.ok) {
354
+ draftError.value = response.error || "Failed to register source";
355
+ busy.value = null;
356
+ return;
357
+ }
358
+ flash(`Registered. Fetching new items…`);
359
+ adding.value = false;
360
+ await refreshList();
361
+ // C: auto-rebuild so the user sees items without an extra click.
362
+ busy.value = "rebuild";
363
+ await rebuildInline();
364
+ busy.value = null;
365
+ }
366
+
367
+ // --- Starter-pack presets ----------------------------------------------
368
+
369
+ interface PresetEntry {
370
+ slug: string;
371
+ title: string;
372
+ url: string;
373
+ fetcherKind: "rss" | "github-releases" | "github-issues" | "arxiv";
374
+ fetcherParams: Record<string, string>;
375
+ categories?: string[];
376
+ }
377
+
378
+ interface Preset {
379
+ id: string;
380
+ label: string;
381
+ description: string;
382
+ entries: PresetEntry[];
383
+ }
384
+
385
+ const PRESETS: Preset[] = [
386
+ {
387
+ id: "tech-news",
388
+ label: "Tech news",
389
+ description: "Hacker News front page — daily tech headlines.",
390
+ entries: [
391
+ {
392
+ slug: "hacker-news",
393
+ title: "Hacker News",
394
+ url: "https://news.ycombinator.com/rss",
395
+ fetcherKind: "rss",
396
+ fetcherParams: { rss_url: "https://news.ycombinator.com/rss" },
397
+ categories: ["tech-news", "startup"],
398
+ },
399
+ ],
400
+ },
401
+ {
402
+ id: "ai-research",
403
+ label: "AI research",
404
+ description: "Latest arXiv papers in NLP (cs.CL) and machine learning (cs.LG).",
405
+ entries: [
406
+ {
407
+ slug: "arxiv-cs-cl",
408
+ title: "arXiv cs.CL",
409
+ url: "https://export.arxiv.org/api/query?search_query=cat:cs.CL",
410
+ fetcherKind: "arxiv",
411
+ fetcherParams: { arxiv_query: "cat:cs.CL" },
412
+ categories: ["ai", "research"],
413
+ },
414
+ {
415
+ slug: "arxiv-cs-lg",
416
+ title: "arXiv cs.LG",
417
+ url: "https://export.arxiv.org/api/query?search_query=cat:cs.LG",
418
+ fetcherKind: "arxiv",
419
+ fetcherParams: { arxiv_query: "cat:cs.LG" },
420
+ categories: ["ai", "research"],
421
+ },
422
+ ],
423
+ },
424
+ {
425
+ id: "claude-code",
426
+ label: "Claude Code updates",
427
+ description: "New releases of the Claude Code CLI from the anthropics/claude-code repo.",
428
+ entries: [
429
+ {
430
+ slug: "claude-code-releases",
431
+ title: "Claude Code releases",
432
+ url: "https://github.com/anthropics/claude-code",
433
+ fetcherKind: "github-releases",
434
+ fetcherParams: { github_repo: "anthropics/claude-code" },
435
+ categories: ["ai", "tech-news"],
436
+ },
437
+ ],
438
+ },
439
+ ];
440
+
441
+ async function installPreset(preset: Preset): Promise<void> {
442
+ busy.value = `preset-${preset.id}`;
443
+ const alreadyHave = new Set(sources.value.map((source) => source.slug));
444
+ const toRegister = preset.entries.filter((entry) => !alreadyHave.has(entry.slug));
445
+ if (toRegister.length === 0) {
446
+ flash(`All sources in "${preset.label}" are already registered.`);
447
+ busy.value = null;
448
+ return;
449
+ }
450
+ const failures: string[] = [];
451
+ for (const entry of toRegister) {
452
+ const response = await apiPost<unknown>(API_ROUTES.sources.create, {
453
+ slug: entry.slug,
454
+ title: entry.title,
455
+ url: entry.url,
456
+ fetcherKind: entry.fetcherKind,
457
+ fetcherParams: entry.fetcherParams,
458
+ // Presets know their categories — skip the classifier
459
+ // CLI call so the first brief is ready sooner.
460
+ categories: entry.categories,
461
+ skipClassify: true,
462
+ });
463
+ if (!response.ok) {
464
+ failures.push(`${entry.slug}: ${response.error}`);
465
+ }
466
+ }
467
+ if (failures.length > 0) {
468
+ flash(`Registered ${toRegister.length - failures.length}/${toRegister.length}. Errors: ${failures.join("; ")}`, true);
469
+ } else {
470
+ flash(`Registered ${toRegister.length} source${toRegister.length === 1 ? "" : "s"} from "${preset.label}". Fetching…`);
471
+ }
472
+ await refreshList();
473
+ await rebuildInline();
474
+ busy.value = null;
475
+ }
476
+
477
+ // Rebuild step extracted so commitAdd can chain it without recursing
478
+ // into rebuild()'s own busy-state machine.
479
+ async function rebuildInline(): Promise<void> {
480
+ const response = await apiPost<RebuildSummary>(API_ROUTES.sources.rebuild);
481
+ if (!response.ok) {
482
+ flash(`Register succeeded but rebuild failed: ${response.error}`, true);
483
+ return;
484
+ }
485
+ const summary = response.data;
486
+ lastRebuild.value = summary;
487
+ flash(`Ready: ${summary.itemCount} items from ${summary.plannedCount} source${summary.plannedCount === 1 ? "" : "s"}.`);
488
+ await loadBrief(summary.isoDate);
489
+ }
490
+
491
+ const sources = computed<Source[]>(() => {
492
+ if (localSources.value !== null) return localSources.value;
493
+ return props.selectedResult.data?.sources ?? [];
494
+ });
495
+
496
+ const highlightSlug = computed(() => props.selectedResult.data?.highlightSlug ?? null);
497
+
498
+ // Initialize lastRebuild from the result if the LLM-side rebuild
499
+ // landed before any in-View button click — but never overwrite a
500
+ // fresher result the user's own click produced.
501
+ if (lastRebuild.value === null && props.selectedResult.data?.lastRebuild !== undefined) {
502
+ lastRebuild.value = props.selectedResult.data.lastRebuild;
503
+ }
504
+
505
+ // Re-sync the local mirrors when the caller selects a different
506
+ // manageSource result (e.g. a new tool_result from the LLM). The
507
+ // existing "never overwrite fresher in-View state" guard still
508
+ // applies — we only accept the prop value when it's strictly
509
+ // newer than what the View has.
510
+ watch(
511
+ () => props.selectedResult.uuid,
512
+ () => {
513
+ const incoming = props.selectedResult.data;
514
+ if (!incoming) return;
515
+ // Replace the source list wholesale — the prop's snapshot is
516
+ // authoritative when the user switches between results.
517
+ localSources.value = incoming.sources ?? [];
518
+ const nextRebuild = incoming.lastRebuild;
519
+ if (nextRebuild && (!lastRebuild.value || nextRebuild.isoDate >= lastRebuild.value.isoDate)) {
520
+ lastRebuild.value = nextRebuild;
521
+ }
522
+ },
523
+ );
524
+
525
+ function kindLabel(kind: Source["fetcherKind"]): string {
526
+ switch (kind) {
527
+ case "rss":
528
+ return "RSS";
529
+ case "github-releases":
530
+ return "GitHub rel";
531
+ case "github-issues":
532
+ return "GitHub iss";
533
+ case "arxiv":
534
+ return "arXiv";
535
+ }
536
+ }
537
+
538
+ function kindBadgeClass(kind: Source["fetcherKind"]): string {
539
+ switch (kind) {
540
+ case "rss":
541
+ return "bg-orange-100 text-orange-700";
542
+ case "github-releases":
543
+ return "bg-purple-100 text-purple-700";
544
+ case "github-issues":
545
+ return "bg-indigo-100 text-indigo-700";
546
+ case "arxiv":
547
+ return "bg-emerald-100 text-emerald-700";
548
+ }
549
+ }
550
+
551
+ function flash(message: string, isError = false): void {
552
+ actionMessage.value = message;
553
+ actionError.value = isError;
554
+ setTimeout(() => {
555
+ if (actionMessage.value === message) actionMessage.value = "";
556
+ }, 4000);
557
+ }
558
+
559
+ async function refreshList(): Promise<void> {
560
+ const response = await apiGet<{ sources: Source[] }>(API_ROUTES.sources.list);
561
+ if (!response.ok) {
562
+ flash(`Failed to refresh sources: ${response.error}`, true);
563
+ return;
564
+ }
565
+ localSources.value = response.data.sources;
566
+ }
567
+
568
+ async function remove(slug: string): Promise<void> {
569
+ if (!confirm(`Remove source "${slug}"?`)) return;
570
+ busy.value = slug;
571
+ const response = await apiDelete<unknown>(API_ROUTES.sources.remove.replace(":slug", encodeURIComponent(slug)));
572
+ busy.value = null;
573
+ if (!response.ok) {
574
+ flash(`Remove failed: ${response.error}`, true);
575
+ return;
576
+ }
577
+ flash(`Removed "${slug}".`);
578
+ await refreshList();
579
+ }
580
+
581
+ async function rebuild(): Promise<void> {
582
+ busy.value = "rebuild";
583
+ const response = await apiPost<RebuildSummary>(API_ROUTES.sources.rebuild);
584
+ if (!response.ok) {
585
+ flash(`Rebuild failed: ${response.error}`, true);
586
+ busy.value = null;
587
+ return;
588
+ }
589
+ const summary = response.data;
590
+ lastRebuild.value = summary;
591
+ flash(`Rebuild complete: ${summary.itemCount} items from ${summary.plannedCount} sources.`);
592
+ await Promise.all([refreshList(), loadBrief(summary.isoDate)]);
593
+ busy.value = null;
594
+ }
595
+
596
+ // --- today's brief -------------------------------------------------------
597
+
598
+ // Fetched markdown (rendered via marked() into briefHtml below). Null
599
+ // while idle; "" after a confirmed empty/404 so the template can show
600
+ // a friendly message instead of a stuck spinner.
601
+ const briefMarkdown = ref<string | null>(null);
602
+ const briefError = ref("");
603
+ const briefLoading = ref(false);
604
+ const briefDate = ref("");
605
+ const briefFilePath = ref("");
606
+
607
+ // Build `news/daily/YYYY/MM/DD.md` from an ISO date. Local-time
608
+ // matches how the pipeline writes the file (see toLocalIsoDate).
609
+ function dailyPathFor(isoDate: string): string {
610
+ const [year, month, day] = isoDate.split("-");
611
+ return `news/daily/${year}/${month}/${day}.md`;
612
+ }
613
+
614
+ function todayIsoDate(): string {
615
+ const now = new Date();
616
+ const year = now.getFullYear();
617
+ const month = String(now.getMonth() + 1).padStart(2, "0");
618
+ const day = String(now.getDate()).padStart(2, "0");
619
+ return `${year}-${month}-${day}`;
620
+ }
621
+
622
+ // Monotonically-increasing token so concurrent loadBrief() calls
623
+ // (mount + rebuild + prop watch racing on slow networks) can drop
624
+ // stale responses that resolve after a newer one has already
625
+ // settled the state. Without this, an older fetch finishing last
626
+ // would clobber the latest brief.
627
+ let briefLoadToken = 0;
628
+
629
+ async function loadBrief(isoDate: string): Promise<void> {
630
+ const token = ++briefLoadToken;
631
+ briefLoading.value = true;
632
+ briefError.value = "";
633
+ briefDate.value = isoDate;
634
+ const relPath = dailyPathFor(isoDate);
635
+ briefFilePath.value = relPath;
636
+ const response = await apiGet<{ content?: string; kind?: string }>(API_ROUTES.files.content, { path: relPath });
637
+ if (token !== briefLoadToken) return;
638
+ if (!response.ok) {
639
+ if (response.status === 404) {
640
+ briefMarkdown.value = "";
641
+ briefError.value = "No brief written for this date yet. Click Rebuild now.";
642
+ } else {
643
+ briefError.value = response.error || "Failed to load brief";
644
+ }
645
+ briefLoading.value = false;
646
+ return;
647
+ }
648
+ briefMarkdown.value = response.data.content ?? "";
649
+ if (!briefMarkdown.value.trim()) {
650
+ briefError.value = "Today's brief is empty.";
651
+ }
652
+ briefLoading.value = false;
653
+ }
654
+
655
+ // The daily file ends with a trailing ```json block that carries
656
+ // the structured item list for later machine consumption (Q2 of the
657
+ // plan: "Markdown + trailing fenced JSON block"). Strip it for the
658
+ // human-facing render so the UI doesn't dump a 1000-line JSON blob
659
+ // after the brief. The file on disk stays unchanged.
660
+ function stripTrailingJsonBlock(markdown: string): string {
661
+ const marker = "\n```json\n";
662
+ const idx = markdown.lastIndexOf(marker);
663
+ if (idx < 0) return markdown;
664
+ // Only strip if everything after the marker looks like it belongs
665
+ // to that block (i.e. it's the last fenced block in the file).
666
+ const tail = markdown.slice(idx);
667
+ if (!tail.trimEnd().endsWith("```")) return markdown;
668
+ return markdown.slice(0, idx).trimEnd();
669
+ }
670
+
671
+ const briefHtml = computed(() => {
672
+ if (!briefMarkdown.value) return "";
673
+ const body = stripTrailingJsonBlock(briefMarkdown.value);
674
+ // marked() preserves raw HTML embedded in the markdown (RSS
675
+ // content:encoded blocks often carry tracking pixels, iframes,
676
+ // inline <script> from scraped sources). Sanitize before
677
+ // binding to v-html.
678
+ return DOMPurify.sanitize(marked(body) as string);
679
+ });
680
+
681
+ // Load on mount — try today's brief first, then last rebuild's date
682
+ // if different (tool result may have been produced earlier in the day
683
+ // but the user only just opened this canvas).
684
+ onMounted(() => {
685
+ const initial = lastRebuild.value?.isoDate ?? todayIsoDate();
686
+ loadBrief(initial);
687
+ });
688
+
689
+ // Re-fetch when the selected result brings a new rebuild summary
690
+ // (e.g. the LLM triggered another rebuild).
691
+ watch(
692
+ () => props.selectedResult.data?.lastRebuild?.isoDate,
693
+ (next) => {
694
+ if (next && next !== briefDate.value) loadBrief(next);
695
+ },
696
+ );
697
+ </script>