Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
uninstallCli,
parseLaunchDirectory,
} from "./lib/cli"
import { startClaudeConfigWatcher, stopClaudeConfigWatcher } from "./lib/claude-config-watcher"
import { cleanupGitWatchers } from "./lib/git/watcher"
import { cancelAllPendingOAuth, handleMcpOAuthCallback } from "./lib/mcp-auth"
import {
Expand Down Expand Up @@ -877,6 +878,11 @@ if (gotTheLock) {
}
}, 3000)

// Watch ~/.claude.json for changes to auto-refresh MCP servers
startClaudeConfigWatcher().catch((error) => {
console.error("[App] Failed to start config watcher:", error)
})

// Handle directory argument from CLI (e.g., `1code /path/to/project`)
parseLaunchDirectory()

Expand Down Expand Up @@ -907,6 +913,7 @@ if (gotTheLock) {
app.on("before-quit", async () => {
console.log("[App] Shutting down...")
cancelAllPendingOAuth()
await stopClaudeConfigWatcher()
await cleanupGitWatchers()
await shutdownAnalytics()
await closeDatabase()
Expand Down
91 changes: 91 additions & 0 deletions src/main/lib/claude-config-watcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Watches ~/.claude.json for changes and notifies renderer to re-initialize MCP servers.
*
* When a user edits their Claude config (e.g., adding/removing MCP servers),
* this watcher detects the change, clears cached MCP data, and notifies
* the renderer so it can refresh MCP server status without requiring a restart.
*/
import { BrowserWindow } from "electron"
import * as os from "os"
import * as path from "path"
import { mcpConfigCache, workingMcpServers } from "./trpc/routers/claude"

const CLAUDE_CONFIG_PATH = path.join(os.homedir(), ".claude.json")

// Simple debounce to batch rapid file changes
function debounce<T extends (...args: unknown[]) => unknown>(
func: T,
wait: number,
): (...args: Parameters<T>) => void {
let timeoutId: NodeJS.Timeout | null = null
return (...args: Parameters<T>) => {
if (timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(() => func(...args), wait)
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
let watcher: any = null

/**
* Start watching ~/.claude.json for changes.
* When changes are detected:
* 1. Clears the in-memory MCP config cache and working servers cache
* 2. Sends an IPC event to all renderer windows so they can refetch MCP config
*/
export async function startClaudeConfigWatcher(): Promise<void> {
if (watcher) return

const chokidar = await import("chokidar")

watcher = chokidar.watch(CLAUDE_CONFIG_PATH, {
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 50,
},
usePolling: false,
followSymlinks: false,
})

const handleChange = debounce(() => {
console.log("[ConfigWatcher] ~/.claude.json changed, clearing MCP caches")

// Clear MCP-related caches so next session/query reads fresh config
mcpConfigCache.clear()
workingMcpServers.clear()

// Notify all renderer windows
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed()) {
try {
win.webContents.send("claude-config-changed")
} catch {
// Window may have been destroyed between check and send
}
}
}
}, 300)

watcher
.on("change", () => handleChange())
.on("add", () => handleChange())
.on("error", (error: Error) => {
console.error("[ConfigWatcher] Error watching ~/.claude.json:", error)
})

console.log("[ConfigWatcher] Watching ~/.claude.json for changes")
}

/**
* Stop watching ~/.claude.json.
* Call this when the app is shutting down.
*/
export async function stopClaudeConfigWatcher(): Promise<void> {
if (watcher) {
await (watcher as any).close()
watcher = null
console.log("[ConfigWatcher] Stopped watching ~/.claude.json")
}
}
2 changes: 1 addition & 1 deletion src/main/lib/trpc/routers/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ function mcpCacheKey(scope: string | null, serverName: string): string {
const symlinksCreated = new Set<string>()

// Cache for MCP config (avoid re-reading ~/.claude.json on every message)
const mcpConfigCache = new Map<string, {
export const mcpConfigCache = new Map<string, {
config: Record<string, any> | undefined
mtime: number
}>()
Expand Down
9 changes: 9 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,13 @@ contextBridge.exposeInMainWorld("desktopApi", {
subscribeToGitWatcher: (worktreePath: string) => ipcRenderer.invoke("git:subscribe-watcher", worktreePath),
unsubscribeFromGitWatcher: (worktreePath: string) => ipcRenderer.invoke("git:unsubscribe-watcher", worktreePath),

// Claude config change events (from ~/.claude.json watcher)
onClaudeConfigChanged: (callback: () => void) => {
const handler = () => callback()
ipcRenderer.on("claude-config-changed", handler)
return () => ipcRenderer.removeListener("claude-config-changed", handler)
},

// VS Code theme scanning
scanVSCodeThemes: () => ipcRenderer.invoke("vscode:scan-themes"),
loadVSCodeTheme: (themePath: string) => ipcRenderer.invoke("vscode:load-theme", themePath),
Expand Down Expand Up @@ -347,6 +354,8 @@ export interface DesktopApi {
onGitStatusChanged: (callback: (data: { worktreePath: string; changes: Array<{ path: string; type: "add" | "change" | "unlink" }> }) => void) => () => void
subscribeToGitWatcher: (worktreePath: string) => Promise<void>
unsubscribeFromGitWatcher: (worktreePath: string) => Promise<void>
// Claude config changes (from ~/.claude.json watcher)
onClaudeConfigChanged: (callback: () => void) => () => void
// VS Code theme scanning
scanVSCodeThemes: () => Promise<DiscoveredTheme[]>
loadVSCodeTheme: (themePath: string) => Promise<VSCodeThemeData>
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
BillingMethodPage,
SelectRepoPage,
} from "./features/onboarding"
import { useClaudeConfigWatcher } from "./lib/hooks/use-file-change-listener"
import { identify, initAnalytics, shutdown } from "./lib/analytics"
import {
anthropicOnboardingCompletedAtom, apiKeyOnboardingCompletedAtom,
Expand Down Expand Up @@ -54,6 +55,9 @@ function AppContent() {
const setSelectedChatId = useSetAtom(selectedAgentChatIdAtom)
const { setActiveSubChat, addToOpenSubChats, setChatId } = useAgentSubChatStore()

// Watch ~/.claude.json for changes and auto-refresh MCP config
useClaudeConfigWatcher()

// Apply initial window params (chatId/subChatId) when opening via "Open in new window"
useEffect(() => {
const params = getInitialWindowParams()
Expand Down
30 changes: 29 additions & 1 deletion src/renderer/lib/hooks/use-file-change-listener.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react"
import { useEffect, useRef, useCallback } from "react"
import { useQueryClient } from "@tanstack/react-query"

/**
Expand Down Expand Up @@ -84,3 +84,31 @@ export function useGitWatcher(worktreePath: string | null | undefined) {
}
}, [worktreePath, queryClient])
}

/**
* Hook that listens for ~/.claude.json changes and invalidates MCP config queries.
* This allows MCP servers to be re-initialized when the user edits their config
* without needing to restart the app or manually refresh.
*/
export function useClaudeConfigWatcher() {
const queryClient = useQueryClient()

const handleConfigChanged = useCallback(() => {
console.log("[useClaudeConfigWatcher] Config changed, invalidating MCP queries")

// Invalidate all MCP-related queries so they refetch fresh data
queryClient.invalidateQueries({
queryKey: [["claude", "getAllMcpConfig"]],
})
queryClient.invalidateQueries({
queryKey: [["claude", "getMcpConfig"]],
})
}, [queryClient])

useEffect(() => {
const cleanup = window.desktopApi?.onClaudeConfigChanged(handleConfigChanged)
return () => {
cleanup?.()
}
}, [handleConfigChanged])
}