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
@@ -4,17 +4,17 @@
4
4
  <div class="flex items-center justify-between gap-2 px-3 py-2 border-b border-gray-100 shrink-0">
5
5
  <div class="flex items-center gap-2 min-w-0">
6
6
  <button
7
- v-if="action !== 'index'"
7
+ v-if="action !== 'index' && isStandaloneWikiRoute"
8
8
  class="h-8 w-8 flex items-center justify-center rounded text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors"
9
9
  :title="t('pluginWiki.backToIndex')"
10
10
  @click="router.back()"
11
11
  >
12
12
  <span class="material-icons text-base">arrow_back</span>
13
13
  </button>
14
- <h2 class="text-lg font-semibold text-gray-800 truncate">{{ title }}</h2>
14
+ <h2 class="text-lg font-semibold text-gray-800 truncate">{{ displayTitle }}</h2>
15
15
  </div>
16
16
  <div class="flex items-center gap-2">
17
- <template v-if="action === 'page' && content">
17
+ <template v-if="(action === 'page' || action === 'page-edit') && content">
18
18
  <button
19
19
  class="h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
20
20
  :disabled="pdfDownloading"
@@ -74,44 +74,12 @@
74
74
  {{ navError }}
75
75
  </div>
76
76
 
77
- <!-- Empty state: specific page (standalone /wiki route only inside
78
- /chat tool results, spawning a fresh session is confusing, same
79
- rationale as the per-page chat composer below) -->
80
- <div v-if="!pageExists && !navError && action === 'page'" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
81
- <div class="text-center space-y-4">
82
- <span class="material-icons text-4xl text-gray-300">article</span>
83
- <p>{{ t("pluginWiki.emptyPage", { title: title }) }}</p>
84
- <button
85
- v-if="isStandaloneWikiRoute"
86
- data-testid="wiki-create-page-button"
87
- class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
88
- @click="requestCreatePage"
89
- >
90
- <span class="material-icons text-base">auto_fix_high</span>
91
- {{ t("pluginWiki.createPage") }}
92
- </button>
93
- </div>
94
- </div>
95
-
96
- <!-- Empty state: page file exists but has no content -->
97
- <div v-else-if="!content && !navError && action === 'page'" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
98
- <div class="text-center space-y-4">
99
- <span class="material-icons text-4xl text-gray-300">article</span>
100
- <p>{{ t("pluginWiki.emptyContent", { title: title }) }}</p>
101
- <button
102
- v-if="isStandaloneWikiRoute"
103
- data-testid="wiki-update-page-button"
104
- class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
105
- @click="requestUpdatePage"
106
- >
107
- <span class="material-icons text-base">auto_fix_high</span>
108
- {{ t("pluginWiki.updatePage") }}
109
- </button>
110
- </div>
111
- </div>
112
-
113
- <!-- Empty state: index or other -->
114
- <div v-else-if="!content && !navError" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
77
+ <!-- Empty state: index / log / lint without content. The page
78
+ action's empty states are rendered INSIDE the Content tab
79
+ body below so the History tab stays reachable when the
80
+ live page is missing or empty (codex review iter-2 #946
81
+ history outlives the page). -->
82
+ <div v-if="!content && !navError && action !== 'page' && action !== 'page-edit'" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
115
83
  <div class="text-center space-y-2">
116
84
  <span class="material-icons text-4xl text-gray-300">menu_book</span>
117
85
  <p>{{ t("pluginWiki.empty") }}</p>
@@ -133,9 +101,9 @@
133
101
  />
134
102
  <FilterChip
135
103
  v-if="selectedTag !== null && !allTags.some(([tag]) => tag === selectedTag)"
136
- :active="true"
104
+ active
137
105
  :label="selectedTag"
138
- :count="1"
106
+ :count="tagCounts.get(selectedTag) ?? 1"
139
107
  :data-testid="`wiki-tag-filter-${selectedTag}`"
140
108
  @click="toggleTagFilter(selectedTag)"
141
109
  />
@@ -147,7 +115,7 @@
147
115
  <div
148
116
  v-for="entry in visibleEntries"
149
117
  :key="entry.slug"
150
- class="flex items-baseline gap-2 px-4 py-1 cursor-pointer hover:bg-blue-50 transition-colors"
118
+ class="group flex items-baseline gap-2 px-4 py-1 cursor-pointer hover:bg-blue-50 transition-colors"
151
119
  :data-testid="`wiki-page-entry-${entry.slug || entry.title}`"
152
120
  @click="navigatePage(entry.slug || entry.title)"
153
121
  >
@@ -155,7 +123,7 @@
155
123
  <span v-if="entry.description" class="text-xs text-gray-500 truncate">
156
124
  {{ entry.description }}
157
125
  </span>
158
- <span v-if="entry.tags && entry.tags.length > 0" class="flex gap-1 flex-wrap shrink-0">
126
+ <span v-if="entry.tags && entry.tags.length > 0" class="flex gap-1 flex-wrap shrink-0 opacity-20 group-hover:opacity-100 transition-opacity">
159
127
  <button
160
128
  v-for="tag in entry.tags"
161
129
  :key="tag"
@@ -170,26 +138,205 @@
170
138
  </div>
171
139
  </div>
172
140
 
