mulmoclaude 0.5.2 → 0.6.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 (486) hide show
  1. package/Dockerfile.sandbox +100 -0
  2. package/README.md +17 -4
  3. package/bin/mulmoclaude.js +46 -15
  4. package/bin/prepare-dist.js +18 -2
  5. package/client/assets/chunk-CernVdwh.js +1 -0
  6. package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
  7. package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
  8. package/client/assets/index-BbgSjFQ8.js +4968 -0
  9. package/client/assets/index-ECD0lgIv.css +2 -0
  10. package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
  11. package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
  12. package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
  13. package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
  14. package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
  15. package/client/assets/vue-1e_vz2LW.js +1 -0
  16. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
  17. package/client/index.html +33 -2
  18. package/package.json +20 -18
  19. package/sandbox-entrypoint.sh +106 -0
  20. package/server/accounting/accountNormalize.ts +32 -0
  21. package/server/accounting/defaultAccounts.ts +87 -0
  22. package/server/accounting/eventPublisher.ts +51 -0
  23. package/server/accounting/journal.ts +252 -0
  24. package/server/accounting/openingBalances.ts +114 -0
  25. package/server/accounting/report.ts +237 -0
  26. package/server/accounting/service.ts +718 -0
  27. package/server/accounting/snapshotCache.ts +333 -0
  28. package/server/accounting/timeSeries.ts +265 -0
  29. package/server/accounting/types.ts +148 -0
  30. package/server/agent/activeTools.ts +128 -0
  31. package/server/agent/attachmentConverter.ts +10 -5
  32. package/server/agent/backend/claude-code.ts +8 -2
  33. package/server/agent/backend/types.ts +1 -1
  34. package/server/agent/config.ts +101 -31
  35. package/server/agent/index.ts +45 -33
  36. package/server/agent/mcp-server.ts +146 -69
  37. package/server/agent/mcp-tools/index.ts +1 -5
  38. package/server/agent/mcp-tools/notify.ts +2 -22
  39. package/server/agent/mcp-tools/x.ts +0 -4
  40. package/server/agent/mcpHealth.ts +168 -0
  41. package/server/agent/plugin-names.ts +20 -77
  42. package/server/agent/prompt.ts +259 -51
  43. package/server/agent/resumeFailover.ts +1 -1
  44. package/server/agent/stream.ts +0 -1
  45. package/server/api/auth/bearerAuth.ts +5 -5
  46. package/server/api/csrfGuard.ts +1 -1
  47. package/server/api/routes/accounting.ts +366 -0
  48. package/server/api/routes/agent.ts +509 -46
  49. package/server/api/routes/attachment.ts +104 -0
  50. package/server/api/routes/chart.ts +2 -1
  51. package/server/api/routes/config.ts +12 -12
  52. package/server/api/routes/files.ts +105 -48
  53. package/server/api/routes/image.ts +70 -25
  54. package/server/api/routes/journal.ts +35 -0
  55. package/server/api/routes/mulmo-script.ts +358 -118
  56. package/server/api/routes/mulmoScriptValidate.ts +1 -1
  57. package/server/api/routes/news.ts +1 -1
  58. package/server/api/routes/notifications.ts +92 -22
  59. package/server/api/routes/notifier.ts +98 -0
  60. package/server/api/routes/pdf.ts +188 -48
  61. package/server/api/routes/plugins.ts +34 -14
  62. package/server/api/routes/presentHtml.ts +58 -3
  63. package/server/api/routes/roles.ts +1 -8
  64. package/server/api/routes/runtime-plugin.ts +224 -0
  65. package/server/api/routes/scheduler.ts +7 -5
  66. package/server/api/routes/schedulerHandlers.ts +1 -1
  67. package/server/api/routes/schedulerTasks.ts +8 -7
  68. package/server/api/routes/sessions.ts +234 -121
  69. package/server/api/routes/skills.ts +56 -51
  70. package/server/api/routes/sources.ts +52 -45
  71. package/server/api/routes/translation.ts +44 -0
  72. package/server/api/routes/wiki/frontmatter.ts +13 -65
  73. package/server/api/routes/wiki/history.ts +261 -0
  74. package/server/api/routes/wiki/pageIndex.ts +1 -1
  75. package/server/api/routes/wiki.ts +50 -26
  76. package/server/events/file-change.ts +83 -0
  77. package/server/events/notifications.ts +247 -91
  78. package/server/events/pub-sub/index.ts +1 -1
  79. package/server/events/relay-client.ts +5 -5
  80. package/server/events/scheduler-adapter.ts +2 -2
  81. package/server/events/session-store/index.ts +110 -22
  82. package/server/events/task-manager/index.ts +10 -9
  83. package/server/index.ts +509 -33
  84. package/server/notifier/engine.ts +419 -0
  85. package/server/notifier/legacy-adapters.ts +76 -0
  86. package/server/notifier/runtime-api.ts +74 -0
  87. package/server/notifier/store.ts +70 -0
  88. package/server/notifier/types.ts +121 -0
  89. package/server/plugins/dev-loader.ts +171 -0
  90. package/server/plugins/dev-watcher.ts +150 -0
  91. package/server/plugins/diagnostics.ts +188 -0
  92. package/server/plugins/preset-list.ts +52 -0
  93. package/server/plugins/preset-loader.ts +112 -0
  94. package/server/plugins/runtime-chat-api.ts +38 -0
  95. package/server/plugins/runtime-loader.ts +430 -0
  96. package/server/plugins/runtime-registry.ts +112 -0
  97. package/server/plugins/runtime-tasks-api.ts +50 -0
  98. package/server/plugins/runtime.ts +378 -0
  99. package/server/services/translation/cache.ts +72 -0
  100. package/server/services/translation/index.ts +106 -0
  101. package/server/services/translation/llm.ts +140 -0
  102. package/server/services/translation/types.ts +35 -0
  103. package/server/system/credentials.ts +13 -2
  104. package/server/system/env.ts +6 -1
  105. package/server/system/logger/formatters.ts +46 -4
  106. package/server/system/logger/index.ts +4 -4
  107. package/server/system/logger/sinks.ts +26 -5
  108. package/server/system/logger/types.ts +2 -2
  109. package/server/utils/dev-plugin-args.d.mts +11 -0
  110. package/server/utils/dev-plugin-args.mjs +43 -0
  111. package/server/utils/errors.ts +13 -4
  112. package/server/utils/files/accounting-io.ts +295 -0
  113. package/server/utils/files/atomic.ts +17 -49
  114. package/server/utils/files/attachment-store.ts +182 -0
  115. package/server/utils/files/html-io.ts +1 -7
  116. package/server/utils/files/html-store.ts +19 -0
  117. package/server/utils/files/image-store.ts +20 -22
  118. package/server/utils/files/index.ts +5 -15
  119. package/server/utils/files/journal-io.ts +7 -35
  120. package/server/utils/files/json.ts +2 -29
  121. package/server/utils/files/markdown-image-fill.ts +6 -37
  122. package/server/utils/files/markdown-store.ts +6 -21
  123. package/server/utils/files/naming.ts +3 -39
  124. package/server/utils/files/plugins-io.ts +100 -0
  125. package/server/utils/files/reference-dirs-io.ts +1 -9
  126. package/server/utils/files/roles-io.ts +2 -10
  127. package/server/utils/files/safe.ts +17 -19
  128. package/server/utils/files/scheduler-io.ts +1 -7
  129. package/server/utils/files/scheduler-overrides-io.ts +3 -12
  130. package/server/utils/files/session-io.ts +21 -30
  131. package/server/utils/files/spreadsheet-store.ts +9 -22
  132. package/server/utils/files/translation-io.ts +46 -0
  133. package/server/utils/files/user-tasks-io.ts +1 -7
  134. package/server/utils/files/workspace-io.ts +3 -79
  135. package/server/utils/gemini.ts +33 -11
  136. package/server/utils/html/htmlArtifactSplicer.ts +41 -0
  137. package/server/utils/markdown/frontmatter.ts +112 -0
  138. package/server/utils/regex.ts +56 -0
  139. package/server/utils/router.ts +41 -0
  140. package/server/utils/slug.ts +5 -3
  141. package/server/utils/time.ts +12 -0
  142. package/server/workspace/chat-index/indexer.ts +15 -2
  143. package/server/workspace/chat-index/summarizer.ts +1 -1
  144. package/server/workspace/custom-dirs.ts +1 -1
  145. package/server/workspace/helps/gemini.md +1 -1
  146. package/server/workspace/helps/guide.md +61 -0
  147. package/server/workspace/helps/index.md +4 -0
  148. package/server/workspace/helps/presenthtml.md +80 -0
  149. package/server/workspace/helps/sandbox.md +7 -0
  150. package/server/workspace/helps/storyteller.md +101 -0
  151. package/server/workspace/helps/telegram.md +1 -0
  152. package/server/workspace/helps/wiki.md +9 -7
  153. package/server/workspace/journal/archivist-cli.ts +7 -33
  154. package/server/workspace/journal/archivist-schemas.ts +5 -43
  155. package/server/workspace/journal/dailyPass.ts +34 -187
  156. package/server/workspace/journal/diff.ts +3 -28
  157. package/server/workspace/journal/index.ts +10 -81
  158. package/server/workspace/journal/indexFile.ts +3 -24
  159. package/server/workspace/journal/latestDaily.ts +51 -0
  160. package/server/workspace/journal/memoryExtractor.ts +4 -20
  161. package/server/workspace/journal/optimizationPass.ts +4 -21
  162. package/server/workspace/journal/paths.ts +4 -23
  163. package/server/workspace/journal/state.ts +6 -29
  164. package/server/workspace/memory/io.ts +213 -0
  165. package/server/workspace/memory/llm-classifier.ts +158 -0
  166. package/server/workspace/memory/migrate.ts +263 -0
  167. package/server/workspace/memory/run.ts +84 -0
  168. package/server/workspace/memory/topic-cluster.ts +218 -0
  169. package/server/workspace/memory/topic-detect.ts +67 -0
  170. package/server/workspace/memory/topic-index-hook.ts +128 -0
  171. package/server/workspace/memory/topic-io.ts +180 -0
  172. package/server/workspace/memory/topic-migrate.ts +248 -0
  173. package/server/workspace/memory/topic-run.ts +172 -0
  174. package/server/workspace/memory/topic-swap.ts +135 -0
  175. package/server/workspace/memory/topic-types.ts +142 -0
  176. package/server/workspace/memory/types.ts +83 -0
  177. package/server/workspace/news/reader.ts +4 -5
  178. package/server/workspace/paths.ts +124 -47
  179. package/server/workspace/roles.ts +2 -11
  180. package/server/workspace/skills/parser.ts +38 -55
  181. package/server/workspace/skills/user-tasks.ts +1 -2
  182. package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
  183. package/server/workspace/skills-preset.ts +196 -0
  184. package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
  185. package/server/workspace/sources/fetchers/index.ts +1 -1
  186. package/server/workspace/sources/fetchers/rssParser.ts +1 -1
  187. package/server/workspace/sources/pipeline/index.ts +2 -2
  188. package/server/workspace/sources/pipeline/notify.ts +3 -3
  189. package/server/workspace/sources/pipeline/write.ts +2 -2
  190. package/server/workspace/sources/registry.ts +39 -61
  191. package/server/workspace/sources/robots.ts +1 -1
  192. package/server/workspace/tool-trace/classify.ts +2 -1
  193. package/server/workspace/tool-trace/index.ts +1 -1
  194. package/server/workspace/tool-trace/writeSearch.ts +6 -1
  195. package/server/workspace/wiki-backlinks/index.ts +19 -7
  196. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
  197. package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
  198. package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
  199. package/server/workspace/wiki-history/provision.ts +181 -0
  200. package/server/workspace/wiki-pages/io.ts +217 -0
  201. package/server/workspace/wiki-pages/snapshot.ts +380 -0
  202. package/server/workspace/workspace.ts +75 -13
  203. package/src/App.vue +115 -40
  204. package/src/_runtime/protocol-vue.ts +21 -0
  205. package/src/_runtime/vue.ts +22 -0
  206. package/src/components/ChatInput.vue +14 -10
  207. package/src/components/CopyChatButton.vue +76 -0
  208. package/src/components/FileContentRenderer.vue +67 -14
  209. package/src/components/FileTree.vue +2 -2
  210. package/src/components/FilesView.vue +17 -1
  211. package/src/components/NewsView.vue +16 -2
  212. package/src/components/NotificationBell.vue +320 -93
  213. package/src/components/PageChatComposer.vue +5 -4
  214. package/src/components/PluginLauncher.vue +42 -6
  215. package/src/components/PluginScopedRoot.vue +87 -0
  216. package/src/components/RoleSelector.vue +12 -1
  217. package/src/components/RolesView.vue +562 -0
  218. package/src/components/SentAttachmentChip.vue +102 -0
  219. package/src/components/SessionHistoryPanel.vue +109 -20
  220. package/src/components/SessionRoleIcon.vue +7 -4
  221. package/src/components/SessionSidebar.vue +20 -7
  222. package/src/components/SessionTabBar.vue +1 -1
  223. package/src/components/SettingsMcpTab.vue +4 -4
  224. package/src/components/SettingsModal.vue +2 -0
  225. package/src/components/SidebarHeader.vue +16 -5
  226. package/src/components/SourcesManager.vue +23 -9
  227. package/src/components/SourcesView.vue +1 -1
  228. package/src/components/StackView.vue +102 -6
  229. package/src/components/SuggestionsPanel.vue +105 -16
  230. package/src/components/SystemFileBanner.vue +1 -1
  231. package/src/components/TodoExplorer.vue +4 -5
  232. package/src/components/todo/TodoAddDialog.vue +2 -3
  233. package/src/components/todo/TodoEditDialog.vue +1 -2
  234. package/src/components/todo/TodoEditPanel.vue +2 -3
  235. package/src/components/todo/TodoKanbanView.vue +8 -5
  236. package/src/components/todo/TodoListView.vue +3 -5
  237. package/src/components/todo/TodoTableView.vue +7 -5
  238. package/src/composables/useAccountingChannel.ts +58 -0
  239. package/src/composables/useActiveSession.ts +4 -25
  240. package/src/composables/useAppApi.ts +6 -44
  241. package/src/composables/useClipboardCopy.ts +3 -20
  242. package/src/composables/useContentDisplay.ts +33 -2
  243. package/src/composables/useDevPluginReload.ts +23 -0
  244. package/src/composables/useDynamicFavicon.ts +5 -31
  245. package/src/composables/useEventListeners.ts +0 -20
  246. package/src/composables/useExpandedDirs.ts +4 -15
  247. package/src/composables/useFaviconState.ts +12 -46
  248. package/src/composables/useFileChange.ts +53 -0
  249. package/src/composables/useFreshPluginData.ts +6 -43
  250. package/src/composables/useHealth.ts +14 -43
  251. package/src/composables/useImageErrorRepair.ts +104 -0
  252. package/src/composables/useLatestDaily.ts +40 -0
  253. package/src/composables/useMarkdownDoc.ts +39 -0
  254. package/src/composables/useMarkdownLinkHandler.ts +1 -1
  255. package/src/composables/useMcpTools.ts +3 -16
  256. package/src/composables/useNotifications.ts +138 -112
  257. package/src/composables/usePdfDownload.ts +17 -3
  258. package/src/composables/usePendingCalls.ts +8 -26
  259. package/src/composables/usePluginErrorBoundary.ts +68 -0
  260. package/src/composables/usePubSub.ts +9 -17
  261. package/src/composables/useRunElapsed.ts +5 -22
  262. package/src/composables/useSandboxStatus.ts +4 -20
  263. package/src/composables/useSessionDerived.ts +7 -15
  264. package/src/composables/useSessionHistory.ts +70 -29
  265. package/src/composables/useSessionSync.ts +25 -3
  266. package/src/composables/useSkillsList.ts +59 -0
  267. package/src/composables/useTranslatedQueries.ts +109 -0
  268. package/src/config/apiRoutes.ts +181 -80
  269. package/src/config/historyFilters.ts +5 -3
  270. package/src/config/hostEvents.ts +17 -0
  271. package/src/config/mcpCatalog.ts +277 -5
  272. package/src/config/pubsubChannels.ts +134 -12
  273. package/src/config/roles.ts +212 -147
  274. package/src/config/systemFileDescriptors.ts +5 -5
  275. package/src/config/toolNames.ts +52 -30
  276. package/src/config/workspacePaths.ts +26 -2
  277. package/src/lang/de.ts +483 -27
  278. package/src/lang/en.ts +448 -27
  279. package/src/lang/es.ts +474 -27
  280. package/src/lang/fr.ts +476 -27
  281. package/src/lang/ja.ts +465 -27
  282. package/src/lang/ko.ts +466 -27
  283. package/src/lang/pt-BR.ts +473 -27
  284. package/src/lang/zh.ts +463 -27
  285. package/src/lib/vue-i18n.ts +1 -1
  286. package/src/lib/wiki-page/slug.ts +66 -0
  287. package/src/main.ts +85 -0
  288. package/src/plugins/_extras.ts +58 -0
  289. package/src/plugins/_generated/metas.ts +42 -0
  290. package/src/plugins/_generated/registrations.ts +44 -0
  291. package/src/plugins/_generated/server-bindings.ts +47 -0
  292. package/src/plugins/accounting/Preview.vue +106 -0
  293. package/src/plugins/accounting/View.vue +632 -0
  294. package/src/plugins/accounting/actions.ts +34 -0
  295. package/src/plugins/accounting/api.ts +301 -0
  296. package/src/plugins/accounting/components/AccountEditor.vue +250 -0
  297. package/src/plugins/accounting/components/AccountRow.vue +50 -0
  298. package/src/plugins/accounting/components/AccountsList.vue +102 -0
  299. package/src/plugins/accounting/components/AccountsModal.vue +300 -0
  300. package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
  301. package/src/plugins/accounting/components/BookSettings.vue +284 -0
  302. package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
  303. package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
  304. package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
  305. package/src/plugins/accounting/components/JournalList.vue +553 -0
  306. package/src/plugins/accounting/components/Ledger.vue +206 -0
  307. package/src/plugins/accounting/components/NewBookForm.vue +211 -0
  308. package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
  309. package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
  310. package/src/plugins/accounting/components/accountDraft.ts +13 -0
  311. package/src/plugins/accounting/components/accountNumbering.ts +103 -0
  312. package/src/plugins/accounting/components/accountValidation.ts +75 -0
  313. package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
  314. package/src/plugins/accounting/countries.ts +158 -0
  315. package/src/plugins/accounting/currencies.ts +64 -0
  316. package/src/plugins/accounting/dates.ts +51 -0
  317. package/src/plugins/accounting/definition.ts +199 -0
  318. package/src/plugins/accounting/fiscalYear.ts +136 -0
  319. package/src/plugins/accounting/index.ts +49 -0
  320. package/src/plugins/accounting/meta.ts +91 -0
  321. package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
  322. package/src/plugins/api.ts +125 -0
  323. package/src/plugins/canvas/View.vue +38 -28
  324. package/src/plugins/canvas/definition.ts +10 -8
  325. package/src/plugins/canvas/index.ts +15 -8
  326. package/src/plugins/canvas/meta.ts +12 -0
  327. package/src/plugins/chart/Preview.vue +1 -1
  328. package/src/plugins/chart/View.vue +2 -2
  329. package/src/plugins/chart/definition.ts +12 -2
  330. package/src/plugins/chart/index.ts +15 -7
  331. package/src/plugins/chart/meta.ts +18 -0
  332. package/src/plugins/editImages/definition.ts +44 -0
  333. package/src/plugins/editImages/index.ts +43 -0
  334. package/src/plugins/editImages/meta.ts +5 -0
  335. package/src/plugins/generateImage/View.vue +3 -1
  336. package/src/plugins/generateImage/definition.ts +2 -0
  337. package/src/plugins/generateImage/index.ts +13 -5
  338. package/src/plugins/generateImage/meta.ts +5 -0
  339. package/src/plugins/index.ts +35 -0
  340. package/src/plugins/manageRoles/Preview.vue +7 -4
  341. package/src/plugins/manageRoles/View.vue +12 -8
  342. package/src/plugins/manageRoles/definition.ts +6 -0
  343. package/src/plugins/manageRoles/index.ts +7 -6
  344. package/src/plugins/manageSkills/View.vue +11 -7
  345. package/src/plugins/manageSkills/definition.ts +4 -1
  346. package/src/plugins/manageSkills/index.ts +14 -7
  347. package/src/plugins/manageSkills/meta.ts +21 -0
  348. package/src/plugins/manageSource/definition.ts +4 -1
  349. package/src/plugins/manageSource/index.ts +15 -7
  350. package/src/plugins/manageSource/meta.ts +21 -0
  351. package/src/plugins/markdown/Preview.vue +10 -8
  352. package/src/plugins/markdown/View.vue +84 -17
  353. package/src/plugins/markdown/definition.ts +7 -1
  354. package/src/plugins/markdown/index.ts +15 -8
  355. package/src/plugins/markdown/meta.ts +16 -0
  356. package/src/plugins/meta-types.ts +97 -0
  357. package/src/plugins/metas.ts +224 -0
  358. package/src/plugins/presentForm/Preview.vue +4 -15
  359. package/src/plugins/presentForm/View.vue +35 -78
  360. package/src/plugins/presentForm/definition.ts +7 -6
  361. package/src/plugins/presentForm/index.ts +12 -5
  362. package/src/plugins/presentForm/meta.ts +11 -0
  363. package/src/plugins/presentForm/plugin.ts +8 -9
  364. package/src/plugins/presentForm/types.ts +0 -24
  365. package/src/plugins/presentHtml/Preview.vue +1 -8
  366. package/src/plugins/presentHtml/View.vue +401 -30
  367. package/src/plugins/presentHtml/definition.ts +8 -5
  368. package/src/plugins/presentHtml/index.ts +15 -8
  369. package/src/plugins/presentHtml/meta.ts +14 -0
  370. package/src/plugins/presentMulmoScript/View.vue +327 -107
  371. package/src/plugins/presentMulmoScript/definition.ts +34 -7
  372. package/src/plugins/presentMulmoScript/helpers.ts +4 -5
  373. package/src/plugins/presentMulmoScript/index.ts +20 -7
  374. package/src/plugins/presentMulmoScript/meta.ts +52 -0
  375. package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
  376. package/src/plugins/scheduler/Preview.vue +5 -2
  377. package/src/plugins/scheduler/TasksTab.vue +16 -36
  378. package/src/plugins/scheduler/View.vue +22 -54
  379. package/src/plugins/scheduler/automationsDefinition.ts +14 -9
  380. package/src/plugins/scheduler/automationsMeta.ts +5 -0
  381. package/src/plugins/scheduler/calendarDefinition.ts +4 -7
  382. package/src/plugins/scheduler/calendarMeta.ts +28 -0
  383. package/src/plugins/scheduler/formatSchedule.ts +6 -24
  384. package/src/plugins/scheduler/index.ts +26 -52
  385. package/src/plugins/scope.ts +57 -0
  386. package/src/plugins/server-bindings-types.ts +38 -0
  387. package/src/plugins/server.ts +32 -0
  388. package/src/plugins/skill/Preview.vue +25 -0
  389. package/src/plugins/skill/View.vue +125 -0
  390. package/src/plugins/skill/definition.ts +23 -0
  391. package/src/plugins/skill/index.ts +36 -0
  392. package/src/plugins/skill/plugin.ts +31 -0
  393. package/src/plugins/skill/types.ts +21 -0
  394. package/src/plugins/spreadsheet/Preview.vue +1 -3
  395. package/src/plugins/spreadsheet/View.vue +29 -49
  396. package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
  397. package/src/plugins/spreadsheet/definition.ts +5 -2
  398. package/src/plugins/spreadsheet/index.ts +15 -8
  399. package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
  400. package/src/plugins/spreadsheet/meta.ts +14 -0
  401. package/src/plugins/textResponse/Preview.vue +9 -1
  402. package/src/plugins/textResponse/View.vue +59 -8
  403. package/src/plugins/textResponse/index.ts +11 -3
  404. package/src/plugins/textResponse/plugin.ts +8 -10
  405. package/src/plugins/textResponse/types.ts +28 -0
  406. package/src/plugins/wiki/Preview.vue +6 -4
  407. package/src/plugins/wiki/View.vue +463 -254
  408. package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
  409. package/src/plugins/wiki/helpers.ts +17 -0
  410. package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
  411. package/src/plugins/wiki/history/HistoryTab.vue +167 -0
  412. package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
  413. package/src/plugins/wiki/history/api.ts +52 -0
  414. package/src/plugins/wiki/history/diff.ts +145 -0
  415. package/src/plugins/wiki/index.ts +42 -32
  416. package/src/plugins/wiki/meta.ts +10 -0
  417. package/src/plugins/wiki/pageEditLoader.ts +53 -0
  418. package/src/plugins/wiki/route.ts +8 -0
  419. package/src/router/guards.ts +2 -1
  420. package/src/router/index.ts +19 -0
  421. package/src/router/pageRoutes.ts +1 -0
  422. package/src/tools/index.ts +50 -51
  423. package/src/tools/runtimeLoader.ts +141 -0
  424. package/src/tools/types.ts +44 -1
  425. package/src/types/notification.ts +23 -0
  426. package/src/types/pastedFile.ts +10 -0
  427. package/src/types/session.ts +61 -3
  428. package/src/types/sse.ts +21 -6
  429. package/src/utils/agent/eventDispatch.ts +12 -9
  430. package/src/utils/agent/pastedAttachment.ts +35 -0
  431. package/src/utils/agent/request.ts +32 -3
  432. package/src/utils/agent/toolCalls.ts +7 -1
  433. package/src/utils/api.ts +1 -1
  434. package/src/utils/chat/exportMarkdown.ts +243 -0
  435. package/src/utils/errors.ts +10 -2
  436. package/src/utils/files/expandedDirs.ts +1 -1
  437. package/src/utils/filesPreview/todoPreview.ts +13 -2
  438. package/src/utils/format/date.ts +1 -3
  439. package/src/utils/format/jsonSyntax.ts +5 -0
  440. package/src/utils/html/iframeHeightReporterScript.ts +62 -0
  441. package/src/utils/html/previewCsp.ts +29 -2
  442. package/src/utils/image/htmlSrcAttrs.ts +122 -0
  443. package/src/utils/image/imageRepairInlineScript.ts +115 -0
  444. package/src/utils/image/resolve.ts +17 -3
  445. package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
  446. package/src/utils/markdown/frontmatter.ts +125 -0
  447. package/src/utils/markdown/taskList.ts +7 -2
  448. package/src/utils/plugin/runtime.ts +132 -0
  449. package/src/utils/session/mergeSessions.ts +40 -37
  450. package/src/utils/session/sessionEntries.ts +74 -18
  451. package/src/utils/session/sessionHelpers.ts +54 -10
  452. package/src/utils/tools/result.ts +76 -14
  453. package/src/vite-env.d.ts +6 -0
  454. package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
  455. package/client/assets/index-CY-WpQUm.css +0 -2
  456. package/client/assets/index-DbTz2Mfs.js +0 -4911
  457. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  458. package/server/api/routes/html.ts +0 -114
  459. package/server/api/routes/todos.ts +0 -293
  460. package/server/api/routes/todosColumnsHandlers.ts +0 -333
  461. package/server/api/routes/todosHandlers.ts +0 -274
  462. package/server/api/routes/todosItemsHandlers.ts +0 -386
  463. package/server/utils/files/todos-io.ts +0 -29
  464. package/src/components/NotificationToast.vue +0 -75
  465. package/src/plugins/editImage/definition.ts +0 -27
  466. package/src/plugins/editImage/index.ts +0 -37
  467. package/src/plugins/presentHtml/helpers.ts +0 -72
  468. package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
  469. package/src/plugins/scheduler/legacyShape.ts +0 -34
  470. package/src/plugins/todo/Preview.vue +0 -68
  471. package/src/plugins/todo/View.vue +0 -378
  472. package/src/plugins/todo/composables/useTodos.ts +0 -179
  473. package/src/plugins/todo/definition.ts +0 -45
  474. package/src/plugins/todo/index.ts +0 -62
  475. package/src/plugins/todo/labels.ts +0 -163
  476. package/src/plugins/todo/priority.ts +0 -98
  477. package/src/plugins/todo/viewModes.ts +0 -19
  478. package/src/plugins/wiki/definition.ts +0 -25
  479. package/src/tools/legacyPluginNames.ts +0 -13
  480. package/src/utils/format/frontmatter.ts +0 -80
  481. package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
  482. package/src/utils/notification/dispatch.ts +0 -58
  483. /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
  484. /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
  485. /package/src/plugins/{editImage → editImages}/View.vue +0 -0
  486. /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
