mulmoclaude 0.1.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/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-3">
|
|
3
|
+
<p class="text-xs text-gray-600 leading-relaxed">
|
|
4
|
+
Add external MCP servers. HTTP servers work in every mode. Stdio servers use the sandbox image's
|
|
5
|
+
<code class="bg-gray-100 px-1 rounded">npx</code> / <code class="bg-gray-100 px-1 rounded">node</code> /
|
|
6
|
+
<code class="bg-gray-100 px-1 rounded">tsx</code>; paths must live under the workspace when Docker is enabled.
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<div v-if="servers.length === 0" class="text-xs text-gray-500 italic" data-testid="mcp-empty">No MCP servers configured yet.</div>
|
|
10
|
+
|
|
11
|
+
<ul v-else class="space-y-2" data-testid="mcp-server-list">
|
|
12
|
+
<li
|
|
13
|
+
v-for="(entry, idx) in servers"
|
|
14
|
+
:key="entry.id + ':' + idx"
|
|
15
|
+
class="border border-gray-200 rounded p-3 space-y-2"
|
|
16
|
+
:data-testid="'mcp-server-' + entry.id"
|
|
17
|
+
>
|
|
18
|
+
<div class="flex items-center justify-between">
|
|
19
|
+
<div class="flex items-center gap-2">
|
|
20
|
+
<span class="text-sm font-semibold text-gray-800">{{ entry.id }}</span>
|
|
21
|
+
<span
|
|
22
|
+
class="text-[10px] uppercase tracking-wide rounded px-1.5 py-0.5"
|
|
23
|
+
:class="entry.spec.type === 'http' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'"
|
|
24
|
+
>{{ entry.spec.type }}</span
|
|
25
|
+
>
|
|
26
|
+
<label class="flex items-center gap-1 text-xs text-gray-600 ml-2">
|
|
27
|
+
<input type="checkbox" :checked="entry.spec.enabled !== false" :data-testid="'mcp-enabled-' + entry.id" @change="onToggleEnabled(idx, $event)" />
|
|
28
|
+
enabled
|
|
29
|
+
</label>
|
|
30
|
+
</div>
|
|
31
|
+
<button class="text-xs text-red-600 hover:text-red-800" :data-testid="'mcp-remove-' + entry.id" @click="emit('remove', idx)">Remove</button>
|
|
32
|
+
</div>
|
|
33
|
+
<div v-if="entry.spec.type === 'http'" class="text-xs space-y-1">
|
|
34
|
+
<div>
|
|
35
|
+
<span class="text-gray-500">URL:</span>
|
|
36
|
+
<code class="ml-1">{{ entry.spec.url }}</code>
|
|
37
|
+
</div>
|
|
38
|
+
<div v-if="dockerMode && wouldRewriteLocalhost((entry.spec as HttpSpec).url)" class="text-amber-700">
|
|
39
|
+
In Docker mode <code>localhost</code> is rewritten to <code>host.docker.internal</code>.
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div v-else-if="entry.spec.type === 'stdio'" class="text-xs space-y-1">
|
|
43
|
+
<div>
|
|
44
|
+
<span class="text-gray-500">Command:</span>
|
|
45
|
+
<code class="ml-1">{{ entry.spec.command }}</code>
|
|
46
|
+
<code v-if="(entry.spec as StdioSpec).args?.length" class="ml-1">
|
|
47
|
+
{{ ((entry.spec as StdioSpec).args ?? []).join(" ") }}
|
|
48
|
+
</code>
|
|
49
|
+
</div>
|
|
50
|
+
<div
|
|
51
|
+
v-if="dockerMode && stdioHasNonWorkspaceArg((entry.spec as StdioSpec).args)"
|
|
52
|
+
class="text-red-600"
|
|
53
|
+
:data-testid="'mcp-docker-warning-' + entry.id"
|
|
54
|
+
>
|
|
55
|
+
⚠ Contains paths outside the workspace — will not resolve inside Docker.
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</li>
|
|
59
|
+
</ul>
|
|
60
|
+
|
|
61
|
+
<button v-if="!adding" class="text-xs px-2 py-1 rounded border border-gray-300 text-gray-700 hover:bg-gray-50" data-testid="mcp-add-btn" @click="startAdd">
|
|
62
|
+
+ Add MCP Server
|
|
63
|
+
</button>
|
|
64
|
+
|
|
65
|
+
<div v-else class="border border-blue-300 rounded p-3 space-y-2" data-testid="mcp-add-form">
|
|
66
|
+
<label class="block text-xs font-semibold text-gray-700">
|
|
67
|
+
Name
|
|
68
|
+
<input
|
|
69
|
+
v-model="draft.id"
|
|
70
|
+
type="text"
|
|
71
|
+
placeholder="my-server"
|
|
72
|
+
class="mt-1 w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
|
|
73
|
+
data-testid="mcp-draft-id"
|
|
74
|
+
@keydown.stop
|
|
75
|
+
/>
|
|
76
|
+
</label>
|
|
77
|
+
<div class="flex gap-3 text-xs">
|
|
78
|
+
<label class="flex items-center gap-1">
|
|
79
|
+
<input v-model="draft.type" type="radio" value="http" data-testid="mcp-draft-type-http" />
|
|
80
|
+
HTTP
|
|
81
|
+
</label>
|
|
82
|
+
<label class="flex items-center gap-1">
|
|
83
|
+
<input v-model="draft.type" type="radio" value="stdio" data-testid="mcp-draft-type-stdio" />
|
|
84
|
+
Stdio (command)
|
|
85
|
+
</label>
|
|
86
|
+
</div>
|
|
87
|
+
<div v-if="draft.type === 'http'" class="space-y-2">
|
|
88
|
+
<label class="block text-xs font-semibold text-gray-700">
|
|
89
|
+
URL
|
|
90
|
+
<input
|
|
91
|
+
v-model="draft.url"
|
|
92
|
+
type="text"
|
|
93
|
+
placeholder="https://example.com/mcp"
|
|
94
|
+
class="mt-1 w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
|
|
95
|
+
data-testid="mcp-draft-url"
|
|
96
|
+
@keydown.stop
|
|
97
|
+
/>
|
|
98
|
+
</label>
|
|
99
|
+
</div>
|
|
100
|
+
<div v-else class="space-y-2">
|
|
101
|
+
<label class="block text-xs font-semibold text-gray-700">
|
|
102
|
+
Command
|
|
103
|
+
<select
|
|
104
|
+
v-model="draft.command"
|
|
105
|
+
class="mt-1 w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
|
|
106
|
+
data-testid="mcp-draft-command"
|
|
107
|
+
>
|
|
108
|
+
<option value="npx">npx</option>
|
|
109
|
+
<option value="node">node</option>
|
|
110
|
+
<option value="tsx">tsx</option>
|
|
111
|
+
</select>
|
|
112
|
+
</label>
|
|
113
|
+
<label class="block text-xs font-semibold text-gray-700">
|
|
114
|
+
Arguments (one per line)
|
|
115
|
+
<textarea
|
|
116
|
+
v-model="draft.argsText"
|
|
117
|
+
class="mt-1 w-full h-20 px-2 py-1 text-sm font-mono border border-gray-300 rounded focus:outline-none focus:border-blue-400"
|
|
118
|
+
placeholder="-y @modelcontextprotocol/server-filesystem /workspace/path"
|
|
119
|
+
data-testid="mcp-draft-args"
|
|
120
|
+
@keydown.stop
|
|
121
|
+
></textarea>
|
|
122
|
+
</label>
|
|
123
|
+
</div>
|
|
124
|
+
<div v-if="draftError" class="text-xs text-red-600" data-testid="mcp-draft-error">
|
|
125
|
+
{{ draftError }}
|
|
126
|
+
</div>
|
|
127
|
+
<div class="flex justify-end gap-2">
|
|
128
|
+
<button class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50" data-testid="mcp-draft-cancel" @click="cancelAdd">
|
|
129
|
+
Cancel
|
|
130
|
+
</button>
|
|
131
|
+
<button class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" data-testid="mcp-draft-add" @click="commitAdd">Add</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</template>
|
|
136
|
+
|
|
137
|
+
<script setup lang="ts">
|
|
138
|
+
import { ref } from "vue";
|
|
139
|
+
|
|
140
|
+
// UI-local representation of a configured server. Matches
|
|
141
|
+
// server/config.ts#McpServerEntry. Re-declared here to avoid a
|
|
142
|
+
// cross-module type import from the server package.
|
|
143
|
+
export interface HttpSpec {
|
|
144
|
+
type: "http";
|
|
145
|
+
url: string;
|
|
146
|
+
headers?: Record<string, string>;
|
|
147
|
+
enabled?: boolean;
|
|
148
|
+
}
|
|
149
|
+
export interface StdioSpec {
|
|
150
|
+
type: "stdio";
|
|
151
|
+
command: string;
|
|
152
|
+
args?: string[];
|
|
153
|
+
env?: Record<string, string>;
|
|
154
|
+
enabled?: boolean;
|
|
155
|
+
}
|
|
156
|
+
export type ServerSpec = HttpSpec | StdioSpec;
|
|
157
|
+
export interface McpServerEntry {
|
|
158
|
+
id: string;
|
|
159
|
+
spec: ServerSpec;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface Props {
|
|
163
|
+
servers: McpServerEntry[];
|
|
164
|
+
dockerMode: boolean;
|
|
165
|
+
}
|
|
166
|
+
const props = defineProps<Props>();
|
|
167
|
+
|
|
168
|
+
const emit = defineEmits<{
|
|
169
|
+
add: [entry: McpServerEntry];
|
|
170
|
+
update: [index: number, entry: McpServerEntry];
|
|
171
|
+
remove: [index: number];
|
|
172
|
+
}>();
|
|
173
|
+
|
|
174
|
+
interface DraftState {
|
|
175
|
+
id: string;
|
|
176
|
+
type: "http" | "stdio";
|
|
177
|
+
url: string;
|
|
178
|
+
command: string;
|
|
179
|
+
argsText: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const adding = ref(false);
|
|
183
|
+
const draft = ref<DraftState>(emptyDraft());
|
|
184
|
+
const draftError = ref("");
|
|
185
|
+
|
|
186
|
+
function emptyDraft(): DraftState {
|
|
187
|
+
return { id: "", type: "http", url: "", command: "npx", argsText: "" };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function startAdd(): void {
|
|
191
|
+
draft.value = emptyDraft();
|
|
192
|
+
draftError.value = "";
|
|
193
|
+
adding.value = true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function cancelAdd(): void {
|
|
197
|
+
adding.value = false;
|
|
198
|
+
draftError.value = "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const ID_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
202
|
+
|
|
203
|
+
// Derive an id from user input when the Name field is left blank.
|
|
204
|
+
// Covers the common shapes: a scoped npm package in stdio args
|
|
205
|
+
// (`@modelcontextprotocol/server-everything` → `everything`), or a
|
|
206
|
+
// hostname for an HTTP url (`mcp.deepwiki.com` → `deepwiki`).
|
|
207
|
+
function suggestIdFromDraft(state: DraftState): string {
|
|
208
|
+
if (state.type === "http") {
|
|
209
|
+
return suggestIdFromUrl(state.url.trim());
|
|
210
|
+
}
|
|
211
|
+
const args = state.argsText
|
|
212
|
+
.split("\n")
|
|
213
|
+
.map((line) => line.trim())
|
|
214
|
+
.filter((line) => line.length > 0);
|
|
215
|
+
return suggestIdFromStdioArgs(args);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function suggestIdFromUrl(rawUrl: string): string {
|
|
219
|
+
try {
|
|
220
|
+
const host = new URL(rawUrl).hostname;
|
|
221
|
+
const parts = host.split(".").filter((part) => part.length > 0);
|
|
222
|
+
// Drop generic subdomain / TLD noise so `mcp.deepwiki.com` → `deepwiki`.
|
|
223
|
+
const filtered = parts.filter(
|
|
224
|
+
(part, i) => !(i === 0 && (part === "mcp" || part === "www" || part === "api")) && !(i === parts.length - 1 && /^[a-z]{2,4}$/.test(part)),
|
|
225
|
+
);
|
|
226
|
+
const candidate = filtered[0] ?? parts[0] ?? "";
|
|
227
|
+
return slugifyToId(candidate);
|
|
228
|
+
} catch {
|
|
229
|
+
return "";
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function suggestIdFromStdioArgs(args: string[]): string {
|
|
234
|
+
// First arg that isn't a flag is typically the package/script name.
|
|
235
|
+
const payload = args.find((arg) => !arg.startsWith("-"));
|
|
236
|
+
if (!payload) return "";
|
|
237
|
+
// For scoped packages / paths, keep only the last segment.
|
|
238
|
+
const lastSegment = payload.split("/").pop() ?? payload;
|
|
239
|
+
// Strip common MCP naming prefixes so `server-everything` → `everything`.
|
|
240
|
+
const stripped = lastSegment.replace(/^(mcp-server-|server-|mcp-)/, "").replace(/\.(?:[jt]s|mjs|cjs)$/, "");
|
|
241
|
+
return slugifyToId(stripped);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function slugifyToId(raw: string): string {
|
|
245
|
+
let slug = raw.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
|
246
|
+
// Strip leading/trailing hyphens with explicit while-loops so the
|
|
247
|
+
// regex engine can't be lured into catastrophic backtracking on a
|
|
248
|
+
// crafted input.
|
|
249
|
+
while (slug.startsWith("-")) slug = slug.slice(1);
|
|
250
|
+
while (slug.endsWith("-")) slug = slug.slice(0, -1);
|
|
251
|
+
slug = slug.slice(0, 64);
|
|
252
|
+
// Must start with a lowercase letter.
|
|
253
|
+
if (!/^[a-z]/.test(slug)) return "";
|
|
254
|
+
return slug;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function ensureUniqueId(base: string): string {
|
|
258
|
+
if (!base) return "";
|
|
259
|
+
if (!props.servers.some((server) => server.id === base)) return base;
|
|
260
|
+
for (let i = 2; i < 1000; i += 1) {
|
|
261
|
+
const candidate = `${base}-${i}`;
|
|
262
|
+
if (!props.servers.some((server) => server.id === candidate)) return candidate;
|
|
263
|
+
}
|
|
264
|
+
return "";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function commitAdd(): void {
|
|
268
|
+
let id = draft.value.id.trim();
|
|
269
|
+
if (!id) {
|
|
270
|
+
const suggested = ensureUniqueId(suggestIdFromDraft(draft.value));
|
|
271
|
+
if (!suggested) {
|
|
272
|
+
draftError.value = "Please provide a Name, or enter a URL / args we can derive one from.";
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
id = suggested;
|
|
276
|
+
}
|
|
277
|
+
if (!ID_RE.test(id)) {
|
|
278
|
+
draftError.value = "Name must start with a lowercase letter and contain only [a-z0-9_-].";
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (props.servers.some((server) => server.id === id)) {
|
|
282
|
+
draftError.value = `Server id "${id}" already exists.`;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
let spec: ServerSpec;
|
|
286
|
+
if (draft.value.type === "http") {
|
|
287
|
+
const url = draft.value.url.trim();
|
|
288
|
+
if (!/^https?:\/\//.test(url)) {
|
|
289
|
+
draftError.value = "HTTP URL must start with http:// or https://";
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
spec = { type: "http", url, enabled: true };
|
|
293
|
+
} else {
|
|
294
|
+
const args = draft.value.argsText
|
|
295
|
+
.split("\n")
|
|
296
|
+
.map((line) => line.trim())
|
|
297
|
+
.filter((line) => line.length > 0);
|
|
298
|
+
spec = {
|
|
299
|
+
type: "stdio",
|
|
300
|
+
command: draft.value.command,
|
|
301
|
+
args,
|
|
302
|
+
enabled: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
emit("add", { id, spec });
|
|
306
|
+
adding.value = false;
|
|
307
|
+
draftError.value = "";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Called by the parent right before Save. If the draft form is open
|
|
311
|
+
// and has any input, commit it (auto-generating a Name if blank). If
|
|
312
|
+
// the draft is empty, silently close the form. Returns false only
|
|
313
|
+
// when validation fails so the parent can surface an error and abort
|
|
314
|
+
// the save — this is what spares users the pre-PR footgun of clicking
|
|
315
|
+
// Save without first clicking the inner Add button.
|
|
316
|
+
function flushDraft(): boolean {
|
|
317
|
+
if (!adding.value) return true;
|
|
318
|
+
const hasInput =
|
|
319
|
+
draft.value.id.trim().length > 0 ||
|
|
320
|
+
(draft.value.type === "http" && draft.value.url.trim().length > 0) ||
|
|
321
|
+
(draft.value.type === "stdio" && draft.value.argsText.trim().length > 0);
|
|
322
|
+
if (!hasInput) {
|
|
323
|
+
cancelAdd();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
commitAdd();
|
|
327
|
+
return !adding.value;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
defineExpose({ flushDraft });
|
|
331
|
+
|
|
332
|
+
function onToggleEnabled(index: number, event: Event): void {
|
|
333
|
+
const target = event.target as HTMLInputElement;
|
|
334
|
+
const entry = props.servers[index];
|
|
335
|
+
if (!entry) return;
|
|
336
|
+
emit("update", index, {
|
|
337
|
+
...entry,
|
|
338
|
+
spec: { ...entry.spec, enabled: target.checked },
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function wouldRewriteLocalhost(url: string): boolean {
|
|
343
|
+
return /^https?:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/.test(url);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function stdioHasNonWorkspaceArg(args?: string[]): boolean {
|
|
347
|
+
if (!args) return false;
|
|
348
|
+
return args.some((arg) => /^\//.test(arg) && arg !== "/workspace" && !arg.startsWith("/workspace/"));
|
|
349
|
+
}
|
|
350
|
+
</script>
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="open" class="fixed inset-0 z-50 bg-black/40 flex items-center justify-center" data-testid="settings-modal-backdrop" @click="close">
|
|
3
|
+
<div
|
|
4
|
+
class="bg-white rounded-lg shadow-xl w-[36rem] max-h-[85vh] flex flex-col"
|
|
5
|
+
role="dialog"
|
|
6
|
+
aria-modal="true"
|
|
7
|
+
aria-labelledby="settings-modal-title"
|
|
8
|
+
data-testid="settings-modal"
|
|
9
|
+
@click.stop
|
|
10
|
+
>
|
|
11
|
+
<div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
12
|
+
<h2 id="settings-modal-title" class="text-base font-semibold text-gray-900">Settings</h2>
|
|
13
|
+
<button class="text-gray-400 hover:text-gray-700" title="Close" data-testid="settings-close-btn" @click="close">
|
|
14
|
+
<span class="material-icons">close</span>
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="flex border-b border-gray-200 px-5">
|
|
19
|
+
<button
|
|
20
|
+
class="px-3 py-2 text-sm border-b-2"
|
|
21
|
+
:class="activeTab === 'tools' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-800'"
|
|
22
|
+
data-testid="settings-tab-tools"
|
|
23
|
+
@click="activeTab = 'tools'"
|
|
24
|
+
>
|
|
25
|
+
Allowed Tools
|
|
26
|
+
</button>
|
|
27
|
+
<button
|
|
28
|
+
class="px-3 py-2 text-sm border-b-2"
|
|
29
|
+
:class="activeTab === 'mcp' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-800'"
|
|
30
|
+
data-testid="settings-tab-mcp"
|
|
31
|
+
@click="activeTab = 'mcp'"
|
|
32
|
+
>
|
|
33
|
+
MCP Servers
|
|
34
|
+
</button>
|
|
35
|
+
<button
|
|
36
|
+
class="px-3 py-2 text-sm border-b-2"
|
|
37
|
+
:class="activeTab === 'dirs' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-800'"
|
|
38
|
+
data-testid="settings-tab-dirs"
|
|
39
|
+
@click="activeTab = 'dirs'"
|
|
40
|
+
>
|
|
41
|
+
Directories
|
|
42
|
+
</button>
|
|
43
|
+
<button
|
|
44
|
+
class="px-3 py-2 text-sm border-b-2"
|
|
45
|
+
:class="activeTab === 'refs' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-800'"
|
|
46
|
+
data-testid="settings-tab-refs"
|
|
47
|
+
@click="activeTab = 'refs'"
|
|
48
|
+
>
|
|
49
|
+
Reference Dirs
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="px-5 py-4 overflow-y-auto flex-1 space-y-4 text-gray-900">
|
|
54
|
+
<div v-if="loadError" class="text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2" role="alert" data-testid="settings-load-error">
|
|
55
|
+
⚠ {{ loadError }}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div v-if="activeTab === 'tools'" class="space-y-3">
|
|
59
|
+
<p class="text-xs text-gray-600 leading-relaxed">
|
|
60
|
+
Extra tool names to pass to Claude via
|
|
61
|
+
<code class="bg-gray-100 px-1 rounded">--allowedTools</code>. One per line. Useful for built-in Claude Code MCP servers like Gmail / Google Calendar
|
|
62
|
+
after you have authenticated via <code class="bg-gray-100 px-1 rounded">claude mcp</code>.
|
|
63
|
+
</p>
|
|
64
|
+
<label class="block">
|
|
65
|
+
<span class="text-xs font-semibold text-gray-700">Tool names</span>
|
|
66
|
+
<textarea
|
|
67
|
+
v-model="toolsText"
|
|
68
|
+
class="mt-1 w-full h-48 px-2 py-1.5 text-sm font-mono border border-gray-300 rounded focus:outline-none focus:border-blue-400"
|
|
69
|
+
placeholder="mcp__claude_ai_Gmail mcp__claude_ai_Google_Calendar"
|
|
70
|
+
data-testid="settings-tools-textarea"
|
|
71
|
+
@keydown.stop
|
|
72
|
+
></textarea>
|
|
73
|
+
</label>
|
|
74
|
+
<p v-if="invalidToolNames.length > 0" class="text-xs text-amber-700">
|
|
75
|
+
These look non-standard (expected prefix
|
|
76
|
+
<code class="bg-gray-100 px-1 rounded">mcp__</code>):
|
|
77
|
+
{{ invalidToolNames.join(", ") }}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div v-else-if="activeTab === 'mcp'" class="space-y-3">
|
|
82
|
+
<div
|
|
83
|
+
v-if="mcpToolsError"
|
|
84
|
+
class="text-xs text-amber-800 bg-amber-50 border border-amber-200 rounded px-2 py-1"
|
|
85
|
+
role="alert"
|
|
86
|
+
data-testid="mcp-tools-error"
|
|
87
|
+
>
|
|
88
|
+
⚠ Could not fetch MCP tool status: {{ mcpToolsError }}. Showing all tools regardless of enablement.
|
|
89
|
+
</div>
|
|
90
|
+
<SettingsMcpTab
|
|
91
|
+
ref="mcpTabRef"
|
|
92
|
+
:servers="mcpServers"
|
|
93
|
+
:docker-mode="dockerMode"
|
|
94
|
+
@add="addMcpServer"
|
|
95
|
+
@update="updateMcpServer"
|
|
96
|
+
@remove="removeMcpServer"
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<SettingsWorkspaceDirsTab v-else-if="activeTab === 'dirs'" />
|
|
101
|
+
|
|
102
|
+
<SettingsReferenceDirsTab v-else-if="activeTab === 'refs'" />
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="px-5 py-3 border-t border-gray-200 flex items-center justify-between gap-3">
|
|
106
|
+
<span v-if="statusMessage" class="text-xs" :class="statusError ? 'text-red-600' : 'text-green-600'" data-testid="settings-status">
|
|
107
|
+
{{ statusMessage }}
|
|
108
|
+
</span>
|
|
109
|
+
<span v-else class="text-xs text-gray-500"> Changes apply on the next message. No restart needed. </span>
|
|
110
|
+
<div class="flex gap-2">
|
|
111
|
+
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" data-testid="settings-cancel-btn" @click="close">
|
|
112
|
+
Cancel
|
|
113
|
+
</button>
|
|
114
|
+
<button
|
|
115
|
+
class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
|
|
116
|
+
:disabled="saving || loading || !!loadError"
|
|
117
|
+
:title="loadError ? 'Cannot save until settings load successfully' : undefined"
|
|
118
|
+
data-testid="settings-save-btn"
|
|
119
|
+
@click="save"
|
|
120
|
+
>
|
|
121
|
+
{{ saving ? "Saving…" : loading ? "Loading…" : "Save" }}
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<script setup lang="ts">
|
|
130
|
+
import { computed, ref, watch } from "vue";
|
|
131
|
+
import SettingsMcpTab from "./SettingsMcpTab.vue";
|
|
132
|
+
import SettingsWorkspaceDirsTab from "./SettingsWorkspaceDirsTab.vue";
|
|
133
|
+
import SettingsReferenceDirsTab from "./SettingsReferenceDirsTab.vue";
|
|
134
|
+
import type { McpServerEntry } from "./SettingsMcpTab.vue";
|
|
135
|
+
import { apiGet, apiPut } from "../utils/api";
|
|
136
|
+
import { API_ROUTES } from "../config/apiRoutes";
|
|
137
|
+
|
|
138
|
+
interface Props {
|
|
139
|
+
open: boolean;
|
|
140
|
+
dockerMode?: boolean;
|
|
141
|
+
// Forwarded from useMcpTools — if non-null, the MCP tab shows a
|
|
142
|
+
// small warning strip so the user knows "all tools visible" is a
|
|
143
|
+
// fallback rather than an accurate listing.
|
|
144
|
+
mcpToolsError?: string | null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
148
|
+
dockerMode: false,
|
|
149
|
+
mcpToolsError: null,
|
|
150
|
+
});
|
|
151
|
+
const emit = defineEmits<{
|
|
152
|
+
"update:open": [value: boolean];
|
|
153
|
+
saved: [];
|
|
154
|
+
}>();
|
|
155
|
+
|
|
156
|
+
// Typed ref to the SettingsMcpTab so save() can flush a pending draft
|
|
157
|
+
// before PUTing (eliminates the "user typed but forgot the inner Add
|
|
158
|
+
// button" footgun). Null when the MCP tab isn't the active one.
|
|
159
|
+
const mcpTabRef = ref<{ flushDraft: () => boolean } | null>(null);
|
|
160
|
+
|
|
161
|
+
const activeTab = ref<"tools" | "mcp" | "dirs" | "refs">("tools");
|
|
162
|
+
const toolsText = ref("");
|
|
163
|
+
const mcpServers = ref<McpServerEntry[]>([]);
|
|
164
|
+
const loadError = ref("");
|
|
165
|
+
const statusMessage = ref("");
|
|
166
|
+
const statusError = ref(false);
|
|
167
|
+
const saving = ref(false);
|
|
168
|
+
// `true` from the moment the modal opens until the first loadConfig()
|
|
169
|
+
// call resolves. Prevents the Save button from submitting the initial
|
|
170
|
+
// empty arrays before the real config arrives, and prevents stale
|
|
171
|
+
// responses (from a previous open) from overwriting fresh input.
|
|
172
|
+
const loading = ref(false);
|
|
173
|
+
// Monotonically increasing token so an in-flight loadConfig() whose
|
|
174
|
+
// modal has been reopened can notice it's stale and discard its result.
|
|
175
|
+
let loadToken = 0;
|
|
176
|
+
|
|
177
|
+
const parsedToolNames = computed(() =>
|
|
178
|
+
toolsText.value
|
|
179
|
+
.split("\n")
|
|
180
|
+
.map((s) => s.trim())
|
|
181
|
+
.filter((s) => s.length > 0),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const invalidToolNames = computed(() => parsedToolNames.value.filter((n) => !n.startsWith("mcp__") && !isBuiltIn(n)));
|
|
185
|
+
|
|
186
|
+
function isBuiltIn(name: string): boolean {
|
|
187
|
+
return ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebFetch", "WebSearch"].includes(name);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function loadConfig(): Promise<void> {
|
|
191
|
+
const token = ++loadToken;
|
|
192
|
+
loading.value = true;
|
|
193
|
+
loadError.value = "";
|
|
194
|
+
statusMessage.value = "";
|
|
195
|
+
const response = await apiGet<{
|
|
196
|
+
settings: { extraAllowedTools: string[] };
|
|
197
|
+
mcp?: { servers: McpServerEntry[] };
|
|
198
|
+
}>(API_ROUTES.config.base);
|
|
199
|
+
// A newer open() has already started another load — drop this one.
|
|
200
|
+
if (token !== loadToken) return;
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
loadError.value = response.status === 0 ? response.error || "Network error" : `Failed to load settings (HTTP ${response.status})`;
|
|
203
|
+
} else {
|
|
204
|
+
toolsText.value = response.data.settings.extraAllowedTools.join("\n");
|
|
205
|
+
mcpServers.value = response.data.mcp?.servers ?? [];
|
|
206
|
+
}
|
|
207
|
+
if (token === loadToken) loading.value = false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function save(): Promise<void> {
|
|
211
|
+
// Extra safety: the button is already disabled while loading, but
|
|
212
|
+
// guard the function body too so any programmatic caller can't
|
|
213
|
+
// submit a half-loaded form.
|
|
214
|
+
if (loading.value) return;
|
|
215
|
+
// Auto-commit any half-entered draft on the MCP tab. If the draft
|
|
216
|
+
// is invalid the tab sets its own inline error — abort the save so
|
|
217
|
+
// the user can fix it.
|
|
218
|
+
if (mcpTabRef.value && !mcpTabRef.value.flushDraft()) {
|
|
219
|
+
statusError.value = true;
|
|
220
|
+
statusMessage.value = "Finish or cancel the pending MCP server entry first.";
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
saving.value = true;
|
|
224
|
+
statusMessage.value = "";
|
|
225
|
+
statusError.value = false;
|
|
226
|
+
// Single atomic endpoint — avoids the partial-save state where
|
|
227
|
+
// extraAllowedTools is persisted but MCP config write fails.
|
|
228
|
+
const response = await apiPut<unknown>(API_ROUTES.config.base, {
|
|
229
|
+
settings: { extraAllowedTools: parsedToolNames.value },
|
|
230
|
+
mcp: { servers: mcpServers.value },
|
|
231
|
+
});
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
statusError.value = true;
|
|
234
|
+
statusMessage.value = response.error || "Save failed";
|
|
235
|
+
} else {
|
|
236
|
+
emit("saved");
|
|
237
|
+
// Close on success. Changes take effect on the next message, so
|
|
238
|
+
// the user has no reason to stay in the modal after a good save.
|
|
239
|
+
close();
|
|
240
|
+
}
|
|
241
|
+
saving.value = false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function close(): void {
|
|
245
|
+
emit("update:open", false);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function addMcpServer(entry: McpServerEntry): void {
|
|
249
|
+
mcpServers.value = [...mcpServers.value, entry];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function updateMcpServer(index: number, entry: McpServerEntry): void {
|
|
253
|
+
const next = [...mcpServers.value];
|
|
254
|
+
next[index] = entry;
|
|
255
|
+
mcpServers.value = next;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function removeMcpServer(index: number): void {
|
|
259
|
+
const next = [...mcpServers.value];
|
|
260
|
+
next.splice(index, 1);
|
|
261
|
+
mcpServers.value = next;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
watch(
|
|
265
|
+
() => props.open,
|
|
266
|
+
(isOpen) => {
|
|
267
|
+
if (isOpen) {
|
|
268
|
+
loadConfig();
|
|
269
|
+
statusMessage.value = "";
|
|
270
|
+
statusError.value = false;
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{ immediate: true },
|
|
274
|
+
);
|
|
275
|
+
</script>
|