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
@@ -0,0 +1,115 @@
1
+ // Inline-script flavour of the image-self-repair behaviour the app
2
+ // shell already runs via `useGlobalImageErrorRepair` (see
3
+ // `src/composables/useImageErrorRepair.ts`).
4
+ //
5
+ // Iframe surfaces (presentHtml result, Files HTML preview) live in
6
+ // their own Document, so the parent's document-level error handler
7
+ // can't see their `<img>` 404s. Server-side `inlineImages` (PDF) and
8
+ // the markdown rewriter (browser) cover the deterministic-resolution
9
+ // half of the routing strategy, but iframes that load HTML files
10
+ // directly off `/artifacts/html/...` need a third leg: an in-iframe
11
+ // `error` listener that does the same one-shot repair.
12
+ //
13
+ // This module is the **pure** form (no Vue, no DOM access at module
14
+ // load) so:
15
+ // - server/index.ts can import it for splicing into HTML responses
16
+ // - the composable in `useImageErrorRepair.ts` re-exports it for
17
+ // back-compat with existing test imports
18
+ //
19
+ // Stage 3 of the image-path-routing redesign — see
20
+ // plans/done/feat-image-path-routing.md and #1025.
21
+
22
+ // All Gemini / canvas / image-edit output lives at
23
+ // `artifacts/images/YYYY/MM/<id>.png` (server/utils/files/image-store.ts).
24
+ // If a rendered URL embeds that segment somewhere, trim everything
25
+ // before the pattern and retry as `/artifacts/images/<rest>` — the
26
+ // static mount will then serve the file directly.
27
+ export const IMAGE_REPAIR_PATTERN = /artifacts\/images\/.+/;
28
+
29
+ // Inline script intended for iframe surfaces. Same decision tree as
30
+ // `useGlobalImageErrorRepair`; kept as a string so it can be embedded
31
+ // into the rendered HTML and run inside the iframe. The regex literal
32
+ // is interpolated from `IMAGE_REPAIR_PATTERN` so the two stay in
33
+ // lockstep automatically.
34
+ export const IMAGE_REPAIR_INLINE_SCRIPT = `
35
+ document.addEventListener("error", function (event) {
36
+ const target = event.target;
37
+ if (!target) return;
38
+ const pattern = ${IMAGE_REPAIR_PATTERN.toString()};
39
+ function fixImg(img) {
40
+ if (img.dataset.imageRepairTried) return;
41
+ const m = String(img.src).match(pattern);
42
+ if (!m) return;
43
+ img.dataset.imageRepairTried = "1";
44
+ img.src = "/" + m[0];
45
+ }
46
+ function fixSource(src) {
47
+ if (src.dataset.imageRepairTried) return;
48
+ let changed = false;
49
+ const srcAttr = src.getAttribute("src");
50
+ if (srcAttr) {
51
+ const m = srcAttr.match(pattern);
52
+ if (m) { src.setAttribute("src", "/" + m[0]); changed = true; }
53
+ }
54
+ if (src.srcset) {
55
+ const orig = src.srcset;
56
+ const next = orig.replace(/[^\\s,]+/g, function (tok) {
57
+ const mm = tok.match(pattern);
58
+ return mm ? "/" + mm[0] : tok;
59
+ });
60
+ if (next !== orig) { src.srcset = next; changed = true; }
61
+ }
62
+ if (changed) src.dataset.imageRepairTried = "1";
63
+ }
64
+ if (target.tagName === "IMG") {
65
+ fixImg(target);
66
+ const pic = target.closest && target.closest("picture");
67
+ if (pic) for (const s of pic.querySelectorAll("source")) fixSource(s);
68
+ } else if (target.tagName === "SOURCE") {
69
+ fixSource(target);
70
+ } else if (target.tagName === "AUDIO" || target.tagName === "VIDEO") {
71
+ for (const s of target.querySelectorAll(":scope > source")) fixSource(s);
72
+ }
73
+ }, true);
74
+ `.trim();
75
+
76
+ // Wrap the script body in a `<script>` tag once at module load; the
77
+ // splicer below uses this directly so each splice is a single string
78
+ // concatenation, not a per-request `<script>...</script>` rebuild.
79
+ const IMAGE_REPAIR_SCRIPT_TAG = `<script>${IMAGE_REPAIR_INLINE_SCRIPT}</script>`;
80
+
81
+ // `</body>` (case-insensitive, whitespace-tolerant). The previous
82
+ // implementation paired this with a `(?![\s\S]*<\/body\s*>)` negative
83
+ // lookahead to anchor at the LAST occurrence — but that lookahead is
84
+ // O(N²) on inputs with many `</body>` tokens (an adversarial / unusual
85
+ // input shape, but cheap to defang). The current implementation runs
86
+ // `matchAll` once over the input (linear) and takes the last hit, so
87
+ // the splice point selection is O(N) regardless of input shape.
88
+ const BODY_CLOSE_RE = /<\/body\s*>/gi;
89
+
90
+ /** Splice `<script>${IMAGE_REPAIR_INLINE_SCRIPT}</script>` into an
91
+ * HTML document just before its **last** closing `</body>`.
92
+ * Anchoring at the last close means nested `</body>` inside e.g.
93
+ * literal example text inside `<pre>` doesn't fool us into splicing
94
+ * too early. If the document has no `</body>` (fragments, hand-
95
+ * written HTML), append the tag at the end so the script still
96
+ * loads.
97
+ *
98
+ * Pure string operation — safe to call on any HTML payload, no
99
+ * DOM parsing, no allocation beyond the result string. Linear time
100
+ * in input length even on adversarial inputs (verified by the
101
+ * `processes 100K </body> tokens in linear time` test). Idempotent:
102
+ * calling on already-spliced output appends a second copy (the
103
+ * script is one-shot per element so duplicates are harmless), so
104
+ * callers should splice exactly once per response. */
105
+ export function injectImageRepairScript(html: string): string {
106
+ if (!html) return html;
107
+ // matchAll → spread into an array → take the last entry. One linear
108
+ // pass over the input regardless of how many `</body>` tokens it
109
+ // contains.
110
+ const matches = [...html.matchAll(BODY_CLOSE_RE)];
111
+ if (matches.length === 0) return html + IMAGE_REPAIR_SCRIPT_TAG;
112
+ const idx = matches[matches.length - 1].index;
113
+ if (idx === undefined) return html + IMAGE_REPAIR_SCRIPT_TAG;
114
+ return `${html.slice(0, idx)}${IMAGE_REPAIR_SCRIPT_TAG}${html.slice(idx)}`;
115
+ }
@@ -1,10 +1,20 @@
1
1
  import { API_ROUTES } from "../../config/apiRoutes";
