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.
- package/Dockerfile.sandbox +100 -0
- package/README.md +17 -4
- package/bin/mulmoclaude.js +46 -15
- package/bin/prepare-dist.js +18 -2
- package/client/assets/chunk-CernVdwh.js +1 -0
- package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
- package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
- package/client/assets/index-BbgSjFQ8.js +4968 -0
- package/client/assets/index-ECD0lgIv.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
- package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
- package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
- package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
- package/client/assets/vue-1e_vz2LW.js +1 -0
- package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
- package/client/index.html +33 -2
- package/package.json +20 -18
- package/sandbox-entrypoint.sh +106 -0
- package/server/accounting/accountNormalize.ts +32 -0
- package/server/accounting/defaultAccounts.ts +87 -0
- package/server/accounting/eventPublisher.ts +51 -0
- package/server/accounting/journal.ts +252 -0
- package/server/accounting/openingBalances.ts +114 -0
- package/server/accounting/report.ts +237 -0
- package/server/accounting/service.ts +718 -0
- package/server/accounting/snapshotCache.ts +333 -0
- package/server/accounting/timeSeries.ts +265 -0
- package/server/accounting/types.ts +148 -0
- package/server/agent/activeTools.ts +128 -0
- package/server/agent/attachmentConverter.ts +10 -5
- package/server/agent/backend/claude-code.ts +8 -2
- package/server/agent/backend/types.ts +1 -1
- package/server/agent/config.ts +101 -31
- package/server/agent/index.ts +45 -33
- package/server/agent/mcp-server.ts +146 -69
- package/server/agent/mcp-tools/index.ts +1 -5
- package/server/agent/mcp-tools/notify.ts +2 -22
- package/server/agent/mcp-tools/x.ts +0 -4
- package/server/agent/mcpHealth.ts +168 -0
- package/server/agent/plugin-names.ts +20 -77
- package/server/agent/prompt.ts +259 -51
- package/server/agent/resumeFailover.ts +1 -1
- package/server/agent/stream.ts +0 -1
- package/server/api/auth/bearerAuth.ts +5 -5
- package/server/api/csrfGuard.ts +1 -1
- package/server/api/routes/accounting.ts +366 -0
- package/server/api/routes/agent.ts +509 -46
- package/server/api/routes/attachment.ts +104 -0
- package/server/api/routes/chart.ts +2 -1
- package/server/api/routes/config.ts +12 -12
- package/server/api/routes/files.ts +105 -48
- package/server/api/routes/image.ts +70 -25
- package/server/api/routes/journal.ts +35 -0
- package/server/api/routes/mulmo-script.ts +358 -118
- package/server/api/routes/mulmoScriptValidate.ts +1 -1
- package/server/api/routes/news.ts +1 -1
- package/server/api/routes/notifications.ts +92 -22
- package/server/api/routes/notifier.ts +98 -0
- package/server/api/routes/pdf.ts +188 -48
- package/server/api/routes/plugins.ts +34 -14
- package/server/api/routes/presentHtml.ts +58 -3
- package/server/api/routes/roles.ts +1 -8
- package/server/api/routes/runtime-plugin.ts +224 -0
- package/server/api/routes/scheduler.ts +7 -5
- package/server/api/routes/schedulerHandlers.ts +1 -1
- package/server/api/routes/schedulerTasks.ts +8 -7
- package/server/api/routes/sessions.ts +234 -121
- package/server/api/routes/skills.ts +56 -51
- package/server/api/routes/sources.ts +52 -45
- package/server/api/routes/translation.ts +44 -0
- package/server/api/routes/wiki/frontmatter.ts +13 -65
- package/server/api/routes/wiki/history.ts +261 -0
- package/server/api/routes/wiki/pageIndex.ts +1 -1
- package/server/api/routes/wiki.ts +50 -26
- package/server/events/file-change.ts +83 -0
- package/server/events/notifications.ts +247 -91
- package/server/events/pub-sub/index.ts +1 -1
- package/server/events/relay-client.ts +5 -5
- package/server/events/scheduler-adapter.ts +2 -2
- package/server/events/session-store/index.ts +110 -22
- package/server/events/task-manager/index.ts +10 -9
- package/server/index.ts +509 -33
- package/server/notifier/engine.ts +419 -0
- package/server/notifier/legacy-adapters.ts +76 -0
- package/server/notifier/runtime-api.ts +74 -0
- package/server/notifier/store.ts +70 -0
- package/server/notifier/types.ts +121 -0
- package/server/plugins/dev-loader.ts +171 -0
- package/server/plugins/dev-watcher.ts +150 -0
- package/server/plugins/diagnostics.ts +188 -0
- package/server/plugins/preset-list.ts +52 -0
- package/server/plugins/preset-loader.ts +112 -0
- package/server/plugins/runtime-chat-api.ts +38 -0
- package/server/plugins/runtime-loader.ts +430 -0
- package/server/plugins/runtime-registry.ts +112 -0
- package/server/plugins/runtime-tasks-api.ts +50 -0
- package/server/plugins/runtime.ts +378 -0
- package/server/services/translation/cache.ts +72 -0
- package/server/services/translation/index.ts +106 -0
- package/server/services/translation/llm.ts +140 -0
- package/server/services/translation/types.ts +35 -0
- package/server/system/credentials.ts +13 -2
- package/server/system/env.ts +6 -1
- package/server/system/logger/formatters.ts +46 -4
- package/server/system/logger/index.ts +4 -4
- package/server/system/logger/sinks.ts +26 -5
- package/server/system/logger/types.ts +2 -2
- package/server/utils/dev-plugin-args.d.mts +11 -0
- package/server/utils/dev-plugin-args.mjs +43 -0
- package/server/utils/errors.ts +13 -4
- package/server/utils/files/accounting-io.ts +295 -0
- package/server/utils/files/atomic.ts +17 -49
- package/server/utils/files/attachment-store.ts +182 -0
- package/server/utils/files/html-io.ts +1 -7
- package/server/utils/files/html-store.ts +19 -0
- package/server/utils/files/image-store.ts +20 -22
- package/server/utils/files/index.ts +5 -15
- package/server/utils/files/journal-io.ts +7 -35
- package/server/utils/files/json.ts +2 -29
- package/server/utils/files/markdown-image-fill.ts +6 -37
- package/server/utils/files/markdown-store.ts +6 -21
- package/server/utils/files/naming.ts +3 -39
- package/server/utils/files/plugins-io.ts +100 -0
- package/server/utils/files/reference-dirs-io.ts +1 -9
- package/server/utils/files/roles-io.ts +2 -10
- package/server/utils/files/safe.ts +17 -19
- package/server/utils/files/scheduler-io.ts +1 -7
- package/server/utils/files/scheduler-overrides-io.ts +3 -12
- package/server/utils/files/session-io.ts +21 -30
- package/server/utils/files/spreadsheet-store.ts +9 -22
- package/server/utils/files/translation-io.ts +46 -0
- package/server/utils/files/user-tasks-io.ts +1 -7
- package/server/utils/files/workspace-io.ts +3 -79
- package/server/utils/gemini.ts +33 -11
- package/server/utils/html/htmlArtifactSplicer.ts +41 -0
- package/server/utils/markdown/frontmatter.ts +112 -0
- package/server/utils/regex.ts +56 -0
- package/server/utils/router.ts +41 -0
- package/server/utils/slug.ts +5 -3
- package/server/utils/time.ts +12 -0
- package/server/workspace/chat-index/indexer.ts +15 -2
- package/server/workspace/chat-index/summarizer.ts +1 -1
- package/server/workspace/custom-dirs.ts +1 -1
- package/server/workspace/helps/gemini.md +1 -1
- package/server/workspace/helps/guide.md +61 -0
- package/server/workspace/helps/index.md +4 -0
- package/server/workspace/helps/presenthtml.md +80 -0
- package/server/workspace/helps/sandbox.md +7 -0
- package/server/workspace/helps/storyteller.md +101 -0
- package/server/workspace/helps/telegram.md +1 -0
- package/server/workspace/helps/wiki.md +9 -7
- package/server/workspace/journal/archivist-cli.ts +7 -33
- package/server/workspace/journal/archivist-schemas.ts +5 -43
- package/server/workspace/journal/dailyPass.ts +34 -187
- package/server/workspace/journal/diff.ts +3 -28
- package/server/workspace/journal/index.ts +10 -81
- package/server/workspace/journal/indexFile.ts +3 -24
- package/server/workspace/journal/latestDaily.ts +51 -0
- package/server/workspace/journal/memoryExtractor.ts +4 -20
- package/server/workspace/journal/optimizationPass.ts +4 -21
- package/server/workspace/journal/paths.ts +4 -23
- package/server/workspace/journal/state.ts +6 -29
- package/server/workspace/memory/io.ts +213 -0
- package/server/workspace/memory/llm-classifier.ts +158 -0
- package/server/workspace/memory/migrate.ts +263 -0
- package/server/workspace/memory/run.ts +84 -0
- package/server/workspace/memory/topic-cluster.ts +218 -0
- package/server/workspace/memory/topic-detect.ts +67 -0
- package/server/workspace/memory/topic-index-hook.ts +128 -0
- package/server/workspace/memory/topic-io.ts +180 -0
- package/server/workspace/memory/topic-migrate.ts +248 -0
- package/server/workspace/memory/topic-run.ts +172 -0
- package/server/workspace/memory/topic-swap.ts +135 -0
- package/server/workspace/memory/topic-types.ts +142 -0
- package/server/workspace/memory/types.ts +83 -0
- package/server/workspace/news/reader.ts +4 -5
- package/server/workspace/paths.ts +124 -47
- package/server/workspace/roles.ts +2 -11
- package/server/workspace/skills/parser.ts +38 -55
- package/server/workspace/skills/user-tasks.ts +1 -2
- package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
- package/server/workspace/skills-preset.ts +196 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
- package/server/workspace/sources/fetchers/index.ts +1 -1
- package/server/workspace/sources/fetchers/rssParser.ts +1 -1
- package/server/workspace/sources/pipeline/index.ts +2 -2
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/write.ts +2 -2
- package/server/workspace/sources/registry.ts +39 -61
- package/server/workspace/sources/robots.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +2 -1
- package/server/workspace/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +6 -1
- package/server/workspace/wiki-backlinks/index.ts +19 -7
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
- package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
- package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
- package/server/workspace/wiki-history/provision.ts +181 -0
- package/server/workspace/wiki-pages/io.ts +217 -0
- package/server/workspace/wiki-pages/snapshot.ts +380 -0
- package/server/workspace/workspace.ts +75 -13
- package/src/App.vue +115 -40
- package/src/_runtime/protocol-vue.ts +21 -0
- package/src/_runtime/vue.ts +22 -0
- package/src/components/ChatInput.vue +14 -10
- package/src/components/CopyChatButton.vue +76 -0
- package/src/components/FileContentRenderer.vue +67 -14
- package/src/components/FileTree.vue +2 -2
- package/src/components/FilesView.vue +17 -1
- package/src/components/NewsView.vue +16 -2
- package/src/components/NotificationBell.vue +320 -93
- package/src/components/PageChatComposer.vue +5 -4
- package/src/components/PluginLauncher.vue +42 -6
- package/src/components/PluginScopedRoot.vue +87 -0
- package/src/components/RoleSelector.vue +12 -1
- package/src/components/RolesView.vue +562 -0
- package/src/components/SentAttachmentChip.vue +102 -0
- package/src/components/SessionHistoryPanel.vue +109 -20
- package/src/components/SessionRoleIcon.vue +7 -4
- package/src/components/SessionSidebar.vue +20 -7
- package/src/components/SessionTabBar.vue +1 -1
- package/src/components/SettingsMcpTab.vue +4 -4
- package/src/components/SettingsModal.vue +2 -0
- package/src/components/SidebarHeader.vue +16 -5
- package/src/components/SourcesManager.vue +23 -9
- package/src/components/SourcesView.vue +1 -1
- package/src/components/StackView.vue +102 -6
- package/src/components/SuggestionsPanel.vue +105 -16
- package/src/components/SystemFileBanner.vue +1 -1
- package/src/components/TodoExplorer.vue +4 -5
- package/src/components/todo/TodoAddDialog.vue +2 -3
- package/src/components/todo/TodoEditDialog.vue +1 -2
- package/src/components/todo/TodoEditPanel.vue +2 -3
- package/src/components/todo/TodoKanbanView.vue +8 -5
- package/src/components/todo/TodoListView.vue +3 -5
- package/src/components/todo/TodoTableView.vue +7 -5
- package/src/composables/useAccountingChannel.ts +58 -0
- package/src/composables/useActiveSession.ts +4 -25
- package/src/composables/useAppApi.ts +6 -44
- package/src/composables/useClipboardCopy.ts +3 -20
- package/src/composables/useContentDisplay.ts +33 -2
- package/src/composables/useDevPluginReload.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +5 -31
- package/src/composables/useEventListeners.ts +0 -20
- package/src/composables/useExpandedDirs.ts +4 -15
- package/src/composables/useFaviconState.ts +12 -46
- package/src/composables/useFileChange.ts +53 -0
- package/src/composables/useFreshPluginData.ts +6 -43
- package/src/composables/useHealth.ts +14 -43
- package/src/composables/useImageErrorRepair.ts +104 -0
- package/src/composables/useLatestDaily.ts +40 -0
- package/src/composables/useMarkdownDoc.ts +39 -0
- package/src/composables/useMarkdownLinkHandler.ts +1 -1
- package/src/composables/useMcpTools.ts +3 -16
- package/src/composables/useNotifications.ts +138 -112
- package/src/composables/usePdfDownload.ts +17 -3
- package/src/composables/usePendingCalls.ts +8 -26
- package/src/composables/usePluginErrorBoundary.ts +68 -0
- package/src/composables/usePubSub.ts +9 -17
- package/src/composables/useRunElapsed.ts +5 -22
- package/src/composables/useSandboxStatus.ts +4 -20
- package/src/composables/useSessionDerived.ts +7 -15
- package/src/composables/useSessionHistory.ts +70 -29
- package/src/composables/useSessionSync.ts +25 -3
- package/src/composables/useSkillsList.ts +59 -0
- package/src/composables/useTranslatedQueries.ts +109 -0
- package/src/config/apiRoutes.ts +181 -80
- package/src/config/historyFilters.ts +5 -3
- package/src/config/hostEvents.ts +17 -0
- package/src/config/mcpCatalog.ts +277 -5
- package/src/config/pubsubChannels.ts +134 -12
- package/src/config/roles.ts +212 -147
- package/src/config/systemFileDescriptors.ts +5 -5
- package/src/config/toolNames.ts +52 -30
- package/src/config/workspacePaths.ts +26 -2
- package/src/lang/de.ts +483 -27
- package/src/lang/en.ts +448 -27
- package/src/lang/es.ts +474 -27
- package/src/lang/fr.ts +476 -27
- package/src/lang/ja.ts +465 -27
- package/src/lang/ko.ts +466 -27
- package/src/lang/pt-BR.ts +473 -27
- package/src/lang/zh.ts +463 -27
- package/src/lib/vue-i18n.ts +1 -1
- package/src/lib/wiki-page/slug.ts +66 -0
- package/src/main.ts +85 -0
- package/src/plugins/_extras.ts +58 -0
- package/src/plugins/_generated/metas.ts +42 -0
- package/src/plugins/_generated/registrations.ts +44 -0
- package/src/plugins/_generated/server-bindings.ts +47 -0
- package/src/plugins/accounting/Preview.vue +106 -0
- package/src/plugins/accounting/View.vue +632 -0
- package/src/plugins/accounting/actions.ts +34 -0
- package/src/plugins/accounting/api.ts +301 -0
- package/src/plugins/accounting/components/AccountEditor.vue +250 -0
- package/src/plugins/accounting/components/AccountRow.vue +50 -0
- package/src/plugins/accounting/components/AccountsList.vue +102 -0
- package/src/plugins/accounting/components/AccountsModal.vue +300 -0
- package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
- package/src/plugins/accounting/components/BookSettings.vue +284 -0
- package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
- package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
- package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
- package/src/plugins/accounting/components/JournalList.vue +553 -0
- package/src/plugins/accounting/components/Ledger.vue +206 -0
- package/src/plugins/accounting/components/NewBookForm.vue +211 -0
- package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
- package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
- package/src/plugins/accounting/components/accountDraft.ts +13 -0
- package/src/plugins/accounting/components/accountNumbering.ts +103 -0
- package/src/plugins/accounting/components/accountValidation.ts +75 -0
- package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
- package/src/plugins/accounting/countries.ts +158 -0
- package/src/plugins/accounting/currencies.ts +64 -0
- package/src/plugins/accounting/dates.ts +51 -0
- package/src/plugins/accounting/definition.ts +199 -0
- package/src/plugins/accounting/fiscalYear.ts +136 -0
- package/src/plugins/accounting/index.ts +49 -0
- package/src/plugins/accounting/meta.ts +91 -0
- package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
- package/src/plugins/api.ts +125 -0
- package/src/plugins/canvas/View.vue +38 -28
- package/src/plugins/canvas/definition.ts +10 -8
- package/src/plugins/canvas/index.ts +15 -8
- package/src/plugins/canvas/meta.ts +12 -0
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +2 -2
- package/src/plugins/chart/definition.ts +12 -2
- package/src/plugins/chart/index.ts +15 -7
- package/src/plugins/chart/meta.ts +18 -0
- package/src/plugins/editImages/definition.ts +44 -0
- package/src/plugins/editImages/index.ts +43 -0
- package/src/plugins/editImages/meta.ts +5 -0
- package/src/plugins/generateImage/View.vue +3 -1
- package/src/plugins/generateImage/definition.ts +2 -0
- package/src/plugins/generateImage/index.ts +13 -5
- package/src/plugins/generateImage/meta.ts +5 -0
- package/src/plugins/index.ts +35 -0
- package/src/plugins/manageRoles/Preview.vue +7 -4
- package/src/plugins/manageRoles/View.vue +12 -8
- package/src/plugins/manageRoles/definition.ts +6 -0
- package/src/plugins/manageRoles/index.ts +7 -6
- package/src/plugins/manageSkills/View.vue +11 -7
- package/src/plugins/manageSkills/definition.ts +4 -1
- package/src/plugins/manageSkills/index.ts +14 -7
- package/src/plugins/manageSkills/meta.ts +21 -0
- package/src/plugins/manageSource/definition.ts +4 -1
- package/src/plugins/manageSource/index.ts +15 -7
- package/src/plugins/manageSource/meta.ts +21 -0
- package/src/plugins/markdown/Preview.vue +10 -8
- package/src/plugins/markdown/View.vue +84 -17
- package/src/plugins/markdown/definition.ts +7 -1
- package/src/plugins/markdown/index.ts +15 -8
- package/src/plugins/markdown/meta.ts +16 -0
- package/src/plugins/meta-types.ts +97 -0
- package/src/plugins/metas.ts +224 -0
- package/src/plugins/presentForm/Preview.vue +4 -15
- package/src/plugins/presentForm/View.vue +35 -78
- package/src/plugins/presentForm/definition.ts +7 -6
- package/src/plugins/presentForm/index.ts +12 -5
- package/src/plugins/presentForm/meta.ts +11 -0
- package/src/plugins/presentForm/plugin.ts +8 -9
- package/src/plugins/presentForm/types.ts +0 -24
- package/src/plugins/presentHtml/Preview.vue +1 -8
- package/src/plugins/presentHtml/View.vue +401 -30
- package/src/plugins/presentHtml/definition.ts +8 -5
- package/src/plugins/presentHtml/index.ts +15 -8
- package/src/plugins/presentHtml/meta.ts +14 -0
- package/src/plugins/presentMulmoScript/View.vue +327 -107
- package/src/plugins/presentMulmoScript/definition.ts +34 -7
- package/src/plugins/presentMulmoScript/helpers.ts +4 -5
- package/src/plugins/presentMulmoScript/index.ts +20 -7
- package/src/plugins/presentMulmoScript/meta.ts +52 -0
- package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
- package/src/plugins/scheduler/Preview.vue +5 -2
- package/src/plugins/scheduler/TasksTab.vue +16 -36
- package/src/plugins/scheduler/View.vue +22 -54
- package/src/plugins/scheduler/automationsDefinition.ts +14 -9
- package/src/plugins/scheduler/automationsMeta.ts +5 -0
- package/src/plugins/scheduler/calendarDefinition.ts +4 -7
- package/src/plugins/scheduler/calendarMeta.ts +28 -0
- package/src/plugins/scheduler/formatSchedule.ts +6 -24
- package/src/plugins/scheduler/index.ts +26 -52
- package/src/plugins/scope.ts +57 -0
- package/src/plugins/server-bindings-types.ts +38 -0
- package/src/plugins/server.ts +32 -0
- package/src/plugins/skill/Preview.vue +25 -0
- package/src/plugins/skill/View.vue +125 -0
- package/src/plugins/skill/definition.ts +23 -0
- package/src/plugins/skill/index.ts +36 -0
- package/src/plugins/skill/plugin.ts +31 -0
- package/src/plugins/skill/types.ts +21 -0
- package/src/plugins/spreadsheet/Preview.vue +1 -3
- package/src/plugins/spreadsheet/View.vue +29 -49
- package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
- package/src/plugins/spreadsheet/definition.ts +5 -2
- package/src/plugins/spreadsheet/index.ts +15 -8
- package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
- package/src/plugins/spreadsheet/meta.ts +14 -0
- package/src/plugins/textResponse/Preview.vue +9 -1
- package/src/plugins/textResponse/View.vue +59 -8
- package/src/plugins/textResponse/index.ts +11 -3
- package/src/plugins/textResponse/plugin.ts +8 -10
- package/src/plugins/textResponse/types.ts +28 -0
- package/src/plugins/wiki/Preview.vue +6 -4
- package/src/plugins/wiki/View.vue +463 -254
- package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
- package/src/plugins/wiki/helpers.ts +17 -0
- package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
- package/src/plugins/wiki/history/HistoryTab.vue +167 -0
- package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
- package/src/plugins/wiki/history/api.ts +52 -0
- package/src/plugins/wiki/history/diff.ts +145 -0
- package/src/plugins/wiki/index.ts +42 -32
- package/src/plugins/wiki/meta.ts +10 -0
- package/src/plugins/wiki/pageEditLoader.ts +53 -0
- package/src/plugins/wiki/route.ts +8 -0
- package/src/router/guards.ts +2 -1
- package/src/router/index.ts +19 -0
- package/src/router/pageRoutes.ts +1 -0
- package/src/tools/index.ts +50 -51
- package/src/tools/runtimeLoader.ts +141 -0
- package/src/tools/types.ts +44 -1
- package/src/types/notification.ts +23 -0
- package/src/types/pastedFile.ts +10 -0
- package/src/types/session.ts +61 -3
- package/src/types/sse.ts +21 -6
- package/src/utils/agent/eventDispatch.ts +12 -9
- package/src/utils/agent/pastedAttachment.ts +35 -0
- package/src/utils/agent/request.ts +32 -3
- package/src/utils/agent/toolCalls.ts +7 -1
- package/src/utils/api.ts +1 -1
- package/src/utils/chat/exportMarkdown.ts +243 -0
- package/src/utils/errors.ts +10 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/filesPreview/todoPreview.ts +13 -2
- package/src/utils/format/date.ts +1 -3
- package/src/utils/format/jsonSyntax.ts +5 -0
- package/src/utils/html/iframeHeightReporterScript.ts +62 -0
- package/src/utils/html/previewCsp.ts +29 -2
- package/src/utils/image/htmlSrcAttrs.ts +122 -0
- package/src/utils/image/imageRepairInlineScript.ts +115 -0
- package/src/utils/image/resolve.ts +17 -3
- package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
- package/src/utils/markdown/frontmatter.ts +125 -0
- package/src/utils/markdown/taskList.ts +7 -2
- package/src/utils/plugin/runtime.ts +132 -0
- package/src/utils/session/mergeSessions.ts +40 -37
- package/src/utils/session/sessionEntries.ts +74 -18
- package/src/utils/session/sessionHelpers.ts +54 -10
- package/src/utils/tools/result.ts +76 -14
- package/src/vite-env.d.ts +6 -0
- package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
- package/client/assets/index-CY-WpQUm.css +0 -2
- package/client/assets/index-DbTz2Mfs.js +0 -4911
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/server/api/routes/html.ts +0 -114
- package/server/api/routes/todos.ts +0 -293
- package/server/api/routes/todosColumnsHandlers.ts +0 -333
- package/server/api/routes/todosHandlers.ts +0 -274
- package/server/api/routes/todosItemsHandlers.ts +0 -386
- package/server/utils/files/todos-io.ts +0 -29
- package/src/components/NotificationToast.vue +0 -75
- package/src/plugins/editImage/definition.ts +0 -27
- package/src/plugins/editImage/index.ts +0 -37
- package/src/plugins/presentHtml/helpers.ts +0 -72
- package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
- package/src/plugins/scheduler/legacyShape.ts +0 -34
- package/src/plugins/todo/Preview.vue +0 -68
- package/src/plugins/todo/View.vue +0 -378
- package/src/plugins/todo/composables/useTodos.ts +0 -179
- package/src/plugins/todo/definition.ts +0 -45
- package/src/plugins/todo/index.ts +0 -62
- package/src/plugins/todo/labels.ts +0 -163
- package/src/plugins/todo/priority.ts +0 -98
- package/src/plugins/todo/viewModes.ts +0 -19
- package/src/plugins/wiki/definition.ts +0 -25
- package/src/tools/legacyPluginNames.ts +0 -13
- package/src/utils/format/frontmatter.ts +0 -80
- package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
- package/src/utils/notification/dispatch.ts +0 -58
- /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
- /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
- /package/src/plugins/{editImage → editImages}/View.vue +0 -0
- /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">{{
|
|
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:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 &&
|
|
190
|
-
:key="
|
|
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
|
|
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 {
|
|
214
|
-
import {
|
|
215
|
-
import {
|
|
216
|
-
import { findTaskLines,
|
|
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 ? `${
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
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",
|
|
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"
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
}>(
|
|
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:
|
|
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
|
-
|
|
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 ===
|
|
819
|
+
const isStandaloneWikiRoute = computed(() => route.name === PAGE_WIKI);
|
|
468
820
|
|
|
469
|
-
// Always route wiki create/update CTAs through
|
|
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
|
|
477
|
-
|
|
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
|
|
484
|
-
|
|
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 ===
|
|
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 } }>(
|
|
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
|
|
560
|
-
const 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>
|