@@ -1,25 +1,12 @@
1
- // Composable for the server /api/health probe.
2
- //
3
- // Owns three refs that the UI reads (gemini key availability +
4
- // sandbox toggle + server CPU load ratio) plus a one-shot fetch
5
- // that populates them on mount, plus an optional periodic refresh
6
- // for the CPU ratio (the favicon's "overloaded" rule needs a live
7
- // signal, not a boot-time snapshot).
8
- //
9
- // On fetch failure we assume Gemini is unavailable so dependent UI
10
- // (e.g. the "generate image" plugin buttons) falls back gracefully
11
- // — the sandbox flag keeps its initial `true` so the lock indicator
12
- // doesn't momentarily flash "sandbox disabled" on a transient error,
13
- // and the CPU ratio goes to null so the favicon resolver skips the
14
- // overloaded rule rather than guessing.
1
+ // On fetch failure: assume gemini unavailable (so "generate image" buttons fall back gracefully); keep sandbox=true
2
+ // so the lock indicator doesn't flash off on a transient error; null cpu so the favicon resolver skips overloaded
3
+ // rather than guessing.
15
4
 
16
5
  import { computed, onScopeDispose, ref, type ComputedRef, type Ref } from "vue";
17
6
  import { API_ROUTES } from "../config/apiRoutes";