2
2
  import { getImageBump } from "./cacheBust";
3
3
 
4
+ // Files saved by `saveImage()` (Gemini, canvas, image edit) all live
5
+ // under this prefix — see server/utils/files/image-store.ts and
6
+ // server/workspace/paths.ts (WORKSPACE_DIRS.images). Express mounts a
7
+ // static handler for the corresponding URL so these paths route
8
+ // directly to the file without going through /api/files/raw.
9
+ const IMAGES_DIR_PREFIX = "artifacts/images/";
10
+
4
11
  /** Convert an imageData value to a displayable URL.
5
- * Handles both legacy data URIs and workspace-relative file paths. */
12
+ * Handles data URIs, paths under `artifacts/images/` (resolved via
13
+ * the static mount), and everything else (resolved via the workspace
14
+ * file server). */
6
15
  export function resolveImageSrc(imageData: string): string {
7
16
  if (imageData.startsWith("data:")) return imageData;
17
+ if (imageData.startsWith(IMAGES_DIR_PREFIX)) return `/${imageData}`;
8
18
  return `${API_ROUTES.files.raw}?path=${encodeURIComponent(imageData)}`;
9
19
  }
10
20
 
@@ -18,7 +28,11 @@ export function resolveImageSrc(imageData: string): string {
18
28
  * redraw, which races with stroke painting and blanks the canvas. */
19
29
  export function resolveImageSrcFresh(imageData: string): string {
20
30
  if (imageData.startsWith("data:")) return imageData;
21
- const base = `${API_ROUTES.files.raw}?path=${encodeURIComponent(imageData)}`;
31
+ const base = resolveImageSrc(imageData);
22
32
  const bump = getImageBump(imageData);
23
- return bump > 0 ? `${base}&v=${bump}` : base;
33
+ if (bump <= 0) return base;
34
+ // Both URL forms append a cache-bust param. The static mount form
35
+ // uses `?v=`, the API form already has `?path=` so we use `&v=`.
36
+ const sep = base.includes("?") ? "&" : "?";
37
+ return `${base}${sep}v=${bump}`;
24
38
  }
@@ -1,6 +1,7 @@
1
1
  import { marked } from "marked";
2
2
  import type { Token, Tokens } from "marked";
3
3
  import { resolveImageSrc } from "./resolve";
4
+ import { transformResolvableUrlsInHtml } from "./htmlSrcAttrs";
4
5
 
5
6
  // Pre-`marked` pass that rewrites workspace-relative image references
6
7
  // in markdown source so they render through the backend file server.
@@ -102,8 +103,43 @@ function rewriteImageToken(token: Tokens.Image, basePath: string): string | null
102
103
  return `![${alt}](${newHref})`;
103
104
  }
104
105
 
