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,35 +1,70 @@
1
1
  <script setup lang="ts">
2
- import { ref, watch, onMounted, onUnmounted } from "vue";
2
+ // Bell badge + popup, backed by the notifier engine (PR 4 of
3
+ // feat-encore). Active section on top, History below, single scroll,
4
+ // no tabs. Active rows use a bell icon (severity-colored) instead of
5
+ // a colored dot — at-a-glance "this is a notification" is more
6
+ // readable than a generic dot. History rows fall back to a faded
7
+ // dot so the row reads as past-tense.
8
+ //
9
+ // Body is displayed only as a native hover tooltip (`:title=`) — the
10
+ // row stays compact and the relative-time meta line is what the user
11
+ // scans for "when was this?".
12
+ //
13
+ // The dev-mode `NotifierDebugPopup` that ran in parallel during PR 3
14
+ // is gone — the production bell now serves both audiences.
15
+ // `NotificationToast.vue` is also gone; the worst-severity badge
16
+ // color (gray / amber / red) is the at-a-distance signal.
17
+
18
+ import { computed, onMounted, onUnmounted, ref, watch } from "vue";
3
19
  import { useI18n } from "vue-i18n";
4
- import { useNotifications } from "../composables/useNotifications";
20
+ import { useRouter } from "vue-router";
21
+ import { useNotifications, type NotifierEntry, type NotifierHistoryEntry } from "../composables/useNotifications";
5
22
  import { formatRelativeTime } from "../utils/format/date";
6
- import { NOTIFICATION_ICONS, NOTIFICATION_ACTION_TYPES, NOTIFICATION_PRIORITIES } from "../types/notification";
7
- import type { NotificationPayload } from "../types/notification";
23
+ import type { NotificationI18n, NotificationKind, NotificationPriority } from "../types/notification";
24
+ import { isRecord } from "../utils/types";
8
25
 
9
26
  const { t } = useI18n();
10
- const { notifications, unreadCount, isRead, markRead, markAllRead, dismiss } = useNotifications();
27
+ const router = useRouter();
28
+
29
+ const { entries, history, badgeCount, badgeColor, clear, cancel } = useNotifications();
30
+
31
+ const props = defineProps<{ forceClose?: boolean }>();
32
+ const emit = defineEmits<{ "update:open": [open: boolean] }>();
33
+
11
34
  const open = ref(false);
12
35
  const rootRef = ref<HTMLElement | null>(null);
13
36
 
37
+ // Bell mirrors the debug popup's "newest first" panel order — sort
38
+ // in reverse of the composable's oldest-first output.
39
+ const visibleEntries = computed(() => [...entries.value].reverse());
40
+ const visibleHistory = computed(() => history.value);
41
+ const fyiCount = computed(() => entries.value.filter((entry) => (entry.lifecycle ?? "fyi") === "fyi").length);
42
+
43
+ const badgeText = computed(() => (badgeCount.value > 99 ? "99+" : String(badgeCount.value)));
44
+
45
+ // Hover state on the bulk-clear button — every fyi row's hover-check
46
+ // icon renders while it's true, mirroring the debug popup's "this is
47
+ // what's about to get swept" affordance.
48
+ const hoveringClearAll = ref(false);
49
+
50
+ function close(): void {
51
+ open.value = false;
52
+ emit("update:open", false);
53
+ }
54
+
55
+ function toggle(): void {
56
+ open.value = !open.value;
57
+ emit("update:open", open.value);
58
+ }
59
+
14
60
  function onDocumentClick(event: MouseEvent): void {
15
61
  if (!open.value || !rootRef.value) return;
16
- if (!rootRef.value.contains(event.target as Node)) {
17
- close();
18
- }
62
+ if (!rootRef.value.contains(event.target as Node)) close();
19
63
  }
20
64
 
21
65
  onMounted(() => document.addEventListener("mousedown", onDocumentClick));
22
66
  onUnmounted(() => document.removeEventListener("mousedown", onDocumentClick));
23
67
 
