fix: move mcp config copy to settings

This commit is contained in:
lucaronin
2026-04-29 13:39:05 +02:00
parent 3a6c97272d
commit 42bdd559ef
9 changed files with 54 additions and 62 deletions

View File

@@ -1643,7 +1643,6 @@ function App() {
onInitializeProperties={handleInitializeProperties}
showAIChat={dialogs.showAIChat}
onToggleAIChat={dialogs.toggleAIChat}
onCopyMcpConfig={handleCopyMcpConfig}
vaultPath={resolvedPath}
noteList={aiNoteList}
noteListFilter={aiNoteListFilter}
@@ -1726,7 +1725,7 @@ function App() {
onCommit={conflictResolver.commitResolution}
onClose={conflictFlow.handleCloseConflictResolver}
/>
<SettingsPanel open={dialogs.showSettings} settings={settings} aiAgentsStatus={aiAgentsStatus} locale={appLocale} systemLocale={systemLocale} isGitVault={isGitVault} onSave={saveSettings} explicitOrganizationEnabled={explicitOrganizationEnabled} onSaveExplicitOrganization={handleSaveExplicitOrganization} onClose={dialogs.closeSettings} />
<SettingsPanel open={dialogs.showSettings} settings={settings} aiAgentsStatus={aiAgentsStatus} locale={appLocale} systemLocale={systemLocale} isGitVault={isGitVault} onSave={saveSettings} onCopyMcpConfig={handleCopyMcpConfig} explicitOrganizationEnabled={explicitOrganizationEnabled} onSaveExplicitOrganization={handleSaveExplicitOrganization} onClose={dialogs.closeSettings} />
<FeedbackDialog open={showFeedback} onClose={closeFeedback} />
<McpSetupDialog open={showMcpSetupDialog} status={mcpStatus} busyAction={mcpDialogAction} manualConfigSnippet={mcpConfigSnippet} manualConfigLoading={mcpConfigLoading} manualConfigError={mcpConfigError} locale={appLocale} onClose={closeMcpSetupDialog} onConnect={handleConnectMcp} onCopyManualConfig={handleCopyMcpConfig} onDisconnect={handleDisconnectMcp} onLoadManualConfig={handleLoadMcpConfigSnippet} />
<CloneVaultModal key={dialogs.showCloneVault ? 'clone-open' : 'clone-closed'} open={dialogs.showCloneVault} onClose={dialogs.closeCloneVault} onVaultCloned={vaultSwitcher.handleVaultCloned} />

View File

@@ -201,14 +201,10 @@ describe('AiPanel', () => {
expect(mockClearConversation).toHaveBeenCalledOnce()
})
it('copies the MCP config from the AI panel header action', () => {
const onCopyMcpConfig = vi.fn()
render(<AiPanel onClose={vi.fn()} onCopyMcpConfig={onCopyMcpConfig} vaultPath="/tmp/vault" />)
it('keeps the MCP config action out of the AI panel header', () => {
render(<AiPanel onClose={vi.fn()} vaultPath="/tmp/vault" />)
expect(screen.getByText('MCP config')).toBeVisible()
fireEvent.click(screen.getByRole('button', { name: 'Copy MCP config' }))
expect(onCopyMcpConfig).toHaveBeenCalledOnce()
expect(screen.queryByRole('button', { name: 'Copy MCP config' })).toBeNull()
})
it('renders empty state without context', () => {

View File

@@ -22,7 +22,6 @@ export type { AiAgentMessage } from '../hooks/useCliAiAgent'
interface AiPanelProps {
onClose: () => void
onCopyMcpConfig?: () => void
onOpenNote?: (path: string) => void
onUnsupportedAiPaste?: (message: string) => void
defaultAiAgent?: AiAgentId
@@ -45,7 +44,6 @@ interface AiPanelProps {
interface AiPanelViewProps {
controller: AiPanelController
onClose: () => void
onCopyMcpConfig?: () => void
onOpenNote?: (path: string) => void
onUnsupportedAiPaste?: (message: string) => void
defaultAiAgent?: AiAgentId
@@ -63,7 +61,6 @@ function readinessFromReadyFlag(ready: boolean | undefined): AiAgentReadiness {
export function AiPanelView({
controller,
onClose,
onCopyMcpConfig,
onOpenNote,
onUnsupportedAiPaste,
defaultAiAgent: providedDefaultAiAgent,
@@ -126,7 +123,6 @@ export function AiPanelView({
permissionModeDisabled={isActive}
onPermissionModeChange={handlePermissionModeChange}
onClose={onClose}
onCopyMcpConfig={onCopyMcpConfig}
onNewChat={handleNewChat}
/>
{activeEntry && (
@@ -160,7 +156,6 @@ export function AiPanelView({
export function AiPanel({
onClose,
onCopyMcpConfig,
onOpenNote,
onUnsupportedAiPaste,
defaultAiAgent: providedDefaultAiAgent,
@@ -202,7 +197,6 @@ export function AiPanel({
<AiPanelView
controller={controller}
onClose={onClose}
onCopyMcpConfig={onCopyMcpConfig}
onOpenNote={onOpenNote}
onUnsupportedAiPaste={onUnsupportedAiPaste}
defaultAiAgent={providedDefaultAiAgent}

View File

@@ -1,6 +1,5 @@
import { useEffect, useRef } from 'react'
import { Robot, X, PaperPlaneRight, Plus, Link } from '@phosphor-icons/react'
import { Copy } from 'lucide-react'
import { AiMessage } from './AiMessage'
import { Button } from '@/components/ui/button'
import { ActionTooltip } from '@/components/ui/action-tooltip'
@@ -25,7 +24,6 @@ interface AiPanelHeaderProps {
permissionModeDisabled: boolean
onPermissionModeChange: (mode: AiAgentPermissionMode) => void
onClose: () => void
onCopyMcpConfig?: () => void
onNewChat: () => void
}
@@ -174,7 +172,6 @@ export function AiPanelHeader({
permissionModeDisabled,
onPermissionModeChange,
onClose,
onCopyMcpConfig,
onNewChat,
}: AiPanelHeaderProps) {
const t = createTranslator(locale)
@@ -195,21 +192,6 @@ export function AiPanelHeader({
{headerStatusText({ agentLabel, agentReadiness, modeLabel, t })}
</span>
</div>
{onCopyMcpConfig ? (
<Button
type="button"
variant="outline"
size="xs"
onClick={onCopyMcpConfig}
className="h-7 gap-1.5 px-2 text-[11px]"
aria-label={t('ai.panel.copyMcpConfig')}
title={t('ai.panel.copyMcpConfig')}
data-testid="ai-copy-mcp-config"
>
<Copy size={15} />
<span>{t('ai.panel.mcpConfig')}</span>
</Button>
) : null}
<Button
type="button"
variant="ghost"

View File

@@ -67,7 +67,6 @@ interface EditorProps {
onInitializeProperties?: (path: string) => void
showAIChat?: boolean
onToggleAIChat?: () => void
onCopyMcpConfig?: () => void
vaultPath?: string
noteList?: NoteListItem[]
noteListFilter?: { type: string | null; query: string }
@@ -339,7 +338,6 @@ function EditorLayout({
showDiffToggle,
showAIChat,
onToggleAIChat,
onCopyMcpConfig,
inspectorCollapsed,
onToggleInspector,
onNavigateWikilink,
@@ -402,7 +400,6 @@ function EditorLayout({
showDiffToggle: boolean
showAIChat?: boolean
onToggleAIChat?: () => void
onCopyMcpConfig?: () => void
inspectorCollapsed: boolean
onToggleInspector: () => void
onNavigateWikilink: (target: string) => void
@@ -523,7 +520,6 @@ function EditorLayout({
noteListFilter={noteListFilter}
onToggleInspector={onToggleInspector}
onToggleAIChat={onToggleAIChat}
onCopyMcpConfig={onCopyMcpConfig}
onNavigateWikilink={onNavigateWikilink}
onViewCommitDiff={handleViewCommitDiff}
onUpdateFrontmatter={onUpdateFrontmatter}
@@ -555,7 +551,6 @@ export const Editor = memo(function Editor(props: EditorProps) {
inspectorEntry, inspectorContent, gitHistory,
onUpdateFrontmatter, onDeleteProperty, onAddProperty, onCreateMissingType, onCreateAndOpenNote, onInitializeProperties,
showAIChat, onToggleAIChat,
onCopyMcpConfig,
vaultPath, noteList, noteListFilter,
onToggleFavorite, onToggleOrganized, onRevealFile, onCopyFilePath, onOpenExternalFile,
onDeleteNote, onArchiveNote, onUnarchiveNote,
@@ -616,7 +611,6 @@ export const Editor = memo(function Editor(props: EditorProps) {
showDiffToggle={showDiffToggle}
showAIChat={showAIChat}
onToggleAIChat={onToggleAIChat}
onCopyMcpConfig={onCopyMcpConfig}
inspectorCollapsed={inspectorCollapsed}
onToggleInspector={onToggleInspector}
onNavigateWikilink={onNavigateWikilink}

View File

@@ -25,7 +25,6 @@ interface EditorRightPanelProps {
noteListFilter?: { type: string | null; query: string }
onToggleInspector: () => void
onToggleAIChat?: () => void
onCopyMcpConfig?: () => void
onNavigateWikilink: (target: string) => void
onViewCommitDiff: (commitHash: string) => Promise<void>
onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise<void>
@@ -49,7 +48,6 @@ export function EditorRightPanel({
inspectorEntry, inspectorContent, entries, gitHistory, vaultPath,
noteList, noteListFilter,
onToggleInspector, onToggleAIChat, onNavigateWikilink, onViewCommitDiff,
onCopyMcpConfig,
onUpdateFrontmatter, onDeleteProperty, onAddProperty, onCreateMissingType, onCreateAndOpenNote, onInitializeProperties, onToggleRawEditor, onOpenNote,
onFileCreated, onFileModified, onVaultChanged,
locale,
@@ -90,7 +88,6 @@ export function EditorRightPanel({
<AiPanelView
controller={aiPanelController}
onClose={() => onToggleAIChat?.()}
onCopyMcpConfig={onCopyMcpConfig}
onOpenNote={onOpenNote}
onUnsupportedAiPaste={onUnsupportedAiPaste}
defaultAiAgent={defaultAiAgent}

View File

@@ -458,6 +458,23 @@ describe('SettingsPanel', () => {
expect(screen.getByText(/to open settings/)).toBeInTheDocument()
})
it('copies the MCP config from the AI Agents section', () => {
const onCopyMcpConfig = vi.fn()
render(
<SettingsPanel
open={true}
settings={emptySettings}
onSave={onSave}
onCopyMcpConfig={onCopyMcpConfig}
onClose={onClose}
/>
)
fireEvent.click(screen.getByRole('button', { name: 'Copy MCP config' }))
expect(onCopyMcpConfig).toHaveBeenCalledOnce()
})
describe('Privacy & Telemetry section', () => {
it('renders crash reporting and analytics toggles', () => {
render(

View File

@@ -16,6 +16,7 @@ import {
type ReactNode,
} from 'react'
import { Moon, Sun, X } from '@phosphor-icons/react'
import { Copy } from 'lucide-react'
import type { Settings } from '../types'
import {
APP_LOCALES,
@@ -56,6 +57,7 @@ interface SettingsPanelProps {
locale?: AppLocale
systemLocale?: AppLocale
onSave: (settings: Settings) => void
onCopyMcpConfig?: () => void
isGitVault?: boolean
explicitOrganizationEnabled?: boolean
onSaveExplicitOrganization?: (enabled: boolean) => void
@@ -95,6 +97,7 @@ interface SettingsBodyProps {
aiAgentsStatus: AiAgentsStatus
defaultAiAgent: AiAgentId
setDefaultAiAgent: (value: AiAgentId) => void
onCopyMcpConfig?: () => void
releaseChannel: ReleaseChannel
setReleaseChannel: (value: ReleaseChannel) => void
themeMode: ThemeMode
@@ -217,6 +220,7 @@ export function SettingsPanel({
locale = 'en',
systemLocale = locale,
onSave,
onCopyMcpConfig,
isGitVault = true,
explicitOrganizationEnabled = true,
onSaveExplicitOrganization,
@@ -231,6 +235,7 @@ export function SettingsPanel({
locale={locale}
systemLocale={systemLocale}
onSave={onSave}
onCopyMcpConfig={onCopyMcpConfig}
isGitVault={isGitVault}
explicitOrganizationEnabled={explicitOrganizationEnabled}
onSaveExplicitOrganization={onSaveExplicitOrganization}
@@ -252,6 +257,7 @@ function SettingsPanelInner({
aiAgentsStatus,
systemLocale,
onSave,
onCopyMcpConfig,
isGitVault,
explicitOrganizationEnabled,
onSaveExplicitOrganization,
@@ -299,12 +305,9 @@ function SettingsPanelInner({
onClose()
}, [draft, onClose, onSave, onSaveExplicitOrganization, settings])
const handleBackdropClick = useCallback(
(event: ReactMouseEvent<HTMLDivElement>) => {
if (event.target === event.currentTarget) onClose()
},
[onClose],
)
const handleBackdropClick = useCallback((event: ReactMouseEvent<HTMLDivElement>) => {
if (event.target === event.currentTarget) onClose()
}, [onClose])
const handleKeyDown = useCallback(
(event: ReactKeyboardEvent) => {
@@ -354,6 +357,7 @@ function SettingsPanelInner({
aiAgentsStatus={aiAgentsStatus}
defaultAiAgent={draft.defaultAiAgent}
setDefaultAiAgent={(value) => updateDraft('defaultAiAgent', value)}
onCopyMcpConfig={onCopyMcpConfig}
releaseChannel={draft.releaseChannel}
setReleaseChannel={(value) => updateDraft('releaseChannel', value)}
themeMode={draft.themeMode}
@@ -415,6 +419,7 @@ function SettingsBody({
aiAgentsStatus,
defaultAiAgent,
setDefaultAiAgent,
onCopyMcpConfig,
releaseChannel,
setReleaseChannel,
themeMode,
@@ -497,6 +502,7 @@ function SettingsBody({
aiAgentsStatus={aiAgentsStatus}
defaultAiAgent={defaultAiAgent}
setDefaultAiAgent={setDefaultAiAgent}
onCopyMcpConfig={onCopyMcpConfig}
/>
</SettingsSection>
@@ -810,7 +816,8 @@ function AiAgentSettingsSection({
aiAgentsStatus,
defaultAiAgent,
setDefaultAiAgent,
}: Pick<SettingsBodyProps, 't' | 'aiAgentsStatus' | 'defaultAiAgent' | 'setDefaultAiAgent'>) {
onCopyMcpConfig,
}: Pick<SettingsBodyProps, 't' | 'aiAgentsStatus' | 'defaultAiAgent' | 'setDefaultAiAgent' | 'onCopyMcpConfig'>) {
return (
<>
<SectionHeading
@@ -829,6 +836,21 @@ function AiAgentSettingsSection({
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', lineHeight: 1.5 }}>
{renderDefaultAiAgentSummary(defaultAiAgent, aiAgentsStatus, t)}
</div>
{onCopyMcpConfig ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={onCopyMcpConfig}
className="w-fit gap-2"
aria-label={t('ai.panel.copyMcpConfig')}
data-testid="settings-copy-mcp-config"
>
<Copy size={15} />
{t('ai.panel.copyMcpConfig')}
</Button>
) : null}
</>
)
}

View File

@@ -8,21 +8,12 @@ test.describe('MCP config copy', () => {
await expect(page.getByTestId('note-list-container')).toBeVisible({ timeout: 5_000 })
})
test('copies the active-vault MCP config from the AI panel using only the keyboard', async ({ context, page }) => {
test('copies the active-vault MCP config from the Settings AI section', async ({ context, page }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
await page.locator('.app__note-list .cursor-pointer').first().click()
await page.locator('.bn-editor').click()
await sendShortcut(page, 'L', ['Meta', 'Shift'])
await expect(page.getByTestId('ai-panel')).toBeVisible({ timeout: 3_000 })
await page.getByTestId('agent-input').focus()
await page.keyboard.press('Shift+Tab')
await page.keyboard.press('Shift+Tab')
await page.keyboard.press('Shift+Tab')
const copyButton = page.getByRole('button', { name: 'Copy MCP config' })
await expect(copyButton).toBeFocused()
await copyButton.press('Enter')
await sendShortcut(page, ',', ['Control'])
await expect(page.getByTestId('settings-panel')).toBeVisible({ timeout: 3_000 })
await page.getByTestId('settings-copy-mcp-config').click()
await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toContain('"mcpServers"')
const copiedConfig = await page.evaluate(() => navigator.clipboard.readText())