18
7
  import { apiGet } from "../utils/api";
19
8
 
20
- // Once every 15 s is enough for a sustained load spike to light the
21
- // favicon. Shorter would mostly flap on short-lived spikes that
22
- // aren't actually user-visible as lag.
9
+ // 15s catches sustained load without flapping on short-lived spikes that aren't user-visible as lag.
23
10
  const HEALTH_REFRESH_MS = 15_000;
24
11
 
25
12
  interface CpuPayload {
@@ -44,37 +31,26 @@ export function useHealth(): {
44
31
  const cpuLoad1 = ref<number | null>(null);
45
32
  const cpuCores = ref<number | null>(null);
46
33
 
47
- // Separate flag so transient poll failures don't flip
48
- // `geminiAvailable` back to false after a successful boot-time
49
- // fetch. `geminiAvailable` / `sandboxEnabled` are config-derived
50
- // and don't change at runtime — once we've observed them once,
51
- // the next 15 s poll's network blip shouldn't mask them.
34
+ // gemini/sandbox are config-derived and don't change at runtime; once observed, a poll blip shouldn't unmask them.
52
35
  let bootFetchCompleted = false;
53
36
 
54
37
  async function fetchHealth(): Promise<void> {
55
38
  const result = await apiGet<HealthResponse>(API_ROUTES.health);
56
39
  if (!result.ok) {
57
- // Only the CPU figures get nulled — the favicon resolver
58
- // reads null as "skip overloaded" which is the correct fail-
59
- // closed behaviour. The config flags keep their last-known
60
- // values, and stay at the initial defaults if we never
61
- // succeeded (gemini=true → request lands, gets an auth error
62
- // handled elsewhere; sandbox=true → lock indicator reads on).
40
+ // Null only the CPU figures — the resolver reads null as "skip overloaded" (fail-closed). Config flags keep
41
+ // their last-known values, defaults if never observed (gemini=true auth error elsewhere, sandbox=true → on).
63
42
  cpuLoad1.value = null;
64
43
  cpuCores.value = null;
65
44
  if (!bootFetchCompleted) {
66
- // On the FIRST fetch we do still flip gemini → false so
67
- // the "Gemini key required" banner can show immediately
68
- // without waiting for a second attempt. Subsequent poll
69
- // failures don't re-enter this branch.
45
+ // First-fetch only: flip gemini → false so the "Gemini key required" banner shows without waiting for retry.
70
46
  geminiAvailable.value = false;
71
47
  }
72
48
  return;
73
49
  }
74
- geminiAvailable.value = !!result.data.geminiAvailable;
75
- sandboxEnabled.value = !!result.data.sandboxEnabled;
50
+ geminiAvailable.value = Boolean(result.data.geminiAvailable);
51
+ sandboxEnabled.value = Boolean(result.data.sandboxEnabled);
76
52
  bootFetchCompleted = true;
77
- const cpu = result.data.cpu;
53
+ const { cpu } = result.data;
78
54
  if (cpu && typeof cpu.load1 === "number" && Number.isFinite(cpu.load1) && typeof cpu.cores === "number" && cpu.cores > 0) {
79
55
  cpuLoad1.value = cpu.load1;
80
56
  cpuCores.value = cpu.cores;
@@ -84,20 +60,15 @@ export function useHealth(): {
84
60
  }
85
61
  }
86
62
 
87
- // Refresh the CPU figure periodically. The flag-style booleans
88
- // (gemini / sandbox) don't change at runtime so re-fetching them
89
- // is waste; but piggy-backing on the same endpoint keeps the
90
- // server side to a single route and the client to a single poll.
63
+ // Piggy-backs cpu refresh on the health endpoint to keep server + client to a single route/poll.
91
64
  const refreshHandle = window.setInterval(() => {
92
65
  fetchHealth().catch(() => {
93
- /* intentionally swallowed a failed poll just stalls the
94
- favicon's overloaded rule, not user-visible UI */
66
+ /* swallowed: a failed poll just stalls the favicon overloaded rule, not user-visible UI */
95
67
  });
96
68
  }, HEALTH_REFRESH_MS);
97
69
  onScopeDispose(() => window.clearInterval(refreshHandle));
98
70
 
99
- // Expose the normalised ratio the favicon resolver expects (load
100
- // per logical core). Null when either component is missing.
71
+ // load1 per logical core; null if either side is missing.
101
72
  const cpuLoadRatio = computed<number | null>(() => {
102
73
  if (cpuLoad1.value === null || cpuCores.value === null) return null;
103
74
  return cpuLoad1.value / cpuCores.value;
@@ -0,0 +1,104 @@
1
+ import { onMounted, onBeforeUnmount } from "vue";
2
+ import { IMAGE_REPAIR_PATTERN, IMAGE_REPAIR_INLINE_SCRIPT } from "../utils/image/imageRepairInlineScript.js";
3
+
4
+ // Re-exported from the pure module so existing callers keep working.
5
+ // New callers (server/index.ts splice, future iframe-injection
6
+ // surfaces) should import from `../utils/image/imageRepairInlineScript.js`
7
+ // directly to avoid pulling Vue lifecycle hooks into Node code paths.
8
+ export { IMAGE_REPAIR_PATTERN, IMAGE_REPAIR_INLINE_SCRIPT };
9
+
10
+ // Whitespace- and comma-bounded URL token inside a `srcset` value.
11
+ // `srcset` is a comma-list of `<url> [descriptor]` entries; the
12
+ // regex picks each non-whitespace, non-comma run so the descriptor
13
+ // (`1x`, `2x`, `100w`, …) survives the repair pass untouched.
14
+ const SRCSET_TOKEN_RE = /[^\s,]+/g;
15
+
16
+ export function repairImageSrc(img: HTMLImageElement): boolean {
17
+ if (img.dataset.imageRepairTried) return false;
18
+ // Set the one-shot marker only AFTER confirming the URL matches the
19
+ // repair pattern. Otherwise an unrelated 404 (different domain, no
20
+ // artifacts/images segment) would pin the marker and silently block
21
+ // any future repair attempt on the same DOM element if the src is
22
+ // later replaced with a repairable one.
23
+ const match = img.src.match(IMAGE_REPAIR_PATTERN);
24
+ if (!match) return false;
25
+ img.dataset.imageRepairTried = "1";
26
+ img.src = `/${match[0]}`;
27
+ return true;
28
+ }
29
+
30
+ // Repair a `<source>` element used inside `<picture>` / `<audio>` /
31
+ // `<video>`. Handles both shapes:
32
+ // - `srcset="..."` (the picture form, often comma-list with size
33
+ // descriptors)
34
+ // - `src="..."` (the audio/video form, single URL)
35
+ // One-shot via the same `imageRepairTried` marker as <img>.
36
+ export function repairSourceSrc(source: HTMLSourceElement): boolean {
37
+ if (source.dataset.imageRepairTried) return false;
38
+ let repaired = false;
39
+ const src = source.getAttribute("src");
40
+ if (src) {
41
+ const match = src.match(IMAGE_REPAIR_PATTERN);
42
+ if (match) {
43
+ source.setAttribute("src", `/${match[0]}`);
44
+ repaired = true;
45
+ }
46
+ }
47
+ if (source.srcset) {
48
+ const original = source.srcset;
49
+ const next = original.replace(SRCSET_TOKEN_RE, (token) => {
50
+ const tokenMatch = token.match(IMAGE_REPAIR_PATTERN);
51
+ return tokenMatch ? `/${tokenMatch[0]}` : token;
52
+ });
53
+ if (next !== original) {
54
+ source.srcset = next;
55
+ repaired = true;
56
+ }
57
+ }
58
+ if (repaired) source.dataset.imageRepairTried = "1";
59
+ return repaired;
60
+ }
61
+
62
+ // Attach a document-level capture-phase error listener so any
63
+ // `<img>` / `<source>` / `<audio>` / `<video>` 404 in the app
64
+ // shell (wiki / markdown / news / Files preview etc) gets one
65
+ // repair attempt. Capture phase is required because the relevant
66
+ // error events don't bubble. The repair is a no-op for src values
67
+ // that don't match the artifacts/images pattern, so attaching at
68
+ // document scope is safe — it never touches non-image-bearing UI.
69
+ export function useGlobalImageErrorRepair(): void {
70
+ function onError(event: Event): void {
71
+ const { target } = event;
72
+ if (target instanceof HTMLImageElement) {
73
+ repairImageSrc(target);
74
+ // Source-element error events don't fire reliably in Chromium
75
+ // when a `<picture><source>` srcset 404s — only the inner
76
+ // `<img>` reaches a target. Walk siblings so a wrong-prefix
77
+ // `<source>` next to a repairable `<img>` gets the same fix.
78
+ const picture = target.closest("picture");
79
+ if (picture) {
80
+ for (const src of picture.querySelectorAll("source")) {
81
+ repairSourceSrc(src);
82
+ }
83
+ }
84
+ } else if (target instanceof HTMLSourceElement) {
85
+ repairSourceSrc(target);
86
+ } else if (target instanceof HTMLMediaElement) {
87
+ // `<audio>` / `<video>` fire `error` on themselves when ALL
88
+ // their `<source>` children fail. The source elements never
89
+ // get a target of their own in that path, so reach into
90
+ // each child and repair it.
91
+ for (const src of target.querySelectorAll<HTMLSourceElement>(":scope > source")) {
92
+ repairSourceSrc(src);
93
+ }
94
+ }
95
+ }
96
+
97
+ onMounted(() => {
98
+ document.addEventListener("error", onError, { capture: true });
99
+ });
100
+
101
+ onBeforeUnmount(() => {
102
+ document.removeEventListener("error", onError, { capture: true });
103
+ });
104
+ }
@@ -0,0 +1,40 @@
1
+ // #876. Distinguish data===null ("no journal yet") from response.ok===false ("load failed") so a real auth/network/
2
+ // backend failure isn't misreported as empty state (Codex review iter 1). Crude alerts pending a toast composable.
3
+
4
+ import { ref } from "vue";
5
+ import { useRouter } from "vue-router";
6
+ import { useI18n } from "vue-i18n";
7
+ import { apiGet } from "../utils/api";
8
+ import { API_ROUTES } from "../config/apiRoutes";
9
+
10
+ interface LatestDailyResult {
11
+ path: string;
12
+ isoDate: string;
13
+ }
14
+
15
+ export function useLatestDaily() {
16
+ const router = useRouter();
17
+ const { t } = useI18n();
18
+ const loading = ref(false);
19
+
20
+ async function openLatestDaily(): Promise<void> {
21
+ if (loading.value) return;
22
+ loading.value = true;
23
+ try {
24
+ const response = await apiGet<LatestDailyResult | null>(API_ROUTES.journal.latestDaily);
25
+ if (!response.ok) {
26
+ window.alert(t("sidebarHeader.todayJournalLoadFailed", { status: response.status, error: response.error }));
27
+ return;
28
+ }
29
+ if (response.data === null) {
30
+ window.alert(t("sidebarHeader.todayJournalNotFound"));
31
+ return;
32
+ }
33
+ await router.push(`/files/${response.data.path}`);
34
+ } finally {
35
+ loading.value = false;
36
+ }
37
+ }
38
+
39
+ return { openLatestDaily, loading };
40
+ }
@@ -0,0 +1,39 @@
1
+ // #895 PR A: shared frontmatter handling for every markdown-from-disk view. parseFrontmatter never throws —
2
+ // malformed header degrades to "render the whole input as body" instead of breaking the view.
3
+
4
+ import { computed, type ComputedRef, type Ref } from "vue";
5
+ import { parseFrontmatter, type ParsedMarkdown } from "../utils/markdown/frontmatter";
6
+
7
+ export interface MarkdownDocField {
8
+ key: string;
9
+ // Templates branch on Array.isArray and pass scalars through formatScalarField — nested objects would otherwise
10
+ // render as `[object Object]` (codex review iter-1 #902).
11
+ value: unknown;
12
+ }
13
+
14
+ export function formatScalarField(value: unknown): string {
15
+ if (value === null || value === undefined) return "";
16
+ if (typeof value === "object") {
17
+ try {
18
+ return JSON.stringify(value);
19
+ } catch {
20
+ // Cyclic object → can't stringify; fall back to String() rather than throw inside a template.
21
+ return String(value);
22
+ }
23
+ }
24
+ return String(value);
25
+ }
26
+
27
+ export interface MarkdownDocView extends ParsedMarkdown {
28
+ fields: MarkdownDocField[];
29
+ }
30
+
31
+ // Pass null/undefined to get the empty state — so callers can wire a load-state ref without a null-guard wrapper.
32
+ export function useMarkdownDoc(content: Ref<string | null | undefined>): ComputedRef<MarkdownDocView> {
33
+ return computed(() => {
34
+ const raw = content.value ?? "";
35
+ const parsed = parseFrontmatter(raw);
36
+ const fields = Object.entries(parsed.meta).map(([key, value]) => ({ key, value }));
37
+ return { ...parsed, fields };
38
+ });
39
+ }
@@ -16,7 +16,7 @@ export function useMarkdownLinkHandler(selectedPath: Ref<string | null>, handler
16
16
  function handleMarkdownLinkClick(event: MouseEvent): void {
17
17
  if (event.button !== 0) return;
18
18
  if (event.ctrlKey || event.metaKey || event.shiftKey) return;
19
- const target = event.target;
19
+ const { target } = event;
20
20
  if (!(target instanceof Element)) return;
21
21
  const anchor = target.closest("a");
22
22
  if (!anchor) return;
@@ -1,9 +1,3 @@
1
- // Composable that owns the MCP tool state used by the sidebar:
2
- // which tools are currently disabled, their per-tool prompts, and
3
- // the derived `availableTools` / `toolDescriptions` computeds. The
4
- // pure rules live in src/utils/mcpTools so they are unit-testable
5
- // independently of fetch / Vue.
6
-
7
1
  import { computed, ref, type ComputedRef } from "vue";
8
2
  import { API_ROUTES } from "../config/apiRoutes";
9
3
  import type { Role } from "../config/roles";
@@ -12,20 +6,14 @@ import { apiGet } from "../utils/api";
12
6
 
13
7
  interface UseMcpToolsOptions {
14
8
  currentRole: ComputedRef<Role>;
15
- // Injection point for the in-app plugin registry lookup. Real
16
- // callers pass `(name) => getPlugin(name)?.toolDefinition ?? null`,
17
- // tests can stub it.
9
+ // Plugin-registry lookup, injectable so tests can stub it.
18
10
  getDefinition: (name: string) => ToolDefinitionMetadata | null;
19
11
  }
20
12
 
21
13
  export function useMcpTools(opts: UseMcpToolsOptions) {
22
14
  const disabledMcpTools = ref(new Set<string>());
23
15
  const mcpToolDescriptions = ref<Record<string, string>>({});
24
- // Surfaces the most recent GET /api/mcp-tools failure so consumers
25
- // (e.g. the Settings modal's MCP tab) can render a small warning.
26
- // We intentionally keep the "all tools visible" fallback below so
27
- // the UI stays usable; this ref lets the UI tell the user *why* the
28
- // list looks incomplete / unfiltered.
16
+ // Surfaces /api/mcp-tools failures so the Settings MCP tab can explain *why* the list looks unfiltered.
29
17
  const mcpToolsError = ref<string | null>(null);
30
18
 
31
19
  const availableTools = computed(() => availableToolsFor(opts.currentRole.value.availablePlugins, disabledMcpTools.value));
@@ -46,8 +34,7 @@ export function useMcpTools(opts: UseMcpToolsOptions) {
46
34
  const result = await apiGet<McpToolStatus[]>(API_ROUTES.mcpTools.list);
47
35
  if (!result.ok) {
48
36
  mcpToolsError.value = result.error;
49
- // Keep the "all tools visible" fallback not clearing
50
- // disabledMcpTools or descriptions means the UI remains usable.
37
+ // Don't clear disabledMcpTools / descriptions — falling back to "all tools visible" keeps the UI usable.
51
38
  return;
52
39
  }
53
40
  if (!Array.isArray(result.data)) {
@@ -1,95 +1,104 @@
1
- // Web-side subscriber for the `notifications` pub-sub channel.
2
- // Stores incoming NotificationPayloads for the bell badge + panel.
1
+ // Bell-side composable backed by the notifier engine.
3
2
  //
4
- // Uses a singleton subscription pattern: the first component that
5
- // calls useNotifications() subscribes to the pub-sub channel; the
6
- // last one to unmount unsubscribes. All consumers share the same
7
- // module-level state (notifications + readIds).
3
+ // PR 4 of feat-encore migrated `publishNotification()` onto the new
4
+ // engine, so the bell consumes the same data as the dev-mode debug
5
+ // popup: a single global pubsub channel (`PUBSUB_CHANNELS.notifier`),
6
+ // primed via `POST /api/notifier {action: "list" | "listHistory"}`.
8
7
  //
9
- // Read tracking is per-id via a Set. The unread badge decreases
10
- // only when the user **interacts** with a notification either
11
- // clicking it (markRead) or dismissing it via × (dismiss removes
12
- // the notification entirely, so it leaves the unread tally as a
13
- // side effect). Opening the panel does NOT auto-mark everything
14
- // read; the user has to explicitly act on each item, or hit the
15
- // "Mark all read" button.
16
-
17
- import { onUnmounted, ref, computed, type Ref, type ComputedRef } from "vue";
8
+ // Singleton state shared across consumers: a single subscription is
9
+ // shared between the bell badge, the panel, and any other surface
10
+ // that wants to render the same source of truth. Subscriber counting
11
+ // + ref-counted teardown matches the legacy composable's pattern so
12
+ // the websocket subscription doesn't leak when every consumer
13
+ // unmounts.
14
+
15
+ import { computed, onUnmounted, ref, type ComputedRef, type Ref } from "vue";
16
+ import { API_ROUTES } from "../config/apiRoutes";
18
17
  import { PUBSUB_CHANNELS } from "../config/pubsubChannels";
18
+ import { apiPost } from "../utils/api";
19
19
  import { usePubSub } from "./usePubSub";
20
- import { NOTIFICATION_ACTION_TYPES, NOTIFICATION_KINDS, NOTIFICATION_VIEWS } from "../types/notification";
21
- import type { NotificationPayload } from "../types/notification";
22
- import { isRecord } from "../utils/types";
23
-
24
- const MAX_RECENT = 50;
25
-
26
- const VALID_KINDS = new Set<string>(Object.values(NOTIFICATION_KINDS));
27
- const VALID_VIEWS = new Set<string>(Object.values(NOTIFICATION_VIEWS));
28
-
29
- function isNotificationPayload(value: unknown): value is NotificationPayload {
30
- if (!isRecord(value)) return false;
31
- if (typeof value.id !== "string") return false;
32
- if (typeof value.kind !== "string" || !VALID_KINDS.has(value.kind)) return false;
33
- if (typeof value.title !== "string") return false;
34
- if (typeof value.firedAt !== "string") return false;
35
- if (!isValidAction(value.action)) return false;
36
- return true;
20
+
21
+ // Mirror of `server/notifier/types.ts` repeated here rather than
22
+ // imported because the server tree pulls in fs / pubsub deps that
23
+ // shouldn't bleed into the client bundle. The two definitions are
24
+ // kept in sync by code review.
25
+ export interface NotifierEntry {
26
+ id: string;
27
+ pluginPkg: string;
28
+ severity: "info" | "nudge" | "urgent";
29
+ lifecycle?: "fyi" | "action";
30
+ title: string;
31
+ body?: string;
32
+ navigateTarget?: string;
33
+ pluginData?: unknown;
34
+ createdAt: string;
37
35
  }
38
36
 
39
- // Tighter than a plain `typeof type === "string"` check — confirms
40
- // the discriminator is one we know AND, for `navigate`, that the
41
- // target carries a known view. Stops malformed payloads from
42
- // landing in the panel and crashing later in the click handler.
43
- function isValidAction(action: unknown): boolean {
44
- if (!isRecord(action)) return false;
45
- if (action.type === NOTIFICATION_ACTION_TYPES.none) return true;
46
- if (action.type !== NOTIFICATION_ACTION_TYPES.navigate) return false;
47
- const target = action.target;
48
- if (!isRecord(target)) return false;
49
- return typeof target.view === "string" && VALID_VIEWS.has(target.view);
37
+ export interface NotifierHistoryEntry extends NotifierEntry {
38
+ terminalType: "cleared" | "cancelled";
39
+ terminalAt: string;
50
40
  }
51
41
 
52
- // Module-level state so all components share the same list and the
53
- // same per-id read state.
54
- const notifications = ref<NotificationPayload[]>([]);
55
- // Set of notification ids the user has explicitly read (clicked or
56
- // dismissed-as-read). A Set so add/lookup are O(1) per entry.
57
- const readIds = ref<Set<string>>(new Set());
42
+ type NotifierEvent = { type: "published"; entry: NotifierEntry } | { type: "cleared"; id: string } | { type: "cancelled"; id: string };
43
+
44
+ const HISTORY_CAP = 50;
45
+
46
+ const entries = ref<NotifierEntry[]>([]);
47
+ const history = ref<NotifierHistoryEntry[]>([]);
48
+ const ready = ref(false);
58
49
 
59
- // Singleton subscription — ref-counted across consumers.
60
50
  let subscriberCount = 0;
61
51
  let unsubscribeFn: (() => void) | null = null;
62
-
63
- function ensureSubscribed(subscribe: ReturnType<typeof usePubSub>["subscribe"]): void {
64
- subscriberCount++;
65
- if (unsubscribeFn) return; // already listening
66
- unsubscribeFn = subscribe(PUBSUB_CHANNELS.notifications, (data) => {
67
- if (!isNotificationPayload(data)) return;
68
- const next = [data, ...notifications.value].slice(0, MAX_RECENT);
69
- notifications.value = next;
70
- // Drop read-state entries for notifications that just rolled
71
- // off the end of the bounded list — readIds is otherwise an
72
- // unbounded leak across a long-lived session.
73
- pruneReadIds(next);
74
- });
52
+ let primePromise: Promise<void> | null = null;
53
+
54
+ async function prime(): Promise<void> {
55
+ if (primePromise) return primePromise;
56
+ primePromise = (async () => {
57
+ const [activeResult, historyResult] = await Promise.all([
58
+ apiPost<{ entries: NotifierEntry[] }>(API_ROUTES.notifier.dispatch, { action: "list" }),
59
+ apiPost<{ history: NotifierHistoryEntry[] }>(API_ROUTES.notifier.dispatch, { action: "listHistory" }),
60
+ ]);
61
+ if (activeResult.ok) entries.value = activeResult.data.entries;
62
+ if (historyResult.ok) history.value = historyResult.data.history;
63
+ ready.value = true;
64
+ })();
65
+ return primePromise;
75
66
  }
76
67
 
77
- function pruneReadIds(currentList: readonly NotificationPayload[]): void {
78
- if (readIds.value.size === 0) return;
79
- const liveIds = new Set(currentList.map((notif) => notif.id));
80
- const next = new Set<string>();
81
- for (const readId of readIds.value) {
82
- if (liveIds.has(readId)) next.add(readId);
83
- }
84
- // Only assign when the contents actually changed — avoids
85
- // unnecessary reactive churn when nothing rolled off.
86
- if (next.size !== readIds.value.size) {
87
- readIds.value = next;
68
+ function applyEvent(event: NotifierEvent): void {
69
+ switch (event.type) {
70
+ case "published":
71
+ // Dedup against optimistic local update — the host UI clear
72
+ // button already removed the entry; ignore the echoing event.
73
+ if (!entries.value.some((entry) => entry.id === event.entry.id)) {
74
+ entries.value = [...entries.value, event.entry];
75
+ }
76
+ return;
77
+ case "cleared":
78
+ case "cancelled": {
79
+ const removed = entries.value.find((entry) => entry.id === event.id);
80
+ entries.value = entries.value.filter((entry) => entry.id !== event.id);
81
+ if (removed) {
82
+ const historyEntry: NotifierHistoryEntry = {
83
+ ...removed,
84
+ terminalType: event.type === "cleared" ? "cleared" : "cancelled",
85
+ terminalAt: new Date().toISOString(),
86
+ };
87
+ history.value = [historyEntry, ...history.value].slice(0, HISTORY_CAP);
88
+ }
89
+ }
88
90
  }
89
91
  }
90
92
 
93
+ function ensureSubscribed(subscribe: ReturnType<typeof usePubSub>["subscribe"]): void {
94
+ subscriberCount += 1;
95
+ if (unsubscribeFn) return;
96
+ unsubscribeFn = subscribe(PUBSUB_CHANNELS.notifier, (data) => applyEvent(data as NotifierEvent));
97
+ void prime();
98
+ }
99
+
91
100
  function releaseSubscription(): void {
92
- subscriberCount--;
101
+ subscriberCount -= 1;
93
102
  if (subscriberCount <= 0 && unsubscribeFn) {
94
103
  unsubscribeFn();
95
104
  unsubscribeFn = null;
@@ -98,56 +107,73 @@ function releaseSubscription(): void {
98
107
  }
99
108
 
100
109
  export function useNotifications(): {
101
- notifications: Ref<NotificationPayload[]>;
102
- latest: ComputedRef<NotificationPayload | null>;
103
- unreadCount: ComputedRef<number>;
104
- isRead: (id: string) => boolean;
105
- markRead: (id: string) => void;
106
- markAllRead: () => void;
107
- dismiss: (id: string) => void;
110
+ entries: ComputedRef<NotifierEntry[]>;
111
+ history: Ref<NotifierHistoryEntry[]>;
112
+ badgeCount: ComputedRef<number>;
113
+ /** Worst-severity-wins Tailwind class. Mirrors the dev debug popup
114
+ * and matches the bell-badge encoding in `feat-notifier-ux.md`. */
115
+ badgeColor: ComputedRef<string>;
116
+ ready: Ref<boolean>;
117
+ clear: (id: string) => Promise<void>;
118
+ cancel: (id: string) => Promise<void>;
108
119
  } {
109
120
  const { subscribe } = usePubSub();
110
121
  ensureSubscribed(subscribe);
111
122
  onUnmounted(releaseSubscription);
112
123
 
113
- const latest = computed(() => notifications.value[0] ?? null);
124
+ // Sort oldest-first so a row's vertical position doesn't jump when
125
+ // a fresh entry arrives at the bottom — the same scan order the
126
+ // debug popup uses. The bell panel then visually inverts via flex
127
+ // direction, matching today's "newest at the top" expectation.
128
+ const sortedEntries = computed(() => [...entries.value].sort((left, right) => left.createdAt.localeCompare(right.createdAt)));
114
129
 
115
- const unreadCount = computed(() => notifications.value.filter((notif) => !readIds.value.has(notif.id)).length);
130
+ const badgeCount = computed(() => entries.value.length);
116
131
 
117
- function isRead(notifId: string): boolean {
118
- return readIds.value.has(notifId);
119
- }
132
+ const badgeColor = computed(() => {
133
+ if (entries.value.some((entry) => entry.severity === "urgent")) return "bg-red-500";
134
+ if (entries.value.some((entry) => entry.severity === "nudge")) return "bg-amber-500";
135
+ return "bg-gray-400";
136
+ });
120
137
 
121
- function markRead(notifId: string): void {
122
- if (readIds.value.has(notifId)) return;
123
- // Replace the Set so Vue's reactivity fires on consumers that
124
- // depend on `readIds` via `unreadCount` / `isRead`.
125
- const next = new Set(readIds.value);
126
- next.add(notifId);
127
- readIds.value = next;
138
+ /** Remove an entry from the active list and prepend a synthetic
139
+ * history record. Used optimistically by `clear` / `cancel` so the
140
+ * bell reacts before the server round-trip completes; `applyEvent`
141
+ * is idempotent on the eventual pubsub echo (the entry is already
142
+ * gone from `entries`, so its `cleared`/`cancelled` branch finds
143
+ * nothing to remove and skips the history append).
144
+ *
145
+ * No-op if the entry was already taken out (e.g. another tab raced
146
+ * us via the pubsub event); preserves single-history-entry-per-id
147
+ * semantics. */
148
+ function moveToHistoryLocally(entryId: string, terminalType: "cleared" | "cancelled"): void {
149
+ const removed = entries.value.find((entry) => entry.id === entryId);
150
+ if (!removed) return;
151
+ entries.value = entries.value.filter((entry) => entry.id !== entryId);
152
+ const historyEntry: NotifierHistoryEntry = {
153
+ ...removed,
154
+ terminalType,
155
+ terminalAt: new Date().toISOString(),
156
+ };
157
+ history.value = [historyEntry, ...history.value].slice(0, HISTORY_CAP);
128
158
  }
129
159
 
130
- function markAllRead(): void {
131
- if (notifications.value.length === 0) return;
132
- const next = new Set(readIds.value);
133
- for (const notif of notifications.value) {
134
- next.add(notif.id);
160
+ async function clear(entryId: string): Promise<void> {
161
+ moveToHistoryLocally(entryId, "cleared");
162
+ const result = await apiPost<{ ok: true }>(API_ROUTES.notifier.dispatch, { action: "clear", id: entryId });
163
+ if (!result.ok) {
164
+ // Rendering this in console rather than the panel itself —
165
+ // the panel is small and we don't want a transient API error
166
+ // shouting at the user. A future prime() would resync state
167
+ // if the server is genuinely out of sync.
168
+ console.error("[useNotifications] clear failed", result.error);
135
169
  }
136
- readIds.value = next;
137
170
  }
138
171
 
139
- function dismiss(notifId: string): void {
140
- notifications.value = notifications.value.filter((notif) => notif.id !== notifId);
141
- // Drop the matching readIds entry too. Without this, a long
142
- // session that dismisses thousands of notifications leaks one
143
- // ~36-char id per dismissal even though the user can't see
144
- // them — pruneReadIds keeps the Set tied to `notifications`.
145
- if (readIds.value.has(notifId)) {
146
- const next = new Set(readIds.value);
147
- next.delete(notifId);
148
- readIds.value = next;
149
- }
172
+ async function cancel(entryId: string): Promise<void> {
173
+ moveToHistoryLocally(entryId, "cancelled");
174
+ const result = await apiPost<{ ok: true }>(API_ROUTES.notifier.dispatch, { action: "cancel", id: entryId });
175
+ if (!result.ok) console.error("[useNotifications] cancel failed", result.error);
150
176
  }
151
177
 
152
- return { notifications, latest, unreadCount, isRead, markRead, markAllRead, dismiss };
178
+ return { entries: sortedEntries, history, badgeCount, badgeColor, ready, clear, cancel };
153
179
  }