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,95 @@
1
+ import type { ToolDefinition } from "gui-chat-protocol";
2
+
3
+ export const TOOL_NAME = "presentMulmoScript";
4
+
5
+ const toolDefinition: ToolDefinition = {
6
+ type: "function",
7
+ name: TOOL_NAME,
8
+ description: `Save and present a MulmoScript story or presentation as a visual storyboard in the canvas.
9
+
10
+ Always use Google providers. Required structure:
11
+
12
+ {
13
+ "$mulmocast": { "version": "1.1" },
14
+ "title": "The Life of a Star",
15
+ "description": "A short educational explainer about stellar evolution",
16
+ "lang": "en",
17
+ "speechParams": {
18
+ "speakers": {
19
+ "Presenter": {
20
+ "provider": "gemini",
21
+ "voiceId": "Kore",
22
+ "displayName": { "en": "Presenter" }
23
+ }
24
+ }
25
+ },
26
+ "imageParams": { "provider": "google", "model": "gemini-3.1-flash-image" },
27
+ "movieParams": { "provider": "google", "model": "veo-3.1-generate" },
28
+ "beats": [
29
+ {
30
+ "speaker": "Presenter",
31
+ "text": "Narration spoken aloud for this beat.",
32
+ "imagePrompt": "Detailed description — AI generates the image"
33
+ },
34
+ {
35
+ "speaker": "Presenter",
36
+ "text": "Bullet point beat.",
37
+ "image": { "type": "textSlide", "slide": { "title": "Slide Title", "bullets": ["Point one", "Point two"] } }
38
+ },
39
+ {
40
+ "speaker": "Presenter",
41
+ "text": "Markdown beat.",
42
+ "image": { "type": "markdown", "markdown": "## Heading\\n\\nBody text here." }
43
+ },
44
+ {
45
+ "speaker": "Presenter",
46
+ "text": "Chart beat — use for data, comparisons, trends.",
47
+ "image": { "type": "chart", "title": "Chart Title", "chartData": { "type": "bar", "data": { "labels": ["A", "B", "C"], "datasets": [{ "label": "Series", "data": [10, 20, 30] }] } } }
48
+ },
49
+ {
50
+ "speaker": "Presenter",
51
+ "text": "Diagram beat — use for flows, architectures, relationships.",
52
+ "image": { "type": "mermaid", "title": "Diagram Title", "code": { "kind": "text", "text": "graph TD\\n A[Start] --> B[Process] --> C[End]" } }
53
+ },
54
+ {
55
+ "speaker": "Presenter",
56
+ "text": "Rich interactive beat — use for custom layouts, animations, or anything that benefits from HTML/CSS.",
57
+ "image": { "type": "html_tailwind", "html": "<div class=\\"flex items-center justify-center h-full text-4xl font-bold text-blue-600\\">Hello World</div>" }
58
+ },
59
+ {
60
+ "speaker": "Presenter",
61
+ "text": "AI video beat.",
62
+ "moviePrompt": "Detailed description — AI generates the video clip"
63
+ }
64
+ ]
65
+ }
66
+
67
+ Beat visual options (choose one per beat):
68
+ - "imagePrompt": "..." → top-level string field — AI generates an image from the prompt
69
+ - "moviePrompt": "..." → top-level string field — AI generates a video clip from the prompt
70
+ - "image": { "type": "textSlide", "slide": { "title", "subtitle"?, "bullets"? } }
71
+ - "image": { "type": "markdown", "markdown": "..." }
72
+ - "image": { "type": "chart", "title": "...", "chartData": { "type": "bar"|"line"|"pie"|..., "data": { "labels": [...], "datasets": [...] } } } ← PREFER for data/numbers/comparisons. chartData is a full Chart.js config: labels/datasets go under "data", not at the top level.
73
+ - "image": { "type": "mermaid", "title": "...", "code": { "kind": "text", "text": "..." } } ← PREFER for flows/diagrams/relationships
74
+ - "image": { "type": "html_tailwind", "html": "...", "script"?: "..." } ← PREFER for rich layouts, animations, custom visuals
75
+
76
+ IMPORTANT: "imagePrompt" and "moviePrompt" are plain string fields on the beat, NOT nested under "image".`,
77
+ parameters: {
78
+ type: "object",
79
+ properties: {
80
+ script: {
81
+ type: "object",
82
+ description:
83
+ "Complete MulmoScript JSON. Must include $mulmocast, speechParams, imageParams, movieParams, and beats array. Always populate the top-level 'description' field with a concise 1–2 sentence summary of the presentation.",
84
+ additionalProperties: true,
85
+ },
86
+ filename: {
87
+ type: "string",
88
+ description: "Optional filename without extension. Defaults to a slug of the script title.",
89
+ },
90
+ },
91
+ required: ["script"],
92
+ },
93
+ };
94
+
95
+ export default toolDefinition;
@@ -0,0 +1,162 @@
1
+ // Pure helpers for presentMulmoScript View.vue. Kept separate so
2
+ // their logic is unit-testable without mounting the Vue component.
3
+
4
+ import { errorMessage } from "../../utils/errors";
5
+ import { isRecord } from "../../utils/types";
6
+
7
+ export type SSEEvent =
8
+ | { type: "beat_image_done"; beatIndex: number }
9
+ | { type: "beat_audio_done"; beatIndex: number }
10
+ | { type: "done"; moviePath: string }
11
+ | { type: "error"; message: string }
12
+ | { type: "unknown" };
13
+
14
+ /**
15
+ * Parse a single SSE line of the form `data: {json}`. Returns
16
+ * null for non-data lines (comments, blank) or lines whose JSON
17
+ * payload fails to parse. Unrecognised event types still parse
18
+ * but resolve to `{ type: "unknown" }` so the caller can ignore
19
+ * them without crashing.
20
+ */
21
+ export function parseSSEEventLine(line: string): SSEEvent | null {
22
+ if (!line.startsWith("data: ")) return null;
23
+ let obj: unknown;
24
+ try {
25
+ obj = JSON.parse(line.slice(6));
26
+ } catch {
27
+ return null;
28
+ }
29
+ if (!isRecord(obj)) return null;
30
+ const event = obj;
31
+ if (event.type === "beat_image_done" && typeof event.beatIndex === "number") {
32
+ return { type: "beat_image_done", beatIndex: event.beatIndex };
33
+ }
34
+ if (event.type === "beat_audio_done" && typeof event.beatIndex === "number") {
35
+ return { type: "beat_audio_done", beatIndex: event.beatIndex };
36
+ }
37
+ if (event.type === "done" && typeof event.moviePath === "string") {
38
+ return { type: "done", moviePath: event.moviePath };
39
+ }
40
+ if (event.type === "error" && typeof event.message === "string") {
41
+ return { type: "error", message: event.message };
42
+ }
43
+ return { type: "unknown" };
44
+ }
45
+
46
+ /**
47
+ * Decide whether a beat should be rendered automatically at
48
+ * script load time. Text-based beats (slides, charts, etc.) are
49
+ * auto-rendered only when the script has no characters —
50
+ * characters must be rendered first so they can be referenced by
51
+ * any character-using beat.
52
+ */
53
+ export function shouldAutoRenderBeat(beat: { image?: { type?: string } | undefined }, hasCharacters: boolean, autoRenderTypes: readonly string[]): boolean {
54
+ if (hasCharacters) return false;
55
+ const type = beat.image?.type;
56
+ if (typeof type !== "string") return false;
57
+ return autoRenderTypes.includes(type);
58
+ }
59
+
60
+ /**
61
+ * Of the given character keys, return those whose image is not
62
+ * yet loaded and is not currently rendering. Used to fetch only
63
+ * what's missing after a movie-generation event arrives.
64
+ */
65
+ export function getMissingCharacterKeys(keys: readonly string[], images: Record<string, unknown>, renderState: Record<string, string | undefined>): string[] {
66
+ return keys.filter((k) => !images[k] && renderState[k] !== "rendering");
67
+ }
68
+
69
+ /**
70
+ * A schema shape that exposes `safeParse` — matches Zod's API
71
+ * without pulling the dep into this module.
72
+ */
73
+ export interface SafeParseSchema {
74
+ safeParse(value: unknown): { success: boolean };
75
+ }
76
+
77
+ /**
78
+ * Validate a candidate Beat JSON string against a schema.
79
+ * Returns false on any JSON parse error or schema mismatch.
80
+ */
81
+ export function validateBeatJSON(json: string, schema: SafeParseSchema): boolean {
82
+ let parsed: unknown;
83
+ try {
84
+ parsed = JSON.parse(json);
85
+ } catch {
86
+ return false;
87
+ }
88
+ return schema.safeParse(parsed).success;
89
+ }
90
+
91
+ /** Convert an unknown thrown value into a human-readable string. */
92
+ export function extractErrorMessage(err: unknown): string {
93
+ return errorMessage(err);
94
+ }
95
+
96
+ /**
97
+ * Callback set for `applyMovieEvent` / `streamMovieEvents`. Each
98
+ * handler is scoped to one event shape; the dispatcher routes to
99
+ * the right one based on the discriminated union's `type` field.
100
+ * Keeping this as named handlers (rather than one big switch in
101
+ * the caller) lets `generateMovie` stay under the
102
+ * sonarjs/cognitive-complexity threshold.
103
+ */
104
+ export interface MovieEventHandlers {
105
+ onBeatImageDone(beatIndex: number): void;
106
+ onBeatAudioDone(beatIndex: number): void;
107
+ onDone(moviePath: string): void;
108
+ }
109
+
110
+ /**
111
+ * Dispatch a single already-parsed SSE event from the movie
112
+ * generation stream to the matching handler. `"error"` events
113
+ * throw so the caller's try/catch can surface the message the
114
+ * same way a network failure would. `"unknown"` events are
115
+ * silently ignored — the server occasionally introduces new
116
+ * event types before the client catches up, and we don't want
117
+ * those to tear down an otherwise-healthy stream.
118
+ */
119
+ export function applyMovieEvent(event: SSEEvent, handlers: MovieEventHandlers): void {
120
+ switch (event.type) {
121
+ case "beat_image_done":
122
+ handlers.onBeatImageDone(event.beatIndex);
123
+ return;
124
+ case "beat_audio_done":
125
+ handlers.onBeatAudioDone(event.beatIndex);
126
+ return;
127
+ case "done":
128
+ handlers.onDone(event.moviePath);
129
+ return;
130
+ case "error":
131
+ throw new Error(event.message);
132
+ case "unknown":
133
+ return;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Read the SSE stream body from the movie-generation endpoint
139
+ * and dispatch every parsed event into the given handlers.
140
+ * Returns when the server closes the stream; throws through any
141
+ * `"error"` event or unhandled read error. Kept here (rather
142
+ * than inline in `generateMovie`) so the reader + decoder +
143
+ * line-buffer state machine is a single named unit instead of a
144
+ * pyramid of `while` / `for` / `if` inside a Vue component.
145
+ */
146
+ export async function streamMovieEvents(body: ReadableStream<Uint8Array>, handlers: MovieEventHandlers): Promise<void> {
147
+ const reader = body.getReader();
148
+ const decoder = new TextDecoder();
149
+ let buffer = "";
150
+ while (true) {
151
+ const { done, value } = await reader.read();
152
+ if (done) break;
153
+ buffer += decoder.decode(value, { stream: true });
154
+ const lines = buffer.split("\n");
155
+ buffer = lines.pop() ?? "";
156
+ for (const line of lines) {
157
+ const event = parseSSEEventLine(line);
158
+ if (!event) continue;
159
+ applyMovieEvent(event, handlers);
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,40 @@
1
+ import type { ToolPlugin } from "../../tools/types";
2
+ import type { ToolResult } from "gui-chat-protocol";
3
+ import type { MulmoScript } from "mulmocast";
4
+ import toolDefinition, { TOOL_NAME } from "./definition";
5
+ import View from "./View.vue";
6
+ import Preview from "./Preview.vue";
7
+ import { apiPost } from "../../utils/api";
8
+ import { API_ROUTES } from "../../config/apiRoutes";
9
+
10
+ export interface MulmoScriptData {
11
+ script: MulmoScript;
12
+ filePath: string;
13
+ }
14
+
15
+ const presentMulmoScriptPlugin: ToolPlugin<MulmoScriptData> = {
16
+ toolDefinition,
17
+
18
+ async execute(_context, args) {
19
+ const result = await apiPost<ToolResult<MulmoScriptData>>(API_ROUTES.mulmoScript.save, args);
20
+ if (!result.ok) {
21
+ return {
22
+ toolName: TOOL_NAME,
23
+ uuid: crypto.randomUUID(),
24
+ message: result.error,
25
+ };
26
+ }
27
+ return {
28
+ ...result.data,
29
+ toolName: TOOL_NAME,
30
+ uuid: crypto.randomUUID(),
31
+ };
32
+ },
33
+
34
+ isEnabled: () => true,
35
+ generatingMessage: "Generating MulmoScript storyboard…",
36
+ viewComponent: View,
37
+ previewComponent: Preview,
38
+ };
39
+
40
+ export default presentMulmoScriptPlugin;
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <div class="text-sm">
3
+ <div class="flex items-center gap-1 font-medium text-gray-700 mb-1">
4
+ <span>📅</span>
5
+ <span>{{ upcomingItems.length }} upcoming</span>
6
+ </div>
7
+ <div v-for="item in preview" :key="item.id" class="text-xs truncate text-gray-600">
8
+ <span v-if="item.props.date" class="text-gray-400 mr-1">{{ item.props.date }}</span>
9
+ {{ item.title }}
10
+ </div>
11
+ <div v-if="more > 0" class="text-xs text-gray-400">+ {{ more }} more…</div>
12
+ </div>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { computed, ref, watch } from "vue";
17
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
18
+ import type { SchedulerData, ScheduledItem } from "./index";
19
+ import { useFreshPluginData } from "../../composables/useFreshPluginData";
20
+ import { API_ROUTES } from "../../config/apiRoutes";
21
+
22
+ const props = defineProps<{ result: ToolResultComplete<SchedulerData> }>();
23
+
24
+ const items = ref<ScheduledItem[]>(props.result.data?.items ?? []);
25
+
26
+ const { refresh } = useFreshPluginData<ScheduledItem[]>({
27
+ endpoint: () => API_ROUTES.scheduler.base,
28
+ extract: (json) => {
29
+ const v = (json as { data?: { items?: ScheduledItem[] } }).data?.items;
30
+ return Array.isArray(v) ? v : null;
31
+ },
32
+ apply: (data) => {
33
+ items.value = data;
34
+ },
35
+ });
36
+
37
+ watch(
38
+ () => props.result.uuid,
39
+ () => {
40
+ items.value = props.result.data?.items ?? [];
41
+ void refresh();
42
+ },
43
+ );
44
+
45
+ const today = new Date().toISOString().slice(0, 10);
46
+
47
+ const upcomingItems = computed(() => {
48
+ const withDate: ScheduledItem[] = [];
49
+ const noDate: ScheduledItem[] = [];
50
+
51
+ for (const item of items.value) {
52
+ const d = item.props.date;
53
+ if (typeof d === "string") {
54
+ if (d >= today) withDate.push(item);
55
+ } else {
56
+ noDate.push(item);
57
+ }
58
+ }
59
+
60
+ withDate.sort((a, b) => (String(a.props.date) < String(b.props.date) ? -1 : 1));
61
+
62
+ return [...withDate, ...noDate];
63
+ });
64
+
65
+ const preview = computed(() => upcomingItems.value.slice(0, 3));
66
+ const more = computed(() => Math.max(0, upcomingItems.value.length - 3));
67
+ </script>
@@ -0,0 +1,205 @@
1
+ <template>
2
+ <div class="flex-1 overflow-y-auto min-h-0 p-4">
3
+ <!-- Mutation error banner -->
4
+ <div v-if="mutationError" class="mb-3 px-4 py-2 bg-red-50 text-red-700 rounded text-sm" data-testid="scheduler-task-error">
5
+ {{ mutationError }}
6
+ </div>
7
+
8
+ <!-- Loading -->
9
+ <div v-if="loading" class="flex items-center justify-center h-32 text-gray-400">Loading...</div>
10
+
11
+ <!-- Error -->
12
+ <div v-else-if="error" class="px-4 py-2 bg-red-50 text-red-700 rounded text-sm">
13
+ {{ error }}
14
+ </div>
15
+
16
+ <!-- Task list -->
17
+ <div v-else>
18
+ <div v-if="tasks.length === 0" class="flex items-center justify-center h-32 text-gray-400">No scheduled tasks</div>
19
+
20
+ <div v-else class="space-y-2">
21
+ <div
22
+ v-for="task in tasks"
23
+ :key="task.id"
24
+ :data-testid="`scheduler-task-${task.id}`"
25
+ class="border border-gray-200 rounded-lg p-3 hover:bg-gray-50"
26
+ :class="{ 'opacity-50': task.enabled === false }"
27
+ >
28
+ <div class="flex items-center justify-between">
29
+ <div class="flex items-center gap-2 min-w-0">
30
+ <!-- Origin badge -->
31
+ <span class="text-xs px-1.5 py-0.5 rounded font-medium shrink-0" :class="originClass(task.origin)">
32
+ {{ originLabel(task.origin) }}
33
+ </span>
34
+ <span class="font-medium text-gray-800 truncate">
35
+ {{ task.name }}
36
+ </span>
37
+ </div>
38
+ <div class="flex items-center gap-1 shrink-0">
39
+ <!-- Run now -->
40
+ <button
41
+ v-if="task.origin === 'user'"
42
+ class="px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 rounded"
43
+ title="Run now"
44
+ aria-label="Run now"
45
+ data-testid="scheduler-task-run"
46
+ @click="runTask(task.id)"
47
+ >
48
+ <span class="material-icons text-sm">play_arrow</span>
49
+ </button>
50
+ <!-- Enable/disable toggle -->
51
+ <button
52
+ v-if="task.origin === 'user'"
53
+ class="px-2 py-1 text-xs rounded"
54
+ :class="task.enabled !== false ? 'text-green-600 hover:bg-green-50' : 'text-gray-400 hover:bg-gray-100'"
55
+ :title="task.enabled !== false ? 'Disable' : 'Enable'"
56
+ @click="toggleEnabled(task)"
57
+ >
58
+ <span class="material-icons text-sm">
59
+ {{ task.enabled !== false ? "toggle_on" : "toggle_off" }}
60
+ </span>
61
+ </button>
62
+ <!-- Delete -->
63
+ <button
64
+ v-if="task.origin === 'user'"
65
+ class="px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded"
66
+ title="Delete"
67
+ aria-label="Delete"
68
+ data-testid="scheduler-task-delete"
69
+ @click="deleteTask(task.id)"
70
+ >
71
+ <span class="material-icons text-sm">delete</span>
72
+ </button>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Details row -->
77
+ <div class="mt-1 flex items-center gap-3 text-xs text-gray-500">
78
+ <span>{{ formatSchedule(task.schedule) }}</span>
79
+ <span v-if="task.state?.lastRunResult" class="flex items-center gap-1">
80
+ <span class="inline-block w-2 h-2 rounded-full" :class="resultDotClass(task.state.lastRunResult)"></span>
81
+ {{ task.state.lastRunResult }}
82
+ </span>
83
+ <span v-if="task.state?.nextScheduledAt"> Next: {{ formatShortTime(task.state.nextScheduledAt) }} </span>
84
+ </div>
85
+
86
+ <!-- Description -->
87
+ <div v-if="task.description" class="mt-1 text-xs text-gray-400 truncate">
88
+ {{ task.description }}
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </template>
95
+
96
+ <script setup lang="ts">
97
+ import { ref, onMounted } from "vue";
98
+ import { apiGet, apiPost, apiPut, apiDelete } from "../../utils/api";
99
+ import { API_ROUTES } from "../../config/apiRoutes";
100
+ import { formatShortTime } from "../../utils/format/date";
101
+
102
+ interface TaskSchedule {
103
+ type: string;
104
+ intervalMs?: number;
105
+ time?: string;
106
+ }
107
+
108
+ interface TaskState {
109
+ lastRunAt?: string | null;
110
+ lastRunResult?: string | null;
111
+ nextScheduledAt?: string | null;
112
+ }
113
+
114
+ interface SchedulerTask {
115
+ id: string;
116
+ name: string;
117
+ description?: string;
118
+ schedule: TaskSchedule;
119
+ origin: string;
120
+ enabled?: boolean;
121
+ state?: TaskState;
122
+ }
123
+
124
+ const tasks = ref<SchedulerTask[]>([]);
125
+ const loading = ref(true);
126
+ const error = ref("");
127
+ const mutationError = ref("");
128
+
129
+ async function fetchTasks(): Promise<void> {
130
+ loading.value = true;
131
+ error.value = "";
132
+ const result = await apiGet<{ tasks: SchedulerTask[] }>(API_ROUTES.scheduler.tasks);
133
+ loading.value = false;
134
+ if (!result.ok) {
135
+ error.value = result.error;
136
+ return;
137
+ }
138
+ tasks.value = result.data.tasks;
139
+ }
140
+
141
+ function originLabel(origin: string): string {
142
+ if (origin === "system") return "System";
143
+ if (origin === "user") return "User";
144
+ return "Skill";
145
+ }
146
+
147
+ function originClass(origin: string): string {
148
+ if (origin === "system") return "bg-gray-100 text-gray-600";
149
+ if (origin === "user") return "bg-blue-100 text-blue-700";
150
+ return "bg-purple-100 text-purple-700";
151
+ }
152
+
153
+ function resultDotClass(result: string): string {
154
+ if (result === "success") return "bg-green-500";
155
+ if (result === "error") return "bg-red-500";
156
+ return "bg-gray-400";
157
+ }
158
+
159
+ function formatSchedule(schedule: TaskSchedule): string {
160
+ if (schedule.type === "interval" && schedule.intervalMs) {
161
+ const mins = Math.round(schedule.intervalMs / 60000);
162
+ if (mins >= 60) return `Every ${Math.round(mins / 60)}h`;
163
+ return `Every ${mins}m`;
164
+ }
165
+ if (schedule.type === "daily" && schedule.time) {
166
+ return `Daily ${schedule.time} UTC`;
167
+ }
168
+ return JSON.stringify(schedule);
169
+ }
170
+
171
+ async function runTask(id: string): Promise<void> {
172
+ mutationError.value = "";
173
+ const url = API_ROUTES.scheduler.taskRun.replace(":id", id);
174
+ const result = await apiPost(url, {});
175
+ if (!result.ok) {
176
+ mutationError.value = `Run failed: ${result.error}`;
177
+ return;
178
+ }
179
+ await fetchTasks();
180
+ }
181
+
182
+ async function toggleEnabled(task: SchedulerTask): Promise<void> {
183
+ mutationError.value = "";
184
+ const url = API_ROUTES.scheduler.task.replace(":id", task.id);
185
+ const result = await apiPut(url, { enabled: task.enabled === false });
186
+ if (!result.ok) {
187
+ mutationError.value = `Toggle failed: ${result.error}`;
188
+ return;
189
+ }
190
+ await fetchTasks();
191
+ }
192
+
193
+ async function deleteTask(id: string): Promise<void> {
194
+ mutationError.value = "";
195
+ const url = API_ROUTES.scheduler.task.replace(":id", id);
196
+ const result = await apiDelete(url);
197
+ if (!result.ok) {
198
+ mutationError.value = `Delete failed: ${result.error}`;
199
+ return;
200
+ }
201
+ await fetchTasks();
202
+ }
203
+
204
+ onMounted(fetchTasks);
205
+ </script>