173
- <!-- Markdown content -->
174
- <div
175
- v-else
176
- ref="scrollRef"
177
- class="flex-1 overflow-y-auto px-6 py-4 prose prose-sm max-w-none wiki-content"
178
- @click="handleContentClick"
179
- v-html="renderedContent"
180
- />
141
+ <!-- Markdown content (with optional metadata bar above) -->
142
+ <template v-else>
143
+ <!-- Metadata bar (#895 PR B). One thin row that surfaces
144
+ `created` / `updated` / `editor` / `tags` from the page's
145
+ frontmatter. Hidden when the page has no header — keeps
146
+ the existing header-less content visually unchanged.
147
+ Stays visible across both Content and History tabs (#944
148
+ Q11=C). -->
149
+ <div
150
+ v-if="(action === 'page' || action === 'page-edit') && hasPageMeta"
151
+ data-testid="wiki-page-metadata-bar"
152
+ class="shrink-0 border-b border-gray-100 px-6 py-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500"
153
+ >
154
+ <span v-if="pageMeta.created" data-testid="wiki-page-metadata-created">
155
+ <span class="text-gray-400">{{ t("pluginWiki.metadataCreated") }}:</span>
156
+ {{ pageMeta.created }}
157
+ </span>
158
+ <span v-if="pageMeta.updated" data-testid="wiki-page-metadata-updated">
159
+ <span class="text-gray-400">{{ t("pluginWiki.metadataUpdated") }}:</span>
160
+ {{ formatUpdated(pageMeta.updated) }}
161
+ </span>
162
+ <span v-if="pageMeta.editor" data-testid="wiki-page-metadata-editor">
163
+ <span class="text-gray-400">{{ t("pluginWiki.metadataEditor") }}:</span>
164
+ {{ pageMeta.editor }}
165
+ </span>
166
+ <span v-if="pageMeta.tags.length > 0" class="flex flex-wrap gap-1" data-testid="wiki-page-metadata-tags">
167
+ <button
168
+ v-for="tag in pageMeta.tags"
169
+ :key="tag"
170
+ class="entry-tag-chip"
171
+ :data-testid="`wiki-page-metadata-tag-${tag}`"
172
+ @click="setTagFilterAndNavigate(tag)"
173
+ >
174
+ {{ `#${tag}` }}
175
+ </button>
176
+ </span>
177
+ </div>
178
+
179
+ <!-- Per-page tab strip: Content | History (#763 PR 3 / #944).
180
+ Mounted on every page view (including missing / empty
181
+ pages) so history outlives the live page (codex iter-2
182
+ #946). Log / lint reports keep the legacy single-pane
183
+ layout — they have no per-page history concept. -->
184
+ <div
185
+ v-if="action === 'page' && currentSlugReactive !== null"
186
+ data-testid="wiki-page-tabs"
187
+ class="shrink-0 border-b border-gray-100 px-3 py-2 flex items-center gap-2"
188
+ >
189
+ <div class="flex border border-gray-300 rounded overflow-hidden">
190
+ <button
191
+ type="button"
192
+ :class="[
193
+ 'h-8 px-2.5 flex items-center gap-1 transition-colors',
194
+ pageTab === PAGE_TAB.content ? 'bg-blue-50 text-blue-600 font-medium' : 'bg-white text-gray-600 hover:bg-gray-50',
195
+ ]"
196
+ data-testid="wiki-page-tab-content"
197
+ @click="pageTab = PAGE_TAB.content"
198
+ >
199
+ <span class="material-icons text-sm">article</span>
200
+ <span>{{ t("pluginWiki.history.tabContent") }}</span>
201
+ </button>
202
+ <button
203
+ type="button"
204
+ :class="[
205
+ 'h-8 px-2.5 flex items-center gap-1 border-l border-gray-200 transition-colors',
206
+ pageTab === PAGE_TAB.history ? 'bg-blue-50 text-blue-600 font-medium' : 'bg-white text-gray-600 hover:bg-gray-50',
207
+ ]"
208
+ data-testid="wiki-page-tab-history"
209
+ @click="pageTab = PAGE_TAB.history"
210
+ >
211
+ <span class="material-icons text-sm">history</span>
212
+ <span>{{ t("pluginWiki.history.tabHistory") }}</span>
213
+ </button>
214
+ </div>
215
+ <!-- Restore success toast — transient banner emitted on the
216
+ Content tab after a successful history restore (Q7=B). -->
217
+ <span
218
+ v-if="restoreToastVisible"
219
+ data-testid="wiki-history-restore-toast"
220
+ class="text-sm text-green-700 bg-green-50 border border-green-200 rounded px-2 py-1"
221
+ >
222
+ {{ t("pluginWiki.history.restoreSuccessToast") }}
223
+ </span>
224
+ </div>
225
+
226
+ <!-- Content tab body. For pages, includes the empty-state
227
+ fallbacks (deleted page / page with no body) so the
228
+ History tab next to it stays reachable in those states. -->
229
+ <template v-if="action === 'page'">
230
+ <div v-show="pageTab === PAGE_TAB.content" class="flex-1 overflow-y-auto flex flex-col">
231
+ <!-- Empty state: page does not exist. -->
232
+ <div v-if="!pageExists" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
233
+ <div class="text-center space-y-4">
234
+ <span class="material-icons text-4xl text-gray-300">article</span>
235
+ <p>{{ t("pluginWiki.emptyPage", { title: title }) }}</p>
236
+ <button
237
+ v-if="isStandaloneWikiRoute"
238
+ data-testid="wiki-create-page-button"
239
+ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
240
+ @click="requestCreatePage"
241
+ >
242
+ <span class="material-icons text-base">auto_fix_high</span>
243
+ {{ t("pluginWiki.createPage") }}
244
+ </button>
245
+ </div>
246
+ </div>
247
+ <!-- Empty state: page exists but has no body. -->
248
+ <div v-else-if="!content" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
249
+ <div class="text-center space-y-4">
250
+ <span class="material-icons text-4xl text-gray-300">article</span>
251
+ <p>{{ t("pluginWiki.emptyContent", { title: title }) }}</p>
252
+ <button
253
+ v-if="isStandaloneWikiRoute"
254
+ data-testid="wiki-update-page-button"
255
+ class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
256
+ @click="requestUpdatePage"
257
+ >
258
+ <span class="material-icons text-base">auto_fix_high</span>
259
+ {{ t("pluginWiki.updatePage") }}
260
+ </button>
261
+ </div>
262
+ </div>
263
+ <!-- Rendered markdown body. -->
264
+ <WikiPageBody
265
+ v-else
266
+ :body="mdDoc.body"
267
+ :base-dir="WIKI_BASE_DIR"
268
+ class="flex-1"
269
+ @task-checkbox-click="onTaskCheckboxClick"
270
+ @wiki-link-click="navigatePage"
271
+ @workspace-link-click="(path) => appApi.navigateToWorkspacePath(path)"
272
+ />
273
+ </div>
274
+ </template>
275
+
276
+ <!-- page-edit (#963) — single-pane snapshot render with
277
+ optional "snapshot expired" banner and a "page deleted"
278
+ placeholder when neither the snapshot nor the live page
279
+ survives. -->
280
+ <div v-else-if="action === 'page-edit'" ref="scrollRef" class="flex-1 overflow-y-auto">
281
+ <div
282
+ v-if="pageEditBanner"
283
+ class="mx-6 mt-4 rounded border border-amber-200 bg-amber-50 px-4 py-2 text-sm text-amber-700"
284
+ data-testid="wiki-page-edit-banner"
285
+ >
286
+ {{ pageEditBanner }}
287
+ </div>
288
+ <div v-if="pageEditDeleted" class="flex items-center justify-center text-gray-400 text-sm py-12" data-testid="wiki-page-edit-deleted">
289
+ <div class="text-center space-y-2">
290
+ <span class="material-icons text-4xl text-gray-300">delete</span>
291
+ <p>{{ t("pluginWiki.pageDeleted") }}</p>
292
+ </div>
293
+ </div>
294
+ <WikiPageBody
295
+ v-else-if="content"
296
+ :body="mdDoc.body"
297
+ :base-dir="WIKI_BASE_DIR"
298
+ @task-checkbox-click="onTaskCheckboxClick"
299
+ @wiki-link-click="navigatePage"
300
+ @workspace-link-click="(path) => appApi.navigateToWorkspacePath(path)"
301
+ />
302
+ </div>
303
+
304
+ <!-- Non-page action: log / lint_report — single-pane render. -->
305
+ <div v-else ref="scrollRef" class="flex-1 overflow-y-auto">
306
+ <WikiPageBody
307
+ :body="mdDoc.body"
308
+ :base-dir="WIKI_BASE_DIR"
309
+ @task-checkbox-click="onTaskCheckboxClick"
310
+ @wiki-link-click="navigatePage"
311
+ @workspace-link-click="(path) => appApi.navigateToWorkspacePath(path)"
312
+ />
313
+ </div>
314
+
315
+ <!-- History tab body (kept mounted across tab toggles for state
316
+ persistence, Q15=B). Mount whenever we have a slug — list /
317
+ detail still work even if the live page was deleted. -->
318
+ <HistoryTab
319
+ v-if="action === 'page' && currentSlugReactive !== null"
320
+ v-show="pageTab === PAGE_TAB.history"
321
+ :slug="currentSlugReactive"
322
+ :current-body="mdDoc.body"
323
+ :current-meta="mdDoc.meta"
324
+ @restored="handleRestored"
325
+ />
326
+ </template>
181
327
 
182
328
  <!-- Per-page chat composer (standalone /wiki route only). Sending
183
329
  spawns a fresh chat session with a prepended "read this page
184
330
  first" instruction — see AppApi.startNewChat. Hidden when
185
331
  WikiView is mounted as a manageWiki tool result inside /chat:
186
332
  the enclosing chat already has its own composer, and spawning
187
- a nested new session from there is confusing. -->
333
+ a nested new session from there is confusing. Also hidden on
334
+ the History tab (#944 Q11=C). -->
188
335
  <PageChatComposer
189
- v-if="action === 'page' && content && isStandaloneWikiRoute && currentSlug() !== null"
190
- :key="currentSlug() ?? ''"
336
+ v-if="action === 'page' && content && isStandaloneWikiRoute && currentSlugReactive !== null && pageTab === PAGE_TAB.content"
337
+ :key="currentSlugReactive ?? ''"
191
338
  :placeholder="t('pluginWiki.chatPlaceholder')"
192
- :prepend-text="`Before answering, read the wiki page at data/wiki/pages/${currentSlug()}.md.`"
339
+ :prepend-text="`Before answering, read the wiki page at ${WIKI_PAGES_DIR}/${currentSlugReactive}.md.`"
193
340
  test-id-prefix="wiki-page-chat"
194
341
  />
195
342
  </div>
@@ -199,32 +346,41 @@
199
346
  import { computed, nextTick, onMounted, ref, watch } from "vue";
200
347
  import { useRoute, useRouter, isNavigationFailure } from "vue-router";
201
348
  import { useI18n } from "vue-i18n";
202
- import { marked } from "marked";
203
349
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
204
- import type { WikiData, WikiPageEntry } from "./index";
205
- import { handleExternalLinkClick } from "../../utils/dom/externalLink";
206
- import { classifyWorkspacePath, resolveWikiHref } from "../../utils/path/workspaceLinkRouter";
350
+ import type { WikiData, WikiPageEntry, WikiEndpoints } from "./index";
207
351
  import { useFreshPluginData } from "../../composables/useFreshPluginData";
208
352
  import { usePdfDownload } from "../../composables/usePdfDownload";
209
353
  import { useAppApi } from "../../composables/useAppApi";
210
354
  import { buildPdfFilename } from "../../utils/files/filename";
211
- import { renderWikiLinks } from "./helpers";
212
355
  import PageChatComposer from "../../components/PageChatComposer.vue";
213
- import { BUILTIN_ROLE_IDS } from "../../config/roles";
214
- import { rewriteMarkdownImageRefs } from "../../utils/image/rewriteMarkdownImageRefs";
215
- import { extractFrontmatter } from "../../utils/format/frontmatter";
216
- import { findTaskLines, makeTasksInteractive, toggleTaskAt } from "../../utils/markdown/taskList";
356
+ import { pluginBuiltinRoleIds, pluginEndpoints, pluginPageRoute } from "../api";
357
+ import { parseFrontmatter } from "../../utils/markdown/frontmatter";
358
+ import { useMarkdownDoc } from "../../composables/useMarkdownDoc";
359
+ import { findTaskLines, toggleTaskAt } from "../../utils/markdown/taskList";
217
360
  import { apiPost } from "../../utils/api";
218
- import { API_ROUTES } from "../../config/apiRoutes";
219
- import { PAGE_ROUTES } from "../../router";
220
361
  import { WIKI_ACTION, WIKI_ROUTE_SECTION, buildWikiRouteParams, isSafeWikiSlug, readWikiRouteTarget, wikiActionFor, type WikiTarget } from "./route";
221
362
  import FilterChip from "../../components/FilterChip.vue";
363
+ import HistoryTab from "./history/HistoryTab.vue";
364
+ import WikiPageBody from "./components/WikiPageBody.vue";
365
+ import { loadPageEdit } from "./pageEditLoader";
366
+
367
+ const wikiEndpoints = pluginEndpoints<WikiEndpoints>("wiki");
368
+ const PAGE_WIKI = pluginPageRoute("wiki");
222
369
 
223
370
  type WikiTabView = typeof WIKI_ACTION.log | typeof WIKI_ACTION.lintReport;
224
371
 
372
+ // Workspace-relative wiki dirs. Centralised so future layout shifts
373
+ // (e.g. the prior `wiki/` → `data/wiki/` move) only need to change
374
+ // these two literals — all callers (image-ref rewriter, wiki-link
375
+ // resolver, agent-prompt strings, the page-chat prepend-text in
376
+ // the template above) derive from them.
377
+ const WIKI_PAGES_DIR = "data/wiki/pages";
378
+ const WIKI_DATA_DIR = "data/wiki";
379
+
225
380
  const route = useRoute();
226
381
  const router = useRouter();
227
382
  const { t } = useI18n();
383
+ const appApi = useAppApi();
228
384
 
229
385
  const props = defineProps<{
230
386
  selectedResult?: ToolResultComplete<WikiData>;
@@ -235,8 +391,21 @@ const emit = defineEmits<{ updateResult: [result: ToolResultComplete] }>();
235
391
  const action = ref(props.selectedResult?.data?.action ?? "index");
236
392
  const title = ref(props.selectedResult?.data?.title ?? "Wiki");
237
393
  const content = ref(props.selectedResult?.data?.content ?? "");
394
+ // Frontmatter view of the loaded page content. Drives the
395
+ // metadata bar (Created / Updated / Editor / Tags) above the
396
+ // rendered body. `useMarkdownDoc` is reactive so editing or
397
+ // switching pages re-derives without manual recomputation.
398
+ const mdDoc = useMarkdownDoc(content);
238
399
  const pageEntries = ref<WikiPageEntry[]>(props.selectedResult?.data?.pageEntries ?? []);
239
400
  const pageExists = ref(props.selectedResult?.data?.pageExists ?? true);
401
+ // `page-edit` action state (Stage 3a, #963). Populated when an LLM
402
+ // Write/Edit toolResult is mounted: `pageEditTs` is the snapshot's
403
+ // own timestamp (used in the header subtitle), `pageEditBanner` is
404
+ // shown only when the snapshot was gc'd and we fell back to the
405
+ // live page, and `pageEditDeleted` flips on when neither survives.
406
+ const pageEditTs = ref<string | null>(null);
407
+ const pageEditBanner = ref<string | null>(null);
408
+ const pageEditDeleted = ref(false);
240
409
  // View-local tag filter. Null = no filter. Not persisted to URL —
241
410
  // kept intentionally ephemeral so it doesn't leak into bookmarks
242
411
  // or the per-session stack history.
@@ -248,6 +417,47 @@ const selectedTag = ref<string | null>(null);
248
417
  // direct loads of /wiki and the fetch would never run.
249
418
  const navError = ref<string | null>(null);
250
419
 
420
+ // Per-page tab state for the Content / History switcher (#763 PR
421
+ // 3 / #944). Defaults to "content" on every page navigation
422
+ // (Q14=A) — the watcher on `currentSlugReactive` resets it. Within
423
+ // the same slug the History tab keeps its own selection state
424
+ // across toggles (Q15=B) because both tabs are kept mounted via
425
+ // v-show.
426
+ const PAGE_TAB = {
427
+ content: "content",
428
+ history: "history",
429
+ } as const;
430
+ type PageTab = (typeof PAGE_TAB)[keyof typeof PAGE_TAB];
431
+ const pageTab = ref<PageTab>(PAGE_TAB.content);
432
+ const restoreToastVisible = ref(false);
433
+ const RESTORE_TOAST_MS = 4000;
434
+ let restoreToastTimer: ReturnType<typeof setTimeout> | null = null;
435
+
436
+ // Computed slug used by the watcher and the template. Mirrors the
437
+ // imperative `currentSlug()` body — declared up here so the
438
+ // pageTab-reset watcher can pick up route + selectedResult changes
439
+ // uniformly without re-walking each call site that mutates the
440
+ // underlying state.
441
+ const currentSlugReactive = computed<string | null>(() => {
442
+ const raw =
443
+ route.name === PAGE_WIKI && route.params.section === WIKI_ROUTE_SECTION.pages && typeof route.params.slug === "string"
444
+ ? route.params.slug
445
+ : (props.selectedResult?.data?.pageName ?? null);
446
+ return isSafeWikiSlug(raw) ? raw : null;
447
+ });
448
+
449
+ watch(currentSlugReactive, (next, prev) => {
450
+ if (next === prev) return;
451
+ pageTab.value = PAGE_TAB.content;
452
+ // Drop any in-flight restore-success toast so it doesn't bleed
453
+ // onto a different page (codex iter-1 #946).
454
+ restoreToastVisible.value = false;
455
+ if (restoreToastTimer !== null) {
456
+ clearTimeout(restoreToastTimer);
457
+ restoreToastTimer = null;
458
+ }
459
+ });
460
+
251
461
  const { refresh, abort: abortFreshFetch } = useFreshPluginData<WikiData>({
252
462
  // Slug-aware: when the view is currently showing a specific page,
253
463
  // fetch that page by slug; otherwise fetch the index. Reads the
@@ -258,7 +468,7 @@ const { refresh, abort: abortFreshFetch } = useFreshPluginData<WikiData>({
258
468
  // and clobber the user's view (#775 / codex iter 2).
259
469
  endpoint: () => {
260
470
  const slug = action.value === "page" ? currentSlug() : null;
261
- return slug ? `${API_ROUTES.wiki.base}?slug=${encodeURIComponent(slug)}` : API_ROUTES.wiki.base;
471
+ return slug ? `${wikiEndpoints.base}?slug=${encodeURIComponent(slug)}` : wikiEndpoints.base;
262
472
  },
263
473
  extract: (json) => (json as { data?: WikiData }).data ?? null,
264
474
  apply: (data) => {
@@ -270,13 +480,38 @@ const { refresh, abort: abortFreshFetch } = useFreshPluginData<WikiData>({
270
480
  },
271
481
  });
272
482
 
483
+ function handleRestored(): void {
484
+ pageTab.value = PAGE_TAB.content;
485
+ restoreToastVisible.value = true;
486
+ if (restoreToastTimer !== null) clearTimeout(restoreToastTimer);
487
+ restoreToastTimer = setTimeout(() => {
488
+ restoreToastVisible.value = false;
489
+ restoreToastTimer = null;
490
+ }, RESTORE_TOAST_MS);
491
+ // Refresh the page content so the restored body shows up.
492
+ void refresh();
493
+ }
494
+
273
495
  onMounted(() => {
274
496
  // On /wiki, the route watcher below fires with `immediate: true` and
275
497
  // is the source of truth for the initial fetch (via POST callApi).
276
498
  // useFreshPluginData's mount fetch is GET-only and always returns
277
499
  // the index payload — if it resolves last, it clobbers log / lint /
278
500
  // page state. Cancel it here so the two can't race.
279
- if (route.name === PAGE_ROUTES.wiki) abortFreshFetch();
501
+ if (route.name === PAGE_WIKI) abortFreshFetch();
502
+ // page-edit toolResults source their content from the snapshot
503
+ // endpoint via loadPageEditData. Cancel the mount fetch (which
504
+ // targets /api/wiki) so it can't clobber state, and kick the
505
+ // loader directly — the selectedResult watcher only fires on
506
+ // subsequent uuid changes, not on the initial mount, so this is
507
+ // the only place to seed page-edit content (#963).
508
+ const data = props.selectedResult?.data;
509
+ if (data?.action === "page-edit") {
510
+ abortFreshFetch();
511
+ if (data.slug && data.stamp) {
512
+ void loadPageEditData(data.slug, data.stamp);
513
+ }
514
+ }
280
515
  });
281
516
 
282
517
  watch(
@@ -285,15 +520,46 @@ watch(
285
520
  const data = props.selectedResult?.data;
286
521
  if (data) {
287
522
  action.value = data.action ?? "index";
288
- title.value = data.title ?? "Wiki";
523
+ title.value = data.title ?? data.slug ?? "Wiki";
289
524
  content.value = data.content ?? "";
290
525
  pageEntries.value = data.pageEntries ?? [];
291
526
  pageExists.value = data.pageExists ?? true;
292
527
  }
528
+ // page-edit (Stage 3a #963): the toolResult only carries
529
+ // {slug, stamp, pagePath} pointers — fetch the snapshot body
530
+ // separately. Skip the generic refresh() that targets /api/wiki
531
+ // (it would overwrite the snapshot content with the live page).
532
+ if (data?.action === "page-edit" && data.slug && data.stamp) {
533
+ void loadPageEditData(data.slug, data.stamp);
534
+ return;
535
+ }
536
+ pageEditTs.value = null;
537
+ pageEditBanner.value = null;
538
+ pageEditDeleted.value = false;
293
539
  void refresh();
294
540
  },
295
541
  );
296
542
 
543
+ async function loadPageEditData(slug: string, stamp: string): Promise<void> {
544
+ pageEditTs.value = null;
545
+ pageEditBanner.value = null;
546
+ pageEditDeleted.value = false;
547
+ content.value = "";
548
+
549
+ const result = await loadPageEdit(slug, stamp);
550
+ if (result.kind === "snapshot") {
551
+ pageEditTs.value = result.ts;
552
+ content.value = result.content;
553
+ return;
554
+ }
555
+ if (result.kind === "current") {
556
+ pageEditBanner.value = t("pluginWiki.snapshotExpired");
557
+ content.value = result.content;
558
+ return;
559
+ }
560
+ pageEditDeleted.value = true;
561
+ }
562
+
297
563
  // URL is the single source of truth for wiki navigation. Button
298
564
  // handlers push to the router; this watcher drives callApi(). Only
299
565
  // runs when WikiView is mounted as the /wiki page — when mounted as
@@ -305,7 +571,7 @@ watch(
305
571
  // params are known-safe. `readWikiRouteTarget` returning `null` here
306
572
  // therefore means an unexpected shape — fall back to the index view.
307
573
  watch(
308
- () => (route.name === PAGE_ROUTES.wiki ? [route.params.section, route.params.slug] : null),
574
+ () => (route.name === PAGE_WIKI ? [route.params.section, route.params.slug] : null),
309
575
  (params) => {
310
576
  if (!params) return;
311
577
  const target = readWikiRouteTarget({ section: params[0], slug: params[1] }) ?? { kind: "index" };
@@ -323,13 +589,32 @@ watch(
323
589
  // tags stay in deterministic order. Singletons are dropped: a tag
324
590
  // used on a single page adds no filtering value, just visual noise.
325
591
  // Per-entry `#tag` chips still render every tag, so singletons stay
326
- // clickable from the row itself.
327
- const allTags = computed<[string, number][]>(() => {
592
+ // clickable from the row itself. Beyond singletons, the minimum count
593
+ // is raised adaptively so the chip row stays around TARGET_FILTER_CHIPS
594
+ // even on wikis with hundreds of pages — the cutoff is the count of
595
+ // the tag at the target position, which keeps tied-popularity tags
596
+ // grouped together rather than slicing them arbitrarily.
597
+ const TARGET_FILTER_CHIPS = 20;
598
+ // Full per-tag count map. Kept as its own computed (rather than
599
+ // folded into `allTags`) so the fallback chip below — rendered when
600
+ // the active filter is a tag the cutoff hides — can look up the
601
+ // real count instead of falling back to a hardcoded 1, which would
602
+ // understate the count of any non-singleton tag the adaptive cutoff
603
+ // drops from the chip row.
604
+ const tagCounts = computed<Map<string, number>>(() => {
328
605
  const counts = new Map<string, number>();
329
606
  for (const entry of pageEntries.value) {
330
607
  for (const tag of entry.tags ?? []) counts.set(tag, (counts.get(tag) ?? 0) + 1);
331
608
  }
332
- return [...counts.entries()].filter(([, count]) => count > 1).sort(([tagA, countA], [tagB, countB]) => countB - countA || tagA.localeCompare(tagB));
609
+ return counts;
610
+ });
611
+ const allTags = computed<[string, number][]>(() => {
612
+ const meaningful = [...tagCounts.value.entries()]
613
+ .filter(([, count]) => count > 1)
614
+ .sort(([tagA, countA], [tagB, countB]) => countB - countA || tagA.localeCompare(tagB));
615
+ if (meaningful.length <= TARGET_FILTER_CHIPS) return meaningful;
616
+ const [, cutoff] = meaningful[TARGET_FILTER_CHIPS - 1];
617
+ return meaningful.filter(([, count]) => count >= cutoff);
333
618
  });
334
619
 
335
620
  const visibleEntries = computed(() =>
@@ -352,13 +637,24 @@ function setTagFilter(tag: string) {
352
637
  selectedTag.value = tag;
353
638
  }
354
639
 
640
+ // Tag chips on the page metadata bar (#895 PR B) live in the
641
+ // `action === 'page'` view. Clicking one should jump to the
642
+ // filtered index — both navigating away from the page and
643
+ // pre-selecting the tag the user wants to explore. Without the
644
+ // navigation step the user would need a separate Back-to-index
645
+ // click to see the filter take effect.
646
+ function setTagFilterAndNavigate(tag: string) {
647
+ setTagFilter(tag);
648
+ navigate("index");
649
+ }
650
+
355
651
  // Spawn a new chat under the General role (which owns the wiki
356
652
  // tooling) regardless of the role the user is currently viewing the
357
653
  // wiki under. "lint my wiki" is a direct instruction to the agent,
358
654
  // not a tool call — the agent decides how to run the lint and
359
655
  // report back.
360
656
  function startLintChat() {
361
- appApi.startNewChat("lint my wiki", BUILTIN_ROLE_IDS.general);
657
+ appApi.startNewChat("lint my wiki", pluginBuiltinRoleIds().general);
362
658
  }
363
659
 
364
660
  // Clear the filter whenever we leave the index view — otherwise
@@ -379,27 +675,76 @@ watch(content, async () => {
379
675
  });
380
676
 
381
677
  /** Base directory for wiki content, adjusted by the current view. */
382
- const WIKI_BASE_DIR = computed(() => (action.value === "page" ? "data/wiki/pages" : "data/wiki"));
383
-
384
- const renderedContent = computed(() => {
385
- if (!content.value) return "";
386
- // Strip YAML frontmatter before rendering marked doesn't parse
387
- // it, so the `---` fences turn into <hr>s and the inner keys
388
- // render as plain text (title / created / updated / tags / source).
389
- const body = extractFrontmatter(content.value).body;
390
- if (!body) return "";
391
- // Rewrite workspace-relative image refs (`![alt](images/foo.png)`)
392
- // to `/api/files/raw?path=...` BEFORE marked parses them without
393
- // this, the browser tries to fetch against the SPA route URL
394
- // (/chat/…/images/foo.png) and 404s. Reuse WIKI_BASE_DIR so a
395
- // page's `../images/foo.png` resolves under `data/wiki/`.
396
- const withImages = rewriteMarkdownImageRefs(body, WIKI_BASE_DIR.value);
397
- // Strip marked's `disabled=""` from GFM task checkboxes and tag
398
- // them with `class="md-task"` so `handleContentClick` can find
399
- // them via DOM delegation (#775). Other view modes (index / log /
400
- // lint_report) get the same transform it's a no-op when no
401
- // checkboxes are present.
402
- return makeTasksInteractive(marked.parse(renderWikiLinks(withImages)) as string);
678
+ const WIKI_BASE_DIR = computed(() => (action.value === "page" || action.value === "page-edit" ? WIKI_PAGES_DIR : WIKI_DATA_DIR));
679
+
680
+ // ── Metadata bar (#895 PR B) ──────────────────────────────────
681
+ //
682
+ // Show a single thin row above the rendered body with
683
+ // `Created` / `Updated` / `Editor` / `Tags` derived from the
684
+ // frontmatter. Hidden when none of those are present (header-less
685
+ // pages render unchanged so old wiki content keeps its current
686
+ // appearance).
687
+
688
+ /** String accessor that survives the `unknown` type from FAILSAFE
689
+ * YAML `meta` values are all strings under FAILSAFE schema, but
690
+ * type-narrowing requires a runtime check. */
691
+ function metaString(value: unknown): string | null {
692
+ if (typeof value !== "string" || value.length === 0) return null;
693
+ return value;
694
+ }
695
+
696
+ /** Array-of-strings accessor for `tags`. Allows the chips template
697
+ * to skip a render branch when the field is missing or malformed. */
698
+ function metaStringArray(value: unknown): string[] {
699
+ if (!Array.isArray(value)) return [];
700
+ return value.filter((item): item is string => typeof item === "string");
701
+ }
702
+
703
+ const pageMeta = computed(() => ({
704
+ created: metaString(mdDoc.value.meta.created),
705
+ updated: metaString(mdDoc.value.meta.updated),
706
+ editor: metaString(mdDoc.value.meta.editor),
707
+ tags: metaStringArray(mdDoc.value.meta.tags),
708
+ }));
709
+
710
+ const hasPageMeta = computed(() => {
711
+ const meta = pageMeta.value;
712
+ return meta.created !== null || meta.updated !== null || meta.editor !== null || meta.tags.length > 0;
713
+ });
714
+
715
+ /** Render `updated` ISO timestamp as `YYYY-MM-DD HH:MM` in the
716
+ * user's local timezone. The on-disk value is UTC ISO
717
+ * (`2026-04-27T14:32:56.789Z`) — showing the raw `14:32` would
718
+ * read like local wall time on a non-UTC machine and mislead
719
+ * the user (codex review iter-1 #905). Falls back to the raw
720
+ * value if it doesn't parse as a Date (defensive — user-supplied
721
+ * frontmatter may have any string here). */
722
+ function formatUpdated(raw: string): string {
723
+ const parsed = new Date(raw);
724
+ if (Number.isNaN(parsed.getTime())) return raw;
725
+ // `sv-SE` locale gives ISO-like `YYYY-MM-DD HH:MM` (with a
726
+ // space, no `T`) which matches the original format intent.
727
+ // `hour12: false` defends against locales that would otherwise
728
+ // emit AM/PM.
729
+ return new Intl.DateTimeFormat("sv-SE", {
730
+ year: "numeric",
731
+ month: "2-digit",
732
+ day: "2-digit",
733
+ hour: "2-digit",
734
+ minute: "2-digit",
735
+ hour12: false,
736
+ }).format(parsed);
737
+ }
738
+
739
+ // Header subtitle for the page-edit action. "Wiki edit · {slug} ·
740
+ // {timestamp}" so the user immediately sees this is a moment-in-
741
+ // time view, not the live page. `formatUpdated` re-uses the same
742
+ // `YYYY-MM-DD HH:MM` shape as the metadata bar.
743
+ const displayTitle = computed(() => {
744
+ if (action.value !== "page-edit") return title.value;
745
+ const stamp = pageEditTs.value;
746
+ const prefix = `${t("pluginWiki.pageEditHeader")} · ${title.value}`;
747
+ return stamp ? `${prefix} · ${formatUpdated(stamp)}` : prefix;
403
748
  });
404
749
 
405
750
  const { pdfDownloading, pdfError, downloadPdf: rawDownloadPdf } = usePdfDownload();
@@ -411,7 +756,12 @@ async function downloadPdf() {
411
756
  fallback: "wiki",
412
757
  timestampMs: uuid ? appApi.getResultTimestamp(uuid) : undefined,
413
758
  });
414
- await rawDownloadPdf(content.value, filename);
759
+ // Wiki pages live under data/wiki/pages/ — pass the source dir so
760
+ // the server resolves relative `<img>` refs (`../../../artifacts/...`)
761
+ // against the same base the browser uses. Wiki pages always carry
762
+ // a frontmatter envelope (#895), so opt in to stripping it from the
763
+ // PDF output.
764
+ await rawDownloadPdf(content.value, filename, { baseDir: "data/wiki/pages", stripFrontmatter: true });
415
765
  }
416
766
 
417
767
  async function callApi(body: Record<string, unknown>) {
@@ -424,7 +774,7 @@ async function callApi(body: Record<string, unknown>) {
424
774
  pageEntries?: WikiPageEntry[];
425
775
  pageExists?: boolean;
426
776
  };
427
- }>(API_ROUTES.wiki.base, body);
777
+ }>(wikiEndpoints.base, body);
428
778
  if (!response.ok) {
429
779
  navError.value = response.status === 0 ? response.error : `Wiki API error ${response.status}: ${response.error}`;
430
780
  return;
@@ -446,7 +796,7 @@ async function callApi(body: Record<string, unknown>) {
446
796
  }
447
797
 
448
798
  function pushWiki(target: WikiTarget) {
449
- router.push({ name: PAGE_ROUTES.wiki, params: buildWikiRouteParams(target) }).catch((err: unknown) => {
799
+ router.push({ name: PAGE_WIKI, params: buildWikiRouteParams(target) }).catch((err: unknown) => {
450
800
  if (!isNavigationFailure(err)) {
451
801
  console.error("[wiki] navigation failed:", err);
452
802
  }
@@ -462,26 +812,28 @@ function navigatePage(pageName: string) {
462
812
  }
463
813
 
464
814
  // --- Per-page chat composer ---
465
- const appApi = useAppApi();
815
+ // (`appApi` itself is hoisted to the top of <script setup> alongside
816
+ // route/router/t so the lint-by-line analysis is happy with earlier
817
+ // uses in `startLintChat` etc.)
466
818
 
467
- const isStandaloneWikiRoute = computed(() => route.name === PAGE_ROUTES.wiki);
819
+ const isStandaloneWikiRoute = computed(() => route.name === PAGE_WIKI);
468
820
 
469
- // Always route wiki create/update CTAs through BUILTIN_ROLE_IDS.general
821
+ // Always route wiki create/update CTAs through pluginBuiltinRoleIds().general
470
822
  // (the wiki-capable role) so the new chat has the tools needed to
471
823
  // actually write the page. Omitting the role would fall through to
472
824
  // `currentRoleId`, which could be anything — including roles without
473
825
  // wiki tooling — and silently produce useless sessions.
474
826
  function requestCreatePage() {
475
827
  appApi.startNewChat(
476
- `Create a wiki page about ${JSON.stringify(title.value)}. Research the topic and write a comprehensive article in data/wiki/pages/.`,
477
- BUILTIN_ROLE_IDS.general,
828
+ `Create a wiki page about ${JSON.stringify(title.value)}. Research the topic and write a comprehensive article in ${WIKI_PAGES_DIR}/.`,
829
+ pluginBuiltinRoleIds().general,
478
830
  );
479
831
  }
480
832
 
481
833
  function requestUpdatePage() {
482
834
  appApi.startNewChat(
483
- `Update the existing wiki page about ${JSON.stringify(title.value)}. The page file exists but has no content. Research the topic and write a comprehensive article in data/wiki/pages/.`,
484
- BUILTIN_ROLE_IDS.general,
835
+ `Update the existing wiki page about ${JSON.stringify(title.value)}. The page file exists but has no content. Research the topic and write a comprehensive article in ${WIKI_PAGES_DIR}/.`,
836
+ pluginBuiltinRoleIds().general,
485
837
  );
486
838
  }
487
839
 
@@ -493,7 +845,7 @@ function currentSlug(): string | null {
493
845
  // standalone /wiki URLs, but the tool-result payload arrives from
494
846
  // the server/agent and can't assume that upstream filter.
495
847
  const raw =
496
- route.name === PAGE_ROUTES.wiki && route.params.section === WIKI_ROUTE_SECTION.pages && typeof route.params.slug === "string"
848
+ route.name === PAGE_WIKI && route.params.section === WIKI_ROUTE_SECTION.pages && typeof route.params.slug === "string"
497
849
  ? route.params.slug
498
850
  : (props.selectedResult?.data?.pageName ?? null);
499
851
  return isSafeWikiSlug(raw) ? raw : null;
@@ -525,7 +877,7 @@ async function persistWikiPage(pageName: string, newContent: string, generation:
525
877
  // params) and the tool-result-embedded view (selectedResult).
526
878
  if (currentSlug() !== pageName) return;
527
879
 
528
- const response = await apiPost<{ data?: { content?: string } }>(API_ROUTES.wiki.base, {
880
+ const response = await apiPost<{ data?: { content?: string } }>(wikiEndpoints.base, {
529
881
  action: WIKI_ACTION.save,
530
882
  pageName,
531
883
  content: newContent,
@@ -556,8 +908,8 @@ async function persistWikiPage(pageName: string, newContent: string, generation:
556
908
  // `prefix + body` round-trips byte-for-byte regardless of
557
909
  // frontmatter shape — the body length is always exact.
558
910
  function splitFrontmatter(): { prefix: string; body: string } {
559
- const frontmatter = extractFrontmatter(content.value);
560
- const body = frontmatter.body;
911
+ const parsed = parseFrontmatter(content.value);
912
+ const { body } = parsed;
561
913
  const prefix = content.value.slice(0, content.value.length - body.length);
562
914
  return { prefix, body };
563
915
  }
@@ -624,45 +976,6 @@ function onTaskCheckboxClick(event: MouseEvent, target: HTMLInputElement): void
624
976
  // `navError` inside `persistWikiPage`'s `!response.ok` branch.
625
977
  taskPersistChain = taskPersistChain.then(() => persistWikiPage(pageName, newContent, generation)).catch(() => undefined);
626
978
  }
627
-
628
- function handleContentClick(event: MouseEvent) {
629
- // 0. GFM task checkbox toggle (#775). Tagged by `makeTasksInteractive`
630
- // on the rendered HTML; only meaningful while we're showing a
631
- // page body. Index / log / lint_report views never carry user
632
- // content to write back.
633
- const target = event.target as HTMLElement;
634
- if (target instanceof HTMLInputElement && target.type === "checkbox" && target.classList.contains("md-task")) {
635
- onTaskCheckboxClick(event, target);
636
- return;
637
- }
638
- // 1. Internal wiki links: `[[Page Name]]` was rewritten to a
639
- // `<span class="wiki-link">` during markdown pre-processing,
640
- // so it doesn't overlap with regular `<a>` handling.
641
- const link = target.closest(".wiki-link") as HTMLElement | null;
642
- if (link?.dataset.page) {
643
- navigatePage(link.dataset.page);
644
- return;
645
- }
646
- // 2. External http(s) links in the rendered markdown body: open
647
- // in a new tab so clicking them doesn't navigate the whole
648
- // SPA away from MulmoClaude. Same-origin and non-http links
649
- // (mailto:, tel:, anchors) fall through to the browser default.
650
- if (handleExternalLinkClick(event)) return;
651
- // 3. Workspace-internal links: resolve relative paths against the
652
- // wiki content's filesystem location and route to the appropriate view.
653
- // Skip modifier-key clicks and middle clicks so the browser's
654
- // "open in new tab" behaviour is preserved.
655
- if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey) return;
656
- const anchor = target.closest("a");
657
- if (!anchor) return;
658
- const href = anchor.getAttribute("href");
659
- if (!href || href.startsWith("#")) return;
660
- const resolved = resolveWikiHref(href, WIKI_BASE_DIR.value);
661
- if (classifyWorkspacePath(resolved)) {
662
- event.preventDefault();
663
- appApi.navigateToWorkspacePath(resolved);
664
- }
665
- }
666
979
  </script>
667
980
 
668
981
  <style scoped>
@@ -682,108 +995,4 @@ function handleContentClick(event: MouseEvent) {
682
995
  background-color: #dbeafe;
683
996
  color: #1d4ed8;
684
997
  }
685
- .wiki-content :deep(.wiki-link) {
686
- color: #2563eb;
687
- cursor: pointer;
688
- text-decoration: underline;
689
- text-decoration-style: dotted;
690
- }
691
- .wiki-content :deep(.wiki-link:hover) {
692
- text-decoration-style: solid;
693
- }
694
- .wiki-content :deep(h1) {
695
- font-size: 1.5rem;
696
- font-weight: 700;
697
- margin-top: 1.5rem;
698
- margin-bottom: 0.75rem;
699
- color: #111827;
700
- }
701
- .wiki-content :deep(h1:first-child),
702
- .wiki-content :deep(h2:first-child),
703
- .wiki-content :deep(h3:first-child),
704
- .wiki-content :deep(p:first-child) {
705
- margin-top: 0;
706
- }
707
- .wiki-content :deep(h2) {
708
- font-size: 1.2rem;
709
- font-weight: 600;
710
- margin-top: 1.25rem;
711
- margin-bottom: 0.5rem;
712
- color: #1f2937;
713
- border-bottom: 1px solid #e5e7eb;
714
- padding-bottom: 0.25rem;
715
- }
716
- .wiki-content :deep(h3) {
717
- font-size: 1rem;
718
- font-weight: 600;
719
- margin-top: 1rem;
720
- margin-bottom: 0.5rem;
721
- color: #374151;
722
- }
723
- .wiki-content :deep(p) {
724
- margin-bottom: 0.75rem;
725
- line-height: 1.6;
726
- color: #374151;
727
- }
728
- .wiki-content :deep(ul),
729
- .wiki-content :deep(ol) {
730
- margin-left: 1.5rem;
731
- margin-bottom: 0.75rem;
732
- }
733
- .wiki-content :deep(li) {
734
- margin-bottom: 0.25rem;
735
- line-height: 1.5;
736
- color: #374151;
737
- }
738
- .wiki-content :deep(ul) {
739
- list-style-type: disc;
740
- }
741
- .wiki-content :deep(ol) {
742
- list-style-type: decimal;
743
- }
744
- .wiki-content :deep(hr) {
745
- border: none;
746
- border-top: 1px solid #e5e7eb;
747
- margin: 1rem 0;
748
- }
749
- .wiki-content :deep(code) {
750
- background: #f3f4f6;
751
- padding: 0.1rem 0.3rem;
752
- border-radius: 0.25rem;
753
- font-size: 0.85em;
754
- font-family: monospace;
755
- }
756
- .wiki-content :deep(pre) {
757
- background: #f3f4f6;
758
- padding: 0.75rem;
759
- border-radius: 0.375rem;
760
- overflow-x: auto;
761
- margin-bottom: 0.75rem;
762
- }
763
- .wiki-content :deep(pre code) {
764
- background: none;
765
- padding: 0;
766
- }
767
- .wiki-content :deep(blockquote) {
768
- border-left: 3px solid #d1d5db;
769
- padding-left: 1rem;
770
- color: #6b7280;
771
- margin: 0.75rem 0;
772
- }
773
- .wiki-content :deep(table) {
774
- border-collapse: collapse;
775
- width: 100%;
776
- margin-bottom: 0.75rem;
777
- font-size: 0.875rem;
778
- }
779
- .wiki-content :deep(th),
780
- .wiki-content :deep(td) {
781
- border: 1px solid #e5e7eb;
782
- padding: 0.5rem 0.75rem;
783
- text-align: left;
784
- }
785
- .wiki-content :deep(th) {
786
- background: #f9fafb;
787
- font-weight: 600;
788
- }
789
998
  </style>