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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Convert an in-memory chat session (a sequence of ToolResultComplete
|
|
2
|
+
// items + per-uuid timestamps) into a self-contained Markdown document
|
|
3
|
+
// suitable for pasting into a doc, an issue, or another chat.
|
|
4
|
+
//
|
|
5
|
+
// Each turn is rendered as a `## ⬜︎ Speaker · HH:MM` heading followed
|
|
6
|
+
// by the message body, with `---` horizontal rules between turns.
|
|
7
|
+
// Headings inside message bodies are demoted by 2 levels (e.g. an
|
|
8
|
+
// assistant `# Heading` becomes `### Heading`) so the speaker headings
|
|
9
|
+
// always sit above message-internal structure in the document outline.
|
|
10
|
+
//
|
|
11
|
+
// Tool calls (anything other than `text-response`) render as a `## ⬛︎
|
|
12
|
+
// toolName HH:MM` heading — a compact marker showing which tools the
|
|
13
|
+
// assistant invoked. Tool payloads are intentionally omitted to keep
|
|
14
|
+
// the export readable; users wanting fidelity can view the raw JSONL.
|
|
15
|
+
// The one exception is `presentDocument`: its `data.markdown` is
|
|
16
|
+
// itself a piece of prose worth reading out of context, so we inline
|
|
17
|
+
// the document body (demoted by 2 levels) under the marker. In real
|
|
18
|
+
// sessions `data.markdown` is usually a workspace path
|
|
19
|
+
// (`artifacts/documents/*.md`) rather than inline text, so the export
|
|
20
|
+
// is async — callers pass a `readFile` resolver that reads the file
|
|
21
|
+
// off the workspace, mirroring the in-app Markdown View's loader.
|
|
22
|
+
|
|
23
|
+
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
24
|
+
import { isRecord } from "../types";
|
|
25
|
+
|
|
26
|
+
const TEXT_RESPONSE_TOOL = "text-response";
|
|
27
|
+
const PRESENT_DOCUMENT_TOOL = "presentDocument";
|
|
28
|
+
|
|
29
|
+
/** Heuristic for the file-path mode of presentDocument's `markdown` field
|
|
30
|
+
* (server-side documents stored under `artifacts/documents/*.md`). When
|
|
31
|
+
* matched, the value is a path — not inline content — and the export
|
|
32
|
+
* has to read the file off the workspace via the caller-supplied
|
|
33
|
+
* resolver to inline the body. */
|
|
34
|
+
function looksLikeDocumentPath(value: string): boolean {
|
|
35
|
+
return value.endsWith(".md") && value.startsWith("artifacts/documents/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ROLE_LABELS = {
|
|
39
|
+
user: "⬜︎ You",
|
|
40
|
+
assistant: "⬛︎ Assistant",
|
|
41
|
+
system: "◇ System",
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
type Role = keyof typeof ROLE_LABELS;
|
|
45
|
+
|
|
46
|
+
export interface ExportChatOptions {
|
|
47
|
+
/** Friendly role / persona name shown in the document title (e.g. "General"). */
|
|
48
|
+
sessionRoleName?: string;
|
|
49
|
+
/** ISO string for the document's "Exported …" line. Defaults to `new Date()`. */
|
|
50
|
+
exportedAt?: string;
|
|
51
|
+
/** Per-uuid epoch-ms map matching `ActiveSession.resultTimestamps`. */
|
|
52
|
+
resultTimestamps?: Map<string, number>;
|
|
53
|
+
/** Resolver for workspace-relative file paths (currently the
|
|
54
|
+
* `artifacts/documents/*.md` form used by presentDocument). Returns
|
|
55
|
+
* the file's text content, or null if the read fails. Omit it to
|
|
56
|
+
* skip file-mode resolution and emit only the marker line. */
|
|
57
|
+
readFile?: (path: string) => Promise<string | null>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Format `epochMs` as `HH:MM` in 24h, locale-independent. */
|
|
61
|
+
function formatHHMM(epochMs: number): string {
|
|
62
|
+
const date = new Date(epochMs);
|
|
63
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
64
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
65
|
+
return `${hours}:${minutes}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Narrow `data?.role` to a known speaker label. Defaults to "assistant". */
|
|
69
|
+
function roleOf(result: ToolResultComplete): Role {
|
|
70
|
+
const { data } = result;
|
|
71
|
+
// Own-property check via Object.prototype.hasOwnProperty so an
|
|
72
|
+
// inherited key on the runtime ROLE_LABELS object (e.g. `toString`,
|
|
73
|
+
// or anything that crawled in via Object.prototype pollution)
|
|
74
|
+
// can't satisfy the gate and produce a `## undefined` speaker line
|
|
75
|
+
// (#1065 review).
|
|
76
|
+
if (isRecord(data) && typeof data.role === "string" && Object.prototype.hasOwnProperty.call(ROLE_LABELS, data.role)) {
|
|
77
|
+
return data.role as Role;
|
|
78
|
+
}
|
|
79
|
+
return "assistant";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Pull the displayable text from a text-response result. Falls back to
|
|
83
|
+
* `message` for older/saved sessions where `data.text` may be missing. */
|
|
84
|
+
function textOf(result: ToolResultComplete): string {
|
|
85
|
+
const { data } = result;
|
|
86
|
+
if (isRecord(data) && typeof data.text === "string") return data.text;
|
|
87
|
+
return result.message ?? "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isTextResponse(result: ToolResultComplete): boolean {
|
|
91
|
+
return result.toolName === TEXT_RESPONSE_TOOL;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Allow up to 3 leading spaces before the fence run, per GFM. A
|
|
95
|
+
// 4-space indent is a regular indented code block and isn't a fence
|
|
96
|
+
// in the first place; the heading-demotion path doesn't need to skip
|
|
97
|
+
// inside those because they preserve the literal characters anyway,
|
|
98
|
+
// but a real fence indented by 1-3 spaces (legal GFM) would
|
|
99
|
+
// previously slip past `matchFenceRun` and any `# heading` line
|
|
100
|
+
// inside got mistakenly demoted (#1065 review).
|
|
101
|
+
const FENCE_RUN_RE = /^ {0,3}(`{3,}|~{3,})/;
|
|
102
|
+
const ATX_HEADING_RE = /^(#{1,6})([ \t].*)$/;
|
|
103
|
+
|
|
104
|
+
interface OpenFence {
|
|
105
|
+
char: "`" | "~";
|
|
106
|
+
len: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Match the leading run of fence characters on `line` (allowing up
|
|
110
|
+
* to 3 leading spaces, per GFM), if any. Captures the fence char +
|
|
111
|
+
* length so the closing logic can apply GFM's rules (close fence
|
|
112
|
+
* must be the same char and at least as long as the open). */
|
|
113
|
+
function matchFenceRun(line: string): OpenFence | null {
|
|
114
|
+
const match = FENCE_RUN_RE.exec(line);
|
|
115
|
+
if (!match) return null;
|
|
116
|
+
const [, run] = match;
|
|
117
|
+
return { char: run[0] as "`" | "~", len: run.length };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A line closes `open` only when it uses the same fence char, has at
|
|
121
|
+
* least as many of them, and carries no info-string after the run.
|
|
122
|
+
* Anything else inside the fence is content (including a different
|
|
123
|
+
* fence type or a shorter run). The fence run can sit after up to
|
|
124
|
+
* 3 leading spaces; the closing check looks at content after the
|
|
125
|
+
* fence run wherever it lands on the line. */
|
|
126
|
+
function isClosingFence(line: string, fence: OpenFence, open: OpenFence): boolean {
|
|
127
|
+
if (fence.char !== open.char) return false;
|
|
128
|
+
if (fence.len < open.len) return false;
|
|
129
|
+
const runStart = line.search(/[`~]/);
|
|
130
|
+
return runStart >= 0 ? line.slice(runStart + fence.len).trim() === "" : false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Demote every ATX heading inside `markdown` by `levels` (`#` → `#`+levels),
|
|
134
|
+
* capping at h6. Skips lines inside fenced code blocks so `# comment`
|
|
135
|
+
* lines in code samples are left alone. Honours GFM fence rules: a
|
|
136
|
+
* block opened with N backticks (or N tildes) only closes on a line of
|
|
137
|
+
* the same character with ≥N of them and nothing else, so nested
|
|
138
|
+
* shorter fences and the opposite fence char both count as content. */
|
|
139
|
+
function demoteHeadings(markdown: string, levels: number): string {
|
|
140
|
+
if (levels <= 0 || markdown.length === 0) return markdown;
|
|
141
|
+
const out: string[] = [];
|
|
142
|
+
let openFence: OpenFence | null = null;
|
|
143
|
+
for (const line of markdown.split(/\r\n|\r|\n/)) {
|
|
144
|
+
const fence = matchFenceRun(line);
|
|
145
|
+
if (openFence !== null) {
|
|
146
|
+
if (fence !== null && isClosingFence(line, fence, openFence)) {
|
|
147
|
+
openFence = null;
|
|
148
|
+
}
|
|
149
|
+
out.push(line);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (fence !== null) {
|
|
153
|
+
openFence = fence;
|
|
154
|
+
out.push(line);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const match = ATX_HEADING_RE.exec(line);
|
|
158
|
+
if (!match) {
|
|
159
|
+
out.push(line);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const [, hashes, rest] = match;
|
|
163
|
+
const newDepth = Math.min(6, hashes.length + levels);
|
|
164
|
+
out.push(`${"#".repeat(newDepth)}${rest}`);
|
|
165
|
+
}
|
|
166
|
+
return out.join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderTextTurn(result: ToolResultComplete, timestamps: Map<string, number>): string {
|
|
170
|
+
const role = roleOf(result);
|
|
171
|
+
const epochMs = timestamps.get(result.uuid);
|
|
172
|
+
// Use `!== undefined` rather than truthiness so a 0 (Unix epoch)
|
|
173
|
+
// timestamp still renders. Practically irrelevant for live chat, but
|
|
174
|
+
// it removes a foot-gun for any synthetic / migrated session that
|
|
175
|
+
// ends up with that boundary value.
|
|
176
|
+
const time = epochMs !== undefined ? ` · ${formatHHMM(epochMs)}` : "";
|
|
177
|
+
// Speaker is `##`; demote any in-body heading by 2 so it always sits
|
|
178
|
+
// strictly below the speaker (`#` → `###`, `##` → `####`, …).
|
|
179
|
+
const body = demoteHeadings(textOf(result).trim(), 2);
|
|
180
|
+
return body.length > 0 ? `## ${ROLE_LABELS[role]}${time}\n\n${body}` : `## ${ROLE_LABELS[role]}${time}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Resolve presentDocument's `data.markdown` to inline content. If the
|
|
184
|
+
* value is a workspace path, defer to `readFile`; otherwise treat it
|
|
185
|
+
* as inline markdown. Returns null when the data is missing/empty or
|
|
186
|
+
* the file read fails. */
|
|
187
|
+
async function presentDocumentBody(result: ToolResultComplete, readFile: ExportChatOptions["readFile"]): Promise<string | null> {
|
|
188
|
+
const { data } = result;
|
|
189
|
+
if (!isRecord(data)) return null;
|
|
190
|
+
const { markdown } = data;
|
|
191
|
+
if (typeof markdown !== "string" || markdown.length === 0) return null;
|
|
192
|
+
if (!looksLikeDocumentPath(markdown)) return markdown;
|
|
193
|
+
if (!readFile) return null;
|
|
194
|
+
// Honour the documented "returns null on failure" contract — a
|
|
195
|
+
// single rejected resolver shouldn't blow up the whole export.
|
|
196
|
+
try {
|
|
197
|
+
return await readFile(markdown);
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function renderToolTurn(result: ToolResultComplete, timestamps: Map<string, number>, readFile: ExportChatOptions["readFile"]): Promise<string> {
|
|
204
|
+
const epochMs = timestamps.get(result.uuid);
|
|
205
|
+
const time = epochMs !== undefined ? ` ${formatHHMM(epochMs)}` : "";
|
|
206
|
+
const marker = `## ⬛︎ ${result.toolName}${time}`;
|
|
207
|
+
|
|
208
|
+
if (result.toolName === PRESENT_DOCUMENT_TOOL) {
|
|
209
|
+
const documentBody = await presentDocumentBody(result, readFile);
|
|
210
|
+
if (documentBody !== null) {
|
|
211
|
+
return `${marker}\n\n${demoteHeadings(documentBody.trim(), 2)}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return marker;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Build the document header. Title and the "Exported" subtitle are
|
|
219
|
+
* intentionally plain — the conversation body is what the reader cares
|
|
220
|
+
* about. The horizontal rule separates header from first turn. */
|
|
221
|
+
function renderHeader(opts: ExportChatOptions): string {
|
|
222
|
+
const role = opts.sessionRoleName?.trim();
|
|
223
|
+
const exportedAt = new Date(opts.exportedAt ?? new Date().toISOString());
|
|
224
|
+
const dateStamp = exportedAt.toISOString().slice(0, 16).replace("T", " ");
|
|
225
|
+
const title = role ? `# Conversation · ${role}` : "# Conversation";
|
|
226
|
+
return `${title}\n\n*Exported ${dateStamp} UTC*\n\n---`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Convert a chat session to a Markdown string. Async because
|
|
230
|
+
* presentDocument's body may live on disk and need a `readFile` round
|
|
231
|
+
* trip. Returns at minimum a non-empty header so callers never have
|
|
232
|
+
* to special-case empty sessions. */
|
|
233
|
+
export async function exportChatToMarkdown(results: readonly ToolResultComplete[], options: ExportChatOptions = {}): Promise<string> {
|
|
234
|
+
const timestamps = options.resultTimestamps ?? new Map<string, number>();
|
|
235
|
+
const turns = await Promise.all(
|
|
236
|
+
results.map((result) =>
|
|
237
|
+
isTextResponse(result) ? Promise.resolve(renderTextTurn(result, timestamps)) : renderToolTurn(result, timestamps, options.readFile),
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
const body = turns.join("\n\n---\n\n");
|
|
241
|
+
const header = renderHeader(options);
|
|
242
|
+
return body.length > 0 ? `${header}\n\n${body}\n` : `${header}\n`;
|
|
243
|
+
}
|
package/src/utils/errors.ts
CHANGED
|
@@ -6,13 +6,21 @@
|
|
|
6
6
|
// `err instanceof Error ? err.message : String(err)` — searching for
|
|
7
7
|
// one canonical helper is easier than grepping for the inline form.
|
|
8
8
|
//
|
|
9
|
+
// Non-Error objects with a `details` (gRPC convention) or `message`
|
|
10
|
+
// string field have that field surfaced — without this, gRPC errors
|
|
11
|
+
// like `{ code, details, metadata }` show up as `[object Object]`.
|
|
12
|
+
//
|
|
9
13
|
// The optional `fallback` covers the common idiom of surfacing a
|
|
10
14
|
// descriptive message ("Invalid JSON", "Connection error.") when a
|
|
11
|
-
// throw turns out to be a non-Error value
|
|
12
|
-
// yields noise like `[object Object]`.
|
|
15
|
+
// throw turns out to be a non-Error value.
|
|
13
16
|
|
|
14
17
|
export function errorMessage(err: unknown, fallback?: string): string {
|
|
15
18
|
if (err instanceof Error) return err.message;
|
|
19
|
+
if (err !== null && typeof err === "object") {
|
|
20
|
+
const obj = err as { details?: unknown; message?: unknown };
|
|
21
|
+
if (typeof obj.details === "string" && obj.details) return obj.details;
|
|
22
|
+
if (typeof obj.message === "string" && obj.message) return obj.message;
|
|
23
|
+
}
|
|
16
24
|
if (fallback !== undefined) return fallback;
|
|
17
25
|
return String(err);
|
|
18
26
|
}
|
|
@@ -6,7 +6,7 @@ export const EXPANDED_DIRS_STORAGE_KEY = "files_expanded_dirs";
|
|
|
6
6
|
// Default: only the workspace root ("") is expanded — matches the
|
|
7
7
|
// pre-persistence behavior of FileTree.vue, where nested dirs start
|
|
8
8
|
// collapsed so opening Files mode doesn't render the whole tree.
|
|
9
|
-
const DEFAULT_EXPANDED:
|
|
9
|
+
const DEFAULT_EXPANDED: readonly string[] = [""];
|
|
10
10
|
|
|
11
11
|
export function parseStoredExpandedDirs(raw: string | null): Set<string> {
|
|
12
12
|
if (raw === null) return new Set(DEFAULT_EXPANDED);
|
|
@@ -3,10 +3,21 @@
|
|
|
3
3
|
// Extracted from FilesView.vue (#507 step 8).
|
|
4
4
|
|
|
5
5
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
6
|
-
import type { StatusColumn, TodoData, TodoItem } from "
|
|
6
|
+
import type { StatusColumn, TodoData, TodoItem } from "@mulmoclaude/todo-plugin/shared";
|
|
7
7
|
import { WORKSPACE_FILES } from "../../config/workspacePaths";
|
|
8
8
|
import { isRecord } from "../types";
|
|
9
9
|
|
|
10
|
+
// `WORKSPACE_FILES.todosItems` lives under
|
|
11
|
+
// `data/plugins/%40mulmoclaude%2Ftodo-plugin/todos.json` — the
|
|
12
|
+
// directory name is the URL-encoded npm package (#1145). When the
|
|
13
|
+
// path round-trips through Vue Router (deep-link via the URL bar),
|
|
14
|
+
// the encoded segment gets decoded once to `@mulmoclaude/todo-plugin`,
|
|
15
|
+
// so a strict equality check against the literal constant misses
|
|
16
|
+
// that case. Compare against both encoded and decoded forms so the
|
|
17
|
+
// comparison works for both router-decoded deep links and
|
|
18
|
+
// tree-click flows that preserve the encoded literal.
|
|
19
|
+
const TODOS_ITEMS_PATHS: ReadonlySet<string> = new Set([WORKSPACE_FILES.todosItems, decodeURIComponent(WORKSPACE_FILES.todosItems)]);
|
|
20
|
+
|
|
10
21
|
function isTodoItem(value: unknown): value is TodoItem {
|
|
11
22
|
if (!isRecord(value)) return false;
|
|
12
23
|
if (typeof value["id"] !== "string" || typeof value["text"] !== "string") return false;
|
|
@@ -20,7 +31,7 @@ function isTodoItemArray(value: unknown): value is TodoItem[] {
|
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
export function toTodoExplorerResult(selectedPath: string | null, rawText: string | null): ToolResultComplete<TodoData> | null {
|
|
23
|
-
if (selectedPath
|
|
34
|
+
if (selectedPath === null || !TODOS_ITEMS_PATHS.has(selectedPath)) return null;
|
|
24
35
|
if (rawText === null) return null;
|
|
25
36
|
let parsed: unknown;
|
|
26
37
|
try {
|
package/src/utils/format/date.ts
CHANGED
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
/** "Apr 11 06:32" — short month + day + 24h time. */
|
|
6
6
|
export function formatDate(iso: string): string {
|
|
7
7
|
const date = new Date(iso);
|
|
8
|
-
return (
|
|
9
|
-
date.toLocaleDateString(undefined, { month: "short", day: "numeric" }) + " " + date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
|
|
10
|
-
);
|
|
8
|
+
return `${date.toLocaleDateString(undefined, { month: "short", day: "numeric" })} ${date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`;
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
/** "Apr 11 06:32" — same format as formatDate but from epoch ms. */
|
|
@@ -25,6 +25,11 @@ export const JSON_TOKEN_CLASS: Record<JsonTokenType, string> = {
|
|
|
25
25
|
// sonarjs/regex-complexity and is easier to reason about.
|
|
26
26
|
const STRING_RE = /^"(?:[^"\\]|\\.)*"/;
|
|
27
27
|
const KEYWORD_RE = /^(?:true|false|null)\b/;
|
|
28
|
+
// Bounded JSON number parser — each `\d+` runs over a digits-only
|
|
29
|
+
// class with hard delimiters (`.`, `e`, `E`, `+`, `-`) between
|
|
30
|
+
// segments. Linear in input length; safe-regex flags the optional
|
|
31
|
+
// groups generically.
|
|
32
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- bounded JSON number parser, no nested-quantifier overlap
|
|
28
33
|
const NUMBER_RE = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
|
|
29
34
|
const WS_RE = /^\s+/;
|
|
30
35
|
const PUNCT_RE = /^[{}[\]:,]/;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Tiny inline script injected into every `/artifacts/html/...` document
|
|
2
|
+
// so the parent (StackView in chat) can size each iframe to its
|
|
3
|
+
// rendered content height — despite the sandbox treating the iframe as
|
|
4
|
+
// cross-origin (#1219 follow-up).
|
|
5
|
+
//
|
|
6
|
+
// Why this matters: presentHtml's iframe runs with
|
|
7
|
+
// `sandbox="allow-scripts"` (deliberate — `allow-same-origin` would let
|
|
8
|
+
// LLM-generated HTML read the parent's cookies / localStorage / bearer
|
|
9
|
+
// token). Without `allow-same-origin`, the parent's
|
|
10
|
+
// `iframe.contentDocument.documentElement.scrollHeight` access throws
|
|
11
|
+
// cross-origin, leaving the iframe at the browser-default 150px and
|
|
12
|
+
// clipping the actual chart / Sankey / report inside.
|
|
13
|
+
//
|
|
14
|
+
// The reporter runs INSIDE the iframe (under its null origin), reads
|
|
15
|
+
// only its own document height, and posts a message to its parent. The
|
|
16
|
+
// parent then sets `iframe.style.height` from the reported value. No
|
|
17
|
+
// parent state is exposed to the iframe.
|
|
18
|
+
//
|
|
19
|
+
// Posts on initial `load`, on every subsequent body resize (charts that
|
|
20
|
+
// populate after DOMContentLoaded, web-font settling, etc.), and once
|
|
21
|
+
// more on `DOMContentLoaded` so the first paint is also sized.
|
|
22
|
+
//
|
|
23
|
+
// On iframe WIDTH changes:
|
|
24
|
+
//
|
|
25
|
+
// 1. Fires a synthetic `window.resize` event so libraries hooked into
|
|
26
|
+
// that signal (Chart.js, ECharts, custom code) get the nudge.
|
|
27
|
+
// 2. Calls `Plotly.Plots.resize` on every `.js-plotly-plot` /
|
|
28
|
+
// `.plotly-graph-div` element if Plotly is loaded. Plotly v2's
|
|
29
|
+
// `responsive: true` is supposed to catch this via its own
|
|
30
|
+
// ResizeObserver, but in the sandboxed iframe context the observer
|
|
31
|
+
// sometimes misses the change. Calling resize directly is the
|
|
32
|
+
// documented escape hatch and a no-op when the chart was already
|
|
33
|
+
// correctly sized. Gated on `window.Plotly` so HTML pages without
|
|
34
|
+
// Plotly are unaffected.
|
|
35
|
+
//
|
|
36
|
+
// Pairs with the listener in `src/components/StackView.vue`. Message
|
|
37
|
+
// shape: `{ type: "mc-iframe-height", height: <pixels> }`. The listener
|
|
38
|
+
// matches the iframe via `event.source === iframe.contentWindow`, then
|
|
39
|
+
// `iframe.style.setProperty("height", "<n>px", "important")` (the
|
|
40
|
+
// `!important` defeats the stack-natural `:deep(.h-full)` override).
|
|
41
|
+
|
|
42
|
+
const REPORTER_SCRIPT = `(()=>{const p=()=>{try{parent.postMessage({type:"mc-iframe-height",height:document.documentElement.scrollHeight},"*")}catch(e){}};const r=()=>{try{window.dispatchEvent(new Event("resize"))}catch(e){}try{const P=window.Plotly;if(P&&P.Plots&&P.Plots.resize){document.querySelectorAll(".js-plotly-plot,.plotly-graph-div").forEach(el=>{try{P.Plots.resize(el)}catch(e){}})}}catch(e){}};addEventListener("DOMContentLoaded",p);addEventListener("load",p);if(window.ResizeObserver&&document.documentElement){let w=0;new ResizeObserver((es)=>{p();const nw=es[0]&&es[0].contentRect?es[0].contentRect.width:0;if(nw&&nw!==w){w=nw;r()}}).observe(document.documentElement)}})();`;
|
|
43
|
+
|
|
44
|
+
export const HEIGHT_REPORTER_SCRIPT_TAG = `<script>${REPORTER_SCRIPT}</script>`;
|
|
45
|
+
|
|
46
|
+
const BODY_CLOSE_RE = /<\/body\s*>/gi;
|
|
47
|
+
|
|
48
|
+
/** Splice the height-reporter `<script>` tag immediately before the
|
|
49
|
+
* document's last `</body>`. Pure string operation — no DOM parsing,
|
|
50
|
+
* linear time in input length, idempotent in effect (the script is
|
|
51
|
+
* one-shot; duplicates produce duplicate postMessages, the parent
|
|
52
|
+
* listener handles them identically). When `</body>` is missing
|
|
53
|
+
* (server-streamed HTML, partial output, hand-written fragment), the
|
|
54
|
+
* tag is appended at the end so the script still loads. */
|
|
55
|
+
export function injectHeightReporterScript(html: string): string {
|
|
56
|
+
if (!html) return html;
|
|
57
|
+
const matches = [...html.matchAll(BODY_CLOSE_RE)];
|
|
58
|
+
if (matches.length === 0) return html + HEIGHT_REPORTER_SCRIPT_TAG;
|
|
59
|
+
const idx = matches[matches.length - 1].index;
|
|
60
|
+
if (idx === undefined) return html + HEIGHT_REPORTER_SCRIPT_TAG;
|
|
61
|
+
return `${html.slice(0, idx)}${HEIGHT_REPORTER_SCRIPT_TAG}${html.slice(idx)}`;
|
|
62
|
+
}
|
|
@@ -14,14 +14,31 @@ export const HTML_PREVIEW_CSP_ALLOWED_CDNS: readonly string[] = [
|
|
|
14
14
|
"https://cdnjs.cloudflare.com",
|
|
15
15
|
"https://fonts.googleapis.com",
|
|
16
16
|
"https://fonts.gstatic.com",
|
|
17
|
+
// Plotly's official CDN. The LLM defaults to this URL when it
|
|
18
|
+
// includes a Sankey or other Plotly chart in presentHtml output —
|
|
19
|
+
// Plotly's docs recommend it, so unconditioned LLM output ends up
|
|
20
|
+
// pointing here. Also reachable through jsdelivr, but adding the
|
|
21
|
+
// first-party CDN keeps historical artifacts (where the URL is
|
|
22
|
+
// already baked into the file on disk) rendering correctly.
|
|
23
|
+
"https://cdn.plot.ly",
|
|
17
24
|
];
|
|
18
25
|
|
|
19
26
|
/**
|
|
20
27
|
* Build the CSP string. Split from the wrapper so tests can exercise
|
|
21
28
|
* the policy without HTML-template noise.
|
|
29
|
+
*
|
|
30
|
+
* `origin`, when provided, replaces `'self'` in `img-src`. The preview
|
|
31
|
+
* iframe is `sandbox="allow-scripts"` only, so its document has an
|
|
32
|
+
* opaque origin: Safari/WebKit matches `'self'` against the (opaque)
|
|
33
|
+
* origin tuple and rejects every same-origin image request. Chrome
|
|
34
|
+
* matches `'self'` against the document URL and works either way. Pass
|
|
35
|
+
* the explicit server origin from HTTP-header callers; leave it
|
|
36
|
+
* undefined for the `srcdoc` fallback (where `'self'` is meaningless
|
|
37
|
+
* either way and there are no same-origin refs to resolve).
|
|
22
38
|
*/
|
|
23
|
-
export function buildHtmlPreviewCsp(cdns: readonly string[] = HTML_PREVIEW_CSP_ALLOWED_CDNS): string {
|
|
39
|
+
export function buildHtmlPreviewCsp(origin?: string, cdns: readonly string[] = HTML_PREVIEW_CSP_ALLOWED_CDNS): string {
|
|
24
40
|
const cdnList = cdns.join(" ");
|
|
41
|
+
const imgSelf = origin ?? "'self'";
|
|
25
42
|
return [
|
|
26
43
|
"default-src 'none'",
|
|
27
44
|
// LLM-authored HTML almost always uses inline <script> blocks
|
|
@@ -37,13 +54,23 @@ export function buildHtmlPreviewCsp(cdns: readonly string[] = HTML_PREVIEW_CSP_A
|
|
|
37
54
|
// could exfiltrate data via image requests even with connect-src
|
|
38
55
|
// blocked. Widen via HTML_PREVIEW_CSP_ALLOWED_CDNS if LLM output
|
|
39
56
|
// legitimately needs more hosts.
|
|
40
|
-
`img-src
|
|
57
|
+
`img-src ${imgSelf} ${cdnList} data: blob:`,
|
|
41
58
|
// Block XHR / fetch / WebSocket so previews can't phone home or
|
|
42
59
|
// exfiltrate anything the inline scripts happen to compute.
|
|
43
60
|
"connect-src 'none'",
|
|
44
61
|
].join("; ");
|
|
45
62
|
}
|
|
46
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Build the CSP string for the print-mode hidden iframe (presentHtml's
|
|
66
|
+
* printToPdf). Same policy as the preview header with the explicit
|
|
67
|
+
* server origin substituted for `'self'` — see `buildHtmlPreviewCsp`
|
|
68
|
+
* for why the substitution is required.
|
|
69
|
+
*/
|
|
70
|
+
export function buildPrintCspContent(origin: string, cdns: readonly string[] = HTML_PREVIEW_CSP_ALLOWED_CDNS): string {
|
|
71
|
+
return buildHtmlPreviewCsp(origin, cdns);
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
const CSP_META_NONCE = ""; // reserved for future use (per-render nonce)
|
|
48
75
|
|
|
49
76
|
/**
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Shared HTML-tag URL rewriter — used by:
|
|
2
|
+
// - browser markdown surface (`rewriteImgSrcAttrsInHtml` in
|
|
3
|
+
// `rewriteMarkdownImageRefs.ts`) → rewrites to
|
|
4
|
+
// `/api/files/raw?path=...`
|
|
5
|
+
// - server PDF surface (`inlineImages` in
|
|
6
|
+
// `server/api/routes/pdf.ts`) → rewrites to `data:` URIs
|
|
7
|
+
//
|
|
8
|
+
// Both used to keep their own copy of the same regex shape with a
|
|
9
|
+
// `// Mirrors the shape used by …` comment. That mirroring drifts the
|
|
10
|
+
// moment one side adds a tag (`<source>`, `<video poster>`) and the
|
|
11
|
+
// other doesn't. Single helper here, two callers, one tag list — the
|
|
12
|
+
// drift becomes structurally impossible (#1011 Stage B).
|
|
13
|
+
//
|
|
14
|
+
// `srcset` (comma-separated descriptor list) and SVG `<image href>` /
|
|
15
|
+
// CSS `url()` are deliberately out of scope — see the deferred-list
|
|
16
|
+
// comment on `RESOLVABLE_TAG_ATTRS` below.
|
|
17
|
+
|
|
18
|
+
// Tag (lowercased) → URL-bearing attribute(s). Adding a row here
|
|
19
|
+
// extends both Markdown and PDF surfaces simultaneously.
|
|
20
|
+
//
|
|
21
|
+
// Deferred (NOT here):
|
|
22
|
+
// - `srcset` on `<img>` / `<source>` — comma-separated list with
|
|
23
|
+
// descriptors (`url 1x, url2 2x`), needs a separate split/rewrite
|
|
24
|
+
// pass. Tracked under #1011 Stage B follow-up.
|
|
25
|
+
// - SVG `<image href>` — gap table item #9, low priority per plan
|
|
26
|
+
// §修正提案 P3-A.
|
|
27
|
+
// - CSS `url()` in `style=` attributes — gap table item #8, same
|
|
28
|
+
// priority.
|
|
29
|
+
export const RESOLVABLE_TAG_ATTRS: Readonly<Record<string, readonly string[]>> = {
|
|
30
|
+
img: ["src"],
|
|
31
|
+
source: ["src"],
|
|
32
|
+
video: ["poster", "src"],
|
|
33
|
+
audio: ["src"],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Outer regex: scan any tag whose name appears in `RESOLVABLE_TAG_ATTRS`,
|
|
37
|
+
// respecting quoted attribute values so `>` inside e.g. `alt="x>y"`
|
|
38
|
+
// doesn't terminate the tag early. The body is one of:
|
|
39
|
+
// - any non-`>` non-quote char `[^>"']`
|
|
40
|
+
// - a complete double-quoted span `"[^"]*"`
|
|
41
|
+
// - a complete single-quoted span `'[^']*'`
|
|
42
|
+
// All branches bounded — no nested quantifiers, no overlap.
|
|
43
|
+
//
|
|
44
|
+
// The tag-name alternation is hand-listed rather than computed from
|
|
45
|
+
// `Object.keys(RESOLVABLE_TAG_ATTRS)` so the regex is a const string
|
|
46
|
+
// (lint-friendly) and the alternation order matches the readable
|
|
47
|
+
// declaration order. Adding a tag means: update the map AND the
|
|
48
|
+
// alternation here. The unit test in test_htmlSrcAttrs.ts pins this
|
|
49
|
+
// in lockstep so the two never disagree silently.
|
|
50
|
+
//
|
|
51
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- bounded alternatives, ReDoS-safe (test in test_htmlSrcAttrs.ts)
|
|
52
|
+
const RESOLVABLE_TAG_OUTER_RE = /<(?:img|source|video|audio)\b(?:[^>"']|"[^"]*"|'[^']*')*\/?>/gi;
|
|
53
|
+
// Tag-name extractor for the matched outer tag. Anchored so we only
|
|
54
|
+
// read the leading `<name`, never an attribute value that happens to
|
|
55
|
+
// look like a tag.
|
|
56
|
+
const TAG_NAME_RE = /^<([a-z]+)/i;
|
|
57
|
+
|
|
58
|
+
// Attribute iterator: walks each `name=value` pair inside a tag. The
|
|
59
|
+
// leading `\s+` ensures we only match real attribute boundaries, not
|
|
60
|
+
// `src=` text embedded inside another attribute's quoted value.
|
|
61
|
+
// Capture groups:
|
|
62
|
+
// 1: leading whitespace
|
|
63
|
+
// 2: attribute name
|
|
64
|
+
// 3: `=` with surrounding spaces (only when value present)
|
|
65
|
+
// 4: full quoted/unquoted value (unused but captured for clarity)
|
|
66
|
+
// 5: double-quoted value (without quotes)
|
|
67
|
+
// 6: single-quoted value (without quotes)
|
|
68
|
+
// 7: unquoted value — refuses leading `"` / `'` so a malformed
|
|
69
|
+
// `<img src="aaaa` (no closing quote) doesn't capture the stray
|
|
70
|
+
// quote as the value
|
|
71
|
+
//
|
|
72
|
+
// All quantifiers bounded — verified ReDoS-safe in test_htmlSrcAttrs.ts.
|
|
73
|
+
// eslint-disable-next-line sonarjs/slow-regex, sonarjs/regex-complexity, security/detect-unsafe-regex -- bounded quantifiers, ReDoS-safe (test in test_htmlSrcAttrs.ts)
|
|
74
|
+
const ATTR_ITER_RE = /(\s+)([A-Za-z][\w:-]*)(?:(\s*=\s*)("([^"]*)"|'([^']*)'|([^\s>"'][^\s>]*)))?/g;
|
|
75
|
+
|
|
76
|
+
/** Transform every URL-bearing attribute on a recognised tag.
|
|
77
|
+
*
|
|
78
|
+
* `transform` is invoked once per matching attribute value. Return:
|
|
79
|
+
* - `string` to substitute the value (callee is responsible for
|
|
80
|
+
* not breaking out of the surrounding quotes — most callers
|
|
81
|
+
* route through `encodeURIComponent` or a fixed-prefix path)
|
|
82
|
+
* - `null` to leave the attribute untouched (e.g. external URL,
|
|
83
|
+
* `data:` URI, escape-the-workspace path)
|
|
84
|
+
*
|
|
85
|
+
* Other attributes (alt, class, style, …) and `src=`-shaped text
|
|
86
|
+
* inside their quoted values are preserved verbatim because we
|
|
87
|
+
* parse attribute-by-attribute, not by free-form regex.
|
|
88
|
+
*
|
|
89
|
+
* Recognised tags + attributes live in `RESOLVABLE_TAG_ATTRS`. Any
|
|
90
|
+
* tag whose name isn't in the map is returned untouched. Any
|
|
91
|
+
* attribute on a recognised tag whose name isn't in the map's entry
|
|
92
|
+
* is also untouched. */
|
|
93
|
+
export function transformResolvableUrlsInHtml(html: string, transform: (url: string) => string | null): string {
|
|
94
|
+
if (!html) return html;
|
|
95
|
+
return html.replace(RESOLVABLE_TAG_OUTER_RE, (tag) => {
|
|
96
|
+
const tagNameMatch = TAG_NAME_RE.exec(tag);
|
|
97
|
+
if (!tagNameMatch) return tag;
|
|
98
|
+
const resolvableAttrs = RESOLVABLE_TAG_ATTRS[tagNameMatch[1].toLowerCase()];
|
|
99
|
+
if (!resolvableAttrs) return tag;
|
|
100
|
+
return tag.replace(ATTR_ITER_RE, (...captures: unknown[]) => replaceAttrIfResolvable(captures, resolvableAttrs, transform));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function replaceAttrIfResolvable(captures: unknown[], resolvableAttrs: readonly string[], transform: (url: string) => string | null): string {
|
|
105
|
+
const [full, leading, name, eqWithSpaces, , doubleQuoted, singleQuoted, bare] = captures as [
|
|
106
|
+
string,
|
|
107
|
+
string,
|
|
108
|
+
string,
|
|
109
|
+
string | undefined,
|
|
110
|
+
string | undefined,
|
|
111
|
+
string | undefined,
|
|
112
|
+
string | undefined,
|
|
113
|
+
string | undefined,
|
|
114
|
+
];
|
|
115
|
+
if (!eqWithSpaces || !resolvableAttrs.includes(name.toLowerCase())) return full;
|
|
116
|
+
const value = (doubleQuoted ?? singleQuoted ?? bare ?? "").trim();
|
|
117
|
+
if (!value) return full;
|
|
118
|
+
const replacement = transform(value);
|
|
119
|
+
if (replacement === null) return full;
|
|
120
|
+
const quote = doubleQuoted !== undefined ? '"' : singleQuoted !== undefined ? "'" : '"';
|
|
121
|
+
return `${leading}${name}${eqWithSpaces}${quote}${replacement}${quote}`;
|
|
122
|
+
}
|