106
+ // Rewrite URL-bearing attributes of every recognised tag inside an
107
+ // HTML fragment, applying the same basePath / shouldSkip /
108
+ // resolveImageSrc pipeline used for `![alt](url)` markdown images.
109
+ // Other attributes (alt, class, style, id, …) are preserved verbatim.
110
+ //
111
+ // Tags + attributes covered (single source of truth at
112
+ // `htmlSrcAttrs.ts:RESOLVABLE_TAG_ATTRS`): `<img src>`, `<source src>`,
113
+ // `<video poster|src>`, `<audio src>`. Add a row there to extend
114
+ // coverage; both this rewriter and the server-side PDF rewriter pick
115
+ // it up automatically (#1011 Stage B).
116
+ //
117
+ // Output URLs come from `resolveImageSrc`, which either returns a
118
+ // mount-rooted path (`/artifacts/images/<file>`) or runs the input
119
+ // through `encodeURIComponent`. `"` becomes `%22`, `'` becomes `%27`,
120
+ // `<` / `>` are encoded — the rewritten attribute can't break out of
121
+ // its own quotes or close the tag.
122
+ //
123
+ // Limitations:
124
+ // - `srcset` (comma-separated descriptor list) is deferred —
125
+ // tracked under #1011 Stage B follow-up.
126
+ // - SVG `<image href>` and CSS `url()` are deferred per plan
127
+ // §修正提案 P3-A.
128
+ // - A regex can't perfectly distinguish a real tag from one
129
+ // embedded in another attribute's value; embedded matches get
130
+ // rewritten too. Harmless because the rewritten URL is encoded
131
+ // safely.
132
+ export function rewriteImgSrcAttrsInHtml(html: string, basePath: string): string {
133
+ return transformResolvableUrlsInHtml(html, (url) => {
134
+ if (shouldSkip(url)) return null;
135
+ const resolved = resolveWorkspacePath(basePath, url);
136
+ if (resolved === null) return null;
137
+ return resolveImageSrc(resolved);
138
+ });
139
+ }
140
+
105
141
  function isSkippable(token: Token): boolean {
106
- return token.type === "code" || token.type === "codespan" || token.type === "html";
142
+ return token.type === "code" || token.type === "codespan";
107
143
  }
108
144
 