24
- const props = defineProps<{
25
- forceClose?: boolean;
26
- }>();
27
-
28
- const emit = defineEmits<{
29
- navigate: [action: NotificationPayload["action"]];
30
- "update:open": [open: boolean];
31
- }>();
32
-
33
68
  watch(
34
69
  () => props.forceClose,
35
70
  (shouldClose) => {
@@ -37,52 +72,165 @@ watch(
37
72
  },
38
73
  );
39
74
 
40
- function toggle(): void {
41
- open.value = !open.value;
42
- // Opening the panel does NOT auto-mark items as read — the user
43
- // has to click an item (markRead via handleClick) or dismiss it
44
- // button) for the unread badge to drop. The "Mark all read"
45
- // button in the panel header still bulk-clears.
46
- emit("update:open", open.value);
75
+ // ── Legacy pluginData typing ────────────────────────────────
76
+ //
77
+ // `publishNotification()` stashes legacy fields under
78
+ // `pluginData.legacy = true`. The bell type-narrows here so it can
79
+ // preserve i18n localization for entries that came through the
80
+ // wrapper. Future direct callers of `notifier.publish()` don't set
81
+ // this shape; their entries fall back to the engine-level
82
+ // `entry.title` / `entry.body`.
83
+
84
+ interface LegacyPluginDataShape {
85
+ legacy: true;
86
+ legacyId: string;
87
+ kind: NotificationKind;
88
+ priority: NotificationPriority;
89
+ i18n?: NotificationI18n;
47
90
  }
48
91
 
49
- function close(): void {
50
- open.value = false;
51
- emit("update:open", false);
92
+ function asLegacy(entry: { pluginData?: unknown }): LegacyPluginDataShape | null {
93
+ const data = entry.pluginData;
94
+ if (!isRecord(data)) return null;
95
+ if (data.legacy !== true) return null;
96
+ if (typeof data.legacyId !== "string") return null;
97
+ if (typeof data.kind !== "string") return null;
98
+ if (typeof data.priority !== "string") return null;
99
+ return data as unknown as LegacyPluginDataShape;
52
100
  }
53
101
 
54
- function iconName(notification: NotificationPayload): string {
55
- return notification.icon ?? NOTIFICATION_ICONS[notification.kind] ?? "notifications";
102
+ function localizeTitle(entry: NotifierEntry | NotifierHistoryEntry): string {
103
+ const legacy = asLegacy(entry);
104
+ if (legacy?.i18n) return t(legacy.i18n.titleKey);
105
+ return entry.title;
106
+ }
107
+
108
+ function localizeBody(entry: NotifierEntry | NotifierHistoryEntry): string | undefined {
109
+ const legacy = asLegacy(entry);
110
+ if (legacy?.i18n?.bodyKey) return t(legacy.i18n.bodyKey, legacy.i18n.bodyParams ?? {});
111
+ return entry.body;
112
+ }
113
+
114
+ function severityIconColor(severity: NotifierEntry["severity"]): string {
115
+ switch (severity) {
116
+ case "urgent":
117
+ return "text-red-500";
118
+ case "nudge":
119
+ return "text-amber-500";
120
+ case "info":
121
+ default:
122
+ return "text-gray-300";
123
+ }
124
+ }
125
+
126
+ // History rows keep the original colored-dot encoding (faded) — the
127
+ // bell icon is reserved for Active so the eye distinguishes "still
128
+ // open" from "already closed" at a glance.
129
+ function severityDotClassForHistory(severity: NotifierEntry["severity"]): string {
130
+ switch (severity) {
131
+ case "urgent":
132
+ return "bg-red-500";
133
+ case "nudge":
134
+ return "bg-amber-500";
135
+ case "info":
136
+ default:
137
+ return "bg-gray-300";
138
+ }
56
139
  }
57
140
 
58
141
  function formatTime(iso: string): string {
59
142
  return formatRelativeTime(iso);
60
143
  }
61
144
 
62
- function handleClick(notification: NotificationPayload): void {
63
- // Mark this single item read regardless of whether it has a
64
- // navigate actionthe user has explicitly engaged with it,
65
- // which is the unambiguous "I've seen this" signal.
66
- markRead(notification.id);
67
- if (notification.action.type === NOTIFICATION_ACTION_TYPES.navigate) {
68
- emit("navigate", notification.action);
69
- close();
70
- }
145
+ /** Strip the leading `@scope/` from a scoped npm package name for
146
+ * display. The `@mulmoclaude/` prefix on plugin pluginPkgs is noise
147
+ * in a meta line what readers want is the suffix. Unscoped names
148
+ * (the legacy `host` / `todo` / `agent` / … pluginPkgs that the
149
+ * wrapper assigns) pass through unchanged. */
150
+ function shortPkg(pluginPkg: string): string {
151
+ return pluginPkg.startsWith("@") ? pluginPkg.split("/").slice(1).join("/") || pluginPkg : pluginPkg;
152
+ }
153
+
154
+ /** Splice `notificationId=<id>` into the navigateTarget so the landing
155
+ * page (action-lifecycle plugin views, e.g. the debug page's
156
+ * `?mode=auto-clear` / `?mode=manual-clear` modes) can identify which
157
+ * entry triggered the navigation and call `clear(id)` accordingly.
158
+ * Harmless on legacy fyi targets that don't read the query.
159
+ *
160
+ * Inserts before any `#hash` so the fragment stays at the end —
161
+ * `?a=1#frag` becomes `?a=1&notificationId=…#frag`, not
162
+ * `?a=1#frag&notificationId=…`. */
163
+ function appendNotificationId(target: string, entryId: string): string {
164
+ const hashIdx = target.indexOf("#");
165
+ const beforeHash = hashIdx >= 0 ? target.slice(0, hashIdx) : target;
166
+ const hash = hashIdx >= 0 ? target.slice(hashIdx) : "";
167
+ const separator = beforeHash.includes("?") ? "&" : "?";
168
+ return `${beforeHash}${separator}notificationId=${encodeURIComponent(entryId)}${hash}`;
169
+ }
170
+
171
+ async function navigateAndClose(target: string, entryId: string): Promise<void> {
172
+ close();
173
+ await router.push(appendNotificationId(target, entryId)).catch(() => {});
174
+ }
175
+
176
+ /** Lifecycle-correct primary action for an Active row. The keyboard
177
+ * Enter/Space handler routes through here so keyboard users get a
178
+ * single, predictable activation regardless of where focus is —
179
+ * separate from the mouse two-tier UX that distinguishes body click
180
+ * vs row-padding click. fyi: navigate (if target) then clear.
181
+ * action: navigate only (plugin owns the clear). */
182
+ function performPrimaryAction(entry: NotifierEntry): void {
183
+ if (entry.navigateTarget) void navigateAndClose(entry.navigateTarget, entry.id);
184
+ if ((entry.lifecycle ?? "fyi") === "fyi") void clear(entry.id);
71
185
  }
72
186
 
73
- function handleDismiss(event: Event, notificationId: string): void {
187
+ // True when activating the row (click, Enter, Space) does anything
188
+ // useful — either clears a fyi notification or navigates to the
189
+ // action's target. Action notifications without a `navigateTarget`
190
+ // fall through both branches, so their row is not a button and
191
+ // should not advertise as one to assistive technology.
192
+ function hasPrimaryAction(entry: NotifierEntry): boolean {
193
+ return Boolean(entry.navigateTarget) || (entry.lifecycle ?? "fyi") === "fyi";
194
+ }
195
+
196
+ // Body click on an Active row. Stop propagation so the outer <li>'s
197
+ // fyi-clear handler doesn't double-fire when a fyi click already
198
+ // lands here (matches the debug popup's two-layer click handling).
199
+ function onActiveRowBodyClick(entry: NotifierEntry, event: MouseEvent): void {
200
+ event.stopPropagation();
201
+ performPrimaryAction(entry);
202
+ }
203
+
204
+ // Outer-row click — fyi only. Action rows must use the body div or
205
+ // the trailing × so the user can't accidentally cancel by clicking
206
+ // padding. Keyboard activation goes through `performPrimaryAction`
207
+ // instead, which works for both lifecycles.
208
+ function onActiveRowClick(entry: NotifierEntry): void {
209
+ const lifecycle = entry.lifecycle ?? "fyi";
210
+ if (lifecycle !== "fyi") return;
211
+ performPrimaryAction(entry);
212
+ }
213
+
214
+ async function handleDismiss(event: Event, entry: NotifierEntry): Promise<void> {
74
215
  event.stopPropagation();
75
- // dismiss removes the entry from the list, so its contribution
76
- // to unreadCount drops automatically. No separate markRead call
77
- // needed — and we don't want the entry to linger as "read" if
78
- // the user explicitly chose to remove it.
79
- dismiss(notificationId);
216
+ const lifecycle = entry.lifecycle ?? "fyi";
217
+ if (lifecycle === "fyi") await clear(entry.id);
218
+ else await cancel(entry.id);
219
+ }
220
+
221
+ async function handleHistoryClick(entry: NotifierHistoryEntry): Promise<void> {
222
+ if (!entry.navigateTarget) return;
223
+ await navigateAndClose(entry.navigateTarget, entry.id);
224
+ }
225
+
226
+ async function clearAllFyi(): Promise<void> {
227
+ const ids = entries.value.filter((entry) => (entry.lifecycle ?? "fyi") === "fyi").map((entry) => entry.id);
228
+ for (const entryId of ids) await clear(entryId);
80
229
  }
81
230
  </script>
82
231
 
83
232
  <template>
84
233
  <div ref="rootRef" class="relative">
85
- <!-- Bell button -->
86
234
  <button
87
235
  class="relative h-8 w-8 flex items-center justify-center rounded text-gray-400 hover:text-gray-700"
88
236
  data-testid="notification-bell"
@@ -91,66 +239,145 @@ function handleDismiss(event: Event, notificationId: string): void {
91
239
  >
92
240
  <span class="material-icons">notifications</span>
93
241
  <span
94
- v-if="unreadCount > 0"
95
- class="absolute -top-1.5 -right-1.5 min-w-[1rem] h-4 px-0.5 bg-red-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center leading-none"
242
+ v-if="badgeCount > 0"
243
+ :class="[
244
+ 'absolute -top-1.5 -right-1.5 min-w-[1rem] h-4 px-0.5 text-white text-[10px] font-bold rounded-full flex items-center justify-center leading-none',
245
+ badgeColor,
246
+ ]"
96
247
  data-testid="notification-badge"
97
248
  >
98
- {{ unreadCount > 99 ? "99+" : unreadCount }}
249
+ {{ badgeText }}
99
250
  </span>
100
251
  </button>
101
252
 
102
- <!-- Dropdown panel -->
103
253
  <div
104
254
  v-if="open"
105
- class="absolute left-0 top-full mt-1 w-72 max-h-80 overflow-y-auto rounded-lg shadow-xl border border-gray-200 bg-white z-50"
255
+ class="absolute left-0 top-full mt-1 w-96 max-h-[80vh] bg-white border border-gray-200 rounded-lg shadow-lg z-50 flex flex-col text-xs overflow-hidden"
106
256
  data-testid="notification-panel"
107
257
  >
108
- <!-- Header -->
109
- <div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
110
- <span class="text-sm font-semibold text-gray-700">{{ t("notificationBell.notifications") }}</span>
111
- <button class="text-xs text-blue-500 hover:text-blue-700" data-testid="notification-mark-all-read" @click="markAllRead">
112
- {{ t("notificationBell.markAllRead") }}
113
- </button>
258
+ <div class="flex items-center gap-2 px-3 py-2 border-b border-gray-100">
259
+ <span class="font-semibold text-gray-700">{{ t("notificationBell.notifications") }}</span>
114
260
  </div>
115
-
116
- <!-- Empty state -->
117
- <div v-if="notifications.length === 0" class="py-8 text-center text-sm text-gray-400">{{ t("notificationBell.noNotifications") }}</div>
118
-
119
- <!-- Items -->
120
- <div v-else>
121
- <div
122
- v-for="n in notifications"
123
- :key="n.id"
124
- role="button"
125
- tabindex="0"
126
- class="flex items-start gap-3 px-4 py-3 border-b border-gray-50 hover:bg-gray-50 focus:bg-gray-100 cursor-pointer outline-none"
127
- :class="isRead(n.id) ? 'bg-white' : 'bg-blue-50/40'"
128
- :data-testid="`notification-item-${n.id}`"
129
- :data-unread="isRead(n.id) ? 'false' : 'true'"
130
- :aria-label="n.title"
131
- @click="handleClick(n)"
132
- @keydown.enter.prevent.self="(e) => !e.repeat && handleClick(n)"
133
- @keydown.space.prevent.self="(e) => !e.repeat && handleClick(n)"
134
- >
135
- <!-- Unread dot — small leading marker so the user can scan
136
- the panel for what's new. Hidden once `markRead` fires. -->
137
- <span class="w-2 h-2 mt-2 shrink-0 rounded-full" :class="isRead(n.id) ? 'bg-transparent' : 'bg-blue-500'" aria-hidden="true" />
138
- <span class="material-icons text-lg mt-0.5 shrink-0" :class="n.priority === NOTIFICATION_PRIORITIES.high ? 'text-red-500' : 'text-gray-400'">
139
- {{ iconName(n) }}
140
- </span>
141
- <div class="flex-1 min-w-0">
142
- <p class="text-sm truncate" :class="isRead(n.id) ? 'text-gray-600 font-normal' : 'text-gray-900 font-semibold'">{{ n.title }}</p>
143
- <p v-if="n.body" class="text-xs text-gray-500 truncate mt-0.5">
144
- {{ n.body }}
145
- </p>
146
- <p class="text-xs text-gray-400 mt-0.5">
147
- {{ formatTime(n.firedAt) }}
148
- </p>
149
- </div>
150
- <button class="text-gray-300 hover:text-gray-500 shrink-0 mt-0.5" :aria-label="t('notificationBell.dismiss')" @click="handleDismiss($event, n.id)">
151
- <span class="material-icons text-sm">close</span>
261
+ <div class="flex-1 overflow-y-auto">
262
+ <div class="flex items-center px-3 py-1.5 border-b border-gray-100 bg-gray-50">
263
+ <span class="text-gray-500 font-medium">{{ t("notificationBell.activeSection") }} ({{ visibleEntries.length }})</span>
264
+ <button
265
+ v-if="fyiCount > 0"
266
+ type="button"
267
+ class="ml-auto text-gray-500 font-medium hover:text-green-600"
268
+ data-testid="notification-clear-all"
269
+ @click="clearAllFyi"
270
+ @mouseenter="hoveringClearAll = true"
271
+ @mouseleave="hoveringClearAll = false"
272
+ >
273
+ {{ t("notificationBell.clearAll") }}
152
274
  </button>
153
275
  </div>
276
+ <p v-if="visibleEntries.length === 0" class="px-3 py-3 text-gray-400 italic" data-testid="notification-empty-active">
277
+ {{ t("notificationBell.noActive") }}
278
+ </p>
279
+ <ul v-else class="divide-y divide-gray-100">
280
+ <li
281
+ v-for="entry in visibleEntries"
282
+ :key="entry.id"
283
+ :data-testid="`notification-item-${entry.id}`"
284
+ :data-lifecycle="entry.lifecycle ?? 'fyi'"
285
+ :role="hasPrimaryAction(entry) ? 'button' : undefined"
286
+ :tabindex="hasPrimaryAction(entry) ? 0 : undefined"
287
+ :aria-label="hasPrimaryAction(entry) ? localizeTitle(entry) : undefined"
288
+ :class="['px-3 py-2 group focus:bg-gray-100 focus:outline-none', (entry.lifecycle ?? 'fyi') === 'fyi' ? 'cursor-pointer hover:bg-gray-50' : '']"
289
+ @click="onActiveRowClick(entry)"
290
+ @keydown.enter.prevent.self="(e) => hasPrimaryAction(entry) && !e.repeat && performPrimaryAction(entry)"
291
+ @keydown.space.prevent.self="(e) => hasPrimaryAction(entry) && !e.repeat && performPrimaryAction(entry)"
292
+ >
293
+ <div class="flex items-start gap-2">
294
+ <span
295
+ :class="['material-icons text-sm shrink-0 leading-none mt-0.5', severityIconColor(entry.severity)]"
296
+ :title="entry.severity"
297
+ aria-hidden="true"
298
+ >
299
+ notifications
300
+ </span>
301
+ <div
302
+ :class="[
303
+ 'flex-1 min-w-0',
304
+ entry.lifecycle === 'action' && entry.navigateTarget
305
+ ? 'cursor-pointer hover:underline'
306
+ : (entry.lifecycle ?? 'fyi') === 'fyi'
307
+ ? 'cursor-pointer'
308
+ : '',
309
+ ]"
310
+ :title="localizeBody(entry) || undefined"
311
+ @click="onActiveRowBodyClick(entry, $event)"
312
+ >
313
+ <div class="flex items-baseline gap-2">
314
+ <span class="font-medium text-gray-800 truncate">{{ localizeTitle(entry) }}</span>
315
+ <span v-if="entry.lifecycle" class="text-[10px] uppercase tracking-wide text-gray-400 shrink-0">{{ entry.lifecycle }}</span>
316
+ </div>
317
+ <div class="text-gray-400 mt-0.5 font-mono text-[10px]">{{ formatTime(entry.createdAt) }} · {{ shortPkg(entry.pluginPkg) }}</div>
318
+ </div>
319
+ <button
320
+ v-if="entry.lifecycle === 'action'"
321
+ type="button"
322
+ class="text-gray-400 hover:text-red-500 shrink-0"
323
+ :title="t('notificationBell.cancel')"
324
+ :aria-label="t('notificationBell.cancel')"
325
+ data-testid="notification-dismiss"
326
+ @click="handleDismiss($event, entry)"
327
+ >
328
+ <span class="material-icons text-sm">close</span>
329
+ </button>
330
+ <span
331
+ v-else
332
+ :class="['text-green-500 shrink-0 transition-opacity', hoveringClearAll ? 'opacity-100' : 'opacity-0 group-hover:opacity-100']"
333
+ aria-hidden="true"
334
+ data-testid="notification-fyi-hover-check"
335
+ >
336
+ <span class="material-icons text-sm">check</span>
337
+ </span>
338
+ </div>
339
+ </li>
340
+ </ul>
341
+ <div class="px-3 py-1.5 text-gray-500 font-medium border-y border-gray-100 bg-gray-50">
342
+ {{ t("notificationBell.historySection") }} ({{ visibleHistory.length }})
343
+ </div>
344
+ <p v-if="visibleHistory.length === 0" class="px-3 py-3 text-gray-400 italic" data-testid="notification-empty-history">
345
+ {{ t("notificationBell.noHistory") }}
346
+ </p>
347
+ <ul v-else class="divide-y divide-gray-100">
348
+ <li
349
+ v-for="entry in visibleHistory"
350
+ :key="`${entry.id}-${entry.terminalAt}`"
351
+ :data-testid="`notification-history-${entry.id}`"
352
+ :role="entry.navigateTarget ? 'button' : undefined"
353
+ :tabindex="entry.navigateTarget ? 0 : undefined"
354
+ :aria-label="entry.navigateTarget ? localizeTitle(entry) : undefined"
355
+ :class="['px-3 py-2 focus:bg-gray-100 focus:outline-none', entry.navigateTarget ? 'cursor-pointer hover:bg-gray-50' : '']"
356
+ @click="entry.navigateTarget && handleHistoryClick(entry)"
357
+ @keydown.enter.prevent.self="(e) => entry.navigateTarget && !e.repeat && handleHistoryClick(entry)"
358
+ @keydown.space.prevent.self="(e) => entry.navigateTarget && !e.repeat && handleHistoryClick(entry)"
359
+ >
360
+ <div class="flex items-start gap-2">
361
+ <!-- eslint-disable @intlify/vue-i18n/no-raw-text --
362
+ Symbolic glyph (✓ / ✗) — language-neutral terminal-state
363
+ marker, identical for every locale. Not a translatable
364
+ string. -->
365
+ <span class="mt-0.5 shrink-0 font-bold text-gray-400">
366
+ {{ entry.terminalType === "cleared" ? "✓" : "✗" }}
367
+ </span>
368
+ <!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
369
+ <span :class="['mt-1 inline-block w-2 h-2 rounded-full shrink-0 opacity-30', severityDotClassForHistory(entry.severity)]" aria-hidden="true" />
370
+ <div :class="['flex-1 min-w-0', entry.navigateTarget ? 'hover:underline' : '']">
371
+ <div class="flex items-baseline gap-2">
372
+ <span class="text-gray-700 truncate">{{ localizeTitle(entry) }}</span>
373
+ </div>
374
+ <div class="text-gray-400 mt-0.5 font-mono text-[10px]">
375
+ {{ formatTime(entry.terminalAt) }} · {{ entry.terminalType }} · {{ shortPkg(entry.pluginPkg) }}
376
+ </div>
377
+ </div>
378
+ </div>
379
+ </li>
380
+ </ul>
154
381
  </div>
155
382
  </div>
156
383
  </div>
@@ -1,9 +1,9 @@
1
1
  <template>
2
2
  <div class="border-t border-gray-200 shrink-0 bg-gray-50">
3
3
  <SuggestionsPanel
4
- v-if="suggestions && suggestions.length > 0"
5
4
  v-model:expanded="suggestionsExpanded"
6
5
  :queries="suggestions"
6
+ :trigger-ref="suggestionsBtnRef"
7
7
  @send="onSuggestionSend"
8
8
  @edit="onSuggestionEdit"
9
9
  />
@@ -22,12 +22,12 @@
22
22
  />
23
23
  <div class="flex flex-col gap-1 shrink-0">
24
24
  <button
25
- v-if="suggestions && suggestions.length > 0"
25
+ ref="suggestionsBtnRef"
26
26
  :data-testid="`${testIdPrefix}-suggestions`"
27
27
  class="rounded w-8 h-8 flex items-center justify-center"
28
28
  :class="suggestionsExpanded ? 'text-blue-600 bg-blue-50' : 'text-gray-400 hover:text-gray-600'"
29
- :title="t('suggestionsPanel.suggestions')"
30
- :aria-label="t('suggestionsPanel.suggestions')"
29
+ :title="t('suggestionsPanel.tooltip')"
30
+ :aria-label="t('suggestionsPanel.tooltip')"
31
31
  @click="suggestionsExpanded = !suggestionsExpanded"
32
32
  >
33
33
  <span class="material-icons text-base leading-none">lightbulb</span>
@@ -71,6 +71,7 @@ const appApi = useAppApi();
71
71
  const draft = ref("");
72
72
  const textareaRef = ref<HTMLTextAreaElement | null>(null);
73
73
  const suggestionsExpanded = ref(false);
74
+ const suggestionsBtnRef = ref<HTMLButtonElement | null>(null);
74
75
 
75
76
  const canSend = computed(() => !props.disabled && (props.allowEmpty || draft.value.trim().length > 0));
76
77
 
@@ -1,26 +1,26 @@
1
1
  <template>
2
2
  <div ref="rootRef" class="inline-flex w-fit border border-gray-300 rounded overflow-hidden text-xs" data-testid="plugin-launcher">
3
- <template v-for="(target, idx) in TARGETS" :key="target.key">
3
+ <template v-for="(target, idx) in visibleTargets" :key="target.key">
4
4
  <!-- Visual separator between data plugins and management plugins -->
5
- <div v-if="idx === SEPARATOR_AFTER_INDEX" class="w-px bg-gray-300 my-0.5" />
5
+ <div v-if="idx === separatorAfterIndex" class="w-px bg-gray-300 my-0.5" />
6
6
  <button
7
7
  :class="[
8
8
  'h-8 px-2.5 flex items-center gap-1 border-r border-gray-200 last:border-r-0 transition-colors',
9
9
  isActive(target) ? 'bg-blue-50 text-blue-600 font-medium' : 'bg-white text-gray-600 hover:bg-gray-50',
10
10
  ]"
11
- :title="t(`pluginLauncher.${target.key}.title`)"
11
+ :title="target.literalTitle ?? t(`pluginLauncher.${target.key}.title`)"
12
12
  :data-testid="`plugin-launcher-${target.key}`"
13
13
  @click="emit('navigate', target)"
14
14
  >
15
15
  <span class="material-icons text-sm">{{ target.icon }}</span>
16
- <span v-if="!compact">{{ t(`pluginLauncher.${target.key}.label`) }}</span>
16
+ <span v-if="!compact">{{ target.literalLabel ?? t(`pluginLauncher.${target.key}.label`) }}</span>
17
17
  </button>
18
18
  </template>
19
19
  </div>
20
20
  </template>
21
21
 
22
22
  <script setup lang="ts">
23
- import { onBeforeUnmount, onMounted, ref } from "vue";
23
+ import { computed, onBeforeUnmount, onMounted, ref } from "vue";
24
24
  import { useI18n } from "vue-i18n";
25
25
 
26
26
  const { t } = useI18n();
@@ -43,10 +43,21 @@ export type PluginLauncherKind = "view"; // Switch the canvas to a dedicated vie
43
43
  // strings out of this file avoids duplication across locales.
44
44
  export interface PluginLauncherTarget {
45
45
  /** Stable key for testid + dispatch in App.vue. */
46
- key: "todos" | "calendar" | "automations" | "wiki" | "sources" | "news" | "skills" | "roles" | "files";
46
+ key: "todos" | "calendar" | "automations" | "wiki" | "sources" | "news" | "skills" | "roles" | "files" | "debug";
47
47
  kind: PluginLauncherKind;
48
48
  /** Material-icons glyph. */
49
49
  icon: string;
50
+ /** When true, only visible if `VITE_DEV_MODE=1`. The corresponding
51
+ * page itself is still reachable via direct URL (`/debug`) — only
52
+ * the launcher button is gated. */
53
+ devOnly?: boolean;
54
+ /** Literal label / tooltip used in place of the i18n lookup. Set on
55
+ * dev-only targets so the host's 8-locale bundle doesn't carry
56
+ * strings that only English-speaking developers ever see. When
57
+ * unset (the production case), label/title come from
58
+ * `pluginLauncher.<key>.{label,title}` in `src/lang/*.ts`. */
59
+ literalLabel?: string;
60
+ literalTitle?: string;
50
61
  }
51
62
 
52
63
  const TARGETS: PluginLauncherTarget[] = [
@@ -67,6 +78,13 @@ const TARGETS: PluginLauncherTarget[] = [
67
78
  { key: "skills", kind: "view", icon: "psychology" },
68
79
  { key: "roles", kind: "view", icon: "manage_accounts" },
69
80
  { key: "files", kind: "view", icon: "folder" },
81
+ // ─── Dev-only ───
82
+ // Encore plan PR 1 follow-up. Hidden in production builds; the
83
+ // /debug route stays reachable by typing the URL even with the
84
+ // button hidden. Owned by `@mulmoclaude/debug-plugin`. Literal
85
+ // label/title — the debug surface is dev-only, so we deliberately
86
+ // keep the strings out of the 8-locale i18n bundle.
87
+ { key: "debug", kind: "view", icon: "bug_report", devOnly: true, literalLabel: "Debug", literalTitle: "Open debug playground (dev mode only)" },
70
88
  ];
71
89
 
72
90
  // Index AFTER which the visual separator is inserted (between data
@@ -75,6 +93,24 @@ const TARGETS: PluginLauncherTarget[] = [
75
93
  // 0-5), so the divider renders before index 6 (skills).
76
94
  const SEPARATOR_AFTER_INDEX = 6;
77
95
 
96
+ // Dev-mode flag — set `VITE_DEV_MODE=1` in `.env`. Anything else
97
+ // (including unset) hides any target with `devOnly: true`.
98
+ const DEV_MODE = import.meta.env.VITE_DEV_MODE === "1";
99
+
100
+ // Targets that should render given the current dev-mode flag.
101
+ const visibleTargets = computed(() => TARGETS.filter((target) => !target.devOnly || DEV_MODE));
102
+
103
+ // Recompute the separator index after the dev-only filter — without
104
+ // this, hiding a dev-only target before the separator would shift the
105
+ // divider one slot to the left. Today the only dev-only target sits
106
+ // at the end, so this matches the static constant; the computed keeps
107
+ // future entries safe.
108
+ const separatorAfterIndex = computed(() => {
109
+ const fullIndexOfSeparator = SEPARATOR_AFTER_INDEX;
110
+ const hiddenBefore = TARGETS.slice(0, fullIndexOfSeparator).filter((target) => target.devOnly && !DEV_MODE).length;
111
+ return fullIndexOfSeparator - hiddenBefore;
112
+ });
113
+
78
114
  function isActive(target: PluginLauncherTarget): boolean {
79
115
  return props.activeViewMode === target.key;
80
116
  }