From efa68a395f6e0c76cc6fc07ca7e44ef4b4c6061a Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Thu, 19 Mar 2026 21:44:28 +0800 Subject: [PATCH 01/19] docs: add cmux/Ghostty terminal integration research MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmux: full CLI support (new-workspace --cwd --command, select- workspace, focus-panel, send), but requires socket mode change from default cmuxOnly to automation/allowAll. Ghostty: macOS limited — no +new-window, no AppleScript dict, no tab-level switching. Fallback to clipboard only. Also document why git branch --show-current is not viable. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/claude-session-integration-design.md | 63 +++++++++++++++++++---- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/docs/claude-session-integration-design.md b/docs/claude-session-integration-design.md index 9066379..7dfceed 100644 --- a/docs/claude-session-integration-design.md +++ b/docs/claude-session-integration-design.md @@ -290,15 +290,60 @@ In the Settings popup: ## Terminal Support Matrix -| Terminal | Detect | Switch | Launch | -|----------|--------|--------|--------| -| iTerm2 ✅ | `ps` + `lsof` + tty | AppleScript tty focus | AppleScript: new tab/window + execute | -| Terminal.app | `ps` + tty | AppleScript focus | AppleScript: new tab + execute | -| Ghostty | `ps` + parent check | AppleScript activate | Activate + paste command / clipboard | -| cmux | `ps` + `cmux list-panes` | `cmux select-workspace` | Activate + paste command / clipboard | -| Custom | — | — | User command template / clipboard | - -**MVP: iTerm2 only.** Others planned for Phase 2+. +| Terminal | Detect | Switch | Launch | External Access | +|----------|--------|--------|--------|----------------| +| iTerm2 ✅ | `ps` + `lsof` + tty | AppleScript tty focus | AppleScript: new tab/window + execute | No restriction | +| cmux ✅ | `ps` + `lsof` | `cmux select-workspace` + `cmux focus-panel` | `cmux new-workspace --cwd --command` | Requires socket mode change (see below) | +| Terminal.app | `ps` + tty | AppleScript focus | AppleScript: new tab + execute | No restriction | +| Ghostty | `ps` + parent check | AppleScript activate (app level only, no tab precision) | `open -na Ghostty.app --args -e ` (opens new instance) or clipboard | macOS `+new-window` not supported | +| Custom | — | — | User command template / clipboard | — | + +### cmux Integration Details + +**CLI commands available:** +- `cmux new-workspace --cwd --command "claude --resume "` — create new workspace with command +- `cmux select-workspace --workspace ` — switch to workspace +- `cmux focus-panel --panel surface:N` — switch to specific tab within workspace +- `cmux send "text"` / `cmux send-key enter` — send text/keys to focused terminal +- `cmux list-workspaces [--json]` / `cmux list-pane-surfaces --pane pane:N` — inspect topology + +**Socket access restriction:** +cmux CLI communicates via Unix socket (`/tmp/cmux.sock`). By default, only processes started inside cmux can connect (`cmuxOnly` mode). External apps like CodeV need the user to change the socket mode: + +| Mode | Access | How to enable | +|------|--------|---------------| +| `cmuxOnly` (default) | cmux child processes only | Default | +| `automation` | Automation-friendly access | cmux Settings UI | +| `allowAll` | Any local process | `CMUX_SOCKET_MODE=allowAll` or Settings UI | +| `password` | Password-authenticated | Settings UI | +| `off` | Disabled | Settings UI | + +**Recommended:** Ask user to set `automation` or `allowAll` mode in cmux Settings. Security impact is minimal — only local processes on the same machine can connect. + +**Switch strategy for cmux:** +1. `ps aux` to find claude process PID +2. Try connecting to `/tmp/cmux.sock` — if access denied, fallback to clipboard +3. If connected: `cmux list-workspaces --json` to find which workspace has the session +4. `cmux select-workspace` + `cmux focus-panel` to switch to correct tab + +**Launch strategy for cmux:** +1. Try `cmux new-workspace --cwd --command "claude --resume "` +2. If socket access denied: activate cmux + copy command to clipboard + +### Ghostty Integration Details + +**macOS limitations:** +- `ghostty +new-window` is **not supported on macOS** ("not supported on this platform") +- On macOS, Ghostty recommends `open -na Ghostty.app --args -e ` but this opens a **new Ghostty instance**, not a new tab in existing window +- No AppleScript dictionary (no .sdef file) — only basic `activate` works +- No CLI for sending text/keys to existing sessions +- No tab-level window switching + +**Fallback approach:** Activate Ghostty app + copy `cd && claude --resume ` to clipboard. User pastes with ⌘V + Enter. + +### Branch Name: Why Not `git branch --show-current` + +`git branch --show-current` returns the repo's **current** branch, but a session may have been created on a different branch that has since been switched away. The JSONL `gitBranch` field preserves the branch at the time of each session entry, which is the correct value to display. ## Phase Plan From 8558242e0d1eec3a2e71d13c361906a7cc4f29b5 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Thu, 19 Mar 2026 22:27:09 +0800 Subject: [PATCH 02/19] feat: add cmux and Ghostty terminal support cmux: full launch via new-workspace --cwd --command, switch via select-workspace with project name matching. Falls back to clipboard if socket access denied. Ghostty: clipboard + activate (macOS limited, no tab control). Settings: Terminal App selector (iTerm2/cmux/Ghostty), iTerm2 Open Mode only shown when iTerm2 selected. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 108 ++++++++++++++++++++++++++++++++++ src/main.ts | 13 +++- src/popup.tsx | 49 ++++++++++++++- src/preload.ts | 2 + 4 files changed, 169 insertions(+), 3 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index 2079c7f..47754fd 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -228,6 +228,31 @@ export const detectActiveSessions = async (): Promise> => { return activeMap; }; +/** + * Open a Claude Code session in the configured terminal. + */ +export const openSession = ( + sessionId: string, + projectPath: string, + isActive: boolean, + activePid?: number, + terminalApp: string = 'iterm2', + terminalMode: string = 'tab', +): void => { + switch (terminalApp) { + case 'cmux': + openSessionInCmux(sessionId, projectPath, isActive, activePid); + break; + case 'ghostty': + openSessionInGhostty(sessionId, projectPath); + break; + case 'iterm2': + default: + openSessionInITerm2(sessionId, projectPath, isActive, activePid, terminalMode); + break; + } +}; + /** * Open a Claude Code session in iTerm2 * If the session is already active, switch to its tab @@ -425,6 +450,89 @@ export const loadLastAssistantResponses = async ( return responses; }; +/** + * Open a Claude Code session in Ghostty. + * Limited on macOS — no tab-level switching, no new-tab command. + * Copies resume command to clipboard and activates Ghostty. + */ +export const openSessionInGhostty = ( + sessionId: string, + projectPath: string, +): void => { + const { exec } = require('child_process'); + copyResumeCommand(sessionId, projectPath); + exec('osascript -e \'tell application "Ghostty" to activate\''); +}; + +/** + * Open a Claude Code session in cmux. + * Requires cmux socket mode set to 'automation' or 'allowAll'. + * Falls back to clipboard if socket access denied. + */ +const CMUX_CLI = '/Applications/cmux.app/Contents/Resources/bin/cmux'; + +export const openSessionInCmux = ( + sessionId: string, + projectPath: string, + isActive: boolean, + activePid?: number, +): void => { + const { exec } = require('child_process'); + const command = `cd "${projectPath}" && claude --resume ${sessionId}`; + + if (isActive) { + // Try to switch to the workspace running this session + // First, find which workspace has the matching tty + exec(`${CMUX_CLI} list-workspaces --json 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 }, + (error: any, stdout: string) => { + if (error || !stdout) { + // Socket access denied or cmux not running — activate + clipboard + copyResumeCommand(sessionId, projectPath); + exec('osascript -e \'tell application "cmux" to activate\''); + return; + } + // Parse workspace list and try to find matching one by cwd + // cmux list-workspaces output has workspace IDs and paths + const lines = stdout.split('\n'); + const projectName = path.basename(projectPath); + let targetWorkspace: string | null = null; + + for (const line of lines) { + const wsMatch = line.match(/^[*\s]*(workspace:\d+)/); + if (wsMatch && line.toLowerCase().includes(projectName.toLowerCase())) { + targetWorkspace = wsMatch[1]; + break; + } + } + + if (targetWorkspace) { + exec(`${CMUX_CLI} select-workspace --workspace ${targetWorkspace}`, (err: any) => { + if (err) { + console.error('Error switching cmux workspace:', err); + } + }); + } else { + // Can't find workspace, just activate cmux + exec('osascript -e \'tell application "cmux" to activate\''); + } + } + ); + } else { + // Launch new workspace with command + exec(`${CMUX_CLI} new-workspace --cwd "${projectPath}" --command "claude --resume ${sessionId}" 2>/dev/null`, + { encoding: 'utf-8', timeout: 5000 }, + (error: any) => { + if (error) { + // Socket access denied — fallback to clipboard + activate + console.error('cmux new-workspace failed, falling back to clipboard:', error.message); + copyResumeCommand(sessionId, projectPath); + exec('osascript -e \'tell application "cmux" to activate\''); + } + } + ); + } +}; + /** * Copy resume command to clipboard (fallback for unsupported terminals) */ diff --git a/src/main.ts b/src/main.ts index 4d0a1da..f1e1807 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,7 +16,7 @@ import { readClaudeSessions, searchClaudeSessions, detectActiveSessions, - openSessionInITerm2, + openSession, copyResumeCommand, invalidateSessionCache, loadSessionEnrichment, @@ -1416,6 +1416,14 @@ ipcMain.on('set-login-item-settings', (_event, openAtLogin: boolean) => { app.setLoginItemSettings({ openAtLogin }); }); +ipcMain.handle('get-session-terminal-app', async () => { + return (await settings.get('session-terminal-app')) || 'iterm2'; +}); + +ipcMain.on('set-session-terminal-app', async (_event, app: string) => { + await settings.set('session-terminal-app', app); +}); + ipcMain.handle('get-session-terminal-mode', async () => { return (await settings.get('session-terminal-mode')) || 'tab'; }); @@ -1455,8 +1463,9 @@ ipcMain.handle('detect-active-sessions', async () => { }); ipcMain.on('open-claude-session', async (_event, sessionId: string, projectPath: string, isActive: boolean, activePid?: number) => { + const terminalApp = ((await settings.get('session-terminal-app')) || 'iterm2') as string; const terminalMode = ((await settings.get('session-terminal-mode')) || 'tab') as string; - openSessionInITerm2(sessionId, projectPath, isActive, activePid, terminalMode); + openSession(sessionId, projectPath, isActive, activePid, terminalApp, terminalMode); }); ipcMain.on('copy-claude-session-command', (_event, sessionId: string, projectPath: string) => { diff --git a/src/popup.tsx b/src/popup.tsx index 94e44ff..419bf53 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -86,6 +86,7 @@ const PopupDefaultExample = ({ const [isOpen, setIsOpen] = useState(false); const [launchAtLogin, setLaunchAtLogin] = useState(false); const [appVersion, setAppVersion] = useState(''); + const [sessionTerminalApp, setSessionTerminalApp] = useState('iterm2'); const [sessionTerminalMode, setSessionTerminalMode] = useState('tab'); const [sessionDisplayMode, setSessionDisplayMode] = useState('first'); const [defaultSwitcherMode, setDefaultSwitcherMode] = useState('projects'); @@ -94,6 +95,9 @@ const PopupDefaultExample = ({ (window as any).electronAPI.getAppVersion().then((version: string) => { setAppVersion(version); }); + (window as any).electronAPI.getSessionTerminalApp().then((app: string) => { + setSessionTerminalApp(app || 'iterm2'); + }); (window as any).electronAPI.getSessionTerminalMode().then((mode: string) => { setSessionTerminalMode(mode || 'tab'); }); @@ -329,7 +333,7 @@ const PopupDefaultExample = ({ - {/* Session Terminal Mode */} + {/* Session Terminal App */}
Session Terminal
+ + + + {/* Session Terminal Mode (only for iTerm2) */} + {sessionTerminalApp === 'iterm2' && ( +
+
+ iTerm2 Open Mode +
+ )} {/* Session Display Mode */}
ipcRenderer.send('ide-preference-changed', preferredIDE), getAppVersion: () => ipcRenderer.invoke('get-app-version'), + getSessionTerminalApp: () => ipcRenderer.invoke('get-session-terminal-app'), + setSessionTerminalApp: (app: string) => ipcRenderer.send('set-session-terminal-app', app), getSessionTerminalMode: () => ipcRenderer.invoke('get-session-terminal-mode'), setSessionTerminalMode: (mode: string) => ipcRenderer.send('set-session-terminal-mode', mode), getSessionDisplayMode: () => ipcRenderer.invoke('get-session-display-mode'), From 486346b75547f16bed470d632708debbe6ebb975 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Thu, 19 Mar 2026 23:28:19 +0800 Subject: [PATCH 03/19] fix: select + activate cmux workspace after launch new-workspace creates but doesn't focus. Add select-workspace with returned workspace ID + activate cmux app. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index 47754fd..ba82a66 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -480,27 +480,27 @@ export const openSessionInCmux = ( const { exec } = require('child_process'); const command = `cd "${projectPath}" && claude --resume ${sessionId}`; + console.log('[cmux] openSession:', { sessionId, projectPath, isActive, activePid }); if (isActive) { // Try to switch to the workspace running this session - // First, find which workspace has the matching tty - exec(`${CMUX_CLI} list-workspaces --json 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 }, + exec(`${CMUX_CLI} list-workspaces 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 }, (error: any, stdout: string) => { + console.log('[cmux] list-workspaces result:', { error: error?.message, stdout: stdout?.substring(0, 500) }); if (error || !stdout) { - // Socket access denied or cmux not running — activate + clipboard copyResumeCommand(sessionId, projectPath); exec('osascript -e \'tell application "cmux" to activate\''); return; } - // Parse workspace list and try to find matching one by cwd - // cmux list-workspaces output has workspace IDs and paths const lines = stdout.split('\n'); const projectName = path.basename(projectPath); let targetWorkspace: string | null = null; + console.log('[cmux] looking for project:', projectName); for (const line of lines) { const wsMatch = line.match(/^[*\s]*(workspace:\d+)/); if (wsMatch && line.toLowerCase().includes(projectName.toLowerCase())) { targetWorkspace = wsMatch[1]; + console.log('[cmux] matched workspace:', targetWorkspace, 'from line:', line.trim()); break; } } @@ -512,21 +512,30 @@ export const openSessionInCmux = ( } }); } else { - // Can't find workspace, just activate cmux + console.log('[cmux] no workspace match found, activating cmux'); exec('osascript -e \'tell application "cmux" to activate\''); } } ); } else { // Launch new workspace with command - exec(`${CMUX_CLI} new-workspace --cwd "${projectPath}" --command "claude --resume ${sessionId}" 2>/dev/null`, + const cmuxCmd = `${CMUX_CLI} new-workspace --cwd "${projectPath}" --command "claude --resume ${sessionId}"`; + console.log('[cmux] launch cmd:', cmuxCmd); + exec(cmuxCmd, { encoding: 'utf-8', timeout: 5000 }, - (error: any) => { + (error: any, stdout: string, stderr: string) => { + console.log('[cmux] launch result:', { error: error?.message, stdout, stderr }); if (error) { - // Socket access denied — fallback to clipboard + activate console.error('cmux new-workspace failed, falling back to clipboard:', error.message); copyResumeCommand(sessionId, projectPath); exec('osascript -e \'tell application "cmux" to activate\''); + } else { + // Select the newly created workspace and activate cmux + const wsMatch = stdout.match(/workspace:\d+/); + if (wsMatch) { + exec(`${CMUX_CLI} select-workspace --workspace ${wsMatch[0]}`); + } + exec('osascript -e \'tell application "cmux" to activate\''); } } ); From 0d3b77f01c12139f602f31828756813a844571a4 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Thu, 19 Mar 2026 23:48:18 +0800 Subject: [PATCH 04/19] fix: use sidebar-state cwd for precise cmux workspace matching list-workspaces output was truncated and titles don't always contain paths. Now iterate all workspaces and check each one's cwd via sidebar-state for exact path matching. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 48 +++++++++++++++++------------------ 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index ba82a66..cac7b06 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -482,39 +482,37 @@ export const openSessionInCmux = ( console.log('[cmux] openSession:', { sessionId, projectPath, isActive, activePid }); if (isActive) { - // Try to switch to the workspace running this session - exec(`${CMUX_CLI} list-workspaces 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 }, - (error: any, stdout: string) => { - console.log('[cmux] list-workspaces result:', { error: error?.message, stdout: stdout?.substring(0, 500) }); + // Get all workspace IDs, then check each one's cwd via sidebar-state + exec(`${CMUX_CLI} list-workspaces 2>/dev/null`, { encoding: 'utf-8', timeout: 3000, maxBuffer: 1024 * 1024 }, + async (error: any, stdout: string) => { if (error || !stdout) { copyResumeCommand(sessionId, projectPath); exec('osascript -e \'tell application "cmux" to activate\''); return; } - const lines = stdout.split('\n'); - const projectName = path.basename(projectPath); - let targetWorkspace: string | null = null; - - console.log('[cmux] looking for project:', projectName); - for (const line of lines) { - const wsMatch = line.match(/^[*\s]*(workspace:\d+)/); - if (wsMatch && line.toLowerCase().includes(projectName.toLowerCase())) { - targetWorkspace = wsMatch[1]; - console.log('[cmux] matched workspace:', targetWorkspace, 'from line:', line.trim()); - break; - } - } - if (targetWorkspace) { - exec(`${CMUX_CLI} select-workspace --workspace ${targetWorkspace}`, (err: any) => { - if (err) { - console.error('Error switching cmux workspace:', err); - } + const wsIds = stdout.match(/workspace:\d+/g) || []; + console.log('[cmux] found workspaces:', wsIds.length); + + // Check each workspace's cwd via sidebar-state + const execPromise = (cmd: string): Promise => + new Promise((resolve) => { + exec(cmd, { encoding: 'utf-8', timeout: 2000 }, (_e: any, out: string) => resolve(out || '')); }); - } else { - console.log('[cmux] no workspace match found, activating cmux'); - exec('osascript -e \'tell application "cmux" to activate\''); + + for (const wsId of wsIds) { + const state = await execPromise(`${CMUX_CLI} sidebar-state --workspace ${wsId} 2>/dev/null`); + const cwdMatch = state.match(/^cwd=(.+)$/m); + if (cwdMatch && cwdMatch[1] === projectPath) { + console.log('[cmux] matched workspace by cwd:', wsId, cwdMatch[1]); + exec(`${CMUX_CLI} select-workspace --workspace ${wsId}`); + exec('osascript -e \'tell application "cmux" to activate\''); + return; + } } + + console.log('[cmux] no workspace cwd match, activating cmux'); + exec('osascript -e \'tell application "cmux" to activate\''); } ); } else { From 0558da063bf1630c2e4ae4eceb77977df5689531 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Fri, 20 Mar 2026 00:00:16 +0800 Subject: [PATCH 05/19] fix: cmux switch with tree fallback for multi-tab workspaces Pass 1: sidebar-state cwd + focused_cwd match (fast, exact) Pass 2: tree --all to find surface title matching project name (handles 2nd tab and renamed workspaces) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 71 +++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index cac7b06..2a88509 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -482,39 +482,62 @@ export const openSessionInCmux = ( console.log('[cmux] openSession:', { sessionId, projectPath, isActive, activePid }); if (isActive) { - // Get all workspace IDs, then check each one's cwd via sidebar-state - exec(`${CMUX_CLI} list-workspaces 2>/dev/null`, { encoding: 'utf-8', timeout: 3000, maxBuffer: 1024 * 1024 }, - async (error: any, stdout: string) => { - if (error || !stdout) { - copyResumeCommand(sessionId, projectPath); + const execPromise = (cmd: string): Promise => + new Promise((resolve) => { + exec(cmd, { encoding: 'utf-8', timeout: 3000, maxBuffer: 1024 * 1024 }, (_e: any, out: string) => resolve(out || '')); + }); + + (async () => { + // Step 1: Get all workspace IDs and check cwd via sidebar-state + const wsListOutput = await execPromise(`${CMUX_CLI} list-workspaces 2>/dev/null`); + if (!wsListOutput) { + copyResumeCommand(sessionId, projectPath); + exec('osascript -e \'tell application "cmux" to activate\''); + return; + } + + const wsIds = wsListOutput.match(/workspace:\d+/g) || []; + console.log('[cmux] found workspaces:', wsIds.length); + + // Pass 1: match by sidebar-state cwd (covers 1st tab / single tab case) + for (const wsId of wsIds) { + const state = await execPromise(`${CMUX_CLI} sidebar-state --workspace ${wsId} 2>/dev/null`); + const cwdMatch = state.match(/^cwd=(.+)$/m); + const focusedCwdMatch = state.match(/^focused_cwd=(.+)$/m); + if ((cwdMatch && cwdMatch[1] === projectPath) || + (focusedCwdMatch && focusedCwdMatch[1] === projectPath)) { + console.log('[cmux] matched workspace by cwd:', wsId); + exec(`${CMUX_CLI} select-workspace --workspace ${wsId}`); exec('osascript -e \'tell application "cmux" to activate\''); return; } + } - const wsIds = stdout.match(/workspace:\d+/g) || []; - console.log('[cmux] found workspaces:', wsIds.length); - - // Check each workspace's cwd via sidebar-state - const execPromise = (cmd: string): Promise => - new Promise((resolve) => { - exec(cmd, { encoding: 'utf-8', timeout: 2000 }, (_e: any, out: string) => resolve(out || '')); - }); - - for (const wsId of wsIds) { - const state = await execPromise(`${CMUX_CLI} sidebar-state --workspace ${wsId} 2>/dev/null`); - const cwdMatch = state.match(/^cwd=(.+)$/m); - if (cwdMatch && cwdMatch[1] === projectPath) { - console.log('[cmux] matched workspace by cwd:', wsId, cwdMatch[1]); - exec(`${CMUX_CLI} select-workspace --workspace ${wsId}`); + // Pass 2: use tree to find surface title matching project name + const projectName = path.basename(projectPath); + if (projectName && projectName !== path.basename(os.homedir())) { + const treeOutput = await execPromise(`${CMUX_CLI} tree --all 2>/dev/null`); + // Parse tree to find workspace containing a surface with matching project name + const treeLines = treeOutput.split('\n'); + let currentWorkspace: string | null = null; + for (const line of treeLines) { + const wsMatch = line.match(/workspace (workspace:\d+)/); + if (wsMatch) { + currentWorkspace = wsMatch[1]; + } + const surfaceMatch = line.match(/surface (surface:\d+)/); + if (surfaceMatch && currentWorkspace && line.toLowerCase().includes(projectName.toLowerCase())) { + console.log('[cmux] matched by tree surface title:', currentWorkspace, 'surface:', surfaceMatch[1]); + exec(`${CMUX_CLI} select-workspace --workspace ${currentWorkspace}`); exec('osascript -e \'tell application "cmux" to activate\''); return; } } - - console.log('[cmux] no workspace cwd match, activating cmux'); - exec('osascript -e \'tell application "cmux" to activate\''); } - ); + + console.log('[cmux] no match found, activating cmux'); + exec('osascript -e \'tell application "cmux" to activate\''); + })(); } else { // Launch new workspace with command const cmuxCmd = `${CMUX_CLI} new-workspace --cwd "${projectPath}" --command "claude --resume ${sessionId}"`; From 3bf28d1722bc4cbaf32aecdc2b4253e7d1b16160 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Fri, 20 Mar 2026 00:15:47 +0800 Subject: [PATCH 06/19] perf: parallel sidebar-state queries for cmux switch Sequential sidebar-state for 33 workspaces took ~5s. Now runs all queries in parallel via Promise.all (~0.5s). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index 2a88509..aef99eb 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -499,18 +499,20 @@ export const openSessionInCmux = ( const wsIds = wsListOutput.match(/workspace:\d+/g) || []; console.log('[cmux] found workspaces:', wsIds.length); - // Pass 1: match by sidebar-state cwd (covers 1st tab / single tab case) - for (const wsId of wsIds) { + // Pass 1: match by sidebar-state cwd (parallel for speed) + const cwdResults = await Promise.all(wsIds.map(async (wsId) => { const state = await execPromise(`${CMUX_CLI} sidebar-state --workspace ${wsId} 2>/dev/null`); const cwdMatch = state.match(/^cwd=(.+)$/m); const focusedCwdMatch = state.match(/^focused_cwd=(.+)$/m); - if ((cwdMatch && cwdMatch[1] === projectPath) || - (focusedCwdMatch && focusedCwdMatch[1] === projectPath)) { - console.log('[cmux] matched workspace by cwd:', wsId); - exec(`${CMUX_CLI} select-workspace --workspace ${wsId}`); - exec('osascript -e \'tell application "cmux" to activate\''); - return; - } + return { wsId, cwd: cwdMatch?.[1], focusedCwd: focusedCwdMatch?.[1] }; + })); + + const cwdHit = cwdResults.find(r => r.cwd === projectPath || r.focusedCwd === projectPath); + if (cwdHit) { + console.log('[cmux] matched workspace by cwd:', cwdHit.wsId); + exec(`${CMUX_CLI} select-workspace --workspace ${cwdHit.wsId}`); + exec('osascript -e \'tell application "cmux" to activate\''); + return; } // Pass 2: use tree to find surface title matching project name From 39afc44cb9067087cfdf5fb18546c942cba6c990 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Fri, 20 Mar 2026 00:28:00 +0800 Subject: [PATCH 07/19] fix: revert cmux switch to CLI approach (AppleScript returns 0 windows) cmux has AppleScript dictionary with terminal.workingDirectory and focus, but testing shows count windows returns 0. Keep CLI sidebar-state + tree approach with parallel queries. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index aef99eb..45c709f 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -482,13 +482,15 @@ export const openSessionInCmux = ( console.log('[cmux] openSession:', { sessionId, projectPath, isActive, activePid }); if (isActive) { + // NOTE: cmux has AppleScript dictionary with terminal.workingDirectory and focus, + // but testing shows count windows returns 0 — AppleScript interface may be buggy. + // Using CLI (sidebar-state + tree) approach instead. const execPromise = (cmd: string): Promise => new Promise((resolve) => { exec(cmd, { encoding: 'utf-8', timeout: 3000, maxBuffer: 1024 * 1024 }, (_e: any, out: string) => resolve(out || '')); }); (async () => { - // Step 1: Get all workspace IDs and check cwd via sidebar-state const wsListOutput = await execPromise(`${CMUX_CLI} list-workspaces 2>/dev/null`); if (!wsListOutput) { copyResumeCommand(sessionId, projectPath); @@ -497,17 +499,16 @@ export const openSessionInCmux = ( } const wsIds = wsListOutput.match(/workspace:\d+/g) || []; - console.log('[cmux] found workspaces:', wsIds.length); - // Pass 1: match by sidebar-state cwd (parallel for speed) - const cwdResults = await Promise.all(wsIds.map(async (wsId) => { + // Pass 1: parallel sidebar-state cwd match + const cwdResults = await Promise.all(wsIds.map(async (wsId: string) => { const state = await execPromise(`${CMUX_CLI} sidebar-state --workspace ${wsId} 2>/dev/null`); const cwdMatch = state.match(/^cwd=(.+)$/m); const focusedCwdMatch = state.match(/^focused_cwd=(.+)$/m); return { wsId, cwd: cwdMatch?.[1], focusedCwd: focusedCwdMatch?.[1] }; })); - const cwdHit = cwdResults.find(r => r.cwd === projectPath || r.focusedCwd === projectPath); + const cwdHit = cwdResults.find((r: any) => r.cwd === projectPath || r.focusedCwd === projectPath); if (cwdHit) { console.log('[cmux] matched workspace by cwd:', cwdHit.wsId); exec(`${CMUX_CLI} select-workspace --workspace ${cwdHit.wsId}`); @@ -515,21 +516,18 @@ export const openSessionInCmux = ( return; } - // Pass 2: use tree to find surface title matching project name + // Pass 2: tree surface title match const projectName = path.basename(projectPath); if (projectName && projectName !== path.basename(os.homedir())) { const treeOutput = await execPromise(`${CMUX_CLI} tree --all 2>/dev/null`); - // Parse tree to find workspace containing a surface with matching project name const treeLines = treeOutput.split('\n'); let currentWorkspace: string | null = null; for (const line of treeLines) { const wsMatch = line.match(/workspace (workspace:\d+)/); - if (wsMatch) { - currentWorkspace = wsMatch[1]; - } + if (wsMatch) currentWorkspace = wsMatch[1]; const surfaceMatch = line.match(/surface (surface:\d+)/); if (surfaceMatch && currentWorkspace && line.toLowerCase().includes(projectName.toLowerCase())) { - console.log('[cmux] matched by tree surface title:', currentWorkspace, 'surface:', surfaceMatch[1]); + console.log('[cmux] matched by tree surface title:', currentWorkspace); exec(`${CMUX_CLI} select-workspace --workspace ${currentWorkspace}`); exec('osascript -e \'tell application "cmux" to activate\''); return; From e91fe52e95cef4e76203e940d2782635d6c5543a Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Fri, 20 Mar 2026 00:43:58 +0800 Subject: [PATCH 08/19] feat: auto-detect terminal app for active sessions + badge Walk parent process tree to detect iTerm2/cmux/Ghostty/Terminal. Active sessions switch using detected terminal (not settings). Non-active sessions use settings terminal for launch. Show small uppercase badge (e.g. ITERM2, CMUX) on active items. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 47 ++++++++++++++++++++++++++++++++--- src/main.ts | 9 +++++++ src/preload.ts | 1 + src/switcher-ui.tsx | 22 ++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index 45c709f..ccc7d46 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -19,6 +19,7 @@ export interface ClaudeSession { messageCount: number; isActive: boolean; // whether a claude process is running for this session activePid?: number; + terminalApp?: string; // detected terminal: 'iterm2', 'cmux', 'ghostty', etc. } interface HistoryLine { @@ -167,6 +168,35 @@ export const searchClaudeSessions = (query: string, limit = 50): ClaudeSession[] * to find which project directory it's working in, then look up the latest * session for that project from history.jsonl */ +/** + * Detect which terminal app a process is running in by walking parent process tree. + * Returns 'iterm2', 'cmux', 'ghostty', 'terminal', or 'unknown'. + */ +export const detectTerminalApp = async (pid: number): Promise => { + const { exec } = require('child_process'); + const execPromise = (cmd: string): Promise => + new Promise((resolve) => { + exec(cmd, { encoding: 'utf-8', timeout: 2000 }, (_e: any, out: string) => resolve(out || '')); + }); + + let currentPid = pid; + for (let i = 0; i < 20; i++) { + const comm = (await execPromise(`ps -o comm= -p ${currentPid} 2>/dev/null`)).trim(); + if (!comm) break; + + const commLower = comm.toLowerCase(); + if (commLower.includes('iterm') || commLower.includes('iterm2')) return 'iterm2'; + if (commLower.includes('cmux')) return 'cmux'; + if (commLower.includes('ghostty')) return 'ghostty'; + if (commLower.includes('terminal.app') || (commLower === 'terminal')) return 'terminal'; + + const ppid = parseInt((await execPromise(`ps -o ppid= -p ${currentPid} 2>/dev/null`)).trim(), 10); + if (!ppid || ppid <= 1) break; + currentPid = ppid; + } + return 'unknown'; +}; + export const detectActiveSessions = async (): Promise> => { const now = Date.now(); if (cachedActiveMap && (now - activeCacheTimestamp) < ACTIVE_CACHE_TTL_MS) { @@ -231,15 +261,26 @@ export const detectActiveSessions = async (): Promise> => { /** * Open a Claude Code session in the configured terminal. */ -export const openSession = ( +export const openSession = async ( sessionId: string, projectPath: string, isActive: boolean, activePid?: number, terminalApp: string = 'iterm2', terminalMode: string = 'tab', -): void => { - switch (terminalApp) { +): Promise => { + let effectiveTerminal = terminalApp; + + // For active sessions, auto-detect which terminal they're running in + if (isActive && activePid) { + const detected = await detectTerminalApp(activePid); + if (detected !== 'unknown') { + effectiveTerminal = detected; + console.log(`[openSession] auto-detected terminal: ${detected} for pid ${activePid}`); + } + } + + switch (effectiveTerminal) { case 'cmux': openSessionInCmux(sessionId, projectPath, isActive, activePid); break; diff --git a/src/main.ts b/src/main.ts index f1e1807..f8fb51e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,6 +16,7 @@ import { readClaudeSessions, searchClaudeSessions, detectActiveSessions, + detectTerminalApp, openSession, copyResumeCommand, invalidateSessionCache, @@ -1462,6 +1463,14 @@ ipcMain.handle('detect-active-sessions', async () => { return Object.fromEntries(activeMap); }); +ipcMain.handle('detect-terminal-apps', async (_event, pidMap: Record) => { + const results: Record = {}; + await Promise.all(Object.entries(pidMap).map(async ([sessionId, pid]) => { + results[sessionId] = await detectTerminalApp(pid); + })); + return results; +}); + ipcMain.on('open-claude-session', async (_event, sessionId: string, projectPath: string, isActive: boolean, activePid?: number) => { const terminalApp = ((await settings.get('session-terminal-app')) || 'iterm2') as string; const terminalMode = ((await settings.get('session-terminal-mode')) || 'tab') as string; diff --git a/src/preload.ts b/src/preload.ts index a971610..e13897b 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -37,6 +37,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getClaudeSessions: (limit?: number) => ipcRenderer.invoke('get-claude-sessions', limit), searchClaudeSessions: (query: string) => ipcRenderer.invoke('search-claude-sessions', query), detectActiveSessions: () => ipcRenderer.invoke('detect-active-sessions'), + detectTerminalApps: (pidMap: Record) => ipcRenderer.invoke('detect-terminal-apps', pidMap), openClaudeSession: (sessionId: string, projectPath: string, isActive: boolean, activePid?: number) => ipcRenderer.send('open-claude-session', sessionId, projectPath, isActive, activePid), copyClaudeSessionCommand: (sessionId: string, projectPath: string) => diff --git a/src/switcher-ui.tsx b/src/switcher-ui.tsx index 16d024a..77e8c6a 100644 --- a/src/switcher-ui.tsx +++ b/src/switcher-ui.tsx @@ -325,6 +325,7 @@ function SwitcherApp() { const [customTitles, setCustomTitles] = useState>({}); const [branches, setBranches] = useState>({}); const [assistantResponses, setAssistantResponses] = useState>({}); + const [terminalApps, setTerminalApps] = useState>({}); const modeRef = useRef('projects'); const activeStateRef = useRef>({}); @@ -387,6 +388,15 @@ function SwitcherApp() { } }); } + + // Step 2c: Detect terminal apps for active sessions + if (Object.keys(activeMap).length > 0) { + (window as any).electronAPI.detectTerminalApps(activeMap).then((apps: Record) => { + if (apps && Object.keys(apps).length > 0) { + setTerminalApps((prev: Record) => ({ ...prev, ...apps })); + } + }); + } } }); @@ -850,6 +860,18 @@ function SwitcherApp() { )}
+ {session.isActive && terminalApps[session.sessionId] && terminalApps[session.sessionId] !== 'unknown' && ( + + {terminalApps[session.sessionId]} + + )} {session.messageCount} msgs From 1b392616cf3e6316cd8790c9e85bb7735f38d371 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Fri, 20 Mar 2026 00:55:32 +0800 Subject: [PATCH 09/19] feat: full Ghostty AppleScript support for launch + switch Switch: match terminal working directory, focus matching terminal Launch: new tab with surface configuration (command + cwd) Fallback to clipboard if AppleScript fails. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 59 ++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index ccc7d46..7ac7f94 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -285,7 +285,7 @@ export const openSession = async ( openSessionInCmux(sessionId, projectPath, isActive, activePid); break; case 'ghostty': - openSessionInGhostty(sessionId, projectPath); + openSessionInGhostty(sessionId, projectPath, isActive); break; case 'iterm2': default: @@ -493,16 +493,65 @@ export const loadLastAssistantResponses = async ( /** * Open a Claude Code session in Ghostty. - * Limited on macOS — no tab-level switching, no new-tab command. - * Copies resume command to clipboard and activates Ghostty. + * Full AppleScript support: working directory matching, focus, new tab with command. */ export const openSessionInGhostty = ( sessionId: string, projectPath: string, + isActive: boolean, ): void => { const { exec } = require('child_process'); - copyResumeCommand(sessionId, projectPath); - exec('osascript -e \'tell application "Ghostty" to activate\''); + + if (isActive) { + // Switch to existing terminal by matching working directory + const tmpScript = '/tmp/codev-ghostty-switch.scpt'; + const switchScript = `tell application "Ghostty" + activate + repeat with w in windows + repeat with t in tabs of w + repeat with term in terminals of t + if working directory of term is "${projectPath}" then + focus term + return "found" + end if + end repeat + end repeat + end repeat + return "not found" +end tell`; + fs.writeFileSync(tmpScript, switchScript); + exec(`osascript ${tmpScript}`, { encoding: 'utf-8', timeout: 5000 }, (error: any, stdout: string) => { + const result = (stdout || '').trim(); + console.log('[ghostty] switch result:', result); + if (result !== 'found') { + // Fallback: clipboard + activate + copyResumeCommand(sessionId, projectPath); + } + try { fs.unlinkSync(tmpScript); } catch {} + }); + } else { + // Launch new tab with command via surface configuration + const command = `cd "${projectPath}" && claude --resume ${sessionId}`; + const tmpScript = '/tmp/codev-ghostty-launch.scpt'; + const launchScript = `tell application "Ghostty" + activate + set cfg to new surface configuration with configuration {command:"${command.replace(/"/g, '\\"')}", initial working directory:"${projectPath}"} + if (count windows) > 0 then + new tab in front window with configuration cfg + else + new window with configuration cfg + end if +end tell`; + fs.writeFileSync(tmpScript, launchScript); + exec(`osascript ${tmpScript}`, { encoding: 'utf-8', timeout: 5000 }, (error: any) => { + if (error) { + console.error('[ghostty] launch error:', error.message); + // Fallback: clipboard + copyResumeCommand(sessionId, projectPath); + } + try { fs.unlinkSync(tmpScript); } catch {} + }); + } }; /** From 2eb9fc50096dd56ae270ec577fa447cf63917f0e Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Fri, 20 Mar 2026 01:02:47 +0800 Subject: [PATCH 10/19] fix: Ghostty AppleScript surface configuration syntax Use 'from' instead of 'with configuration' for record parameter. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index 7ac7f94..8aa4896 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -535,7 +535,7 @@ end tell`; const tmpScript = '/tmp/codev-ghostty-launch.scpt'; const launchScript = `tell application "Ghostty" activate - set cfg to new surface configuration with configuration {command:"${command.replace(/"/g, '\\"')}", initial working directory:"${projectPath}"} + set cfg to new surface configuration from {command:"${command.replace(/"/g, '\\"')}", initial working directory:"${projectPath}"} if (count windows) > 0 then new tab in front window with configuration cfg else From dcd1d432440ab7d73623db866caa95f76fae4875 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Fri, 20 Mar 2026 01:04:36 +0800 Subject: [PATCH 11/19] fix: Ghostty launch uses initialInput instead of command command parameter is passed to exec directly (not shell), so cd && claude fails. Use initial working directory for cwd and initial input to type the resume command + newline. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index 8aa4896..52989e9 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -531,11 +531,12 @@ end tell`; }); } else { // Launch new tab with command via surface configuration - const command = `cd "${projectPath}" && claude --resume ${sessionId}`; + // Use initial working directory for cd, and initialInput to type the resume command const tmpScript = '/tmp/codev-ghostty-launch.scpt'; + const resumeCmd = `claude --resume ${sessionId}`; const launchScript = `tell application "Ghostty" activate - set cfg to new surface configuration from {command:"${command.replace(/"/g, '\\"')}", initial working directory:"${projectPath}"} + set cfg to new surface configuration from {initial working directory:"${projectPath}", initial input:"${resumeCmd}\\n"} if (count windows) > 0 then new tab in front window with configuration cfg else From cd065b0649f5ace14342f88b90e81a5b9668bc14 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Fri, 20 Mar 2026 01:10:04 +0800 Subject: [PATCH 12/19] fix: Ghostty label + Open Mode for Ghostty + new window support Remove '(clipboard)' from Ghostty label (now full support). Show Open Mode (New Tab/Window) for both iTerm2 and Ghostty. Ghostty launch respects terminalMode setting. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/claude-session-utility.ts | 11 +++++++++-- src/popup.tsx | 8 ++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/claude-session-utility.ts b/src/claude-session-utility.ts index 52989e9..cc47fca 100644 --- a/src/claude-session-utility.ts +++ b/src/claude-session-utility.ts @@ -285,7 +285,7 @@ export const openSession = async ( openSessionInCmux(sessionId, projectPath, isActive, activePid); break; case 'ghostty': - openSessionInGhostty(sessionId, projectPath, isActive); + openSessionInGhostty(sessionId, projectPath, isActive, terminalMode); break; case 'iterm2': default: @@ -499,6 +499,7 @@ export const openSessionInGhostty = ( sessionId: string, projectPath: string, isActive: boolean, + terminalMode: string = 'tab', ): void => { const { exec } = require('child_process'); @@ -534,7 +535,13 @@ end tell`; // Use initial working directory for cd, and initialInput to type the resume command const tmpScript = '/tmp/codev-ghostty-launch.scpt'; const resumeCmd = `claude --resume ${sessionId}`; - const launchScript = `tell application "Ghostty" + const launchScript = terminalMode === 'window' + ? `tell application "Ghostty" + activate + set cfg to new surface configuration from {initial working directory:"${projectPath}", initial input:"${resumeCmd}\\n"} + new window with configuration cfg +end tell` + : `tell application "Ghostty" activate set cfg to new surface configuration from {initial working directory:"${projectPath}", initial input:"${resumeCmd}\\n"} if (count windows) > 0 then diff --git a/src/popup.tsx b/src/popup.tsx index 419bf53..7a5ecb7 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -370,12 +370,12 @@ const PopupDefaultExample = ({ > - +
- {/* Session Terminal Mode (only for iTerm2) */} - {sessionTerminalApp === 'iterm2' && ( + {/* Session Terminal Mode (for iTerm2 and Ghostty) */} + {(sessionTerminalApp === 'iterm2' || sessionTerminalApp === 'ghostty') && (
- iTerm2 Open Mode + Open Mode
Date: Fri, 20 Mar 2026 01:29:12 +0800 Subject: [PATCH 16/19] style: rename Open Mode to Launch Mode for consistency Co-Authored-By: Claude Opus 4.6 (1M context) --- src/popup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popup.tsx b/src/popup.tsx index 4f16721..ae8cf4d 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -393,7 +393,7 @@ const PopupDefaultExample = ({ color: THEME.text.primary, }} > - Open Mode + Launch Mode From e24ff3dabb6531310303685163b09175ed792333 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Fri, 20 Mar 2026 01:34:31 +0800 Subject: [PATCH 18/19] chore: bump version to 1.0.35 Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce96480..5665577 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "CodeV", "productName": "CodeV", - "version": "1.0.34", + "version": "1.0.35", "description": "My Electron application description", "main": ".webpack/main", "scripts": { From 0eba5d0cb8884cbd09440ec4b3731be8541ffea1 Mon Sep 17 00:00:00 2001 From: Grimmer Kang Date: Fri, 20 Mar 2026 01:35:21 +0800 Subject: [PATCH 19/19] chore: update package description Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5665577..cff9a0e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "CodeV", "productName": "CodeV", "version": "1.0.35", - "description": "My Electron application description", + "description": "Quick switcher for VS Code, Cursor, and Claude Code sessions", "main": ".webpack/main", "scripts": { "db": "prisma migrate dev",