109
145
  function getContainerChildren(token: Token): Token[] | null {
@@ -135,11 +171,14 @@ function renderContainerChildren(raw: string, children: Token[], basePath: strin
135
171
  }
136
172
 
137
173
  // Recursively render a token back to markdown, rewriting image refs
138
- // in-place. Code / codespan / html tokens are emitted verbatim so
139
- // image-ref syntax inside them stays literal. Token-tree recursion
140
- // uses the lexer's structural knowledge and never crosses a skip
141
- // boundary unlike the earlier `indexOf` splice which could rewrite
142
- // a code-block literal when the same ref appeared in real markdown.
174
+ // in-place. Code / codespan tokens are emitted verbatim so image-ref
175
+ // syntax inside them stays literal. HTML tokens get a separate pass
176
+ // (`rewriteImgSrcAttrsInHtml`) so raw `<img>` tags route through the
177
+ // same basePath + shouldSkip pipeline as the markdown image syntax.
178
+ // Token-tree recursion uses the lexer's structural knowledge and never
179
+ // crosses a skip boundary — unlike the earlier `indexOf` splice which
180
+ // could rewrite a code-block literal when the same ref appeared in
181
+ // real markdown.
143
182
  function renderToken(token: Token, basePath: string, out: string[]): void {
144
183
  if (isSkippable(token)) {
145
184
  out.push(token.raw);
@@ -150,6 +189,16 @@ function renderToken(token: Token, basePath: string, out: string[]): void {
150
189
  out.push(replacement ?? token.raw);
151
190
  return;
152
191
  }
192
+ if (token.type === "html") {
193
+ // Block / inline HTML — rewrite raw <img> tags inside before
194
+ // emitting. Markdown image syntax (![alt](url)) is handled by the
195
+ // image-token branch above; this branch covers the HTML-fallback
196
+ // path (#1011 Stage A). Fall back to verbatim raw if `raw` is
197
+ // unexpectedly missing — defensive against future marked changes.
198
+ const raw = (token as { raw?: string }).raw ?? "";
199
+ out.push(rewriteImgSrcAttrsInHtml(raw, basePath));
200
+ return;
201
+ }
153
202
  const raw = (token as { raw?: string }).raw ?? "";
154
203
  const children = getContainerChildren(token);
155
204
  if (children && renderContainerChildren(raw, children, basePath, out)) {
@@ -167,14 +216,18 @@ function renderToken(token: Token, basePath: string, out: string[]): void {
167
216
  * file (e.g. `"wiki/pages"` for `wiki/pages/foo.md`). Omit or pass
168
217
  * `""` when resolving refs against the workspace root.
169
218
  *
219
+ * Also rewrites the `src` attribute of raw `<img>` tags inside HTML
220
+ * blocks / inline HTML so a page mixing both syntaxes resolves the
221
+ * same way. Markdown image syntax inside code blocks / inline code
222
+ * spans is left alone.
223
+ *
170
224
  * Absolute URLs, data URIs, and existing API paths pass through
171
225
  * untouched. Refs that would escape the workspace root (more `..`
172
226
  * than `basePath` depth) also pass through untouched — they would
173
227
  * 404 regardless, and passing through lets the user see the broken
174
- * ref instead of silently re-pointing it. Image-ref syntax inside
175
- * code blocks / inline code spans is left alone.
228
+ * ref instead of silently re-pointing it.
176
229
  */
177
- export function rewriteMarkdownImageRefs(markdown: string, basePath: string = ""): string {
230
+ export function rewriteMarkdownImageRefs(markdown: string, basePath = ""): string {
178
231
  const tokens = marked.lexer(markdown);
179
232
  const parts: string[] = [];
180
233
  for (const token of tokens) renderToken(token, basePath, parts);
@@ -0,0 +1,125 @@
1
+ // Canonical YAML-frontmatter parser / serializer / merger for the
2
+ // `---\nkey: value\n---\nbody` markdown convention. Used on the Vue
3
+ // side; the server side has a mirror at
4
+ // `server/utils/markdown/frontmatter.ts` (#895 PR B). The two share
5
+ // the same shape but live in separate files because the build
6
+ // targets are different (browser vs Node) and a shared package is
7
+ // overkill for ~10 lines of glue.
8
+ //
9
+ // Implementation uses `js-yaml` so we get full YAML coverage (block
10
+ // lists, multi-line strings, escaping) instead of the regex
11
+ // approximation in the legacy `src/utils/format/frontmatter.ts`.
12
+
13
+ import yaml from "js-yaml";
14
+
15
+ export interface ParsedMarkdown {
16
+ /** Parsed YAML object. Empty `{}` when the document has no
17
+ * frontmatter or the YAML failed to parse. Insertion order
18
+ * matches the source — `Object.entries(meta)` is the right way
19
+ * to iterate for an ordered properties panel. */
20
+ meta: Record<string, unknown>;
21
+ /** Body after stripping the frontmatter envelope. Trailing
22
+ * newline at the end of the closing `---` line is consumed; a
23
+ * no-frontmatter document returns the raw input verbatim. */
24
+ body: string;
25
+ /** True iff a well-formed `---\n...\n---\n` envelope was
26
+ * detected and parsed. False for documents without an envelope
27
+ * or where the envelope is malformed (in which case the body
28
+ * is returned verbatim and `meta` is `{}`). */
29
+ hasHeader: boolean;
30
+ }
31
+
32
+ const FRONTMATTER_OPEN = /^---\r?\n/;
33
+ // `(?:^|\r?\n)` lets the closing fence sit at the very start of
34
+ // `afterOpen` — needed for the empty-envelope case `---\n---\n`
35
+ // where the closing `---` is the first thing after the open is
36
+ // stripped. Without the alternation the regex required a preceding
37
+ // newline and silently treated empty headers as malformed.
38
+ const FRONTMATTER_CLOSE = /(?:^|\r?\n)---\s*(?:\r?\n|$)/;
39
+
40
+ /** Parse a markdown document, splitting frontmatter from body.
41
+ * Always returns an object — never throws. Malformed YAML inside
42
+ * a well-formed envelope falls back to `{ meta: {}, hasHeader: false }`
43
+ * so a typo in the header doesn't break rendering. */
44
+ export function parseFrontmatter(raw: string): ParsedMarkdown {
45
+ if (!FRONTMATTER_OPEN.test(raw)) {
46
+ return { meta: {}, body: raw, hasHeader: false };
47
+ }
48
+ const afterOpen = raw.replace(FRONTMATTER_OPEN, "");
49
+ const closeMatch = FRONTMATTER_CLOSE.exec(afterOpen);
50
+ if (!closeMatch || closeMatch.index === undefined) {
51
+ return { meta: {}, body: raw, hasHeader: false };
52
+ }
53
+ const yamlText = afterOpen.slice(0, closeMatch.index);
54
+ const body = afterOpen.slice(closeMatch.index + closeMatch[0].length);
55
+ const meta = safeYamlLoad(yamlText);
56
+ if (meta === null) {
57
+ return { meta: {}, body: raw, hasHeader: false };
58
+ }
59
+ return { meta, body, hasHeader: true };
60
+ }
61
+
62
+ /** Serialize a meta object + body back into the canonical
63
+ * `---\n...\n---\n\nbody` shape. An empty `meta` returns the body
64
+ * alone (no envelope) — the lazy-on-write contract: don't add
65
+ * ceremony to documents that don't have anything to record.
66
+ *
67
+ * Round-trip semantics: VALUE-preserving, NOT byte-preserving.
68
+ * `js-yaml` adds quotes to ambiguous scalars (`'1.20'`, `'true'`)
69
+ * so they parse back as the same string under FAILSAFE_SCHEMA.
70
+ * Source-text formatting (unquoted vs quoted) may change on save
71
+ * but the parsed value is stable across rounds (codex review
72
+ * iter-2 #902). */
73
+ export function serializeWithFrontmatter(meta: Record<string, unknown>, body: string): string {
74
+ if (Object.keys(meta).length === 0) return body;
75
+ // `lineWidth: -1` disables auto-wrap so long URLs / titles stay on
76
+ // one line. `noRefs: true` avoids YAML anchor syntax (`&id001`)
77
+ // which is technically valid but visually noisy in plain-text
78
+ // markdown. js-yaml's default already trims a trailing newline.
79
+ const yamlText = yaml.dump(meta, { lineWidth: -1, noRefs: true }).trimEnd();
80
+ return `---\n${yamlText}\n---\n\n${body}`;
81
+ }
82
+
83
+ /** Merge a patch into an existing meta object. Unknown keys in
84
+ * `existing` are preserved verbatim; keys present in `patch`
85
+ * overwrite. A `null` or `undefined` patch value DELETES the key
86
+ * (pattern borrowed from REST PATCH semantics) — callers that
87
+ * want "leave alone" should omit the key entirely. */
88
+ export function mergeFrontmatter(existing: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
89
+ const out: Record<string, unknown> = { ...existing };
90
+ for (const [key, value] of Object.entries(patch)) {
91
+ if (value === null || value === undefined) {
92
+ Reflect.deleteProperty(out, key);
93
+ } else {
94
+ out[key] = value;
95
+ }
96
+ }
97
+ return out;
98
+ }
99
+
100
+ function safeYamlLoad(text: string): Record<string, unknown> | null {
101
+ try {
102
+ // `FAILSAFE_SCHEMA` keeps every scalar as a string and skips
103
+ // type coercion. Two motivating cases:
104
+ //
105
+ // - YAML 1.1 dates: `created: 2026-04-27` would become a
106
+ // `Date` object under DEFAULT_SCHEMA, breaking round-trip.
107
+ // - Numeric-looking strings: `version: 1.20` → number 1.2
108
+ // under JSON_SCHEMA, dropping the trailing zero on save
109
+ // (codex review iter-1 #902).
110
+ //
111
+ // For our domain — title / created / updated / tags / editor —
112
+ // everything that should be a string IS one, and the rare
113
+ // caller that wants a number can coerce explicitly. Mappings
114
+ // and sequences still parse normally (FAILSAFE keeps those).
115
+ const loaded = yaml.load(text, { schema: yaml.FAILSAFE_SCHEMA });
116
+ // `yaml.load` returns `undefined` for empty input, a primitive
117
+ // for scalar-only YAML, or an object for the normal case. Only
118
+ // accept plain objects — anything else is a malformed header.
119
+ if (loaded === null || loaded === undefined) return {};
120
+ if (typeof loaded !== "object" || Array.isArray(loaded)) return null;
121
+ return loaded as Record<string, unknown>;
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
@@ -28,6 +28,10 @@
28
28
  // so they need to be counted (and writable) by the index walker.
29
29
  //
30
30
  // Captures: prefix (indent + any `>` chains), bullet, separator, mark.
31
+ // `\s*` and `>\s*` operate on disjoint character classes from the
32
+ // surrounding bullet / separator / mark, so the nested quantifiers
33
+ // can't overlap to produce ReDoS — each pass is linear in line length.
34
+ // eslint-disable-next-line security/detect-unsafe-regex -- markdown task-line parser, bounded captures with hard delimiters
31
35
  const TASK_LINE = /^(\s*(?:>\s*)*)([-*+]|\d+[.)])(\s+)\[([ xX])\]/;
32
36
 
33
37
  // Fenced code block opener/closer. CommonMark allows fences to be
@@ -43,6 +47,7 @@ const TASK_LINE = /^(\s*(?:>\s*)*)([-*+]|\d+[.)])(\s+)\[([ xX])\]/;
43
47
  // be miscounted as a task — making the View's count-cross-check
44
48
  // refuse all toggles in the whole document.
45
49
  const FENCE_LINE = /^( {0,3})(`{3,}|~{3,})/;
50
+ // eslint-disable-next-line security/detect-unsafe-regex -- bounded blockquote-prefix parser; `\s*` / `>\s?` / outer `+` operate on disjoint character classes (no overlap)
46
51
  const BLOCKQUOTE_PREFIX = /^(\s*(?:>\s?)+)/;
47
52
 
48
53
  // Mutable state for the line walker. Pulled out so the main toggle
@@ -66,7 +71,7 @@ function stepFence(line: string, state: FenceState): boolean {
66
71
  const content = quoteMatch ? line.slice(quoteMatch[0].length) : line;
67
72
  const fenceMatch = content.match(FENCE_LINE);
68
73
  if (fenceMatch) {
69
- const marker = fenceMatch[2];
74
+ const [, , marker] = fenceMatch;
70
75
  if (!state.inFence) {
71
76
  // Openers may carry an info string after the marker
72
77
  // (e.g. "```ts"). We don't need to keep it — just enter
@@ -103,7 +108,7 @@ function stepFence(line: string, state: FenceState): boolean {
103
108
  function flipMark(line: string, match: RegExpMatchArray): string {
104
109
  const [whole, prefix, bullet, sep, mark] = match;
105
110
  const flipped = mark === " " ? "x" : " ";
106
- return `${prefix}${bullet}${sep}[${flipped}]` + line.slice(whole.length);
111
+ return `${prefix}${bullet}${sep}[${flipped}]${line.slice(whole.length)}`;
107
112
  }
108
113
 
109
114
  /** Find the source-line index of every task-list item, in document
@@ -0,0 +1,132 @@
1
+ // Browser-side plugin runtime construction (#1110). The host's runtime
2
+ // plugin loader provides one of these per plugin via Vue's
3
+ // `provide(PLUGIN_RUNTIME_KEY, ...)`; the plugin's components fetch
4
+ // it via `useRuntime()` from `gui-chat-protocol/vue`.
5
+ //
6
+ // Every helper closes over `pkgName` so the plugin's pubsub channel
7
+ // and notify call cannot leak into another plugin's namespace.
8
+
9
+ import { computed, type Ref } from "vue";
10
+ import { useI18n } from "vue-i18n";
11
+ import type { BrowserPluginRuntime } from "gui-chat-protocol/vue";
12
+ import { usePubSub } from "../../composables/usePubSub";
13
+ import { apiPost } from "../api";
14
+ import { API_ROUTES } from "../../config/apiRoutes";
15
+
16
+ /** Build the channel name for a plugin's event. Must stay in lockstep
17
+ * with `server/plugins/runtime.ts:pluginChannelName`. */
18
+ export function pluginChannelName(pkgName: string, eventName: string): string {
19
+ return `plugin:${pkgName}:${eventName}`;
20
+ }
21
+
22
+ function makeScopedPubSub(pkgName: string): BrowserPluginRuntime["pubsub"] {
23
+ const { subscribe } = usePubSub();
24
+ return {
25
+ subscribe(eventName, handler) {
26
+ // The host pubsub fans payloads as `unknown`; the plugin
27
+ // declares the expected shape via the generic at the call
28
+ // site. Validation is the plugin's responsibility (Zod or
29
+ // hand-written guard).
30
+ return subscribe(pluginChannelName(pkgName, eventName), handler as (data: unknown) => void);
31
+ },
32
+ };
33
+ }
34
+
35
+ function makeScopedLogger(pkgName: string): BrowserPluginRuntime["log"] {
36
+ // Frontend logger maps to `console.*` in v1. The host's central
37
+ // logger lives server-side; routing browser logs there is a future
38
+ // enhancement that doesn't change this surface.
39
+ const tag = `[plugin/${pkgName}]`;
40
+ return {
41
+ debug: (msg, data) => console.debug(tag, msg, data),
42
+ info: (msg, data) => console.info(tag, msg, data),
43
+ warn: (msg, data) => console.warn(tag, msg, data),
44
+ error: (msg, data) => console.error(tag, msg, data),
45
+ };
46
+ }
47
+
48
+ /** Allowlisted URL schemes for `runtime.openUrl`. The two http schemes
49
+ * cover the legitimate "open this external page" use case; everything
50
+ * else (`javascript:`, `data:`, `vbscript:`, `file:`, custom schemes)
51
+ * is rejected. The `noopener,noreferrer` flags on `window.open`
52
+ * prevent the opened tab from snooping the opener but do NOT stop
53
+ * `javascript:` execution — that's why scheme filtering is the actual
54
+ * XSS guard. CodeRabbit review caught this on PR #1124. */
55
+ const OPEN_URL_ALLOWED_SCHEMES: ReadonlySet<string> = new Set(["http:", "https:"]);
56
+
57
+ function makeOpenUrl(pkgName: string): BrowserPluginRuntime["openUrl"] {
58
+ return (url: string) => {
59
+ let parsed: URL;
60
+ try {
61
+ parsed = new URL(url);
62
+ } catch {
63
+ console.warn(`[plugin/${pkgName}] openUrl rejected unparseable URL`, { url });
64
+ return;
65
+ }
66
+ if (!OPEN_URL_ALLOWED_SCHEMES.has(parsed.protocol)) {
67
+ console.warn(`[plugin/${pkgName}] openUrl rejected non-http(s) scheme`, { scheme: parsed.protocol });
68
+ return;
69
+ }
70
+ // `noopener` prevents the opened tab from accessing `window.opener`
71
+ // and snooping; `noreferrer` strips the Referer header so the
72
+ // destination can't see what page sent the user. Forced at the
73
+ // platform level so individual plugin links can't drop them.
74
+ const opened = window.open(url, "_blank", "noopener,noreferrer");
75
+ if (!opened) {
76
+ // Popup blocker engaged.
77
+ console.warn(`[plugin/${pkgName}] window.open returned null`, { url });
78
+ }
79
+ };
80
+ }
81
+
82
+ function makeDispatch(pkgName: string): BrowserPluginRuntime["dispatch"] {
83
+ // Substitute `:pkg` in the contracted dispatch route. encodeURIComponent
84
+ // collapses scoped names (`@org/pkg`) into one URL path segment;
85
+ // the parameter pattern `:pkg` matches any segment.
86
+ const url = API_ROUTES.plugins.runtimeDispatch.replace(":pkg", encodeURIComponent(pkgName));
87
+ return async <T = unknown>(args: object): Promise<T> => {
88
+ const result = await apiPost<T>(url, args);
89
+ if (!result.ok) {
90
+ throw new Error(`plugin/${pkgName} dispatch failed (${result.status}): ${result.error}`);
91
+ }
92
+ return result.data;
93
+ };
94
+ }
95
+
96
+ export interface MakeBrowserPluginRuntimeDeps {
97
+ /** npm package name. Used both as the namespace prefix for
98
+ * pubsub channels and as the log prefix. */
99
+ pkgName: string;
100
+ /** Optional URL map exposed via `runtime.endpoints` for multi-URL
101
+ * built-in plugins. Runtime-loaded plugins (the common
102
+ * single-dispatch shape) leave this undefined. Built-in plugins
103
+ * (#1141) hand `{ method, url }` records; host-shared scopes hand
104
+ * plain string URLs — kept opaque here, narrowed at the consumer
105
+ * via `pluginEndpoints<E>(scope)`. See `BrowserPluginRuntime.endpoints`
106
+ * in `gui-chat-protocol@>=0.3.1` for the contract. */
107
+ endpoints?: Readonly<Record<string, unknown>>;
108
+ }
109
+
110
+ export function makeBrowserPluginRuntime(deps: MakeBrowserPluginRuntimeDeps): BrowserPluginRuntime {
111
+ const { pkgName, endpoints } = deps;
112
+ // `useI18n()` exposes `locale` as `WritableComputedRef<Locales>`.
113
+ // Wrapping in a fresh `computed` widens it to `Ref<string>` for
114
+ // plugin authors (so they don't need to import the host's locale
115
+ // union) while preserving reactivity.
116
+ const { locale: hostLocale } = useI18n();
117
+ const locale = computed(() => String(hostLocale.value)) as Ref<string>;
118
+ return {
119
+ pubsub: makeScopedPubSub(pkgName),
120
+ locale,
121
+ log: makeScopedLogger(pkgName),
122
+ openUrl: makeOpenUrl(pkgName),
123
+ dispatch: makeDispatch(pkgName),
124
+ // `BrowserPluginRuntime.endpoints` is now typed as the runtime's
125
+ // `E` type parameter (gui-chat-protocol@>=0.3.2, default
126
+ // `Readonly<Record<string, unknown>>`). Plugin authors pin the
127
+ // shape via `useRuntime<TheirShape>()` and read `runtime.endpoints!`
128
+ // without a cast. No coercion needed at this construction site —
129
+ // the host populates the field opaquely; each consumer narrows.
130
+ endpoints,
131
+ };
132
+ }
@@ -11,6 +11,43 @@
11
11
  import { isUserTextResponse } from "../tools/result";
12
12
  import type { SessionSummary, ActiveSession } from "../../types/session";
13
13
 
14
+ // Server-side fields the sidebar inherits when present. `summary` /
15
+ // `keywords` are AI-generated; `origin` distinguishes scheduler /
16
+ // skill / bridge / human sessions; `isBookmarked` / `hasUnread` /
17
+ // `statusMessage` are server-tracked flags. None of these have a
18
+ // local fallback — copy them only when the server has actually set
19
+ // them, otherwise they'd surface as explicit `undefined` in shallow
20
+ // copies downstream.
21
+ const SERVER_OVERRIDE_KEYS = ["summary", "keywords", "origin", "isBookmarked", "hasUnread", "statusMessage"] as const;
22
+
23
+ export function pickServerOverrides(serverEntry: SessionSummary | undefined): Partial<SessionSummary> {
24
+ if (!serverEntry) return {};
25
+ const overrides: Partial<SessionSummary> = {};
26
+ for (const key of SERVER_OVERRIDE_KEYS) {
27
+ const value = serverEntry[key];
28
+ if (value !== undefined) {
29
+ Object.assign(overrides, { [key]: value });
30
+ }
31
+ }
32
+ return overrides;
33
+ }
34
+
35
+ // Fold every in-memory signal into isRunning so the sidebar spinner
36
+ // reacts as fast as the fastest source:
37
+ // - serverEntry.isRunning: authoritative but arrives on a /api/sessions
38
+ // refetch
39
+ // - live.isRunning: mirrored from the server via refreshSessionStates;
40
+ // may be ahead during the refetch window, and covers live-only
41
+ // sessions with no serverEntry yet
42
+ // - live.pendingGenerations: updates on the socket round-trip of a
43
+ // generationStarted event, before any REST refetch
44
+ // OR them so any one is enough. `live.isRunning` is always defined on
45
+ // an ActiveSession, so the summary always carries a boolean here.
46
+ export function computeLiveIsRunning(serverEntry: SessionSummary | undefined, live: Pick<ActiveSession, "isRunning" | "pendingGenerations">): boolean {
47
+ const pendingCount = Object.keys(live.pendingGenerations ?? {}).length;
48
+ return Boolean(serverEntry?.isRunning) || live.isRunning || pendingCount > 0;
49
+ }
50
+
14
51
  // Build the summary shape the sidebar expects for a single live
15
52
  // session. Server-side data (AI-generated title, summary,
16
53
  // keywords) takes precedence over the local first-user-message
@@ -19,48 +56,14 @@ import type { SessionSummary, ActiveSession } from "../../types/session";
19
56
  function buildLiveSummary(live: ActiveSession, serverEntry: SessionSummary | undefined): SessionSummary {
20
57
  const firstUserMsg = live.toolResults.find(isUserTextResponse);
21
58
  const preview = serverEntry?.preview || (firstUserMsg?.message ?? "");
22
- const base: SessionSummary = {
59
+ return {
23
60
  id: live.id,
24
61
  roleId: live.roleId,
25
62
  startedAt: live.startedAt,
26
63
  updatedAt: live.updatedAt,
27
64
  preview,
28
- };
29
- // Fold every in-memory signal into isRunning so the sidebar spinner
30
- // reacts as fast as the fastest source:
31
- // - serverEntry.isRunning: authoritative but arrives on a /api/sessions
32
- // refetch
33
- // - live.isRunning: mirrored from the server via refreshSessionStates;
34
- // may be ahead during the refetch window, and covers live-only
35
- // sessions with no serverEntry yet
36
- // - live.pendingGenerations: updates on the socket round-trip of a
37
- // generationStarted event, before any REST refetch
38
- // OR them so any one is enough. `live.isRunning` is always defined on
39
- // an ActiveSession, so the summary always carries a boolean here.
40
- const pending = live.pendingGenerations ?? {};
41
- const isRunning = !!serverEntry?.isRunning || live.isRunning || Object.keys(pending).length > 0;
42
- // Carry summary / keywords ONLY if the server already has them.
43
- // Object-spread with a conditional object keeps us from adding
44
- // `undefined` values that would otherwise show up as explicit
45
- // `summary: undefined` in a later shallow-copy.
46
- return {
47
- ...base,
48
- ...(serverEntry?.summary !== undefined && { summary: serverEntry.summary }),
49
- ...(serverEntry?.keywords !== undefined && {
50
- keywords: serverEntry.keywords,
51
- }),
52
- // `origin` is set once by the server when the session is created
53
- // (scheduler / skill / bridge / human). A live session promoted
54
- // from the server list must keep it — the tab bar renders the
55
- // non-human glyph off this field.
56
- ...(serverEntry?.origin !== undefined && { origin: serverEntry.origin }),
57
- isRunning,
58
- ...(serverEntry?.hasUnread !== undefined && {
59
- hasUnread: serverEntry.hasUnread,
60
- }),
61
- ...(serverEntry?.statusMessage !== undefined && {
62
- statusMessage: serverEntry.statusMessage,
63
- }),
65
+ ...pickServerOverrides(serverEntry),
66
+ isRunning: computeLiveIsRunning(serverEntry, live),
64
67
  };
65
68
  }
66
69