From 344651a981ae297becf8ccd9ed13065538b23592 Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 10 Dec 2025 21:56:56 +0100 Subject: [PATCH 01/24] refactor(markdown): update styling for theme adaptability - Enhanced the Markdown component to support theme-aware styling, ensuring proper rendering across all predefined themes. - Updated typography and color classes for headings, paragraphs, lists, code blocks, strong text, links, blockquotes, and horizontal rules to align with the new theme structure. --- app/src/components/ui/markdown.tsx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/components/ui/markdown.tsx b/app/src/components/ui/markdown.tsx index 84473624..5a080e4b 100644 --- a/app/src/components/ui/markdown.tsx +++ b/app/src/components/ui/markdown.tsx @@ -10,7 +10,7 @@ interface MarkdownProps { /** * Reusable Markdown component for rendering markdown content - * Styled for dark mode with proper typography + * Theme-aware styling that adapts to all predefined themes */ export function Markdown({ children, className }: MarkdownProps) { return ( @@ -18,27 +18,27 @@ export function Markdown({ children, className }: MarkdownProps) { className={cn( "prose prose-sm prose-invert max-w-none", // Headings - "[&_h1]:text-xl [&_h1]:text-zinc-200 [&_h1]:font-semibold [&_h1]:mt-4 [&_h1]:mb-2", - "[&_h2]:text-lg [&_h2]:text-zinc-200 [&_h2]:font-semibold [&_h2]:mt-4 [&_h2]:mb-2", - "[&_h3]:text-base [&_h3]:text-zinc-200 [&_h3]:font-semibold [&_h3]:mt-3 [&_h3]:mb-2", - "[&_h4]:text-sm [&_h4]:text-zinc-200 [&_h4]:font-semibold [&_h4]:mt-2 [&_h4]:mb-1", + "[&_h1]:text-xl [&_h1]:text-foreground [&_h1]:font-semibold [&_h1]:mt-4 [&_h1]:mb-2", + "[&_h2]:text-lg [&_h2]:text-foreground [&_h2]:font-semibold [&_h2]:mt-4 [&_h2]:mb-2", + "[&_h3]:text-base [&_h3]:text-foreground [&_h3]:font-semibold [&_h3]:mt-3 [&_h3]:mb-2", + "[&_h4]:text-sm [&_h4]:text-foreground [&_h4]:font-semibold [&_h4]:mt-2 [&_h4]:mb-1", // Paragraphs - "[&_p]:text-zinc-300 [&_p]:leading-relaxed [&_p]:my-2", + "[&_p]:text-foreground-secondary [&_p]:leading-relaxed [&_p]:my-2", // Lists "[&_ul]:my-2 [&_ul]:pl-4 [&_ol]:my-2 [&_ol]:pl-4", - "[&_li]:text-zinc-300 [&_li]:my-0.5", + "[&_li]:text-foreground-secondary [&_li]:my-0.5", // Code - "[&_code]:text-cyan-400 [&_code]:bg-zinc-800/50 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm", - "[&_pre]:bg-zinc-900/80 [&_pre]:border [&_pre]:border-white/10 [&_pre]:rounded-lg [&_pre]:my-2 [&_pre]:p-3 [&_pre]:overflow-x-auto", + "[&_code]:text-chart-2 [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm", + "[&_pre]:bg-card [&_pre]:border [&_pre]:border-border [&_pre]:rounded-lg [&_pre]:my-2 [&_pre]:p-3 [&_pre]:overflow-x-auto", "[&_pre_code]:bg-transparent [&_pre_code]:p-0", // Strong/Bold - "[&_strong]:text-zinc-200 [&_strong]:font-semibold", + "[&_strong]:text-foreground [&_strong]:font-semibold", // Links - "[&_a]:text-blue-400 [&_a]:no-underline hover:[&_a]:underline", + "[&_a]:text-brand-500 [&_a]:no-underline hover:[&_a]:underline", // Blockquotes - "[&_blockquote]:border-l-2 [&_blockquote]:border-zinc-600 [&_blockquote]:pl-4 [&_blockquote]:text-zinc-400 [&_blockquote]:italic [&_blockquote]:my-2", + "[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground [&_blockquote]:italic [&_blockquote]:my-2", // Horizontal rules - "[&_hr]:border-zinc-700 [&_hr]:my-4", + "[&_hr]:border-border [&_hr]:my-4", className )} > From a6da65e318eebd7980b0ed70a756b01905952c3a Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 10 Dec 2025 22:54:29 +0100 Subject: [PATCH 02/24] feat(keyboard): introduce visual keyboard map and shortcut customization - Added a new `KeyboardMap` component for a visual representation of keyboard shortcuts, allowing users to easily customize their shortcuts with single-modifier support. - Integrated `ShortcutReferencePanel` for editing shortcuts directly within the settings view. - Updated the settings view to include a button for opening the keyboard map dialog, enhancing user experience in managing keyboard shortcuts. - Refactored keyboard shortcut handling to support modifier keys and improve shortcut parsing and formatting. --- app/electron/services/claude-cli-detector.js | 299 +++++---- app/src/components/layout/sidebar.tsx | 24 +- app/src/components/ui/keyboard-map.tsx | 640 +++++++++++++++++++ app/src/components/views/settings-view.tsx | 439 ++----------- app/src/hooks/use-keyboard-shortcuts.ts | 46 +- app/src/store/app-store.ts | 67 +- 6 files changed, 1008 insertions(+), 507 deletions(-) create mode 100644 app/src/components/ui/keyboard-map.tsx diff --git a/app/electron/services/claude-cli-detector.js b/app/electron/services/claude-cli-detector.js index f8f4739e..f18f9d0e 100644 --- a/app/electron/services/claude-cli-detector.js +++ b/app/electron/services/claude-cli-detector.js @@ -1,7 +1,7 @@ -const { execSync, spawn } = require('child_process'); -const fs = require('fs'); -const path = require('path'); -const os = require('os'); +const { execSync, spawn } = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const os = require("os"); /** * Claude CLI Detector @@ -21,41 +21,47 @@ class ClaudeCliDetector { */ static getUpdatedPathFromShellConfig() { const homeDir = os.homedir(); - const shell = process.env.SHELL || '/bin/bash'; + const shell = process.env.SHELL || "/bin/bash"; const shellName = path.basename(shell); - + // Common shell config files const configFiles = []; - if (shellName.includes('zsh')) { - configFiles.push(path.join(homeDir, '.zshrc')); - configFiles.push(path.join(homeDir, '.zshenv')); - configFiles.push(path.join(homeDir, '.zprofile')); - } else if (shellName.includes('bash')) { - configFiles.push(path.join(homeDir, '.bashrc')); - configFiles.push(path.join(homeDir, '.bash_profile')); - configFiles.push(path.join(homeDir, '.profile')); + if (shellName.includes("zsh")) { + configFiles.push(path.join(homeDir, ".zshrc")); + configFiles.push(path.join(homeDir, ".zshenv")); + configFiles.push(path.join(homeDir, ".zprofile")); + } else if (shellName.includes("bash")) { + configFiles.push(path.join(homeDir, ".bashrc")); + configFiles.push(path.join(homeDir, ".bash_profile")); + configFiles.push(path.join(homeDir, ".profile")); } - + // Also check common locations const commonPaths = [ - path.join(homeDir, '.local', 'bin'), - path.join(homeDir, '.cargo', 'bin'), - '/usr/local/bin', - '/opt/homebrew/bin', - path.join(homeDir, 'bin'), + path.join(homeDir, ".local", "bin"), + path.join(homeDir, ".cargo", "bin"), + "/usr/local/bin", + "/opt/homebrew/bin", + path.join(homeDir, "bin"), ]; - + // Try to extract PATH additions from config files for (const configFile of configFiles) { if (fs.existsSync(configFile)) { try { - const content = fs.readFileSync(configFile, 'utf-8'); + const content = fs.readFileSync(configFile, "utf-8"); // Look for PATH exports that might include claude installation paths - const pathMatches = content.match(/export\s+PATH=["']?([^"'\n]+)["']?/g); + const pathMatches = content.match( + /export\s+PATH=["']?([^"'\n]+)["']?/g + ); if (pathMatches) { for (const match of pathMatches) { - const pathValue = match.replace(/export\s+PATH=["']?/, '').replace(/["']?$/, ''); - const paths = pathValue.split(':').filter(p => p && !p.includes('$')); + const pathValue = match + .replace(/export\s+PATH=["']?/, "") + .replace(/["']?$/, ""); + const paths = pathValue + .split(":") + .filter((p) => p && !p.includes("$")); commonPaths.push(...paths); } } @@ -64,26 +70,33 @@ class ClaudeCliDetector { } } } - + return [...new Set(commonPaths)]; // Remove duplicates } static detectClaudeInstallation() { - console.log('[ClaudeCliDetector] Detecting Claude installation...'); + console.log("[ClaudeCliDetector] Detecting Claude installation..."); try { // Method 1: Check if 'claude' command is in PATH (Unix) - if (process.platform !== 'win32') { + if (process.platform !== "win32") { try { - const claudePath = execSync('which claude 2>/dev/null', { encoding: 'utf-8' }).trim(); + const claudePath = execSync("which claude 2>/dev/null", { + encoding: "utf-8", + }).trim(); if (claudePath) { const version = this.getClaudeVersion(claudePath); - console.log('[ClaudeCliDetector] Found claude at:', claudePath, 'version:', version); + console.log( + "[ClaudeCliDetector] Found claude at:", + claudePath, + "version:", + version + ); return { installed: true, path: claudePath, version: version, - method: 'cli' + method: "cli", }; } } catch (error) { @@ -92,17 +105,26 @@ class ClaudeCliDetector { } // Method 2: Check Windows path - if (process.platform === 'win32') { + if (process.platform === "win32") { try { - const claudePath = execSync('where claude 2>nul', { encoding: 'utf-8' }).trim().split('\n')[0]; + const claudePath = execSync("where claude 2>nul", { + encoding: "utf-8", + }) + .trim() + .split("\n")[0]; if (claudePath) { const version = this.getClaudeVersion(claudePath); - console.log('[ClaudeCliDetector] Found claude at:', claudePath, 'version:', version); + console.log( + "[ClaudeCliDetector] Found claude at:", + claudePath, + "version:", + version + ); return { installed: true, path: claudePath, version: version, - method: 'cli' + method: "cli", }; } } catch (error) { @@ -111,34 +133,49 @@ class ClaudeCliDetector { } // Method 3: Check for local installation - const localClaudePath = path.join(os.homedir(), '.claude', 'local', 'claude'); + const localClaudePath = path.join( + os.homedir(), + ".claude", + "local", + "claude" + ); if (fs.existsSync(localClaudePath)) { const version = this.getClaudeVersion(localClaudePath); - console.log('[ClaudeCliDetector] Found local claude at:', localClaudePath, 'version:', version); + console.log( + "[ClaudeCliDetector] Found local claude at:", + localClaudePath, + "version:", + version + ); return { installed: true, path: localClaudePath, version: version, - method: 'cli-local' + method: "cli-local", }; } // Method 4: Check common installation locations (including those from shell config) const commonPaths = this.getUpdatedPathFromShellConfig(); - const binaryNames = ['claude', 'claude-code']; - + const binaryNames = ["claude", "claude-code"]; + for (const basePath of commonPaths) { for (const binaryName of binaryNames) { const claudePath = path.join(basePath, binaryName); if (fs.existsSync(claudePath)) { try { const version = this.getClaudeVersion(claudePath); - console.log('[ClaudeCliDetector] Found claude at:', claudePath, 'version:', version); + console.log( + "[ClaudeCliDetector] Found claude at:", + claudePath, + "version:", + version + ); return { installed: true, path: claudePath, version: version, - method: 'cli' + method: "cli", }; } catch (error) { // File exists but can't get version, might not be executable @@ -148,29 +185,37 @@ class ClaudeCliDetector { } // Method 5: Try to source shell config and check PATH again (for Unix) - if (process.platform !== 'win32') { + if (process.platform !== "win32") { try { - const shell = process.env.SHELL || '/bin/bash'; + const shell = process.env.SHELL || "/bin/bash"; const shellName = path.basename(shell); const homeDir = os.homedir(); - - let sourceCmd = ''; - if (shellName.includes('zsh')) { + + let sourceCmd = ""; + if (shellName.includes("zsh")) { sourceCmd = `source ${homeDir}/.zshrc 2>/dev/null && which claude`; - } else if (shellName.includes('bash')) { + } else if (shellName.includes("bash")) { sourceCmd = `source ${homeDir}/.bashrc 2>/dev/null && which claude`; } - + if (sourceCmd) { - const claudePath = execSync(`bash -c "${sourceCmd}"`, { encoding: 'utf-8', timeout: 2000 }).trim(); - if (claudePath && claudePath.startsWith('/')) { + const claudePath = execSync(`bash -c "${sourceCmd}"`, { + encoding: "utf-8", + timeout: 2000, + }).trim(); + if (claudePath && claudePath.startsWith("/")) { const version = this.getClaudeVersion(claudePath); - console.log('[ClaudeCliDetector] Found claude via shell config at:', claudePath, 'version:', version); + console.log( + "[ClaudeCliDetector] Found claude via shell config at:", + claudePath, + "version:", + version + ); return { installed: true, path: claudePath, version: version, - method: 'cli' + method: "cli", }; } } @@ -179,21 +224,24 @@ class ClaudeCliDetector { } } - console.log('[ClaudeCliDetector] Claude CLI not found'); + console.log("[ClaudeCliDetector] Claude CLI not found"); return { installed: false, path: null, version: null, - method: 'none' + method: "none", }; } catch (error) { - console.error('[ClaudeCliDetector] Error detecting Claude installation:', error); + console.error( + "[ClaudeCliDetector] Error detecting Claude installation:", + error + ); return { installed: false, path: null, version: null, - method: 'none', - error: error.message + method: "none", + error: error.message, }; } } @@ -206,8 +254,8 @@ class ClaudeCliDetector { static getClaudeVersion(claudePath) { try { const version = execSync(`"${claudePath}" --version 2>/dev/null`, { - encoding: 'utf-8', - timeout: 5000 + encoding: "utf-8", + timeout: 5000, }).trim(); return version || null; } catch (error) { @@ -226,10 +274,10 @@ class ClaudeCliDetector { * @returns {Object} Authentication status */ static getAuthStatus(appCredentialsPath) { - console.log('[ClaudeCliDetector] Checking auth status...'); + console.log("[ClaudeCliDetector] Checking auth status..."); const envApiKey = process.env.ANTHROPIC_API_KEY; - console.log('[ClaudeCliDetector] Env ANTHROPIC_API_KEY:', !!envApiKey); + console.log("[ClaudeCliDetector] Env ANTHROPIC_API_KEY:", !!envApiKey); // Check app's stored credentials let storedOAuthToken = null; @@ -237,38 +285,44 @@ class ClaudeCliDetector { if (appCredentialsPath && fs.existsSync(appCredentialsPath)) { try { - const content = fs.readFileSync(appCredentialsPath, 'utf-8'); + const content = fs.readFileSync(appCredentialsPath, "utf-8"); const credentials = JSON.parse(content); storedOAuthToken = credentials.anthropic_oauth_token || null; - storedApiKey = credentials.anthropic || credentials.anthropic_api_key || null; - console.log('[ClaudeCliDetector] App credentials:', { + storedApiKey = + credentials.anthropic || credentials.anthropic_api_key || null; + console.log("[ClaudeCliDetector] App credentials:", { hasOAuthToken: !!storedOAuthToken, - hasApiKey: !!storedApiKey + hasApiKey: !!storedApiKey, }); } catch (error) { - console.error('[ClaudeCliDetector] Error reading app credentials:', error); + console.error( + "[ClaudeCliDetector] Error reading app credentials:", + error + ); } } // Determine authentication method // Priority: Stored OAuth Token > Stored API Key > Env API Key let authenticated = false; - let method = 'none'; + let method = "none"; if (storedOAuthToken) { authenticated = true; - method = 'oauth_token'; - console.log('[ClaudeCliDetector] Using stored OAuth token (subscription)'); + method = "oauth_token"; + console.log( + "[ClaudeCliDetector] Using stored OAuth token (subscription)" + ); } else if (storedApiKey) { authenticated = true; - method = 'api_key'; - console.log('[ClaudeCliDetector] Using stored API key'); + method = "api_key"; + console.log("[ClaudeCliDetector] Using stored API key"); } else if (envApiKey) { authenticated = true; - method = 'api_key_env'; - console.log('[ClaudeCliDetector] Using environment API key'); + method = "api_key_env"; + console.log("[ClaudeCliDetector] Using environment API key"); } else { - console.log('[ClaudeCliDetector] No authentication found'); + console.log("[ClaudeCliDetector] No authentication found"); } const result = { @@ -276,12 +330,26 @@ class ClaudeCliDetector { method, hasStoredOAuthToken: !!storedOAuthToken, hasStoredApiKey: !!storedApiKey, - hasEnvApiKey: !!envApiKey + hasEnvApiKey: !!envApiKey, }; - console.log('[ClaudeCliDetector] Auth status result:', result); + console.log("[ClaudeCliDetector] Auth status result:", result); return result; } + /** + * Get installation info (installation status only, no auth) + * @returns {Object} Installation info with status property + */ + static getInstallationInfo() { + const installation = this.detectClaudeInstallation(); + return { + status: installation.installed ? "installed" : "not_installed", + installed: installation.installed, + path: installation.path, + version: installation.version, + method: installation.method, + }; + } /** * Get full status including installation and auth @@ -294,12 +362,12 @@ class ClaudeCliDetector { return { success: true, - status: installation.installed ? 'installed' : 'not_installed', + status: installation.installed ? "installed" : "not_installed", installed: installation.installed, path: installation.path, version: installation.version, method: installation.method, - auth + auth, }; } @@ -309,9 +377,9 @@ class ClaudeCliDetector { */ static getInstallCommands() { return { - macos: 'curl -fsSL https://claude.ai/install.sh | bash', - windows: 'irm https://claude.ai/install.ps1 | iex', - linux: 'curl -fsSL https://claude.ai/install.sh | bash' + macos: "curl -fsSL https://claude.ai/install.sh | bash", + windows: "irm https://claude.ai/install.ps1 | iex", + linux: "curl -fsSL https://claude.ai/install.sh | bash", }; } @@ -325,64 +393,69 @@ class ClaudeCliDetector { const platform = process.platform; let command, args; - if (platform === 'win32') { - command = 'powershell'; - args = ['-Command', 'irm https://claude.ai/install.ps1 | iex']; + if (platform === "win32") { + command = "powershell"; + args = ["-Command", "irm https://claude.ai/install.ps1 | iex"]; } else { - command = 'bash'; - args = ['-c', 'curl -fsSL https://claude.ai/install.sh | bash']; + command = "bash"; + args = ["-c", "curl -fsSL https://claude.ai/install.sh | bash"]; } - console.log('[ClaudeCliDetector] Installing Claude CLI...'); + console.log("[ClaudeCliDetector] Installing Claude CLI..."); const proc = spawn(command, args, { - stdio: ['pipe', 'pipe', 'pipe'], - shell: false + stdio: ["pipe", "pipe", "pipe"], + shell: false, }); - let output = ''; - let errorOutput = ''; + let output = ""; + let errorOutput = ""; - proc.stdout.on('data', (data) => { + proc.stdout.on("data", (data) => { const text = data.toString(); output += text; if (onProgress) { - onProgress({ type: 'stdout', data: text }); + onProgress({ type: "stdout", data: text }); } }); - proc.stderr.on('data', (data) => { + proc.stderr.on("data", (data) => { const text = data.toString(); errorOutput += text; if (onProgress) { - onProgress({ type: 'stderr', data: text }); + onProgress({ type: "stderr", data: text }); } }); - proc.on('close', (code) => { + proc.on("close", (code) => { if (code === 0) { - console.log('[ClaudeCliDetector] Installation completed successfully'); + console.log( + "[ClaudeCliDetector] Installation completed successfully" + ); resolve({ success: true, output, - message: 'Claude CLI installed successfully' + message: "Claude CLI installed successfully", }); } else { - console.error('[ClaudeCliDetector] Installation failed with code:', code); + console.error( + "[ClaudeCliDetector] Installation failed with code:", + code + ); reject({ success: false, error: errorOutput || `Installation failed with code ${code}`, - output + output, }); } }); - proc.on('error', (error) => { - console.error('[ClaudeCliDetector] Installation error:', error); + proc.on("error", (error) => { + console.error("[ClaudeCliDetector] Installation error:", error); reject({ success: false, error: error.message, - output + output, }); }); }); @@ -398,22 +471,22 @@ class ClaudeCliDetector { if (!detection.installed) { return { success: false, - error: 'Claude CLI is not installed. Please install it first.', - installCommands: this.getInstallCommands() + error: "Claude CLI is not installed. Please install it first.", + installCommands: this.getInstallCommands(), }; } return { success: true, - command: 'claude setup-token', + command: "claude setup-token", instructions: [ - '1. Open your terminal', - '2. Run: claude setup-token', - '3. Follow the prompts to authenticate', - '4. Copy the token that is displayed', - '5. Paste the token in the field below' + "1. Open your terminal", + "2. Run: claude setup-token", + "3. Follow the prompts to authenticate", + "4. Copy the token that is displayed", + "5. Paste the token in the field below", ], - note: 'This token is from your Claude subscription and allows you to use Claude without API charges.' + note: "This token is from your Claude subscription and allows you to use Claude without API charges.", }; } } diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index e40c72dd..ddc06f2e 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef } from "react"; import { cn } from "@/lib/utils"; -import { useAppStore } from "@/store/app-store"; +import { useAppStore, formatShortcut } from "@/store/app-store"; import { FolderOpen, Plus, @@ -722,7 +722,7 @@ export function Sidebar() { className="ml-1 px-1 py-0.5 bg-brand-500/10 border border-brand-500/30 rounded text-[10px] font-mono text-brand-400/70" data-testid="sidebar-toggle-shortcut" > - {shortcuts.toggleSidebar} + {formatShortcut(shortcuts.toggleSidebar, true)} @@ -779,8 +779,8 @@ export function Sidebar() { data-testid="open-project-button" > - - {shortcuts.openProject} + + {formatShortcut(shortcuts.openProject, true)} + ); + + // Wrap in tooltip if bound + if (isBound) { + return ( + + {keyElement} + +
+ {shortcuts.map((shortcut) => { + const shortcutStr = keyboardShortcuts[shortcut]; + const displayShortcut = formatShortcut(shortcutStr, true); + return ( +
+ + {SHORTCUT_LABELS[shortcut]} + + {displayShortcut} + + {keyboardShortcuts[shortcut] !== DEFAULT_KEYBOARD_SHORTCUTS[shortcut] && ( + (custom) + )} +
+ ); + })} +
+
+
+ ); + } + + return keyElement; + }; + + return ( + +
+ {/* Legend */} +
+ {Object.entries(CATEGORY_COLORS).map(([key, colors]) => ( +
+
+ {colors.label} +
+ ))} +
+
+ Available +
+
+
+ Modified +
+
+ + {/* Keyboard layout */} +
+ {KEYBOARD_ROWS.map((row, rowIndex) => ( +
+ {row.map(renderKey)} +
+ ))} +
+ + {/* Stats */} +
+ + {Object.keys(keyboardShortcuts).length} shortcuts + configured + + + + {Object.keys(keyToShortcuts).length} + {" "} + keys in use + + + + {KEYBOARD_ROWS.flat().length - Object.keys(keyToShortcuts).length} + {" "} + keys available + +
+
+ + ); +} + +// Full shortcut reference panel with editing capability +interface ShortcutReferencePanelProps { + editable?: boolean; +} + +export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePanelProps) { + const { keyboardShortcuts, setKeyboardShortcut, resetKeyboardShortcuts } = useAppStore(); + const [editingShortcut, setEditingShortcut] = React.useState(null); + const [keyValue, setKeyValue] = React.useState(""); + const [modifiers, setModifiers] = React.useState({ shift: false, cmdCtrl: false, alt: false }); + const [shortcutError, setShortcutError] = React.useState(null); + + const groupedShortcuts = React.useMemo(() => { + const groups: Record> = { + navigation: [], + ui: [], + action: [], + }; + + (Object.entries(SHORTCUT_CATEGORIES) as [keyof KeyboardShortcuts, string][]).forEach( + ([shortcut, category]) => { + groups[category].push({ + key: shortcut, + label: SHORTCUT_LABELS[shortcut], + value: keyboardShortcuts[shortcut], + }); + } + ); + + return groups; + }, [keyboardShortcuts]); + + // Build the full shortcut string from key + modifiers + const buildShortcutString = React.useCallback((key: string, mods: typeof modifiers) => { + const parts: string[] = []; + if (mods.cmdCtrl) parts.push(isMac ? "Cmd" : "Ctrl"); + if (mods.alt) parts.push(isMac ? "Opt" : "Alt"); + if (mods.shift) parts.push("Shift"); + parts.push(key.toUpperCase()); + return parts.join("+"); + }, []); + + // Check for conflicts with other shortcuts + const checkConflict = React.useCallback((shortcutStr: string, currentKey: keyof KeyboardShortcuts) => { + const conflict = Object.entries(keyboardShortcuts).find( + ([k, v]) => k !== currentKey && v.toUpperCase() === shortcutStr.toUpperCase() + ); + return conflict ? SHORTCUT_LABELS[conflict[0] as keyof KeyboardShortcuts] : null; + }, [keyboardShortcuts]); + + const handleStartEdit = (key: keyof KeyboardShortcuts) => { + const currentValue = keyboardShortcuts[key]; + const parsed = parseShortcut(currentValue); + setEditingShortcut(key); + setKeyValue(parsed.key); + setModifiers({ + shift: parsed.shift || false, + cmdCtrl: parsed.cmdCtrl || false, + alt: parsed.alt || false, + }); + setShortcutError(null); + }; + + const handleSaveShortcut = () => { + if (!editingShortcut || shortcutError || !keyValue) return; + const shortcutStr = buildShortcutString(keyValue, modifiers); + setKeyboardShortcut(editingShortcut, shortcutStr); + setEditingShortcut(null); + setKeyValue(""); + setModifiers({ shift: false, cmdCtrl: false, alt: false }); + setShortcutError(null); + }; + + const handleCancelEdit = () => { + setEditingShortcut(null); + setKeyValue(""); + setModifiers({ shift: false, cmdCtrl: false, alt: false }); + setShortcutError(null); + }; + + const handleKeyChange = (value: string, currentKey: keyof KeyboardShortcuts) => { + setKeyValue(value); + // Check for conflicts with full shortcut string + if (!value) { + setShortcutError("Key cannot be empty"); + } else { + const shortcutStr = buildShortcutString(value, modifiers); + const conflictLabel = checkConflict(shortcutStr, currentKey); + if (conflictLabel) { + setShortcutError(`Already used by "${conflictLabel}"`); + } else { + setShortcutError(null); + } + } + }; + + const handleModifierChange = (modifier: keyof typeof modifiers, checked: boolean, currentKey: keyof KeyboardShortcuts) => { + // Enforce single modifier: when checking, uncheck all others (radio-button behavior) + const newModifiers = checked + ? { shift: false, cmdCtrl: false, alt: false, [modifier]: true } + : { ...modifiers, [modifier]: false }; + + setModifiers(newModifiers); + + // Recheck for conflicts + if (keyValue) { + const shortcutStr = buildShortcutString(keyValue, newModifiers); + const conflictLabel = checkConflict(shortcutStr, currentKey); + if (conflictLabel) { + setShortcutError(`Already used by "${conflictLabel}"`); + } else { + setShortcutError(null); + } + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !shortcutError && keyValue) { + handleSaveShortcut(); + } else if (e.key === "Escape") { + handleCancelEdit(); + } + }; + + const handleResetShortcut = (key: keyof KeyboardShortcuts) => { + setKeyboardShortcut(key, DEFAULT_KEYBOARD_SHORTCUTS[key]); + }; + + return ( + +
+ {editable && ( +
+ +
+ )} + {Object.entries(groupedShortcuts).map(([category, shortcuts]) => { + const colors = CATEGORY_COLORS[category as keyof typeof CATEGORY_COLORS]; + return ( +
+

+ {colors.label} +

+
+ {shortcuts.map(({ key, label, value }) => { + const isModified = keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key]; + const isEditing = editingShortcut === key; + + return ( +
editable && !isEditing && handleStartEdit(key)} + data-testid={`shortcut-row-${key}`} + > + {label} +
+ {isEditing ? ( +
e.stopPropagation()}> + {/* Modifier checkboxes */} +
+
+ handleModifierChange("cmdCtrl", !!checked, key)} + className="h-3.5 w-3.5" + /> + +
+
+ handleModifierChange("alt", !!checked, key)} + className="h-3.5 w-3.5" + /> + +
+
+ handleModifierChange("shift", !!checked, key)} + className="h-3.5 w-3.5" + /> + +
+
+ + + handleKeyChange(e.target.value, key)} + onKeyDown={handleKeyDown} + className={cn( + "w-12 h-7 text-center font-mono text-xs uppercase", + shortcutError && "border-red-500 focus-visible:ring-red-500" + )} + placeholder="Key" + maxLength={1} + autoFocus + data-testid={`edit-shortcut-input-${key}`} + /> + + +
+ ) : ( + <> + + {formatShortcut(value, true)} + + {isModified && editable && ( + + + + + + Reset to default ({DEFAULT_KEYBOARD_SHORTCUTS[key]}) + + + )} + {isModified && !editable && ( + + )} + {editable && !isModified && ( + + )} + + )} +
+
+ ); + })} +
+ {editingShortcut && shortcutError && SHORTCUT_CATEGORIES[editingShortcut] === category && ( +

{shortcutError}

+ )} +
+ ); + })} +
+
+ ); +} diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index b8c386b7..120cb554 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -1,8 +1,7 @@ "use client"; import { useState, useEffect, useRef, useCallback } from "react"; -import { useAppStore, DEFAULT_KEYBOARD_SHORTCUTS } from "@/store/app-store"; -import type { KeyboardShortcuts } from "@/store/app-store"; +import { useAppStore } from "@/store/app-store"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -41,9 +40,9 @@ import { Settings2, RefreshCw, Info, + Keyboard, } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; -import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -53,6 +52,8 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { useSetupStore } from "@/store/setup-store"; +import { KeyboardMap, ShortcutReferencePanel } from "@/components/ui/keyboard-map"; +import { Checkbox } from "../ui/checkbox"; // Navigation items for the side panel const NAV_ITEMS = [ @@ -84,9 +85,6 @@ export function SettingsView() { setShowProfilesOnly, currentProject, moveProjectToTrash, - keyboardShortcuts, - setKeyboardShortcut, - resetKeyboardShortcuts, } = useAppStore(); // Compute the effective theme for the current project @@ -108,6 +106,7 @@ export function SettingsView() { const [showOpenaiKey, setShowOpenaiKey] = useState(false); const [saved, setSaved] = useState(false); const [testingConnection, setTestingConnection] = useState(false); + const [testResult, setTestResult] = useState<{ success: boolean; message: string; @@ -155,6 +154,7 @@ export function SettingsView() { } | null>(null); const [activeSection, setActiveSection] = useState("api-keys"); const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false); const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false); const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false); const [apiKeyStatus, setApiKeyStatus] = useState<{ @@ -1608,376 +1608,35 @@ export function SettingsView() {

- Customize keyboard shortcuts for navigation and actions. Click - on any shortcut to edit it. + Customize keyboard shortcuts for navigation and actions using the visual keyboard map.

-
- {/* Navigation Shortcuts */} -
-
-

- Navigation +
+ {/* Centered message directing to keyboard map */} +
+
+ +
+
+
+

+ Use the Visual Keyboard Map

- -
-
- {[ - { key: "board" as keyof KeyboardShortcuts, label: "Kanban Board" }, - { key: "agent" as keyof KeyboardShortcuts, label: "Agent Runner" }, - { key: "spec" as keyof KeyboardShortcuts, label: "Spec Editor" }, - { key: "context" as keyof KeyboardShortcuts, label: "Context" }, - { key: "tools" as keyof KeyboardShortcuts, label: "Agent Tools" }, - { key: "profiles" as keyof KeyboardShortcuts, label: "AI Profiles" }, - { key: "settings" as keyof KeyboardShortcuts, label: "Settings" }, - ].map(({ key, label }) => ( -
- {label} -
- {editingShortcut === key ? ( - <> - { - const value = e.target.value.toUpperCase(); - setShortcutValue(value); - // Check for conflicts - const conflict = Object.entries(keyboardShortcuts).find( - ([k, v]) => k !== key && v.toUpperCase() === value - ); - if (conflict) { - setShortcutError(`Already used by ${conflict[0]}`); - } else { - setShortcutError(null); - } - }} - onKeyDown={(e) => { - if (e.key === "Enter" && !shortcutError && shortcutValue) { - setKeyboardShortcut(key, shortcutValue); - setEditingShortcut(null); - setShortcutValue(""); - setShortcutError(null); - } else if (e.key === "Escape") { - setEditingShortcut(null); - setShortcutValue(""); - setShortcutError(null); - } - }} - className="w-24 h-8 text-center font-mono" - placeholder="Key" - maxLength={2} - autoFocus - data-testid={`edit-shortcut-${key}`} - /> - - - - ) : ( - <> - - {keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && ( - (modified) - )} - - )} -
-
- ))} -
- {shortcutError && ( -

{shortcutError}

- )} -
- - {/* UI Shortcuts */} -
-

- UI Controls -

-
- {[ - { key: "toggleSidebar" as keyof KeyboardShortcuts, label: "Toggle Sidebar" }, - ].map(({ key, label }) => ( -
- {label} -
- {editingShortcut === key ? ( - <> - { - const value = e.target.value; - setShortcutValue(value); - // Check for conflicts - const conflict = Object.entries(keyboardShortcuts).find( - ([k, v]) => k !== key && v === value - ); - if (conflict) { - setShortcutError(`Already used by ${conflict[0]}`); - } else { - setShortcutError(null); - } - }} - onKeyDown={(e) => { - if (e.key === "Enter" && !shortcutError && shortcutValue) { - setKeyboardShortcut(key, shortcutValue); - setEditingShortcut(null); - setShortcutValue(""); - setShortcutError(null); - } else if (e.key === "Escape") { - setEditingShortcut(null); - setShortcutValue(""); - setShortcutError(null); - } - }} - className="w-24 h-8 text-center font-mono" - placeholder="Key" - maxLength={2} - autoFocus - data-testid={`edit-shortcut-${key}`} - /> - - - - ) : ( - <> - - {keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && ( - (modified) - )} - - )} -
-
- ))} -
-
- - {/* Action Shortcuts */} -
-

- Actions -

-
- {[ - { key: "addFeature" as keyof KeyboardShortcuts, label: "Add Feature" }, - { key: "addContextFile" as keyof KeyboardShortcuts, label: "Add Context File" }, - { key: "startNext" as keyof KeyboardShortcuts, label: "Start Next Features" }, - { key: "newSession" as keyof KeyboardShortcuts, label: "New Session" }, - { key: "openProject" as keyof KeyboardShortcuts, label: "Open Project" }, - { key: "projectPicker" as keyof KeyboardShortcuts, label: "Project Picker" }, - { key: "cyclePrevProject" as keyof KeyboardShortcuts, label: "Previous Project" }, - { key: "cycleNextProject" as keyof KeyboardShortcuts, label: "Next Project" }, - { key: "addProfile" as keyof KeyboardShortcuts, label: "Add Profile" }, - ].map(({ key, label }) => ( -
- {label} -
- {editingShortcut === key ? ( - <> - { - const value = e.target.value.toUpperCase(); - setShortcutValue(value); - // Check for conflicts - const conflict = Object.entries(keyboardShortcuts).find( - ([k, v]) => k !== key && v.toUpperCase() === value - ); - if (conflict) { - setShortcutError(`Already used by ${conflict[0]}`); - } else { - setShortcutError(null); - } - }} - onKeyDown={(e) => { - if (e.key === "Enter" && !shortcutError && shortcutValue) { - setKeyboardShortcut(key, shortcutValue); - setEditingShortcut(null); - setShortcutValue(""); - setShortcutError(null); - } else if (e.key === "Escape") { - setEditingShortcut(null); - setShortcutValue(""); - setShortcutError(null); - } - }} - className="w-24 h-8 text-center font-mono" - placeholder="Key" - maxLength={2} - autoFocus - data-testid={`edit-shortcut-${key}`} - /> - - - - ) : ( - <> - - {keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && ( - (modified) - )} - - )} -
-
- ))} -
-
- - {/* Information */} -
- -
-

- About Keyboard Shortcuts -

-

- Shortcuts won't trigger when typing in input fields. Use - single keys (A-Z, 0-9) or special keys like ` (backtick). - Changes take effect immediately. +

+ Click the "View Keyboard Map" button above to customize your keyboard shortcuts. + The visual interface shows all available keys and lets you easily edit shortcuts with + single-modifier restrictions.

+
@@ -2169,6 +1828,44 @@ export function SettingsView() {

+ {/* Keyboard Map Dialog */} + + + + + + Keyboard Shortcut Map + + + Visual overview of all keyboard shortcuts. Keys in color are bound to shortcuts. + Click on any shortcut below to edit it. + + + +
+ {/* Visual Keyboard Map */} + + + {/* Shortcut Reference - Editable */} +
+

+ All Shortcuts Reference (Click to Edit) +

+ +
+
+ + + + +
+
+ {/* Delete Project Confirmation Dialog */} diff --git a/app/src/hooks/use-keyboard-shortcuts.ts b/app/src/hooks/use-keyboard-shortcuts.ts index a9b901ff..8e12c2f5 100644 --- a/app/src/hooks/use-keyboard-shortcuts.ts +++ b/app/src/hooks/use-keyboard-shortcuts.ts @@ -1,10 +1,10 @@ "use client"; import { useEffect, useCallback } from "react"; -import { useAppStore } from "@/store/app-store"; +import { useAppStore, parseShortcut } from "@/store/app-store"; export interface KeyboardShortcut { - key: string; + key: string; // Can be simple "K" or with modifiers "Shift+N", "Cmd+K" action: () => void; description?: string; } @@ -59,9 +59,44 @@ function isInputFocused(): boolean { return false; } +/** + * Check if a keyboard event matches a shortcut definition + */ +function matchesShortcut(event: KeyboardEvent, shortcutStr: string): boolean { + const shortcut = parseShortcut(shortcutStr); + + // Check if the key matches (case-insensitive) + if (event.key.toLowerCase() !== shortcut.key.toLowerCase()) { + return false; + } + + // Check modifier keys + const cmdCtrlPressed = event.metaKey || event.ctrlKey; + const shiftPressed = event.shiftKey; + const altPressed = event.altKey; + + // If shortcut requires cmdCtrl, it must be pressed + if (shortcut.cmdCtrl && !cmdCtrlPressed) return false; + // If shortcut doesn't require cmdCtrl, it shouldn't be pressed + if (!shortcut.cmdCtrl && cmdCtrlPressed) return false; + + // If shortcut requires shift, it must be pressed + if (shortcut.shift && !shiftPressed) return false; + // If shortcut doesn't require shift, it shouldn't be pressed + if (!shortcut.shift && shiftPressed) return false; + + // If shortcut requires alt, it must be pressed + if (shortcut.alt && !altPressed) return false; + // If shortcut doesn't require alt, it shouldn't be pressed + if (!shortcut.alt && altPressed) return false; + + return true; +} + /** * Hook to manage keyboard shortcuts * Shortcuts won't fire when user is typing in inputs, textareas, or when dialogs are open + * Supports modifier keys: Shift, Cmd/Ctrl, Alt/Option */ export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) { const handleKeyDown = useCallback( @@ -71,14 +106,9 @@ export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) { return; } - // Don't trigger if any modifier keys are pressed (except for specific combos we want) - if (event.ctrlKey || event.altKey || event.metaKey) { - return; - } - // Find matching shortcut const matchingShortcut = shortcuts.find( - (shortcut) => shortcut.key.toLowerCase() === event.key.toLowerCase() + (shortcut) => matchesShortcut(event, shortcut.key) ); if (matchingShortcut) { diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index 7c9ced56..a6f86a78 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -37,7 +37,68 @@ export interface ApiKeys { openai: string; } -// Keyboard Shortcuts +// Keyboard Shortcut with optional modifiers +export interface ShortcutKey { + key: string; // The main key (e.g., "K", "N", "1") + shift?: boolean; // Shift key modifier + cmdCtrl?: boolean; // Cmd on Mac, Ctrl on Windows/Linux + alt?: boolean; // Alt/Option key modifier +} + +// Helper to parse shortcut string to ShortcutKey object +export function parseShortcut(shortcut: string): ShortcutKey { + const parts = shortcut.split("+").map(p => p.trim()); + const result: ShortcutKey = { key: parts[parts.length - 1] }; + + for (let i = 0; i < parts.length - 1; i++) { + const modifier = parts[i].toLowerCase(); + if (modifier === "shift") result.shift = true; + else if (modifier === "cmd" || modifier === "ctrl" || modifier === "win" || modifier === "super" || modifier === "⌘" || modifier === "^" || modifier === "⊞" || modifier === "◆") result.cmdCtrl = true; + else if (modifier === "alt" || modifier === "opt" || modifier === "option" || modifier === "⌥") result.alt = true; + } + + return result; +} + +// Helper to format ShortcutKey to display string +export function formatShortcut(shortcut: string, forDisplay = false): string { + const parsed = parseShortcut(shortcut); + const parts: string[] = []; + + // Improved OS detection + let platform: 'darwin' | 'win32' | 'linux' = 'linux'; + if (typeof navigator !== 'undefined') { + const p = navigator.platform.toLowerCase(); + if (p.includes('mac')) platform = 'darwin'; + else if (p.includes('win')) platform = 'win32'; + } + + // Primary modifier - OS-specific + if (parsed.cmdCtrl) { + if (forDisplay) { + parts.push(platform === 'darwin' ? '⌘' : platform === 'win32' ? '⊞' : '◆'); + } else { + parts.push(platform === 'darwin' ? 'Cmd' : platform === 'win32' ? 'Win' : 'Super'); + } + } + + // Alt/Option + if (parsed.alt) { + parts.push(forDisplay ? (platform === 'darwin' ? '⌥' : 'Alt') : (platform === 'darwin' ? 'Opt' : 'Alt')); + } + + // Shift + if (parsed.shift) { + parts.push(forDisplay ? '⇧' : 'Shift'); + } + + parts.push(parsed.key.toUpperCase()); + + // Add spacing when displaying symbols + return parts.join(forDisplay ? " " : "+"); +} + +// Keyboard Shortcuts - stored as strings like "K", "Shift+N", "Cmd+K" export interface KeyboardShortcuts { // Navigation shortcuts board: string; @@ -47,10 +108,10 @@ export interface KeyboardShortcuts { tools: string; settings: string; profiles: string; - + // UI shortcuts toggleSidebar: string; - + // Action shortcuts addFeature: string; addContextFile: string; From 0d462ba08053fa9e7c697c166bb85f75bc9815ba Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 10 Dec 2025 23:16:16 +0100 Subject: [PATCH 03/24] fix(settings-view): adjust padding and remove unused dialog footer - Updated padding in the settings view for better layout consistency. - Removed the unused dialog footer containing the close button to streamline the interface. --- app/src/components/views/settings-view.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 120cb554..2df8d855 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -1842,7 +1842,7 @@ export function SettingsView() { -
+
{/* Visual Keyboard Map */} @@ -1854,15 +1854,6 @@ export function SettingsView() {
- - - -
From 6086d22a44f0da0ead5ca48aff6a9f6f97966069 Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 10 Dec 2025 23:19:09 +0100 Subject: [PATCH 04/24] refactor(settings-view): streamline authentication status handling - Removed unused state variables related to shortcut editing in the settings view. - Updated authentication status handling for Claude and Codex to use more precise type definitions, improving type safety and clarity. - Enhanced the ElectronAPI and SetupAPI interfaces to include optional properties for stored OAuth and API keys, ensuring better alignment with the runtime API responses. --- app/src/components/views/settings-view.tsx | 23 +++++++++------------- app/src/lib/electron.ts | 10 ++++++++++ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 8b287e50..9d47889f 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -41,7 +41,6 @@ import { RefreshCw, Info, Keyboard, - RotateCcw, } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; import { @@ -52,7 +51,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { useSetupStore } from "@/store/setup-store"; +import { useSetupStore, type ClaudeAuthStatus, type CodexAuthStatus } from "@/store/setup-store"; import { KeyboardMap, ShortcutReferencePanel } from "@/components/ui/keyboard-map"; import { Checkbox } from "../ui/checkbox"; @@ -163,9 +162,6 @@ export function SettingsView() { hasOpenAIKey: boolean; hasGoogleKey: boolean; } | null>(null); - const [editingShortcut, setEditingShortcut] = useState(null); - const [shortcutValue, setShortcutValue] = useState(""); - const [shortcutError, setShortcutError] = useState(null); const scrollContainerRef = useRef(null); // Get authentication status from setup store @@ -217,20 +213,19 @@ export function SettingsView() { try { const result = await api.setup.getClaudeStatus(); if (result.success && result.auth) { - // Cast to any because runtime API returns more properties than type definition - const auth = result.auth as any; - const authStatus = { + const auth = result.auth; + const authStatus: ClaudeAuthStatus = { authenticated: auth.authenticated, method: auth.method === "oauth_token" ? "oauth" : auth.method?.includes("api_key") ? "api_key" : "none", - hasCredentialsFile: false, + hasCredentialsFile: auth.hasCredentialsFile ?? false, oauthTokenValid: auth.hasStoredOAuthToken, apiKeyValid: auth.hasStoredApiKey || auth.hasEnvApiKey, }; - setClaudeAuthStatus(authStatus as any); + setClaudeAuthStatus(authStatus); } } catch (error) { console.error("Failed to check Claude auth status:", error); @@ -242,13 +237,13 @@ export function SettingsView() { try { const result = await api.setup.getCodexStatus(); if (result.success && result.auth) { - // Cast to any because runtime API returns more properties than type definition - const auth = result.auth as any; - setCodexAuthStatus({ + const auth = result.auth; + const authStatus: CodexAuthStatus = { authenticated: auth.authenticated, method: auth.hasEnvApiKey ? "env" : auth.hasStoredApiKey ? "api_key" : "none", apiKeyValid: auth.hasStoredApiKey || auth.hasEnvApiKey, - }); + }; + setCodexAuthStatus(authStatus); } } catch (error) { console.error("Failed to check Codex auth status:", error); diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index 4ea34ead..c4f58396 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -184,6 +184,9 @@ export interface ElectronAPI { method: string; hasCredentialsFile: boolean; hasToken: boolean; + hasStoredOAuthToken?: boolean; + hasStoredApiKey?: boolean; + hasEnvApiKey?: boolean; }; error?: string; }>; @@ -198,6 +201,8 @@ export interface ElectronAPI { method: string; hasAuthFile: boolean; hasEnvKey: boolean; + hasStoredApiKey?: boolean; + hasEnvApiKey?: boolean; }; error?: string; }>; @@ -542,6 +547,9 @@ interface SetupAPI { method: string; hasCredentialsFile: boolean; hasToken: boolean; + hasStoredOAuthToken?: boolean; + hasStoredApiKey?: boolean; + hasEnvApiKey?: boolean; }; error?: string; }>; @@ -556,6 +564,8 @@ interface SetupAPI { method: string; hasAuthFile: boolean; hasEnvKey: boolean; + hasStoredApiKey?: boolean; + hasEnvApiKey?: boolean; }; error?: string; }>; From d5d6cdf80f6f4aa7a3ce051d45235c48804d6faa Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 10 Dec 2025 23:35:09 +0100 Subject: [PATCH 05/24] refactor(auth): enhance authentication detection and status handling - Improved the CodexCliDetector to provide detailed logging and better error handling when reading the authentication file. - Updated the authentication method determination in the settings and setup views to prioritize CLI-based methods over traditional API key methods. - Expanded the CodexAuthStatus interface to include new authentication methods, ensuring accurate representation of the authentication state. - Enhanced UI feedback in the settings view to reflect the new authentication methods, improving user experience. --- app/electron/services/codex-cli-detector.js | 126 ++++++++++++++++---- app/src/components/views/settings-view.tsx | 27 ++++- app/src/components/views/setup-view.tsx | 19 ++- app/src/lib/electron.ts | 4 +- app/src/store/setup-store.ts | 2 +- 5 files changed, 143 insertions(+), 35 deletions(-) diff --git a/app/electron/services/codex-cli-detector.js b/app/electron/services/codex-cli-detector.js index f45a8db2..8a2f2a45 100644 --- a/app/electron/services/codex-cli-detector.js +++ b/app/electron/services/codex-cli-detector.js @@ -72,38 +72,109 @@ class CodexCliDetector { // Check if auth file exists if (fs.existsSync(authPath)) { - const content = fs.readFileSync(authPath, 'utf-8'); - const auth = JSON.parse(content); + console.log('[CodexCliDetector] Auth file exists, reading content...'); + let auth = null; + try { + const content = fs.readFileSync(authPath, 'utf-8'); + auth = JSON.parse(content); + console.log('[CodexCliDetector] Auth file content keys:', Object.keys(auth)); + console.log('[CodexCliDetector] Auth file has token object:', !!auth.token); + if (auth.token) { + console.log('[CodexCliDetector] Token object keys:', Object.keys(auth.token)); + } - // Check for token object structure (from codex auth login) - // Structure: { token: { Id_token, access_token, refresh_token }, last_refresh: ... } - if (auth.token && typeof auth.token === 'object') { - const token = auth.token; - if (token.Id_token || token.access_token || token.refresh_token || token.id_token) { - return { + // Check for token object structure (from codex auth login) + // Structure: { token: { Id_token, access_token, refresh_token }, last_refresh: ... } + if (auth.token && typeof auth.token === 'object') { + const token = auth.token; + if (token.Id_token || token.access_token || token.refresh_token || token.id_token) { + const result = { + authenticated: true, + method: 'cli_tokens', // Distinguish token-based auth from API key auth + hasAuthFile: true, + hasEnvKey: !!envApiKey, + authPath + }; + console.log('[CodexCliDetector] Auth result (cli_tokens):', result); + return result; + } + } + + // Check for tokens at root level (alternative structure) + if (auth.access_token || auth.refresh_token || auth.Id_token || auth.id_token) { + const result = { + authenticated: true, + method: 'cli_tokens', // These are tokens, not API keys + hasAuthFile: true, + hasEnvKey: !!envApiKey, + authPath + }; + console.log('[CodexCliDetector] Auth result (cli_tokens - root level):', result); + return result; + } + + // Check for various possible API key fields that codex might use + // Note: access_token is NOT an API key, it's a token, so we check for it above + if (auth.api_key || auth.openai_api_key || auth.apiKey) { + const result = { authenticated: true, method: 'auth_file', hasAuthFile: true, hasEnvKey: !!envApiKey, authPath }; + console.log('[CodexCliDetector] Auth result (auth_file - API key):', result); + return result; } - } - - // Check for various possible auth fields that codex might use - if (auth.api_key || auth.openai_api_key || auth.access_token || auth.apiKey) { + } catch (error) { + console.error('[CodexCliDetector] Error reading/parsing auth file:', error.message); + // If we can't parse the file, we can't determine auth status return { - authenticated: true, - method: 'auth_file', - hasAuthFile: true, + authenticated: false, + method: 'none', + hasAuthFile: false, hasEnvKey: !!envApiKey, authPath }; } // Also check if the file has any meaningful content (non-empty object) + // This is a fallback - but we should still try to detect if it's tokens + if (!auth) { + // File exists but couldn't be parsed + return { + authenticated: false, + method: 'none', + hasAuthFile: true, + hasEnvKey: !!envApiKey, + authPath + }; + } + const keys = Object.keys(auth); + console.log('[CodexCliDetector] File has content, keys:', keys); if (keys.length > 0) { + // Check again for tokens in case we missed them (maybe nested differently) + const hasTokens = keys.some(key => + key.toLowerCase().includes('token') || + key.toLowerCase().includes('refresh') || + (auth[key] && typeof auth[key] === 'object' && ( + auth[key].access_token || auth[key].refresh_token || auth[key].Id_token || auth[key].id_token + )) + ); + + if (hasTokens) { + const result = { + authenticated: true, + method: 'cli_tokens', + hasAuthFile: true, + hasEnvKey: !!envApiKey, + authPath + }; + console.log('[CodexCliDetector] Auth result (cli_tokens - fallback detection):', result); + return result; + } + // File exists and has content, likely authenticated // Try to verify by checking if codex command works try { @@ -116,34 +187,45 @@ class CodexCliDetector { timeout: 3000 }); // If command succeeds, assume authenticated - return { + // But check if it's likely tokens vs API key based on file structure + const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh')); + const result = { authenticated: true, - method: 'auth_file', + method: likelyTokens ? 'cli_tokens' : 'auth_file', hasAuthFile: true, hasEnvKey: !!envApiKey, authPath }; + console.log('[CodexCliDetector] Auth result (verified via CLI, method:', result.method, '):', result); + return result; } catch (cmdError) { // Command failed, but file exists - might still be authenticated - // Return authenticated if file has content - return { + // Check if it's likely tokens + const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh')); + const result = { authenticated: true, - method: 'auth_file', + method: likelyTokens ? 'cli_tokens' : 'auth_file', hasAuthFile: true, hasEnvKey: !!envApiKey, authPath }; + console.log('[CodexCliDetector] Auth result (file exists, method:', result.method, '):', result); + return result; } } } catch (verifyError) { // Verification failed, but file exists with content - return { + // Check if it's likely tokens + const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh')); + const result = { authenticated: true, - method: 'auth_file', + method: likelyTokens ? 'cli_tokens' : 'auth_file', hasAuthFile: true, hasEnvKey: !!envApiKey, authPath }; + console.log('[CodexCliDetector] Auth result (fallback, method:', result.method, '):', result); + return result; } } } diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 9d47889f..a6236ade 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -238,10 +238,20 @@ export function SettingsView() { const result = await api.setup.getCodexStatus(); if (result.success && result.auth) { const auth = result.auth; + // Determine method - prioritize cli_verified and cli_tokens over auth_file + const method = auth.method === "cli_verified" || auth.method === "cli_tokens" + ? auth.method === "cli_verified" ? "cli_verified" : "cli_tokens" + : auth.method === "auth_file" + ? "api_key" + : auth.method === "env_var" + ? "env" + : "none"; + const authStatus: CodexAuthStatus = { authenticated: auth.authenticated, - method: auth.hasEnvApiKey ? "env" : auth.hasStoredApiKey ? "api_key" : "none", - apiKeyValid: auth.hasStoredApiKey || auth.hasEnvApiKey, + method, + // Only set apiKeyValid for actual API key methods, not CLI login + apiKeyValid: method === "cli_verified" || method === "cli_tokens" ? undefined : (auth.hasAuthFile || auth.hasEnvKey), }; setCodexAuthStatus(authStatus); } @@ -932,7 +942,9 @@ export function SettingsView() { Method:{" "} - {codexAuthStatus.method === "api_key" + {codexAuthStatus.method === "cli_verified" || codexAuthStatus.method === "cli_tokens" + ? "CLI Login (OpenAI Account)" + : codexAuthStatus.method === "api_key" ? "API Key (Auth File)" : codexAuthStatus.method === "env" ? "API Key (Environment)" @@ -940,12 +952,17 @@ export function SettingsView() {
- {codexAuthStatus.apiKeyValid && ( + {codexAuthStatus.method === "cli_verified" || codexAuthStatus.method === "cli_tokens" ? ( +
+ + Account authenticated +
+ ) : codexAuthStatus.apiKeyValid ? (
API key configured
- )} + ) : null} {apiKeyStatus?.hasOpenAIKey && (
diff --git a/app/src/components/views/setup-view.tsx b/app/src/components/views/setup-view.tsx index 229d955c..bcad97f4 100644 --- a/app/src/components/views/setup-view.tsx +++ b/app/src/components/views/setup-view.tsx @@ -11,7 +11,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { useSetupStore } from "@/store/setup-store"; +import { useSetupStore, type CodexAuthStatus } from "@/store/setup-store"; import { useAppStore } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; import { @@ -805,13 +805,22 @@ function CodexSetupStep({ setCodexCliStatus(cliStatus); if (result.auth) { - const authStatus = { + const method = result.auth.method === "cli_verified" || result.auth.method === "cli_tokens" + ? (result.auth.method === "cli_verified" ? "cli_verified" : "cli_tokens") + : result.auth.method === "auth_file" + ? "api_key" + : result.auth.method === "env_var" + ? "env" + : "none"; + + const authStatus: CodexAuthStatus = { authenticated: result.auth.authenticated, - method: result.auth.method === "auth_file" ? "api_key" : result.auth.method === "env_var" ? "env" : "none", - apiKeyValid: result.auth.authenticated, + method, + // Only set apiKeyValid for actual API key methods, not CLI login + apiKeyValid: method === "cli_verified" || method === "cli_tokens" ? undefined : result.auth.authenticated, }; console.log("[Codex Setup] Auth Status:", authStatus); - setCodexAuthStatus(authStatus as any); + setCodexAuthStatus(authStatus); } else { console.log("[Codex Setup] No auth info in result"); } diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index c4f58396..426985b4 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -198,7 +198,7 @@ export interface ElectronAPI { path?: string; auth?: { authenticated: boolean; - method: string; + method: string; // Can be: "cli_verified", "cli_tokens", "auth_file", "env_var", "none" hasAuthFile: boolean; hasEnvKey: boolean; hasStoredApiKey?: boolean; @@ -561,7 +561,7 @@ interface SetupAPI { path?: string; auth?: { authenticated: boolean; - method: string; + method: string; // Can be: "cli_verified", "cli_tokens", "auth_file", "env_var", "none" hasAuthFile: boolean; hasEnvKey: boolean; hasStoredApiKey?: boolean; diff --git a/app/src/store/setup-store.ts b/app/src/store/setup-store.ts index 6d4ded1d..8741ba69 100644 --- a/app/src/store/setup-store.ts +++ b/app/src/store/setup-store.ts @@ -23,7 +23,7 @@ export interface ClaudeAuthStatus { // Codex Auth Status export interface CodexAuthStatus { authenticated: boolean; - method: "api_key" | "env" | "none"; + method: "api_key" | "env" | "cli_verified" | "cli_tokens" | "none"; apiKeyValid?: boolean; mcpConfigured?: boolean; error?: string; From da78bed47d4c48f57246fe531c402463bab8491f Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:24:18 +0100 Subject: [PATCH 06/24] refactor(settings): reorganize api-keys section into folder - Move api-keys-section.tsx into api-keys/ folder - Move child components (api-key-field, authentication-status-display, security-notice) into api-keys/ - Move custom hook (use-api-key-management) into api-keys/hooks/ - Move config (api-provider-config) into api-keys/config/ - Update import paths in use-api-key-management.ts - Update settings-view.tsx to import from new location - All TypeScript diagnostics passing - Improves code organization and maintainability --- app/src/components/views/settings-view.tsx | 1632 +---------------- .../settings-view/api-keys/api-key-field.tsx | 117 ++ .../api-keys/api-keys-section.tsx | 72 + .../authentication-status-display.tsx | 212 +++ .../api-keys/config/api-provider-config.ts | 149 ++ .../api-keys/hooks/use-api-key-management.ts | 265 +++ .../api-keys/security-notice.tsx | 21 + 7 files changed, 924 insertions(+), 1544 deletions(-) create mode 100644 app/src/components/views/settings-view/api-keys/api-key-field.tsx create mode 100644 app/src/components/views/settings-view/api-keys/api-keys-section.tsx create mode 100644 app/src/components/views/settings-view/api-keys/authentication-status-display.tsx create mode 100644 app/src/components/views/settings-view/api-keys/config/api-provider-config.ts create mode 100644 app/src/components/views/settings-view/api-keys/hooks/use-api-key-management.ts create mode 100644 app/src/components/views/settings-view/api-keys/security-notice.tsx diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index a6236ade..881df19f 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -2,45 +2,21 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { useAppStore } from "@/store/app-store"; +import { useSetupStore } from "@/store/setup-store"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { Settings, Key, - Eye, - EyeOff, - CheckCircle2, - AlertCircle, - Loader2, - Zap, - Sun, - Moon, - Palette, - Terminal, - Ghost, - Snowflake, - Flame, - Sparkles, - Eclipse, - Trees, - Cat, - Atom, - Radio, - LayoutGrid, - Minimize2, - Square, - Maximize2, - FlaskConical, + Keyboard, Trash2, Folder, - GitBranch, - TestTube, + Terminal, + Atom, + Palette, + LayoutGrid, Settings2, - RefreshCw, - Info, - Keyboard, + FlaskConical, } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; import { @@ -51,9 +27,18 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { useSetupStore, type ClaudeAuthStatus, type CodexAuthStatus } from "@/store/setup-store"; import { KeyboardMap, ShortcutReferencePanel } from "@/components/ui/keyboard-map"; -import { Checkbox } from "../ui/checkbox"; +// Import extracted sections +import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section"; +import { + ClaudeCliStatus, + CodexCliStatus, +} from "./settings-view/cli-status-section"; +import { AppearanceSection } from "./settings-view/appearance-section"; +import { KanbanDisplaySection } from "./settings-view/kanban-display-section"; +import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts-section"; +import { FeatureDefaultsSection } from "./settings-view/feature-defaults-section"; +import { DangerZoneSection } from "./settings-view/danger-zone-section"; // Navigation items for the side panel const NAV_ITEMS = [ @@ -69,8 +54,6 @@ const NAV_ITEMS = [ export function SettingsView() { const { - apiKeys, - setApiKeys, setCurrentView, theme, setTheme, @@ -87,6 +70,9 @@ export function SettingsView() { moveProjectToTrash, } = useAppStore(); + const { claudeAuthStatus, codexAuthStatus, setClaudeAuthStatus, setCodexAuthStatus } = + useSetupStore(); + // Compute the effective theme for the current project const effectiveTheme = currentProject?.theme || theme; @@ -98,24 +84,7 @@ export function SettingsView() { setTheme(newTheme); } }; - const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic); - const [googleKey, setGoogleKey] = useState(apiKeys.google); - const [openaiKey, setOpenaiKey] = useState(apiKeys.openai); - const [showAnthropicKey, setShowAnthropicKey] = useState(false); - const [showGoogleKey, setShowGoogleKey] = useState(false); - const [showOpenaiKey, setShowOpenaiKey] = useState(false); - const [saved, setSaved] = useState(false); - const [testingConnection, setTestingConnection] = useState(false); - const [testResult, setTestResult] = useState<{ - success: boolean; - message: string; - } | null>(null); - const [testingGeminiConnection, setTestingGeminiConnection] = useState(false); - const [geminiTestResult, setGeminiTestResult] = useState<{ - success: boolean; - message: string; - } | null>(null); const [claudeCliStatus, setClaudeCliStatus] = useState<{ success: boolean; status?: string; @@ -131,6 +100,7 @@ export function SettingsView() { }; error?: string; } | null>(null); + const [codexCliStatus, setCodexCliStatus] = useState<{ success: boolean; status?: string; @@ -147,31 +117,13 @@ export function SettingsView() { }; error?: string; } | null>(null); - const [testingOpenaiConnection, setTestingOpenaiConnection] = useState(false); - const [openaiTestResult, setOpenaiTestResult] = useState<{ - success: boolean; - message: string; - } | null>(null); + const [activeSection, setActiveSection] = useState("api-keys"); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false); const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false); const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false); - const [apiKeyStatus, setApiKeyStatus] = useState<{ - hasAnthropicKey: boolean; - hasOpenAIKey: boolean; - hasGoogleKey: boolean; - } | null>(null); const scrollContainerRef = useRef(null); - - // Get authentication status from setup store - const { claudeAuthStatus, codexAuthStatus, setClaudeAuthStatus, setCodexAuthStatus } = useSetupStore(); - - useEffect(() => { - setAnthropicKey(apiKeys.anthropic); - setGoogleKey(apiKeys.google); - setOpenaiKey(apiKeys.openai); - }, [apiKeys]); useEffect(() => { const checkCliStatus = async () => { @@ -192,21 +144,6 @@ export function SettingsView() { console.error("Failed to check Codex CLI status:", error); } } - // Check API key status from environment - if (api?.setup?.getApiKeys) { - try { - const status = await api.setup.getApiKeys(); - if (status.success) { - setApiKeyStatus({ - hasAnthropicKey: status.hasAnthropicKey, - hasOpenAIKey: status.hasOpenAIKey, - hasGoogleKey: status.hasGoogleKey, - }); - } - } catch (error) { - console.error("Failed to check API key status:", error); - } - } // Check Claude auth status (re-fetch on mount to ensure persistence) if (api?.setup?.getClaudeStatus) { @@ -214,13 +151,14 @@ export function SettingsView() { const result = await api.setup.getClaudeStatus(); if (result.success && result.auth) { const auth = result.auth; - const authStatus: ClaudeAuthStatus = { + const authStatus = { authenticated: auth.authenticated, - method: auth.method === "oauth_token" - ? "oauth" - : auth.method?.includes("api_key") - ? "api_key" - : "none", + method: + auth.method === "oauth_token" + ? "oauth" as const + : auth.method?.includes("api_key") + ? "api_key" as const + : "none" as const, hasCredentialsFile: auth.hasCredentialsFile ?? false, oauthTokenValid: auth.hasStoredOAuthToken, apiKeyValid: auth.hasStoredApiKey || auth.hasEnvApiKey, @@ -239,19 +177,25 @@ export function SettingsView() { if (result.success && result.auth) { const auth = result.auth; // Determine method - prioritize cli_verified and cli_tokens over auth_file - const method = auth.method === "cli_verified" || auth.method === "cli_tokens" - ? auth.method === "cli_verified" ? "cli_verified" : "cli_tokens" - : auth.method === "auth_file" - ? "api_key" - : auth.method === "env_var" - ? "env" - : "none"; - - const authStatus: CodexAuthStatus = { + const method = + auth.method === "cli_verified" || auth.method === "cli_tokens" + ? auth.method === "cli_verified" + ? "cli_verified" as const + : "cli_tokens" as const + : auth.method === "auth_file" + ? "api_key" as const + : auth.method === "env_var" + ? "env" as const + : "none" as const; + + const authStatus = { authenticated: auth.authenticated, method, // Only set apiKeyValid for actual API key methods, not CLI login - apiKeyValid: method === "cli_verified" || method === "cli_tokens" ? undefined : (auth.hasAuthFile || auth.hasEnvKey), + apiKeyValid: + method === "cli_verified" || method === "cli_tokens" + ? undefined + : auth.hasAuthFile || auth.hasEnvKey, }; setCodexAuthStatus(authStatus); } @@ -325,133 +269,6 @@ export function SettingsView() { } }, []); - const handleTestConnection = async () => { - setTestingConnection(true); - setTestResult(null); - - try { - const response = await fetch("/api/claude/test", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ apiKey: anthropicKey }), - }); - - const data = await response.json(); - - if (response.ok && data.success) { - setTestResult({ - success: true, - message: data.message || "Connection successful! Claude responded.", - }); - } else { - setTestResult({ - success: false, - message: data.error || "Failed to connect to Claude API.", - }); - } - } catch { - setTestResult({ - success: false, - message: "Network error. Please check your connection.", - }); - } finally { - setTestingConnection(false); - } - }; - - const handleTestGeminiConnection = async () => { - setTestingGeminiConnection(true); - setGeminiTestResult(null); - - try { - const response = await fetch("/api/gemini/test", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ apiKey: googleKey }), - }); - - const data = await response.json(); - - if (response.ok && data.success) { - setGeminiTestResult({ - success: true, - message: data.message || "Connection successful! Gemini responded.", - }); - } else { - setGeminiTestResult({ - success: false, - message: data.error || "Failed to connect to Gemini API.", - }); - } - } catch { - setGeminiTestResult({ - success: false, - message: "Network error. Please check your connection.", - }); - } finally { - setTestingGeminiConnection(false); - } - }; - - const handleTestOpenaiConnection = async () => { - setTestingOpenaiConnection(true); - setOpenaiTestResult(null); - - try { - const api = getElectronAPI(); - if (api?.testOpenAIConnection) { - const result = await api.testOpenAIConnection(openaiKey); - if (result.success) { - setOpenaiTestResult({ - success: true, - message: - result.message || "Connection successful! OpenAI API responded.", - }); - } else { - setOpenaiTestResult({ - success: false, - message: result.error || "Failed to connect to OpenAI API.", - }); - } - } else { - // Fallback to web API test - const response = await fetch("/api/openai/test", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ apiKey: openaiKey }), - }); - - const data = await response.json(); - - if (response.ok && data.success) { - setOpenaiTestResult({ - success: true, - message: - data.message || "Connection successful! OpenAI API responded.", - }); - } else { - setOpenaiTestResult({ - success: false, - message: data.error || "Failed to connect to OpenAI API.", - }); - } - } - } catch { - setOpenaiTestResult({ - success: false, - message: "Network error. Please check your connection.", - }); - } finally { - setTestingOpenaiConnection(false); - } - }; - const handleRefreshClaudeCli = useCallback(async () => { setIsCheckingClaudeCli(true); try { @@ -482,16 +299,6 @@ export function SettingsView() { } }, []); - const handleSave = () => { - setApiKeys({ - anthropic: anthropicKey, - google: googleKey, - openai: openaiKey, - }); - setSaved(true); - setTimeout(() => setSaved(false), 2000); - }; - return (
{/* API Keys Section */} -
-
-
- -

- API Keys -

-
-

- Configure your AI provider API keys. Keys are stored locally - in your browser. -

-
-
- {/* Claude/Anthropic API Key */} -
-
- - {apiKeys.anthropic && ( - - )} -
-
-
- setAnthropicKey(e.target.value)} - placeholder="sk-ant-..." - className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground" - data-testid="anthropic-api-key-input" - /> - -
- -
-

- Used for Claude AI features. Get your key at{" "} - - console.anthropic.com - - . Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment - variable can be used. -

- {testResult && ( -
- {testResult.success ? ( - - ) : ( - - )} - - {testResult.message} - -
- )} -
- - {/* Google API Key */} -
-
- - {apiKeys.google && ( - - )} -
-
-
- setGoogleKey(e.target.value)} - placeholder="AIza..." - className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground" - data-testid="google-api-key-input" - /> - -
- -
-

- Used for Gemini AI features (including image/design - prompts). Get your key at{" "} - - makersuite.google.com - -

- {geminiTestResult && ( -
- {geminiTestResult.success ? ( - - ) : ( - - )} - - {geminiTestResult.message} - -
- )} -
- - {/* OpenAI API Key */} -
-
- - {apiKeys.openai && ( - - )} -
-
-
- setOpenaiKey(e.target.value)} - placeholder="sk-..." - className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground" - data-testid="openai-api-key-input" - /> - -
- -
-

- Used for OpenAI Codex CLI and GPT models. Get your key at{" "} - - platform.openai.com - -

- {openaiTestResult && ( -
- {openaiTestResult.success ? ( - - ) : ( - - )} - - {openaiTestResult.message} - -
- )} -
- - {/* Authentication Status Display */} -
-
- - -
- -
- {/* Claude Authentication Status */} -
-
- - - Claude (Anthropic) - -
-
- {claudeAuthStatus?.authenticated ? ( - <> -
- - - Method:{" "} - - {claudeAuthStatus.method === "oauth" - ? "OAuth Token" - : claudeAuthStatus.method === "api_key" - ? "API Key" - : "Unknown"} - - -
- {claudeAuthStatus.oauthTokenValid && ( -
- - OAuth token configured -
- )} - {claudeAuthStatus.apiKeyValid && ( -
- - API key configured -
- )} - {apiKeyStatus?.hasAnthropicKey && ( -
- - Environment variable detected -
- )} - {apiKeys.anthropic && ( -
- - Manual API key in settings -
- )} - - ) : apiKeyStatus?.hasAnthropicKey ? ( -
- - Using environment variable (ANTHROPIC_API_KEY) -
- ) : apiKeys.anthropic ? ( -
- - Using manual API key from settings -
- ) : ( -
- - Not Setup -
- )} -
-
- - {/* Codex/OpenAI Authentication Status */} -
-
- - - Codex (OpenAI) - -
-
- {codexAuthStatus?.authenticated ? ( - <> -
- - - Method:{" "} - - {codexAuthStatus.method === "cli_verified" || codexAuthStatus.method === "cli_tokens" - ? "CLI Login (OpenAI Account)" - : codexAuthStatus.method === "api_key" - ? "API Key (Auth File)" - : codexAuthStatus.method === "env" - ? "API Key (Environment)" - : "Unknown"} - - -
- {codexAuthStatus.method === "cli_verified" || codexAuthStatus.method === "cli_tokens" ? ( -
- - Account authenticated -
- ) : codexAuthStatus.apiKeyValid ? ( -
- - API key configured -
- ) : null} - {apiKeyStatus?.hasOpenAIKey && ( -
- - Environment variable detected -
- )} - {apiKeys.openai && ( -
- - Manual API key in settings -
- )} - - ) : apiKeyStatus?.hasOpenAIKey ? ( -
- - Using environment variable (OPENAI_API_KEY) -
- ) : apiKeys.openai ? ( -
- - Using manual API key from settings -
- ) : ( -
- - Not Setup -
- )} -
-
- - {/* Google/Gemini Authentication Status */} -
-
- - - Gemini (Google) - -
-
- {apiKeyStatus?.hasGoogleKey ? ( -
- - Using environment variable (GOOGLE_API_KEY) -
- ) : apiKeys.google ? ( -
- - Using manual API key from settings -
- ) : ( -
- - Not Setup -
- )} -
-
-
-
- - {/* Security Notice */} -
- -
-

- Security Notice -

-

- API keys are stored in your browser's local storage. - Never share your API keys or commit them to version - control. -

-
-
-
-
+ {/* Claude CLI Status Section */} {claudeCliStatus && ( -
-
-
-
- -

- Claude Code CLI -

-
- -
-

- Claude Code CLI provides better performance for long-running - tasks, especially with ultrathink. -

-
-
- {claudeCliStatus.success && - claudeCliStatus.status === "installed" ? ( -
-
- -
-

- Claude Code CLI Installed -

-
- {claudeCliStatus.method && ( -

- Method:{" "} - - {claudeCliStatus.method} - -

- )} - {claudeCliStatus.version && ( -

- Version:{" "} - - {claudeCliStatus.version} - -

- )} - {claudeCliStatus.path && ( -

- Path:{" "} - - {claudeCliStatus.path} - -

- )} -
-
-
- {claudeCliStatus.recommendation && ( -

- {claudeCliStatus.recommendation} -

- )} -
- ) : ( -
-
- -
-

- Claude Code CLI Not Detected -

-

- {claudeCliStatus.recommendation || - "Consider installing Claude Code CLI for optimal performance with ultrathink."} -

-
-
- {claudeCliStatus.installCommands && ( -
-

- Installation Commands: -

-
- {claudeCliStatus.installCommands.npm && ( -
-

- npm: -

- - {claudeCliStatus.installCommands.npm} - -
- )} - {claudeCliStatus.installCommands.macos && ( -
-

- macOS/Linux: -

- - {claudeCliStatus.installCommands.macos} - -
- )} - {claudeCliStatus.installCommands.windows && ( -
-

- Windows (PowerShell): -

- - {claudeCliStatus.installCommands.windows} - -
- )} -
-
- )} -
- )} -
-
+ )} {/* Codex CLI Status Section */} {codexCliStatus && ( -
-
-
-
- -

- OpenAI Codex CLI -

-
- -
-

- Codex CLI enables GPT-5.1 Codex models for autonomous coding - tasks. -

-
-
- {codexCliStatus.success && - codexCliStatus.status === "installed" ? ( -
-
- -
-

- Codex CLI Installed -

-
- {codexCliStatus.method && ( -

- Method:{" "} - - {codexCliStatus.method} - -

- )} - {codexCliStatus.version && ( -

- Version:{" "} - - {codexCliStatus.version} - -

- )} - {codexCliStatus.path && ( -

- Path:{" "} - - {codexCliStatus.path} - -

- )} -
-
-
- {codexCliStatus.recommendation && ( -

- {codexCliStatus.recommendation} -

- )} -
- ) : codexCliStatus.status === "api_key_only" ? ( -
-
- -
-

- API Key Detected - CLI Not Installed -

-

- {codexCliStatus.recommendation || - "OPENAI_API_KEY found but Codex CLI not installed. Install the CLI for full agentic capabilities."} -

-
-
- {codexCliStatus.installCommands && ( -
-

- Installation Commands: -

-
- {codexCliStatus.installCommands.npm && ( -
-

- npm: -

- - {codexCliStatus.installCommands.npm} - -
- )} -
-
- )} -
- ) : ( -
-
- -
-

- Codex CLI Not Detected -

-

- {codexCliStatus.recommendation || - "Install OpenAI Codex CLI to use GPT-5.1 Codex models for autonomous coding."} -

-
-
- {codexCliStatus.installCommands && ( -
-

- Installation Commands: -

-
- {codexCliStatus.installCommands.npm && ( -
-

- npm: -

- - {codexCliStatus.installCommands.npm} - -
- )} - {codexCliStatus.installCommands.macos && ( -
-

- macOS (Homebrew): -

- - {codexCliStatus.installCommands.macos} - -
- )} -
-
- )} -
- )} -
-
+ )} {/* Appearance Section */} -
-
-
- -

- Appearance -

-
-

- Customize the look and feel of your application. -

-
-
-
- -
- - - - - - - - - - - - -
-
-
-
+ {/* Kanban Card Display Section */} -
-
-
- -

- Kanban Card Display -

-
-

- Control how much information is displayed on Kanban cards. -

-
-
-
- -
- - - -
-

- Minimal: Shows only title and category -
- Standard: Adds steps preview and progress - bar -
- Detailed: Shows all info including model, - tool calls, task list, and summaries -

-
-
-
+ {/* Keyboard Shortcuts Section */} -
-
-
- -

- Keyboard Shortcuts -

-
-

- Customize keyboard shortcuts for navigation and actions using the visual keyboard map. -

-
-
- {/* Centered message directing to keyboard map */} -
-
- -
-
-
-

- Use the Visual Keyboard Map -

-

- Click the "View Keyboard Map" button above to customize your keyboard shortcuts. - The visual interface shows all available keys and lets you easily edit shortcuts with - single-modifier restrictions. -

-
- -
-
-
+ setShowKeyboardMapDialog(true)} + /> {/* Feature Defaults Section */} -
-
-
- -

- Feature Defaults -

-
-

- Configure default settings for new features. -

-
-
- {/* Profiles Only Setting */} -
-
- - setShowProfilesOnly(checked === true) - } - className="mt-0.5" - data-testid="show-profiles-only-checkbox" - /> -
- -

- When enabled, the Add Feature dialog will show only AI - profiles and hide advanced model tweaking options - (Claude SDK, thinking levels, and OpenAI Codex CLI). - This creates a cleaner, less overwhelming UI. You can - always disable this to access advanced settings. -

-
-
-
+ - {/* Separator */} -
- - {/* Skip Tests Setting */} -
-
- - setDefaultSkipTests(checked === true) - } - className="mt-0.5" - data-testid="default-skip-tests-checkbox" - /> -
- -

- When enabled, new features will default to manual - verification instead of TDD (test-driven development). - You can still override this for individual features. -

-
-
-
- - {/* Worktree Isolation Setting */} -
-
- - setUseWorktrees(checked === true) - } - className="mt-0.5" - data-testid="use-worktrees-checkbox" - /> -
- -

- Creates isolated git branches for each feature. When - disabled, agents work directly in the main project - directory. This feature is experimental and may require - additional setup like branch selection and merge - configuration. -

-
-
-
-
-
- - {/* Delete Project Section - Only show when a project is selected */} - {currentProject && ( -
-
-
- -

- Danger Zone -

-
-

- Permanently remove this project from Automaker. -

-
-
-
-
-
- -
-
-

- {currentProject.name} -

-

- {currentProject.path} -

-
-
- -
-
-
- )} + {/* Danger Zone Section - Only show when a project is selected */} + setShowDeleteDialog(true)} + /> {/* Save Button */}
-
{/* Keyboard Map Dialog */} - + @@ -1896,8 +440,8 @@ export function SettingsView() { Keyboard Shortcut Map - Visual overview of all keyboard shortcuts. Keys in color are bound to shortcuts. - Click on any shortcut below to edit it. + Visual overview of all keyboard shortcuts. Keys in color are bound + to shortcuts. Click on any shortcut below to edit it. diff --git a/app/src/components/views/settings-view/api-keys/api-key-field.tsx b/app/src/components/views/settings-view/api-keys/api-key-field.tsx new file mode 100644 index 00000000..b62f4d9a --- /dev/null +++ b/app/src/components/views/settings-view/api-keys/api-key-field.tsx @@ -0,0 +1,117 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { AlertCircle, CheckCircle2, Eye, EyeOff, Loader2, Zap } from "lucide-react"; +import type { ProviderConfig } from "./shared/api-provider-config"; + +interface ApiKeyFieldProps { + config: ProviderConfig; +} + +export function ApiKeyField({ config }: ApiKeyFieldProps) { + const { + label, + inputId, + placeholder, + value, + setValue, + showValue, + setShowValue, + hasStoredKey, + inputTestId, + toggleTestId, + testButton, + result, + resultTestId, + resultMessageTestId, + descriptionPrefix, + descriptionLinkHref, + descriptionLinkText, + descriptionSuffix, + } = config; + + return ( +
+
+ + {hasStoredKey && } +
+
+
+ setValue(e.target.value)} + placeholder={placeholder} + className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground" + data-testid={inputTestId} + /> + +
+ +
+

+ {descriptionPrefix}{" "} + + {descriptionLinkText} + + {descriptionSuffix} +

+ {result && ( +
+ {result.success ? ( + + ) : ( + + )} + + {result.message} + +
+ )} +
+ ); +} diff --git a/app/src/components/views/settings-view/api-keys/api-keys-section.tsx b/app/src/components/views/settings-view/api-keys/api-keys-section.tsx new file mode 100644 index 00000000..8e6711e5 --- /dev/null +++ b/app/src/components/views/settings-view/api-keys/api-keys-section.tsx @@ -0,0 +1,72 @@ +import { useAppStore } from "@/store/app-store"; +import { useSetupStore } from "@/store/setup-store"; +import { Button } from "@/components/ui/button"; +import { Key, CheckCircle2 } from "lucide-react"; +import { ApiKeyField } from "./api-key-field"; +import { buildProviderConfigs } from "./shared/api-provider-config"; +import { AuthenticationStatusDisplay } from "./authentication-status-display"; +import { SecurityNotice } from "./security-notice"; +import { useApiKeyManagement } from "./hooks/use-api-key-management"; + +export function ApiKeysSection() { + const { apiKeys } = useAppStore(); + const { claudeAuthStatus, codexAuthStatus } = useSetupStore(); + + const { providerConfigParams, apiKeyStatus, handleSave, saved } = + useApiKeyManagement(); + + const providerConfigs = buildProviderConfigs(providerConfigParams); + + return ( +
+
+
+ +

API Keys

+
+

+ Configure your AI provider API keys. Keys are stored locally in your + browser. +

+
+
+ {/* API Key Fields */} + {providerConfigs.map((provider) => ( + + ))} + + {/* Authentication Status Display */} + + + {/* Security Notice */} + + + {/* Save Button */} +
+ +
+
+
+ ); +} diff --git a/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx b/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx new file mode 100644 index 00000000..515db7c7 --- /dev/null +++ b/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx @@ -0,0 +1,212 @@ +import { Label } from "@/components/ui/label"; +import { + CheckCircle2, + AlertCircle, + Info, + Terminal, + Atom, + Sparkles, +} from "lucide-react"; +import type { ClaudeAuthStatus, CodexAuthStatus } from "@/store/setup-store"; + +interface AuthenticationStatusDisplayProps { + claudeAuthStatus: ClaudeAuthStatus | null; + codexAuthStatus: CodexAuthStatus | null; + apiKeyStatus: { + hasAnthropicKey: boolean; + hasOpenAIKey: boolean; + hasGoogleKey: boolean; + } | null; + apiKeys: { + anthropic: string; + google: string; + openai: string; + }; +} + +export function AuthenticationStatusDisplay({ + claudeAuthStatus, + codexAuthStatus, + apiKeyStatus, + apiKeys, +}: AuthenticationStatusDisplayProps) { + return ( +
+
+ + +
+ +
+ {/* Claude Authentication Status */} +
+
+ + + Claude (Anthropic) + +
+
+ {claudeAuthStatus?.authenticated ? ( + <> +
+ + + Method:{" "} + + {claudeAuthStatus.method === "oauth" + ? "OAuth Token" + : claudeAuthStatus.method === "api_key" + ? "API Key" + : "Unknown"} + + +
+ {claudeAuthStatus.oauthTokenValid && ( +
+ + OAuth token configured +
+ )} + {claudeAuthStatus.apiKeyValid && ( +
+ + API key configured +
+ )} + {apiKeyStatus?.hasAnthropicKey && ( +
+ + Environment variable detected +
+ )} + {apiKeys.anthropic && ( +
+ + Manual API key in settings +
+ )} + + ) : apiKeyStatus?.hasAnthropicKey ? ( +
+ + Using environment variable (ANTHROPIC_API_KEY) +
+ ) : apiKeys.anthropic ? ( +
+ + Using manual API key from settings +
+ ) : ( +
+ + Not Setup +
+ )} +
+
+ + {/* Codex/OpenAI Authentication Status */} +
+
+ + + Codex (OpenAI) + +
+
+ {codexAuthStatus?.authenticated ? ( + <> +
+ + + Method:{" "} + + {codexAuthStatus.method === "cli_verified" || + codexAuthStatus.method === "cli_tokens" + ? "CLI Login (OpenAI Account)" + : codexAuthStatus.method === "api_key" + ? "API Key (Auth File)" + : codexAuthStatus.method === "env" + ? "API Key (Environment)" + : "Unknown"} + + +
+ {codexAuthStatus.method === "cli_verified" || + codexAuthStatus.method === "cli_tokens" ? ( +
+ + Account authenticated +
+ ) : codexAuthStatus.apiKeyValid ? ( +
+ + API key configured +
+ ) : null} + {apiKeyStatus?.hasOpenAIKey && ( +
+ + Environment variable detected +
+ )} + {apiKeys.openai && ( +
+ + Manual API key in settings +
+ )} + + ) : apiKeyStatus?.hasOpenAIKey ? ( +
+ + Using environment variable (OPENAI_API_KEY) +
+ ) : apiKeys.openai ? ( +
+ + Using manual API key from settings +
+ ) : ( +
+ + Not Setup +
+ )} +
+
+ + {/* Google/Gemini Authentication Status */} +
+
+ + + Gemini (Google) + +
+
+ {apiKeyStatus?.hasGoogleKey ? ( +
+ + Using environment variable (GOOGLE_API_KEY) +
+ ) : apiKeys.google ? ( +
+ + Using manual API key from settings +
+ ) : ( +
+ + Not Setup +
+ )} +
+
+
+
+ ); +} diff --git a/app/src/components/views/settings-view/api-keys/config/api-provider-config.ts b/app/src/components/views/settings-view/api-keys/config/api-provider-config.ts new file mode 100644 index 00000000..b0a9bf78 --- /dev/null +++ b/app/src/components/views/settings-view/api-keys/config/api-provider-config.ts @@ -0,0 +1,149 @@ +import type { Dispatch, SetStateAction } from "react"; +import type { LucideIcon } from "lucide-react"; +import type { ApiKeys } from "@/store/app-store"; + +export type ProviderKey = "anthropic" | "google" | "openai"; + +export interface ProviderConfig { + key: ProviderKey; + label: string; + inputId: string; + placeholder: string; + value: string; + setValue: Dispatch>; + showValue: boolean; + setShowValue: Dispatch>; + hasStoredKey: string | null | undefined; + inputTestId: string; + toggleTestId: string; + testButton: { + onClick: () => Promise | void; + disabled: boolean; + loading: boolean; + testId: string; + }; + result: { success: boolean; message: string } | null; + resultTestId: string; + resultMessageTestId: string; + descriptionPrefix: string; + descriptionLinkHref: string; + descriptionLinkText: string; + descriptionSuffix?: string; +} + +export interface ProviderConfigParams { + apiKeys: ApiKeys; + anthropic: { + value: string; + setValue: Dispatch>; + show: boolean; + setShow: Dispatch>; + testing: boolean; + onTest: () => Promise; + result: { success: boolean; message: string } | null; + }; + google: { + value: string; + setValue: Dispatch>; + show: boolean; + setShow: Dispatch>; + testing: boolean; + onTest: () => Promise; + result: { success: boolean; message: string } | null; + }; + openai: { + value: string; + setValue: Dispatch>; + show: boolean; + setShow: Dispatch>; + testing: boolean; + onTest: () => Promise; + result: { success: boolean; message: string } | null; + }; +} + +export const buildProviderConfigs = ({ + apiKeys, + anthropic, + google, + openai, +}: ProviderConfigParams): ProviderConfig[] => [ + { + key: "anthropic", + label: "Anthropic API Key (Claude)", + inputId: "anthropic-key", + placeholder: "sk-ant-...", + value: anthropic.value, + setValue: anthropic.setValue, + showValue: anthropic.show, + setShowValue: anthropic.setShow, + hasStoredKey: apiKeys.anthropic, + inputTestId: "anthropic-api-key-input", + toggleTestId: "toggle-anthropic-visibility", + testButton: { + onClick: anthropic.onTest, + disabled: !anthropic.value || anthropic.testing, + loading: anthropic.testing, + testId: "test-claude-connection", + }, + result: anthropic.result, + resultTestId: "test-connection-result", + resultMessageTestId: "test-connection-message", + descriptionPrefix: "Used for Claude AI features. Get your key at", + descriptionLinkHref: "https://console.anthropic.com/account/keys", + descriptionLinkText: "console.anthropic.com", + descriptionSuffix: + ". Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment variable can be used.", + }, + { + key: "google", + label: "Google API Key (Gemini)", + inputId: "google-key", + placeholder: "AIza...", + value: google.value, + setValue: google.setValue, + showValue: google.show, + setShowValue: google.setShow, + hasStoredKey: apiKeys.google, + inputTestId: "google-api-key-input", + toggleTestId: "toggle-google-visibility", + testButton: { + onClick: google.onTest, + disabled: !google.value || google.testing, + loading: google.testing, + testId: "test-gemini-connection", + }, + result: google.result, + resultTestId: "gemini-test-connection-result", + resultMessageTestId: "gemini-test-connection-message", + descriptionPrefix: + "Used for Gemini AI features (including image/design prompts). Get your key at", + descriptionLinkHref: "https://makersuite.google.com/app/apikey", + descriptionLinkText: "makersuite.google.com", + }, + { + key: "openai", + label: "OpenAI API Key (Codex/GPT)", + inputId: "openai-key", + placeholder: "sk-...", + value: openai.value, + setValue: openai.setValue, + showValue: openai.show, + setShowValue: openai.setShow, + hasStoredKey: apiKeys.openai, + inputTestId: "openai-api-key-input", + toggleTestId: "toggle-openai-visibility", + testButton: { + onClick: openai.onTest, + disabled: !openai.value || openai.testing, + loading: openai.testing, + testId: "test-openai-connection", + }, + result: openai.result, + resultTestId: "openai-test-connection-result", + resultMessageTestId: "openai-test-connection-message", + descriptionPrefix: "Used for OpenAI Codex CLI and GPT models. Get your key at", + descriptionLinkHref: "https://platform.openai.com/api-keys", + descriptionLinkText: "platform.openai.com", + }, +]; diff --git a/app/src/components/views/settings-view/api-keys/hooks/use-api-key-management.ts b/app/src/components/views/settings-view/api-keys/hooks/use-api-key-management.ts new file mode 100644 index 00000000..6b284540 --- /dev/null +++ b/app/src/components/views/settings-view/api-keys/hooks/use-api-key-management.ts @@ -0,0 +1,265 @@ +import { useState, useEffect } from "react"; +import { useAppStore } from "@/store/app-store"; +import { getElectronAPI } from "@/lib/electron"; +import type { ProviderConfigParams } from "../config/api-provider-config"; + +interface TestResult { + success: boolean; + message: string; +} + +interface ApiKeyStatus { + hasAnthropicKey: boolean; + hasOpenAIKey: boolean; + hasGoogleKey: boolean; +} + +/** + * Custom hook for managing API key state and operations + * Handles input values, visibility toggles, connection testing, and saving + */ +export function useApiKeyManagement() { + const { apiKeys, setApiKeys } = useAppStore(); + + // API key values + const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic); + const [googleKey, setGoogleKey] = useState(apiKeys.google); + const [openaiKey, setOpenaiKey] = useState(apiKeys.openai); + + // Visibility toggles + const [showAnthropicKey, setShowAnthropicKey] = useState(false); + const [showGoogleKey, setShowGoogleKey] = useState(false); + const [showOpenaiKey, setShowOpenaiKey] = useState(false); + + // Test connection states + const [testingConnection, setTestingConnection] = useState(false); + const [testResult, setTestResult] = useState(null); + const [testingGeminiConnection, setTestingGeminiConnection] = useState(false); + const [geminiTestResult, setGeminiTestResult] = useState( + null + ); + const [testingOpenaiConnection, setTestingOpenaiConnection] = useState(false); + const [openaiTestResult, setOpenaiTestResult] = useState( + null + ); + + // API key status from environment + const [apiKeyStatus, setApiKeyStatus] = useState(null); + + // Save state + const [saved, setSaved] = useState(false); + + // Sync local state with store + useEffect(() => { + setAnthropicKey(apiKeys.anthropic); + setGoogleKey(apiKeys.google); + setOpenaiKey(apiKeys.openai); + }, [apiKeys]); + + // Check API key status from environment on mount + useEffect(() => { + const checkApiKeyStatus = async () => { + const api = getElectronAPI(); + if (api?.setup?.getApiKeys) { + try { + const status = await api.setup.getApiKeys(); + if (status.success) { + setApiKeyStatus({ + hasAnthropicKey: status.hasAnthropicKey, + hasOpenAIKey: status.hasOpenAIKey, + hasGoogleKey: status.hasGoogleKey, + }); + } + } catch (error) { + console.error("Failed to check API key status:", error); + } + } + }; + checkApiKeyStatus(); + }, []); + + // Test Anthropic/Claude connection + const handleTestAnthropicConnection = async () => { + setTestingConnection(true); + setTestResult(null); + + try { + const response = await fetch("/api/claude/test", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ apiKey: anthropicKey }), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + setTestResult({ + success: true, + message: data.message || "Connection successful! Claude responded.", + }); + } else { + setTestResult({ + success: false, + message: data.error || "Failed to connect to Claude API.", + }); + } + } catch { + setTestResult({ + success: false, + message: "Network error. Please check your connection.", + }); + } finally { + setTestingConnection(false); + } + }; + + // Test Google/Gemini connection + const handleTestGeminiConnection = async () => { + setTestingGeminiConnection(true); + setGeminiTestResult(null); + + try { + const response = await fetch("/api/gemini/test", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ apiKey: googleKey }), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + setGeminiTestResult({ + success: true, + message: data.message || "Connection successful! Gemini responded.", + }); + } else { + setGeminiTestResult({ + success: false, + message: data.error || "Failed to connect to Gemini API.", + }); + } + } catch { + setGeminiTestResult({ + success: false, + message: "Network error. Please check your connection.", + }); + } finally { + setTestingGeminiConnection(false); + } + }; + + // Test OpenAI connection + const handleTestOpenaiConnection = async () => { + setTestingOpenaiConnection(true); + setOpenaiTestResult(null); + + try { + const api = getElectronAPI(); + if (api?.testOpenAIConnection) { + const result = await api.testOpenAIConnection(openaiKey); + if (result.success) { + setOpenaiTestResult({ + success: true, + message: + result.message || "Connection successful! OpenAI API responded.", + }); + } else { + setOpenaiTestResult({ + success: false, + message: result.error || "Failed to connect to OpenAI API.", + }); + } + } else { + // Fallback to web API test + const response = await fetch("/api/openai/test", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ apiKey: openaiKey }), + }); + + const data = await response.json(); + + if (response.ok && data.success) { + setOpenaiTestResult({ + success: true, + message: + data.message || "Connection successful! OpenAI API responded.", + }); + } else { + setOpenaiTestResult({ + success: false, + message: data.error || "Failed to connect to OpenAI API.", + }); + } + } + } catch { + setOpenaiTestResult({ + success: false, + message: "Network error. Please check your connection.", + }); + } finally { + setTestingOpenaiConnection(false); + } + }; + + // Save API keys + const handleSave = () => { + setApiKeys({ + anthropic: anthropicKey, + google: googleKey, + openai: openaiKey, + }); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + }; + + // Build provider config params for buildProviderConfigs + const providerConfigParams: ProviderConfigParams = { + apiKeys, + anthropic: { + value: anthropicKey, + setValue: setAnthropicKey, + show: showAnthropicKey, + setShow: setShowAnthropicKey, + testing: testingConnection, + onTest: handleTestAnthropicConnection, + result: testResult, + }, + google: { + value: googleKey, + setValue: setGoogleKey, + show: showGoogleKey, + setShow: setShowGoogleKey, + testing: testingGeminiConnection, + onTest: handleTestGeminiConnection, + result: geminiTestResult, + }, + openai: { + value: openaiKey, + setValue: setOpenaiKey, + show: showOpenaiKey, + setShow: setShowOpenaiKey, + testing: testingOpenaiConnection, + onTest: handleTestOpenaiConnection, + result: openaiTestResult, + }, + }; + + return { + // Provider config params for buildProviderConfigs + providerConfigParams, + + // API key status from environment + apiKeyStatus, + + // Save handler and state + handleSave, + saved, + }; +} diff --git a/app/src/components/views/settings-view/api-keys/security-notice.tsx b/app/src/components/views/settings-view/api-keys/security-notice.tsx new file mode 100644 index 00000000..0d6357ce --- /dev/null +++ b/app/src/components/views/settings-view/api-keys/security-notice.tsx @@ -0,0 +1,21 @@ +import { AlertCircle } from "lucide-react"; + +interface SecurityNoticeProps { + title?: string; + message?: string; +} + +export function SecurityNotice({ + title = "Security Notice", + message = "API keys are stored in your browser's local storage. Never share your API keys or commit them to version control.", +}: SecurityNoticeProps) { + return ( +
+ +
+

{title}

+

{message}

+
+
+ ); +} From 9af6866a9d0e4b18520ac1458b0367ec4b6c979b Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:24:36 +0100 Subject: [PATCH 07/24] refactor(settings): remove empty hooks directory --- .../settings-view/appearance-section.tsx | 61 ++++ .../settings-view/cli-status-section.tsx | 304 ++++++++++++++++++ .../settings-view/danger-zone-section.tsx | 57 ++++ .../feature-defaults-section.tsx | 134 ++++++++ .../settings-view/kanban-display-section.tsx | 96 ++++++ .../keyboard-shortcuts-section.tsx | 60 ++++ .../settings-view/shared/theme-options.ts | 88 +++++ .../components/views/settings-view/types.ts | 47 +++ 8 files changed, 847 insertions(+) create mode 100644 app/src/components/views/settings-view/appearance-section.tsx create mode 100644 app/src/components/views/settings-view/cli-status-section.tsx create mode 100644 app/src/components/views/settings-view/danger-zone-section.tsx create mode 100644 app/src/components/views/settings-view/feature-defaults-section.tsx create mode 100644 app/src/components/views/settings-view/kanban-display-section.tsx create mode 100644 app/src/components/views/settings-view/keyboard-shortcuts-section.tsx create mode 100644 app/src/components/views/settings-view/shared/theme-options.ts create mode 100644 app/src/components/views/settings-view/types.ts diff --git a/app/src/components/views/settings-view/appearance-section.tsx b/app/src/components/views/settings-view/appearance-section.tsx new file mode 100644 index 00000000..ed60b135 --- /dev/null +++ b/app/src/components/views/settings-view/appearance-section.tsx @@ -0,0 +1,61 @@ +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Palette } from "lucide-react"; +import { themeOptions } from "./shared/theme-options"; +import type { Theme, Project } from "./types"; + +interface AppearanceSectionProps { + effectiveTheme: Theme; + currentProject: Project | null; + onThemeChange: (theme: Theme) => void; +} + +export function AppearanceSection({ + effectiveTheme, + currentProject, + onThemeChange, +}: AppearanceSectionProps) { + return ( +
+
+
+ +

Appearance

+
+

+ Customize the look and feel of your application. +

+
+
+
+ +
+ {themeOptions.map(({ value, label, Icon, testId }) => { + const isActive = effectiveTheme === value; + return ( + + ); + })} +
+
+
+
+ ); +} diff --git a/app/src/components/views/settings-view/cli-status-section.tsx b/app/src/components/views/settings-view/cli-status-section.tsx new file mode 100644 index 00000000..050453b8 --- /dev/null +++ b/app/src/components/views/settings-view/cli-status-section.tsx @@ -0,0 +1,304 @@ +import { Button } from "@/components/ui/button"; +import { + Terminal, + CheckCircle2, + AlertCircle, + RefreshCw, + Atom, +} from "lucide-react"; +import type { CliStatus } from "./types"; + +interface CliStatusProps { + status: CliStatus | null; + isChecking: boolean; + onRefresh: () => void; +} + +export function ClaudeCliStatus({ + status, + isChecking, + onRefresh, +}: CliStatusProps) { + if (!status) return null; + + return ( +
+
+
+
+ +

+ Claude Code CLI +

+
+ +
+

+ Claude Code CLI provides better performance for long-running tasks, + especially with ultrathink. +

+
+
+ {status.success && status.status === "installed" ? ( +
+
+ +
+

+ Claude Code CLI Installed +

+
+ {status.method && ( +

+ Method: {status.method} +

+ )} + {status.version && ( +

+ Version:{" "} + {status.version} +

+ )} + {status.path && ( +

+ Path:{" "} + + {status.path} + +

+ )} +
+
+
+ {status.recommendation && ( +

+ {status.recommendation} +

+ )} +
+ ) : ( +
+
+ +
+

+ Claude Code CLI Not Detected +

+

+ {status.recommendation || + "Consider installing Claude Code CLI for optimal performance with ultrathink."} +

+
+
+ {status.installCommands && ( +
+

+ Installation Commands: +

+
+ {status.installCommands.npm && ( +
+

npm:

+ + {status.installCommands.npm} + +
+ )} + {status.installCommands.macos && ( +
+

+ macOS/Linux: +

+ + {status.installCommands.macos} + +
+ )} + {status.installCommands.windows && ( +
+

+ Windows (PowerShell): +

+ + {status.installCommands.windows} + +
+ )} +
+
+ )} +
+ )} +
+
+ ); +} + +export function CodexCliStatus({ + status, + isChecking, + onRefresh, +}: CliStatusProps) { + if (!status) return null; + + return ( +
+
+
+
+ +

+ OpenAI Codex CLI +

+
+ +
+

+ Codex CLI enables GPT-5.1 Codex models for autonomous coding tasks. +

+
+
+ {status.success && status.status === "installed" ? ( +
+
+ +
+

+ Codex CLI Installed +

+
+ {status.method && ( +

+ Method: {status.method} +

+ )} + {status.version && ( +

+ Version:{" "} + {status.version} +

+ )} + {status.path && ( +

+ Path:{" "} + + {status.path} + +

+ )} +
+
+
+ {status.recommendation && ( +

+ {status.recommendation} +

+ )} +
+ ) : status.status === "api_key_only" ? ( +
+
+ +
+

+ API Key Detected - CLI Not Installed +

+

+ {status.recommendation || + "OPENAI_API_KEY found but Codex CLI not installed. Install the CLI for full agentic capabilities."} +

+
+
+ {status.installCommands && ( +
+

+ Installation Commands: +

+
+ {status.installCommands.npm && ( +
+

npm:

+ + {status.installCommands.npm} + +
+ )} +
+
+ )} +
+ ) : ( +
+
+ +
+

+ Codex CLI Not Detected +

+

+ {status.recommendation || + "Install OpenAI Codex CLI to use GPT-5.1 Codex models for autonomous coding."} +

+
+
+ {status.installCommands && ( +
+

+ Installation Commands: +

+
+ {status.installCommands.npm && ( +
+

npm:

+ + {status.installCommands.npm} + +
+ )} + {status.installCommands.macos && ( +
+

+ macOS (Homebrew): +

+ + {status.installCommands.macos} + +
+ )} +
+
+ )} +
+ )} +
+
+ ); +} diff --git a/app/src/components/views/settings-view/danger-zone-section.tsx b/app/src/components/views/settings-view/danger-zone-section.tsx new file mode 100644 index 00000000..f791d763 --- /dev/null +++ b/app/src/components/views/settings-view/danger-zone-section.tsx @@ -0,0 +1,57 @@ +import { Button } from "@/components/ui/button"; +import { Trash2, Folder } from "lucide-react"; +import type { Project } from "./types"; + +interface DangerZoneSectionProps { + project: Project | null; + onDeleteClick: () => void; +} + +export function DangerZoneSection({ + project, + onDeleteClick, +}: DangerZoneSectionProps) { + if (!project) return null; + + return ( +
+
+
+ +

Danger Zone

+
+

+ Permanently remove this project from Automaker. +

+
+
+
+
+
+ +
+
+

+ {project.name} +

+

+ {project.path} +

+
+
+ +
+
+
+ ); +} diff --git a/app/src/components/views/settings-view/feature-defaults-section.tsx b/app/src/components/views/settings-view/feature-defaults-section.tsx new file mode 100644 index 00000000..e7c78582 --- /dev/null +++ b/app/src/components/views/settings-view/feature-defaults-section.tsx @@ -0,0 +1,134 @@ +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { FlaskConical, Settings2, TestTube, GitBranch } from "lucide-react"; + +interface FeatureDefaultsSectionProps { + showProfilesOnly: boolean; + defaultSkipTests: boolean; + useWorktrees: boolean; + onShowProfilesOnlyChange: (value: boolean) => void; + onDefaultSkipTestsChange: (value: boolean) => void; + onUseWorktreesChange: (value: boolean) => void; +} + +export function FeatureDefaultsSection({ + showProfilesOnly, + defaultSkipTests, + useWorktrees, + onShowProfilesOnlyChange, + onDefaultSkipTestsChange, + onUseWorktreesChange, +}: FeatureDefaultsSectionProps) { + return ( +
+
+
+ +

+ Feature Defaults +

+
+

+ Configure default settings for new features. +

+
+
+ {/* Profiles Only Setting */} +
+
+ + onShowProfilesOnlyChange(checked === true) + } + className="mt-0.5" + data-testid="show-profiles-only-checkbox" + /> +
+ +

+ When enabled, the Add Feature dialog will show only AI profiles + and hide advanced model tweaking options (Claude SDK, thinking + levels, and OpenAI Codex CLI). This creates a cleaner, less + overwhelming UI. You can always disable this to access advanced + settings. +

+
+
+
+ + {/* Separator */} +
+ + {/* Skip Tests Setting */} +
+
+ + onDefaultSkipTestsChange(checked === true) + } + className="mt-0.5" + data-testid="default-skip-tests-checkbox" + /> +
+ +

+ When enabled, new features will default to manual verification + instead of TDD (test-driven development). You can still override + this for individual features. +

+
+
+
+ + {/* Worktree Isolation Setting */} +
+
+ + onUseWorktreesChange(checked === true) + } + className="mt-0.5" + data-testid="use-worktrees-checkbox" + /> +
+ +

+ Creates isolated git branches for each feature. When disabled, + agents work directly in the main project directory. This feature + is experimental and may require additional setup like branch + selection and merge configuration. +

+
+
+
+
+
+ ); +} diff --git a/app/src/components/views/settings-view/kanban-display-section.tsx b/app/src/components/views/settings-view/kanban-display-section.tsx new file mode 100644 index 00000000..0f22ee3c --- /dev/null +++ b/app/src/components/views/settings-view/kanban-display-section.tsx @@ -0,0 +1,96 @@ +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { LayoutGrid, Minimize2, Square, Maximize2 } from "lucide-react"; +import type { KanbanDetailLevel } from "./types"; + +interface KanbanDisplaySectionProps { + detailLevel: KanbanDetailLevel; + onChange: (level: KanbanDetailLevel) => void; +} + +export function KanbanDisplaySection({ + detailLevel, + onChange, +}: KanbanDisplaySectionProps) { + return ( +
+
+
+ +

+ Kanban Card Display +

+
+

+ Control how much information is displayed on Kanban cards. +

+
+
+
+ +
+ + + +
+

+ Minimal: Shows only title and category +
+ Standard: Adds steps preview and progress bar +
+ Detailed: Shows all info including model, tool + calls, task list, and summaries +

+
+
+
+ ); +} diff --git a/app/src/components/views/settings-view/keyboard-shortcuts-section.tsx b/app/src/components/views/settings-view/keyboard-shortcuts-section.tsx new file mode 100644 index 00000000..2baf9320 --- /dev/null +++ b/app/src/components/views/settings-view/keyboard-shortcuts-section.tsx @@ -0,0 +1,60 @@ +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Settings2, Keyboard } from "lucide-react"; + +interface KeyboardShortcutsSectionProps { + onOpenKeyboardMap: () => void; +} + +export function KeyboardShortcutsSection({ + onOpenKeyboardMap, +}: KeyboardShortcutsSectionProps) { + return ( +
+
+
+ +

+ Keyboard Shortcuts +

+
+

+ Customize keyboard shortcuts for navigation and actions using the + visual keyboard map. +

+
+
+ {/* Centered message directing to keyboard map */} +
+
+ +
+
+
+

+ Use the Visual Keyboard Map +

+

+ Click the "View Keyboard Map" button above to customize + your keyboard shortcuts. The visual interface shows all available + keys and lets you easily edit shortcuts with single-modifier + restrictions. +

+
+ +
+
+
+ ); +} diff --git a/app/src/components/views/settings-view/shared/theme-options.ts b/app/src/components/views/settings-view/shared/theme-options.ts new file mode 100644 index 00000000..e59da056 --- /dev/null +++ b/app/src/components/views/settings-view/shared/theme-options.ts @@ -0,0 +1,88 @@ +import { + type LucideIcon, + Atom, + Cat, + Eclipse, + Flame, + Ghost, + Moon, + Radio, + Snowflake, + Sparkles, + Sun, + Terminal, + Trees, +} from "lucide-react"; +import { Theme } from "../types"; + +export interface ThemeOption { + value: Theme; + label: string; + Icon: LucideIcon; + testId: string; +} + +export const themeOptions: ReadonlyArray = [ + { value: "dark", label: "Dark", Icon: Moon, testId: "dark-mode-button" }, + { value: "light", label: "Light", Icon: Sun, testId: "light-mode-button" }, + { + value: "retro", + label: "Retro", + Icon: Terminal, + testId: "retro-mode-button", + }, + { + value: "dracula", + label: "Dracula", + Icon: Ghost, + testId: "dracula-mode-button", + }, + { + value: "nord", + label: "Nord", + Icon: Snowflake, + testId: "nord-mode-button", + }, + { + value: "monokai", + label: "Monokai", + Icon: Flame, + testId: "monokai-mode-button", + }, + { + value: "tokyonight", + label: "Tokyo Night", + Icon: Sparkles, + testId: "tokyonight-mode-button", + }, + { + value: "solarized", + label: "Solarized", + Icon: Eclipse, + testId: "solarized-mode-button", + }, + { + value: "gruvbox", + label: "Gruvbox", + Icon: Trees, + testId: "gruvbox-mode-button", + }, + { + value: "catppuccin", + label: "Catppuccin", + Icon: Cat, + testId: "catppuccin-mode-button", + }, + { + value: "onedark", + label: "One Dark", + Icon: Atom, + testId: "onedark-mode-button", + }, + { + value: "synthwave", + label: "Synthwave", + Icon: Radio, + testId: "synthwave-mode-button", + }, +]; diff --git a/app/src/components/views/settings-view/types.ts b/app/src/components/views/settings-view/types.ts new file mode 100644 index 00000000..e28966a6 --- /dev/null +++ b/app/src/components/views/settings-view/types.ts @@ -0,0 +1,47 @@ +// Shared TypeScript types for settings view components + +export interface CliStatus { + success: boolean; + status?: string; + method?: string; + version?: string; + path?: string; + hasApiKey?: boolean; + recommendation?: string; + installCommands?: { + macos?: string; + windows?: string; + linux?: string; + npm?: string; + }; + error?: string; +} + +export type Theme = + | "dark" + | "light" + | "retro" + | "dracula" + | "nord" + | "monokai" + | "tokyonight" + | "solarized" + | "gruvbox" + | "catppuccin" + | "onedark" + | "synthwave"; + +export type KanbanDetailLevel = "minimal" | "standard" | "detailed"; + +export interface Project { + id: string; + name: string; + path: string; + theme?: Theme; +} + +export interface ApiKeys { + anthropic: string; + google: string; + openai: string; +} From 7e3819da4b8469053e5541343af46a4c95851cd8 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:29:06 +0100 Subject: [PATCH 08/24] refactor(settings): reorganize appearance section into folder - Move appearance-section.tsx into appearance/ folder - Move theme-options.ts into appearance/config/ - Update import paths in appearance-section.tsx - Update settings-view.tsx to import from new location - All TypeScript diagnostics passing - Follows api-keys folder pattern --- app/src/components/views/settings-view.tsx | 2 +- .../appearance/appearance-section.tsx | 61 +++++++++++++ .../appearance/config/theme-options.ts | 88 +++++++++++++++++++ 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 app/src/components/views/settings-view/appearance/appearance-section.tsx create mode 100644 app/src/components/views/settings-view/appearance/config/theme-options.ts diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 881df19f..5ee96fe0 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -34,7 +34,7 @@ import { ClaudeCliStatus, CodexCliStatus, } from "./settings-view/cli-status-section"; -import { AppearanceSection } from "./settings-view/appearance-section"; +import { AppearanceSection } from "./settings-view/appearance/appearance-section"; import { KanbanDisplaySection } from "./settings-view/kanban-display-section"; import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts-section"; import { FeatureDefaultsSection } from "./settings-view/feature-defaults-section"; diff --git a/app/src/components/views/settings-view/appearance/appearance-section.tsx b/app/src/components/views/settings-view/appearance/appearance-section.tsx new file mode 100644 index 00000000..4ccd0c24 --- /dev/null +++ b/app/src/components/views/settings-view/appearance/appearance-section.tsx @@ -0,0 +1,61 @@ +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Palette } from "lucide-react"; +import { themeOptions } from "./config/theme-options"; +import type { Theme, Project } from "../types"; + +interface AppearanceSectionProps { + effectiveTheme: Theme; + currentProject: Project | null; + onThemeChange: (theme: Theme) => void; +} + +export function AppearanceSection({ + effectiveTheme, + currentProject, + onThemeChange, +}: AppearanceSectionProps) { + return ( +
+
+
+ +

Appearance

+
+

+ Customize the look and feel of your application. +

+
+
+
+ +
+ {themeOptions.map(({ value, label, Icon, testId }) => { + const isActive = effectiveTheme === value; + return ( + + ); + })} +
+
+
+
+ ); +} diff --git a/app/src/components/views/settings-view/appearance/config/theme-options.ts b/app/src/components/views/settings-view/appearance/config/theme-options.ts new file mode 100644 index 00000000..e59da056 --- /dev/null +++ b/app/src/components/views/settings-view/appearance/config/theme-options.ts @@ -0,0 +1,88 @@ +import { + type LucideIcon, + Atom, + Cat, + Eclipse, + Flame, + Ghost, + Moon, + Radio, + Snowflake, + Sparkles, + Sun, + Terminal, + Trees, +} from "lucide-react"; +import { Theme } from "../types"; + +export interface ThemeOption { + value: Theme; + label: string; + Icon: LucideIcon; + testId: string; +} + +export const themeOptions: ReadonlyArray = [ + { value: "dark", label: "Dark", Icon: Moon, testId: "dark-mode-button" }, + { value: "light", label: "Light", Icon: Sun, testId: "light-mode-button" }, + { + value: "retro", + label: "Retro", + Icon: Terminal, + testId: "retro-mode-button", + }, + { + value: "dracula", + label: "Dracula", + Icon: Ghost, + testId: "dracula-mode-button", + }, + { + value: "nord", + label: "Nord", + Icon: Snowflake, + testId: "nord-mode-button", + }, + { + value: "monokai", + label: "Monokai", + Icon: Flame, + testId: "monokai-mode-button", + }, + { + value: "tokyonight", + label: "Tokyo Night", + Icon: Sparkles, + testId: "tokyonight-mode-button", + }, + { + value: "solarized", + label: "Solarized", + Icon: Eclipse, + testId: "solarized-mode-button", + }, + { + value: "gruvbox", + label: "Gruvbox", + Icon: Trees, + testId: "gruvbox-mode-button", + }, + { + value: "catppuccin", + label: "Catppuccin", + Icon: Cat, + testId: "catppuccin-mode-button", + }, + { + value: "onedark", + label: "One Dark", + Icon: Atom, + testId: "onedark-mode-button", + }, + { + value: "synthwave", + label: "Synthwave", + Icon: Radio, + testId: "synthwave-mode-button", + }, +]; From bd1ae73bb923e6f755246be765a59b1f193003d9 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:29:16 +0100 Subject: [PATCH 09/24] refactor(settings): remove empty shared directory --- .../settings-view/appearance-section.tsx | 61 ------------- .../settings-view/shared/theme-options.ts | 88 ------------------- 2 files changed, 149 deletions(-) delete mode 100644 app/src/components/views/settings-view/appearance-section.tsx delete mode 100644 app/src/components/views/settings-view/shared/theme-options.ts diff --git a/app/src/components/views/settings-view/appearance-section.tsx b/app/src/components/views/settings-view/appearance-section.tsx deleted file mode 100644 index ed60b135..00000000 --- a/app/src/components/views/settings-view/appearance-section.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Palette } from "lucide-react"; -import { themeOptions } from "./shared/theme-options"; -import type { Theme, Project } from "./types"; - -interface AppearanceSectionProps { - effectiveTheme: Theme; - currentProject: Project | null; - onThemeChange: (theme: Theme) => void; -} - -export function AppearanceSection({ - effectiveTheme, - currentProject, - onThemeChange, -}: AppearanceSectionProps) { - return ( -
-
-
- -

Appearance

-
-

- Customize the look and feel of your application. -

-
-
-
- -
- {themeOptions.map(({ value, label, Icon, testId }) => { - const isActive = effectiveTheme === value; - return ( - - ); - })} -
-
-
-
- ); -} diff --git a/app/src/components/views/settings-view/shared/theme-options.ts b/app/src/components/views/settings-view/shared/theme-options.ts deleted file mode 100644 index e59da056..00000000 --- a/app/src/components/views/settings-view/shared/theme-options.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { - type LucideIcon, - Atom, - Cat, - Eclipse, - Flame, - Ghost, - Moon, - Radio, - Snowflake, - Sparkles, - Sun, - Terminal, - Trees, -} from "lucide-react"; -import { Theme } from "../types"; - -export interface ThemeOption { - value: Theme; - label: string; - Icon: LucideIcon; - testId: string; -} - -export const themeOptions: ReadonlyArray = [ - { value: "dark", label: "Dark", Icon: Moon, testId: "dark-mode-button" }, - { value: "light", label: "Light", Icon: Sun, testId: "light-mode-button" }, - { - value: "retro", - label: "Retro", - Icon: Terminal, - testId: "retro-mode-button", - }, - { - value: "dracula", - label: "Dracula", - Icon: Ghost, - testId: "dracula-mode-button", - }, - { - value: "nord", - label: "Nord", - Icon: Snowflake, - testId: "nord-mode-button", - }, - { - value: "monokai", - label: "Monokai", - Icon: Flame, - testId: "monokai-mode-button", - }, - { - value: "tokyonight", - label: "Tokyo Night", - Icon: Sparkles, - testId: "tokyonight-mode-button", - }, - { - value: "solarized", - label: "Solarized", - Icon: Eclipse, - testId: "solarized-mode-button", - }, - { - value: "gruvbox", - label: "Gruvbox", - Icon: Trees, - testId: "gruvbox-mode-button", - }, - { - value: "catppuccin", - label: "Catppuccin", - Icon: Cat, - testId: "catppuccin-mode-button", - }, - { - value: "onedark", - label: "One Dark", - Icon: Atom, - testId: "onedark-mode-button", - }, - { - value: "synthwave", - label: "Synthwave", - Icon: Radio, - testId: "synthwave-mode-button", - }, -]; From 2afb5a764548136f5a5fbf828add97230672502e Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:32:03 +0100 Subject: [PATCH 10/24] refactor(settings): split CLI status into separate components - Split cli-status-section.tsx into two separate files: - claude-cli-status.tsx (ClaudeCliStatus component) - codex-cli-status.tsx (CodexCliStatus component) - Move both components into cli-status/ folder - Update settings-view.tsx to import from new locations - Update type imports to use ../types - All TypeScript diagnostics passing - Improves modularity and follows one-component-per-file pattern --- app/src/components/views/settings-view.tsx | 6 +- .../cli-status/claude-cli-status.tsx | 148 +++++++++++++++ .../cli-status/codex-cli-status.tsx | 169 ++++++++++++++++++ 3 files changed, 319 insertions(+), 4 deletions(-) create mode 100644 app/src/components/views/settings-view/cli-status/claude-cli-status.tsx create mode 100644 app/src/components/views/settings-view/cli-status/codex-cli-status.tsx diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 5ee96fe0..61b7c7db 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -30,10 +30,8 @@ import { import { KeyboardMap, ShortcutReferencePanel } from "@/components/ui/keyboard-map"; // Import extracted sections import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section"; -import { - ClaudeCliStatus, - CodexCliStatus, -} from "./settings-view/cli-status-section"; +import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status"; +import { CodexCliStatus } from "./settings-view/cli-status/codex-cli-status"; import { AppearanceSection } from "./settings-view/appearance/appearance-section"; import { KanbanDisplaySection } from "./settings-view/kanban-display-section"; import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts-section"; diff --git a/app/src/components/views/settings-view/cli-status/claude-cli-status.tsx b/app/src/components/views/settings-view/cli-status/claude-cli-status.tsx new file mode 100644 index 00000000..b5c4afe6 --- /dev/null +++ b/app/src/components/views/settings-view/cli-status/claude-cli-status.tsx @@ -0,0 +1,148 @@ +import { Button } from "@/components/ui/button"; +import { + Terminal, + CheckCircle2, + AlertCircle, + RefreshCw, +} from "lucide-react"; +import type { CliStatus } from "../types"; + +interface CliStatusProps { + status: CliStatus | null; + isChecking: boolean; + onRefresh: () => void; +} + +export function ClaudeCliStatus({ + status, + isChecking, + onRefresh, +}: CliStatusProps) { + if (!status) return null; + + return ( +
+
+
+
+ +

+ Claude Code CLI +

+
+ +
+

+ Claude Code CLI provides better performance for long-running tasks, + especially with ultrathink. +

+
+
+ {status.success && status.status === "installed" ? ( +
+
+ +
+

+ Claude Code CLI Installed +

+
+ {status.method && ( +

+ Method: {status.method} +

+ )} + {status.version && ( +

+ Version:{" "} + {status.version} +

+ )} + {status.path && ( +

+ Path:{" "} + + {status.path} + +

+ )} +
+
+
+ {status.recommendation && ( +

+ {status.recommendation} +

+ )} +
+ ) : ( +
+
+ +
+

+ Claude Code CLI Not Detected +

+

+ {status.recommendation || + "Consider installing Claude Code CLI for optimal performance with ultrathink."} +

+
+
+ {status.installCommands && ( +
+

+ Installation Commands: +

+
+ {status.installCommands.npm && ( +
+

npm:

+ + {status.installCommands.npm} + +
+ )} + {status.installCommands.macos && ( +
+

+ macOS/Linux: +

+ + {status.installCommands.macos} + +
+ )} + {status.installCommands.windows && ( +
+

+ Windows (PowerShell): +

+ + {status.installCommands.windows} + +
+ )} +
+
+ )} +
+ )} +
+
+ ); +} diff --git a/app/src/components/views/settings-view/cli-status/codex-cli-status.tsx b/app/src/components/views/settings-view/cli-status/codex-cli-status.tsx new file mode 100644 index 00000000..6dc41d72 --- /dev/null +++ b/app/src/components/views/settings-view/cli-status/codex-cli-status.tsx @@ -0,0 +1,169 @@ +import { Button } from "@/components/ui/button"; +import { + Terminal, + CheckCircle2, + AlertCircle, + RefreshCw, +} from "lucide-react"; +import type { CliStatus } from "../types"; + +interface CliStatusProps { + status: CliStatus | null; + isChecking: boolean; + onRefresh: () => void; +} + +export function CodexCliStatus({ + status, + isChecking, + onRefresh, +}: CliStatusProps) { + if (!status) return null; + + return ( +
+
+
+
+ +

+ OpenAI Codex CLI +

+
+ +
+

+ Codex CLI enables GPT-5.1 Codex models for autonomous coding tasks. +

+
+
+ {status.success && status.status === "installed" ? ( +
+
+ +
+

+ Codex CLI Installed +

+
+ {status.method && ( +

+ Method: {status.method} +

+ )} + {status.version && ( +

+ Version:{" "} + {status.version} +

+ )} + {status.path && ( +

+ Path:{" "} + + {status.path} + +

+ )} +
+
+
+ {status.recommendation && ( +

+ {status.recommendation} +

+ )} +
+ ) : status.status === "api_key_only" ? ( +
+
+ +
+

+ API Key Detected - CLI Not Installed +

+

+ {status.recommendation || + "OPENAI_API_KEY found but Codex CLI not installed. Install the CLI for full agentic capabilities."} +

+
+
+ {status.installCommands && ( +
+

+ Installation Commands: +

+
+ {status.installCommands.npm && ( +
+

npm:

+ + {status.installCommands.npm} + +
+ )} +
+
+ )} +
+ ) : ( +
+
+ +
+

+ Codex CLI Not Detected +

+

+ {status.recommendation || + "Install OpenAI Codex CLI to use GPT-5.1 Codex models for autonomous coding."} +

+
+
+ {status.installCommands && ( +
+

+ Installation Commands: +

+
+ {status.installCommands.npm && ( +
+

npm:

+ + {status.installCommands.npm} + +
+ )} + {status.installCommands.macos && ( +
+

+ macOS (Homebrew): +

+ + {status.installCommands.macos} + +
+ )} +
+
+ )} +
+ )} +
+
+ ); +} From 45bd2c64b945ed384e6f314aca5d014caff8da06 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:34:11 +0100 Subject: [PATCH 11/24] refactor(settings): move remaining sections into folders - Move feature-defaults-section.tsx into feature-defaults/ - Move keyboard-shortcuts-section.tsx into keyboard-shortcuts/ - Move kanban-display-section.tsx into kanban-display/ - Move danger-zone-section.tsx into danger-zone/ - Update settings-view.tsx to import from new locations - Update type imports in kanban-display and danger-zone to ../types - All TypeScript diagnostics passing - Git preserves file history with rename detection --- app/src/components/views/settings-view.tsx | 8 +- .../settings-view/cli-status-section.tsx | 304 ------------------ .../{ => danger-zone}/danger-zone-section.tsx | 2 +- .../feature-defaults-section.tsx | 0 .../kanban-display-section.tsx | 2 +- .../keyboard-shortcuts-section.tsx | 0 6 files changed, 6 insertions(+), 310 deletions(-) delete mode 100644 app/src/components/views/settings-view/cli-status-section.tsx rename app/src/components/views/settings-view/{ => danger-zone}/danger-zone-section.tsx (97%) rename app/src/components/views/settings-view/{ => feature-defaults}/feature-defaults-section.tsx (100%) rename app/src/components/views/settings-view/{ => kanban-display}/kanban-display-section.tsx (98%) rename app/src/components/views/settings-view/{ => keyboard-shortcuts}/keyboard-shortcuts-section.tsx (100%) diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 61b7c7db..a61df1ea 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -33,10 +33,10 @@ import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section"; import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status"; import { CodexCliStatus } from "./settings-view/cli-status/codex-cli-status"; import { AppearanceSection } from "./settings-view/appearance/appearance-section"; -import { KanbanDisplaySection } from "./settings-view/kanban-display-section"; -import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts-section"; -import { FeatureDefaultsSection } from "./settings-view/feature-defaults-section"; -import { DangerZoneSection } from "./settings-view/danger-zone-section"; +import { KanbanDisplaySection } from "./settings-view/kanban-display/kanban-display-section"; +import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section"; +import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section"; +import { DangerZoneSection } from "./settings-view/danger-zone/danger-zone-section"; // Navigation items for the side panel const NAV_ITEMS = [ diff --git a/app/src/components/views/settings-view/cli-status-section.tsx b/app/src/components/views/settings-view/cli-status-section.tsx deleted file mode 100644 index 050453b8..00000000 --- a/app/src/components/views/settings-view/cli-status-section.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Terminal, - CheckCircle2, - AlertCircle, - RefreshCw, - Atom, -} from "lucide-react"; -import type { CliStatus } from "./types"; - -interface CliStatusProps { - status: CliStatus | null; - isChecking: boolean; - onRefresh: () => void; -} - -export function ClaudeCliStatus({ - status, - isChecking, - onRefresh, -}: CliStatusProps) { - if (!status) return null; - - return ( -
-
-
-
- -

- Claude Code CLI -

-
- -
-

- Claude Code CLI provides better performance for long-running tasks, - especially with ultrathink. -

-
-
- {status.success && status.status === "installed" ? ( -
-
- -
-

- Claude Code CLI Installed -

-
- {status.method && ( -

- Method: {status.method} -

- )} - {status.version && ( -

- Version:{" "} - {status.version} -

- )} - {status.path && ( -

- Path:{" "} - - {status.path} - -

- )} -
-
-
- {status.recommendation && ( -

- {status.recommendation} -

- )} -
- ) : ( -
-
- -
-

- Claude Code CLI Not Detected -

-

- {status.recommendation || - "Consider installing Claude Code CLI for optimal performance with ultrathink."} -

-
-
- {status.installCommands && ( -
-

- Installation Commands: -

-
- {status.installCommands.npm && ( -
-

npm:

- - {status.installCommands.npm} - -
- )} - {status.installCommands.macos && ( -
-

- macOS/Linux: -

- - {status.installCommands.macos} - -
- )} - {status.installCommands.windows && ( -
-

- Windows (PowerShell): -

- - {status.installCommands.windows} - -
- )} -
-
- )} -
- )} -
-
- ); -} - -export function CodexCliStatus({ - status, - isChecking, - onRefresh, -}: CliStatusProps) { - if (!status) return null; - - return ( -
-
-
-
- -

- OpenAI Codex CLI -

-
- -
-

- Codex CLI enables GPT-5.1 Codex models for autonomous coding tasks. -

-
-
- {status.success && status.status === "installed" ? ( -
-
- -
-

- Codex CLI Installed -

-
- {status.method && ( -

- Method: {status.method} -

- )} - {status.version && ( -

- Version:{" "} - {status.version} -

- )} - {status.path && ( -

- Path:{" "} - - {status.path} - -

- )} -
-
-
- {status.recommendation && ( -

- {status.recommendation} -

- )} -
- ) : status.status === "api_key_only" ? ( -
-
- -
-

- API Key Detected - CLI Not Installed -

-

- {status.recommendation || - "OPENAI_API_KEY found but Codex CLI not installed. Install the CLI for full agentic capabilities."} -

-
-
- {status.installCommands && ( -
-

- Installation Commands: -

-
- {status.installCommands.npm && ( -
-

npm:

- - {status.installCommands.npm} - -
- )} -
-
- )} -
- ) : ( -
-
- -
-

- Codex CLI Not Detected -

-

- {status.recommendation || - "Install OpenAI Codex CLI to use GPT-5.1 Codex models for autonomous coding."} -

-
-
- {status.installCommands && ( -
-

- Installation Commands: -

-
- {status.installCommands.npm && ( -
-

npm:

- - {status.installCommands.npm} - -
- )} - {status.installCommands.macos && ( -
-

- macOS (Homebrew): -

- - {status.installCommands.macos} - -
- )} -
-
- )} -
- )} -
-
- ); -} diff --git a/app/src/components/views/settings-view/danger-zone-section.tsx b/app/src/components/views/settings-view/danger-zone/danger-zone-section.tsx similarity index 97% rename from app/src/components/views/settings-view/danger-zone-section.tsx rename to app/src/components/views/settings-view/danger-zone/danger-zone-section.tsx index f791d763..3efdbaa7 100644 --- a/app/src/components/views/settings-view/danger-zone-section.tsx +++ b/app/src/components/views/settings-view/danger-zone/danger-zone-section.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; import { Trash2, Folder } from "lucide-react"; -import type { Project } from "./types"; +import type { Project } from "../types"; interface DangerZoneSectionProps { project: Project | null; diff --git a/app/src/components/views/settings-view/feature-defaults-section.tsx b/app/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx similarity index 100% rename from app/src/components/views/settings-view/feature-defaults-section.tsx rename to app/src/components/views/settings-view/feature-defaults/feature-defaults-section.tsx diff --git a/app/src/components/views/settings-view/kanban-display-section.tsx b/app/src/components/views/settings-view/kanban-display/kanban-display-section.tsx similarity index 98% rename from app/src/components/views/settings-view/kanban-display-section.tsx rename to app/src/components/views/settings-view/kanban-display/kanban-display-section.tsx index 0f22ee3c..55cc0789 100644 --- a/app/src/components/views/settings-view/kanban-display-section.tsx +++ b/app/src/components/views/settings-view/kanban-display/kanban-display-section.tsx @@ -1,7 +1,7 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { LayoutGrid, Minimize2, Square, Maximize2 } from "lucide-react"; -import type { KanbanDetailLevel } from "./types"; +import type { KanbanDetailLevel } from "../types"; interface KanbanDisplaySectionProps { detailLevel: KanbanDetailLevel; diff --git a/app/src/components/views/settings-view/keyboard-shortcuts-section.tsx b/app/src/components/views/settings-view/keyboard-shortcuts/keyboard-shortcuts-section.tsx similarity index 100% rename from app/src/components/views/settings-view/keyboard-shortcuts-section.tsx rename to app/src/components/views/settings-view/keyboard-shortcuts/keyboard-shortcuts-section.tsx From 2d937bc47f382c391fcb4fc0c51363dba3e0d8d3 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:35:45 +0100 Subject: [PATCH 12/24] refactor(settings): move types.ts to shared folder - Move types.ts to shared/types.ts - Update all section files to import from ../shared/types - Update theme-options.ts to import from ../../shared/types - All TypeScript diagnostics passing - Completes settings-view folder restructuring Final structure: - api-keys/ (with hooks/, config/) - appearance/ (with config/) - cli-status/ (claude, codex) - feature-defaults/ - keyboard-shortcuts/ - kanban-display/ - danger-zone/ - shared/ (types.ts) --- .../views/settings-view/appearance/appearance-section.tsx | 2 +- .../views/settings-view/appearance/config/theme-options.ts | 2 +- .../views/settings-view/cli-status/claude-cli-status.tsx | 2 +- .../views/settings-view/cli-status/codex-cli-status.tsx | 2 +- .../views/settings-view/danger-zone/danger-zone-section.tsx | 2 +- .../settings-view/kanban-display/kanban-display-section.tsx | 2 +- app/src/components/views/settings-view/{ => shared}/types.ts | 0 7 files changed, 6 insertions(+), 6 deletions(-) rename app/src/components/views/settings-view/{ => shared}/types.ts (100%) diff --git a/app/src/components/views/settings-view/appearance/appearance-section.tsx b/app/src/components/views/settings-view/appearance/appearance-section.tsx index 4ccd0c24..e24c355d 100644 --- a/app/src/components/views/settings-view/appearance/appearance-section.tsx +++ b/app/src/components/views/settings-view/appearance/appearance-section.tsx @@ -2,7 +2,7 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Palette } from "lucide-react"; import { themeOptions } from "./config/theme-options"; -import type { Theme, Project } from "../types"; +import type { Theme, Project } from "../shared/types"; interface AppearanceSectionProps { effectiveTheme: Theme; diff --git a/app/src/components/views/settings-view/appearance/config/theme-options.ts b/app/src/components/views/settings-view/appearance/config/theme-options.ts index e59da056..9e52deb0 100644 --- a/app/src/components/views/settings-view/appearance/config/theme-options.ts +++ b/app/src/components/views/settings-view/appearance/config/theme-options.ts @@ -13,7 +13,7 @@ import { Terminal, Trees, } from "lucide-react"; -import { Theme } from "../types"; +import { Theme } from "../../shared/types"; export interface ThemeOption { value: Theme; diff --git a/app/src/components/views/settings-view/cli-status/claude-cli-status.tsx b/app/src/components/views/settings-view/cli-status/claude-cli-status.tsx index b5c4afe6..868024a5 100644 --- a/app/src/components/views/settings-view/cli-status/claude-cli-status.tsx +++ b/app/src/components/views/settings-view/cli-status/claude-cli-status.tsx @@ -5,7 +5,7 @@ import { AlertCircle, RefreshCw, } from "lucide-react"; -import type { CliStatus } from "../types"; +import type { CliStatus } from "../shared/types"; interface CliStatusProps { status: CliStatus | null; diff --git a/app/src/components/views/settings-view/cli-status/codex-cli-status.tsx b/app/src/components/views/settings-view/cli-status/codex-cli-status.tsx index 6dc41d72..5f0bde25 100644 --- a/app/src/components/views/settings-view/cli-status/codex-cli-status.tsx +++ b/app/src/components/views/settings-view/cli-status/codex-cli-status.tsx @@ -5,7 +5,7 @@ import { AlertCircle, RefreshCw, } from "lucide-react"; -import type { CliStatus } from "../types"; +import type { CliStatus } from "../shared/types"; interface CliStatusProps { status: CliStatus | null; diff --git a/app/src/components/views/settings-view/danger-zone/danger-zone-section.tsx b/app/src/components/views/settings-view/danger-zone/danger-zone-section.tsx index 3efdbaa7..f6bec6a8 100644 --- a/app/src/components/views/settings-view/danger-zone/danger-zone-section.tsx +++ b/app/src/components/views/settings-view/danger-zone/danger-zone-section.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; import { Trash2, Folder } from "lucide-react"; -import type { Project } from "../types"; +import type { Project } from "../shared/types"; interface DangerZoneSectionProps { project: Project | null; diff --git a/app/src/components/views/settings-view/kanban-display/kanban-display-section.tsx b/app/src/components/views/settings-view/kanban-display/kanban-display-section.tsx index 55cc0789..d371966e 100644 --- a/app/src/components/views/settings-view/kanban-display/kanban-display-section.tsx +++ b/app/src/components/views/settings-view/kanban-display/kanban-display-section.tsx @@ -1,7 +1,7 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { LayoutGrid, Minimize2, Square, Maximize2 } from "lucide-react"; -import type { KanbanDetailLevel } from "../types"; +import type { KanbanDetailLevel } from "../shared/types"; interface KanbanDisplaySectionProps { detailLevel: KanbanDetailLevel; diff --git a/app/src/components/views/settings-view/types.ts b/app/src/components/views/settings-view/shared/types.ts similarity index 100% rename from app/src/components/views/settings-view/types.ts rename to app/src/components/views/settings-view/shared/types.ts From 6bbcc36409722756cf5f5c36bfb5610759202ea6 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:44:41 +0100 Subject: [PATCH 13/24] refactor(settings): extract CLI status into custom hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create hooks/use-cli-status.ts to manage all CLI status logic - Move Claude and Codex CLI status state management to hook - Move CLI checking useEffect and refresh handlers to hook - Update settings-view.tsx to use new useCliStatus hook - Reduce settings-view.tsx by ~130 lines - Improve testability by isolating CLI status logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/src/components/views/settings-view.tsx | 157 ++-------------- .../settings-view/hooks/use-cli-status.ts | 169 ++++++++++++++++++ 2 files changed, 180 insertions(+), 146 deletions(-) create mode 100644 app/src/components/views/settings-view/hooks/use-cli-status.ts diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index a61df1ea..63efdd7b 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -28,6 +28,8 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { KeyboardMap, ShortcutReferencePanel } from "@/components/ui/keyboard-map"; +// Import custom hooks +import { useCliStatus } from "./settings-view/hooks/use-cli-status"; // Import extracted sections import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section"; import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status"; @@ -83,128 +85,21 @@ export function SettingsView() { } }; - const [claudeCliStatus, setClaudeCliStatus] = useState<{ - success: boolean; - status?: string; - method?: string; - version?: string; - path?: string; - recommendation?: string; - installCommands?: { - macos?: string; - windows?: string; - linux?: string; - npm?: string; - }; - error?: string; - } | null>(null); - - const [codexCliStatus, setCodexCliStatus] = useState<{ - success: boolean; - status?: string; - method?: string; - version?: string; - path?: string; - hasApiKey?: boolean; - recommendation?: string; - installCommands?: { - macos?: string; - windows?: string; - linux?: string; - npm?: string; - }; - error?: string; - } | null>(null); + // Use CLI status hook + const { + claudeCliStatus, + codexCliStatus, + isCheckingClaudeCli, + isCheckingCodexCli, + handleRefreshClaudeCli, + handleRefreshCodexCli, + } = useCliStatus(); const [activeSection, setActiveSection] = useState("api-keys"); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false); - const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false); - const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false); const scrollContainerRef = useRef(null); - useEffect(() => { - const checkCliStatus = async () => { - const api = getElectronAPI(); - if (api?.checkClaudeCli) { - try { - const status = await api.checkClaudeCli(); - setClaudeCliStatus(status); - } catch (error) { - console.error("Failed to check Claude CLI status:", error); - } - } - if (api?.checkCodexCli) { - try { - const status = await api.checkCodexCli(); - setCodexCliStatus(status); - } catch (error) { - console.error("Failed to check Codex CLI status:", error); - } - } - - // Check Claude auth status (re-fetch on mount to ensure persistence) - if (api?.setup?.getClaudeStatus) { - try { - const result = await api.setup.getClaudeStatus(); - if (result.success && result.auth) { - const auth = result.auth; - const authStatus = { - authenticated: auth.authenticated, - method: - auth.method === "oauth_token" - ? "oauth" as const - : auth.method?.includes("api_key") - ? "api_key" as const - : "none" as const, - hasCredentialsFile: auth.hasCredentialsFile ?? false, - oauthTokenValid: auth.hasStoredOAuthToken, - apiKeyValid: auth.hasStoredApiKey || auth.hasEnvApiKey, - }; - setClaudeAuthStatus(authStatus); - } - } catch (error) { - console.error("Failed to check Claude auth status:", error); - } - } - - // Check Codex auth status (re-fetch on mount to ensure persistence) - if (api?.setup?.getCodexStatus) { - try { - const result = await api.setup.getCodexStatus(); - if (result.success && result.auth) { - const auth = result.auth; - // Determine method - prioritize cli_verified and cli_tokens over auth_file - const method = - auth.method === "cli_verified" || auth.method === "cli_tokens" - ? auth.method === "cli_verified" - ? "cli_verified" as const - : "cli_tokens" as const - : auth.method === "auth_file" - ? "api_key" as const - : auth.method === "env_var" - ? "env" as const - : "none" as const; - - const authStatus = { - authenticated: auth.authenticated, - method, - // Only set apiKeyValid for actual API key methods, not CLI login - apiKeyValid: - method === "cli_verified" || method === "cli_tokens" - ? undefined - : auth.hasAuthFile || auth.hasEnvKey, - }; - setCodexAuthStatus(authStatus); - } - } catch (error) { - console.error("Failed to check Codex auth status:", error); - } - } - }; - checkCliStatus(); - }, [setClaudeAuthStatus, setCodexAuthStatus]); - // Track scroll position to highlight active nav item useEffect(() => { const container = scrollContainerRef.current; @@ -267,36 +162,6 @@ export function SettingsView() { } }, []); - const handleRefreshClaudeCli = useCallback(async () => { - setIsCheckingClaudeCli(true); - try { - const api = getElectronAPI(); - if (api?.checkClaudeCli) { - const status = await api.checkClaudeCli(); - setClaudeCliStatus(status); - } - } catch (error) { - console.error("Failed to refresh Claude CLI status:", error); - } finally { - setIsCheckingClaudeCli(false); - } - }, []); - - const handleRefreshCodexCli = useCallback(async () => { - setIsCheckingCodexCli(true); - try { - const api = getElectronAPI(); - if (api?.checkCodexCli) { - const status = await api.checkCodexCli(); - setCodexCliStatus(status); - } - } catch (error) { - console.error("Failed to refresh Codex CLI status:", error); - } finally { - setIsCheckingCodexCli(false); - } - }, []); - return (
(null); + + const [codexCliStatus, setCodexCliStatus] = + useState(null); + + const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false); + const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false); + + // Check CLI status on mount + useEffect(() => { + const checkCliStatus = async () => { + const api = getElectronAPI(); + + // Check Claude CLI + if (api?.checkClaudeCli) { + try { + const status = await api.checkClaudeCli(); + setClaudeCliStatus(status); + } catch (error) { + console.error("Failed to check Claude CLI status:", error); + } + } + + // Check Codex CLI + if (api?.checkCodexCli) { + try { + const status = await api.checkCodexCli(); + setCodexCliStatus(status); + } catch (error) { + console.error("Failed to check Codex CLI status:", error); + } + } + + // Check Claude auth status (re-fetch on mount to ensure persistence) + if (api?.setup?.getClaudeStatus) { + try { + const result = await api.setup.getClaudeStatus(); + if (result.success && result.auth) { + const auth = result.auth; + const authStatus = { + authenticated: auth.authenticated, + method: + auth.method === "oauth_token" + ? ("oauth" as const) + : auth.method?.includes("api_key") + ? ("api_key" as const) + : ("none" as const), + hasCredentialsFile: auth.hasCredentialsFile ?? false, + oauthTokenValid: auth.hasStoredOAuthToken, + apiKeyValid: auth.hasStoredApiKey || auth.hasEnvApiKey, + }; + setClaudeAuthStatus(authStatus); + } + } catch (error) { + console.error("Failed to check Claude auth status:", error); + } + } + + // Check Codex auth status (re-fetch on mount to ensure persistence) + if (api?.setup?.getCodexStatus) { + try { + const result = await api.setup.getCodexStatus(); + if (result.success && result.auth) { + const auth = result.auth; + // Determine method - prioritize cli_verified and cli_tokens over auth_file + const method = + auth.method === "cli_verified" || auth.method === "cli_tokens" + ? auth.method === "cli_verified" + ? ("cli_verified" as const) + : ("cli_tokens" as const) + : auth.method === "auth_file" + ? ("api_key" as const) + : auth.method === "env_var" + ? ("env" as const) + : ("none" as const); + + const authStatus = { + authenticated: auth.authenticated, + method, + // Only set apiKeyValid for actual API key methods, not CLI login + apiKeyValid: + method === "cli_verified" || method === "cli_tokens" + ? undefined + : auth.hasAuthFile || auth.hasEnvKey, + }; + setCodexAuthStatus(authStatus); + } + } catch (error) { + console.error("Failed to check Codex auth status:", error); + } + } + }; + + checkCliStatus(); + }, [setClaudeAuthStatus, setCodexAuthStatus]); + + // Refresh Claude CLI status + const handleRefreshClaudeCli = useCallback(async () => { + setIsCheckingClaudeCli(true); + try { + const api = getElectronAPI(); + if (api?.checkClaudeCli) { + const status = await api.checkClaudeCli(); + setClaudeCliStatus(status); + } + } catch (error) { + console.error("Failed to refresh Claude CLI status:", error); + } finally { + setIsCheckingClaudeCli(false); + } + }, []); + + // Refresh Codex CLI status + const handleRefreshCodexCli = useCallback(async () => { + setIsCheckingCodexCli(true); + try { + const api = getElectronAPI(); + if (api?.checkCodexCli) { + const status = await api.checkCodexCli(); + setCodexCliStatus(status); + } + } catch (error) { + console.error("Failed to refresh Codex CLI status:", error); + } finally { + setIsCheckingCodexCli(false); + } + }, []); + + return { + claudeCliStatus, + codexCliStatus, + isCheckingClaudeCli, + isCheckingCodexCli, + handleRefreshClaudeCli, + handleRefreshCodexCli, + }; +} From 60fc043b1ea5552be0428fec73b4363004ed1100 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:46:42 +0100 Subject: [PATCH 14/24] refactor(settings): extract scroll tracking into custom hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create hooks/use-scroll-tracking.ts for scroll-based navigation - Move scroll position tracking logic and useEffect to hook - Move scrollToSection callback to hook - Update settings-view.tsx to use new useScrollTracking hook - Remove useState, useEffect, useRef, useCallback imports (no longer needed) - Reduce settings-view.tsx by ~60 lines - Improve code organization and testability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/src/components/views/settings-view.tsx | 71 ++------------- .../hooks/use-scroll-tracking.ts | 90 +++++++++++++++++++ 2 files changed, 96 insertions(+), 65 deletions(-) create mode 100644 app/src/components/views/settings-view/hooks/use-scroll-tracking.ts diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 63efdd7b..0e2074b7 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState } from "react"; import { useAppStore } from "@/store/app-store"; import { useSetupStore } from "@/store/setup-store"; import { Button } from "@/components/ui/button"; @@ -30,6 +30,7 @@ import { import { KeyboardMap, ShortcutReferencePanel } from "@/components/ui/keyboard-map"; // Import custom hooks import { useCliStatus } from "./settings-view/hooks/use-cli-status"; +import { useScrollTracking } from "./settings-view/hooks/use-scroll-tracking"; // Import extracted sections import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section"; import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status"; @@ -95,72 +96,12 @@ export function SettingsView() { handleRefreshCodexCli, } = useCliStatus(); - const [activeSection, setActiveSection] = useState("api-keys"); + // Use scroll tracking hook + const { activeSection, scrollToSection, scrollContainerRef } = + useScrollTracking(NAV_ITEMS, currentProject); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false); - const scrollContainerRef = useRef(null); - - // Track scroll position to highlight active nav item - useEffect(() => { - const container = scrollContainerRef.current; - if (!container) return; - - const handleScroll = () => { - const sections = NAV_ITEMS.filter( - (item) => item.id !== "danger" || currentProject - ) - .map((item) => ({ - id: item.id, - element: document.getElementById(item.id), - })) - .filter((s) => s.element); - - const containerRect = container.getBoundingClientRect(); - const scrollTop = container.scrollTop; - const scrollHeight = container.scrollHeight; - const clientHeight = container.clientHeight; - - // Check if scrolled to bottom (within a small threshold) - const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50; - - if (isAtBottom && sections.length > 0) { - // If at bottom, highlight the last visible section - setActiveSection(sections[sections.length - 1].id); - return; - } - - for (let i = sections.length - 1; i >= 0; i--) { - const section = sections[i]; - if (section.element) { - const rect = section.element.getBoundingClientRect(); - const relativeTop = rect.top - containerRect.top + scrollTop; - if (scrollTop >= relativeTop - 100) { - setActiveSection(section.id); - break; - } - } - } - }; - - container.addEventListener("scroll", handleScroll); - return () => container.removeEventListener("scroll", handleScroll); - }, [currentProject]); - - const scrollToSection = useCallback((sectionId: string) => { - const element = document.getElementById(sectionId); - if (element && scrollContainerRef.current) { - const container = scrollContainerRef.current; - const containerRect = container.getBoundingClientRect(); - const elementRect = element.getBoundingClientRect(); - const relativeTop = - elementRect.top - containerRect.top + container.scrollTop; - - container.scrollTo({ - top: relativeTop - 24, - behavior: "smooth", - }); - } - }, []); return (
(null); + + // Track scroll position to highlight active nav item + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) return; + + const handleScroll = () => { + const sections = navItems + .filter((item) => item.id !== "danger" || currentProject) + .map((item) => ({ + id: item.id, + element: document.getElementById(item.id), + })) + .filter((s) => s.element); + + const containerRect = container.getBoundingClientRect(); + const scrollTop = container.scrollTop; + const scrollHeight = container.scrollHeight; + const clientHeight = container.clientHeight; + + // Check if scrolled to bottom (within a small threshold) + const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50; + + if (isAtBottom && sections.length > 0) { + // If at bottom, highlight the last visible section + setActiveSection(sections[sections.length - 1].id); + return; + } + + for (let i = sections.length - 1; i >= 0; i--) { + const section = sections[i]; + if (section.element) { + const rect = section.element.getBoundingClientRect(); + const relativeTop = rect.top - containerRect.top + scrollTop; + if (scrollTop >= relativeTop - 100) { + setActiveSection(section.id); + break; + } + } + } + }; + + container.addEventListener("scroll", handleScroll); + return () => container.removeEventListener("scroll", handleScroll); + }, [currentProject, navItems]); + + // Scroll to a specific section with smooth animation + const scrollToSection = useCallback((sectionId: string) => { + const element = document.getElementById(sectionId); + if (element && scrollContainerRef.current) { + const container = scrollContainerRef.current; + const containerRect = container.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + const relativeTop = + elementRect.top - containerRect.top + container.scrollTop; + + container.scrollTo({ + top: relativeTop - 24, + behavior: "smooth", + }); + } + }, []); + + return { + activeSection, + scrollToSection, + scrollContainerRef, + }; +} From 8010a03a7cea97e0b87bcec6a4408055ba0bb45e Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:48:18 +0100 Subject: [PATCH 15/24] refactor(settings): extract navigation config to separate file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create config/navigation.ts with NAV_ITEMS and NavigationItem type - Remove NAV_ITEMS constant from settings-view.tsx - Update use-scroll-tracking.ts to import NavigationItem type - Remove unused icon imports from settings-view.tsx - Improve code organization and maintainability - Reduce settings-view.tsx by ~10 lines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/src/components/views/settings-view.tsx | 28 ++---------------- .../views/settings-view/config/navigation.ts | 29 +++++++++++++++++++ .../hooks/use-scroll-tracking.ts | 8 +---- 3 files changed, 33 insertions(+), 32 deletions(-) create mode 100644 app/src/components/views/settings-view/config/navigation.ts diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 0e2074b7..1bf13beb 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -5,19 +5,7 @@ import { useAppStore } from "@/store/app-store"; import { useSetupStore } from "@/store/setup-store"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import { - Settings, - Key, - Keyboard, - Trash2, - Folder, - Terminal, - Atom, - Palette, - LayoutGrid, - Settings2, - FlaskConical, -} from "lucide-react"; +import { Settings, Keyboard, Trash2, Folder } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; import { Dialog, @@ -31,6 +19,8 @@ import { KeyboardMap, ShortcutReferencePanel } from "@/components/ui/keyboard-ma // Import custom hooks import { useCliStatus } from "./settings-view/hooks/use-cli-status"; import { useScrollTracking } from "./settings-view/hooks/use-scroll-tracking"; +// Import config +import { NAV_ITEMS } from "./settings-view/config/navigation"; // Import extracted sections import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section"; import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status"; @@ -41,18 +31,6 @@ import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/key import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section"; import { DangerZoneSection } from "./settings-view/danger-zone/danger-zone-section"; -// Navigation items for the side panel -const NAV_ITEMS = [ - { id: "api-keys", label: "API Keys", icon: Key }, - { id: "claude", label: "Claude", icon: Terminal }, - { id: "codex", label: "Codex", icon: Atom }, - { id: "appearance", label: "Appearance", icon: Palette }, - { id: "kanban", label: "Kanban Display", icon: LayoutGrid }, - { id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 }, - { id: "defaults", label: "Feature Defaults", icon: FlaskConical }, - { id: "danger", label: "Danger Zone", icon: Trash2 }, -]; - export function SettingsView() { const { setCurrentView, diff --git a/app/src/components/views/settings-view/config/navigation.ts b/app/src/components/views/settings-view/config/navigation.ts new file mode 100644 index 00000000..c6f5432a --- /dev/null +++ b/app/src/components/views/settings-view/config/navigation.ts @@ -0,0 +1,29 @@ +import type { LucideIcon } from "lucide-react"; +import { + Key, + Terminal, + Atom, + Palette, + LayoutGrid, + Settings2, + FlaskConical, + Trash2, +} from "lucide-react"; + +export interface NavigationItem { + id: string; + label: string; + icon: LucideIcon; +} + +// Navigation items for the settings side panel +export const NAV_ITEMS: NavigationItem[] = [ + { id: "api-keys", label: "API Keys", icon: Key }, + { id: "claude", label: "Claude", icon: Terminal }, + { id: "codex", label: "Codex", icon: Atom }, + { id: "appearance", label: "Appearance", icon: Palette }, + { id: "kanban", label: "Kanban Display", icon: LayoutGrid }, + { id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 }, + { id: "defaults", label: "Feature Defaults", icon: FlaskConical }, + { id: "danger", label: "Danger Zone", icon: Trash2 }, +]; diff --git a/app/src/components/views/settings-view/hooks/use-scroll-tracking.ts b/app/src/components/views/settings-view/hooks/use-scroll-tracking.ts index 0a62f716..1d76b5a7 100644 --- a/app/src/components/views/settings-view/hooks/use-scroll-tracking.ts +++ b/app/src/components/views/settings-view/hooks/use-scroll-tracking.ts @@ -1,12 +1,6 @@ import { useState, useEffect, useRef, useCallback } from "react"; -import type { LucideIcon } from "lucide-react"; import type { Project } from "@/store/app-store"; - -interface NavigationItem { - id: string; - label: string; - icon: LucideIcon; -} +import type { NavigationItem } from "../config/navigation"; /** * Custom hook for managing scroll-based navigation tracking From d8f55f26db61f1c616d24498073a6c76e96fee85 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:50:38 +0100 Subject: [PATCH 16/24] refactor(settings): extract keyboard map dialog to component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create components/keyboard-map-dialog.tsx - Move keyboard shortcut map dialog JSX to new component - Update settings-view.tsx to use KeyboardMapDialog component - Remove unused Keyboard icon import - Remove unused KeyboardMap and ShortcutReferencePanel imports - Remove unused useSetupStore import and destructuring - Reduce settings-view.tsx by ~30 lines - Improve component modularity and reusability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/src/components/views/settings-view.tsx | 42 ++--------------- .../components/keyboard-map-dialog.tsx | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 app/src/components/views/settings-view/components/keyboard-map-dialog.tsx diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 1bf13beb..3f3373e3 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -2,11 +2,9 @@ import { useState } from "react"; import { useAppStore } from "@/store/app-store"; -import { useSetupStore } from "@/store/setup-store"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import { Settings, Keyboard, Trash2, Folder } from "lucide-react"; -import { getElectronAPI } from "@/lib/electron"; +import { Settings, Trash2, Folder } from "lucide-react"; import { Dialog, DialogContent, @@ -15,13 +13,10 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { KeyboardMap, ShortcutReferencePanel } from "@/components/ui/keyboard-map"; -// Import custom hooks import { useCliStatus } from "./settings-view/hooks/use-cli-status"; import { useScrollTracking } from "./settings-view/hooks/use-scroll-tracking"; -// Import config import { NAV_ITEMS } from "./settings-view/config/navigation"; -// Import extracted sections +import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog"; import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section"; import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status"; import { CodexCliStatus } from "./settings-view/cli-status/codex-cli-status"; @@ -49,9 +44,6 @@ export function SettingsView() { moveProjectToTrash, } = useAppStore(); - const { claudeAuthStatus, codexAuthStatus, setClaudeAuthStatus, setCodexAuthStatus } = - useSetupStore(); - // Compute the effective theme for the current project const effectiveTheme = currentProject?.theme || theme; @@ -211,36 +203,10 @@ export function SettingsView() {
{/* Keyboard Map Dialog */} - - - - - - Keyboard Shortcut Map - - - Visual overview of all keyboard shortcuts. Keys in color are bound - to shortcuts. Click on any shortcut below to edit it. - - - -
- {/* Visual Keyboard Map */} - - - {/* Shortcut Reference - Editable */} -
-

- All Shortcuts Reference (Click to Edit) -

- -
-
-
-
+ /> {/* Delete Project Confirmation Dialog */} diff --git a/app/src/components/views/settings-view/components/keyboard-map-dialog.tsx b/app/src/components/views/settings-view/components/keyboard-map-dialog.tsx new file mode 100644 index 00000000..b69a37cb --- /dev/null +++ b/app/src/components/views/settings-view/components/keyboard-map-dialog.tsx @@ -0,0 +1,46 @@ +import { Keyboard } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { KeyboardMap, ShortcutReferencePanel } from "@/components/ui/keyboard-map"; + +interface KeyboardMapDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function KeyboardMapDialog({ open, onOpenChange }: KeyboardMapDialogProps) { + return ( + + + + + + Keyboard Shortcut Map + + + Visual overview of all keyboard shortcuts. Keys in color are bound to + shortcuts. Click on any shortcut below to edit it. + + + +
+ {/* Visual Keyboard Map */} + + + {/* Shortcut Reference - Editable */} +
+

+ All Shortcuts Reference (Click to Edit) +

+ +
+
+
+
+ ); +} From f71d6da37d8dbcdc9c83cf04d460073858e84838 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:52:18 +0100 Subject: [PATCH 17/24] refactor(settings): extract delete project dialog to component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create components/delete-project-dialog.tsx - Move delete project confirmation dialog JSX to new component - Update settings-view.tsx to use DeleteProjectDialog component - Remove unused Trash2, Folder icon imports - Remove unused Dialog component imports - Reduce settings-view.tsx by ~50 lines - Improve component modularity and testability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/src/components/views/settings-view.tsx | 70 ++--------------- .../components/delete-project-dialog.tsx | 78 +++++++++++++++++++ 2 files changed, 86 insertions(+), 62 deletions(-) create mode 100644 app/src/components/views/settings-view/components/delete-project-dialog.tsx diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 3f3373e3..eac56179 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -4,19 +4,12 @@ import { useState } from "react"; import { useAppStore } from "@/store/app-store"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import { Settings, Trash2, Folder } from "lucide-react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; +import { Settings } from "lucide-react"; import { useCliStatus } from "./settings-view/hooks/use-cli-status"; import { useScrollTracking } from "./settings-view/hooks/use-scroll-tracking"; import { NAV_ITEMS } from "./settings-view/config/navigation"; import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog"; +import { DeleteProjectDialog } from "./settings-view/components/delete-project-dialog"; import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section"; import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status"; import { CodexCliStatus } from "./settings-view/cli-status/codex-cli-status"; @@ -209,59 +202,12 @@ export function SettingsView() { /> {/* Delete Project Confirmation Dialog */} - - - - - - Delete Project - - - Are you sure you want to move this project to Trash? - - - - {currentProject && ( -
-
- -
-
-

- {currentProject.name} -

-

- {currentProject.path} -

-
-
- )} - -

- The folder will remain on disk until you permanently delete it from - Trash. -

- - - - - -
-
+
); } diff --git a/app/src/components/views/settings-view/components/delete-project-dialog.tsx b/app/src/components/views/settings-view/components/delete-project-dialog.tsx new file mode 100644 index 00000000..0ac5870b --- /dev/null +++ b/app/src/components/views/settings-view/components/delete-project-dialog.tsx @@ -0,0 +1,78 @@ +import { Trash2, Folder } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import type { Project } from "@/store/app-store"; + +interface DeleteProjectDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + project: Project | null; + onConfirm: (projectId: string) => void; +} + +export function DeleteProjectDialog({ + open, + onOpenChange, + project, + onConfirm, +}: DeleteProjectDialogProps) { + const handleConfirm = () => { + if (project) { + onConfirm(project.id); + onOpenChange(false); + } + }; + + return ( + + + + + + Delete Project + + + Are you sure you want to move this project to Trash? + + + + {project && ( +
+
+ +
+
+

{project.name}

+

{project.path}

+
+
+ )} + +

+ The folder will remain on disk until you permanently delete it from Trash. +

+ + + + + +
+
+ ); +} From 215ae87950cbffd50032dcd7467c7d96232ca276 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:54:09 +0100 Subject: [PATCH 18/24] refactor(settings): extract settings navigation to component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create components/settings-navigation.tsx - Move side navigation JSX to new component - Update settings-view.tsx to use SettingsNavigation component - Remove unused cn utility import - Reduce settings-view.tsx by ~30 lines - Improve component modularity and reusability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/src/components/views/settings-view.tsx | 38 +++----------- .../components/settings-navigation.tsx | 50 +++++++++++++++++++ 2 files changed, 57 insertions(+), 31 deletions(-) create mode 100644 app/src/components/views/settings-view/components/settings-navigation.tsx diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index eac56179..db469e2d 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -3,13 +3,13 @@ import { useState } from "react"; import { useAppStore } from "@/store/app-store"; import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; import { Settings } from "lucide-react"; import { useCliStatus } from "./settings-view/hooks/use-cli-status"; import { useScrollTracking } from "./settings-view/hooks/use-scroll-tracking"; import { NAV_ITEMS } from "./settings-view/config/navigation"; import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog"; import { DeleteProjectDialog } from "./settings-view/components/delete-project-dialog"; +import { SettingsNavigation } from "./settings-view/components/settings-navigation"; import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section"; import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status"; import { CodexCliStatus } from "./settings-view/cli-status/codex-cli-status"; @@ -91,36 +91,12 @@ export function SettingsView() { {/* Content Area with Sidebar */}
{/* Sticky Side Navigation */} - + {/* Scrollable Content */}
diff --git a/app/src/components/views/settings-view/components/settings-navigation.tsx b/app/src/components/views/settings-view/components/settings-navigation.tsx new file mode 100644 index 00000000..7a90b7ca --- /dev/null +++ b/app/src/components/views/settings-view/components/settings-navigation.tsx @@ -0,0 +1,50 @@ +import { cn } from "@/lib/utils"; +import type { Project } from "@/store/app-store"; +import type { NavigationItem } from "../config/navigation"; + +interface SettingsNavigationProps { + navItems: NavigationItem[]; + activeSection: string; + currentProject: Project | null; + onNavigate: (sectionId: string) => void; +} + +export function SettingsNavigation({ + navItems, + activeSection, + currentProject, + onNavigate, +}: SettingsNavigationProps) { + return ( + + ); +} From ac3ea909501c0a5ae32c3ba2aca0b874eba39a30 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 00:55:33 +0100 Subject: [PATCH 19/24] refactor(settings): extract settings header to component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create components/settings-header.tsx - Move header section JSX to new component - Update settings-view.tsx to use SettingsHeader component - Remove unused Settings icon import - Make header configurable with title and description props - Reduce settings-view.tsx by ~16 lines - Improve component modularity and reusability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/src/components/views/settings-view.tsx | 18 ++----------- .../components/settings-header.tsx | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 app/src/components/views/settings-view/components/settings-header.tsx diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index db469e2d..2576c88a 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -3,10 +3,10 @@ import { useState } from "react"; import { useAppStore } from "@/store/app-store"; import { Button } from "@/components/ui/button"; -import { Settings } from "lucide-react"; import { useCliStatus } from "./settings-view/hooks/use-cli-status"; import { useScrollTracking } from "./settings-view/hooks/use-scroll-tracking"; import { NAV_ITEMS } from "./settings-view/config/navigation"; +import { SettingsHeader } from "./settings-view/components/settings-header"; import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog"; import { DeleteProjectDialog } from "./settings-view/components/delete-project-dialog"; import { SettingsNavigation } from "./settings-view/components/settings-navigation"; @@ -72,21 +72,7 @@ export function SettingsView() { data-testid="settings-view" > {/* Header Section */} -
-
-
-
- -
-
-

Settings

-

- Configure your API keys and preferences -

-
-
-
-
+ {/* Content Area with Sidebar */}
diff --git a/app/src/components/views/settings-view/components/settings-header.tsx b/app/src/components/views/settings-view/components/settings-header.tsx new file mode 100644 index 00000000..163c9148 --- /dev/null +++ b/app/src/components/views/settings-view/components/settings-header.tsx @@ -0,0 +1,27 @@ +import { Settings } from "lucide-react"; + +interface SettingsHeaderProps { + title?: string; + description?: string; +} + +export function SettingsHeader({ + title = "Settings", + description = "Configure your API keys and preferences", +}: SettingsHeaderProps) { + return ( +
+
+
+
+ +
+
+

{title}

+

{description}

+
+
+
+
+ ); +} From 82cc8abd297a78c2c84b4f81492e85b2f9037c25 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 01:00:42 +0100 Subject: [PATCH 20/24] refactor(settings): enhance project handling in SettingsView - Introduce type conversion for ElectronProject to SettingsProject - Update effective theme calculation to use converted settingsProject - Refactor currentProject references to use settingsProject in Appearance and DangerZone sections - Improve type safety and maintainability in settings-view.tsx --- app/src/components/views/settings-view.tsx | 21 ++- .../shared/api-provider-config.ts | 148 ++++++++++++++++++ 2 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 app/src/components/views/settings-view/shared/api-provider-config.ts diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 2576c88a..8f6806ec 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -18,6 +18,8 @@ import { KanbanDisplaySection } from "./settings-view/kanban-display/kanban-disp import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section"; import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section"; import { DangerZoneSection } from "./settings-view/danger-zone/danger-zone-section"; +import type { Project as SettingsProject, Theme } from "./settings-view/shared/types"; +import type { Project as ElectronProject } from "@/lib/electron"; export function SettingsView() { const { @@ -37,8 +39,21 @@ export function SettingsView() { moveProjectToTrash, } = useAppStore(); + // Convert electron Project to settings-view Project type + const convertProject = (project: ElectronProject | null): SettingsProject | null => { + if (!project) return null; + return { + id: project.id, + name: project.name, + path: project.path, + theme: project.theme as Theme | undefined, + }; + }; + + const settingsProject = convertProject(currentProject); + // Compute the effective theme for the current project - const effectiveTheme = currentProject?.theme || theme; + const effectiveTheme = (settingsProject?.theme || theme) as Theme; // Handler to set theme - saves to project if one is selected, otherwise to global const handleSetTheme = (newTheme: typeof theme) => { @@ -111,7 +126,7 @@ export function SettingsView() { {/* Appearance Section */} @@ -138,7 +153,7 @@ export function SettingsView() { {/* Danger Zone Section - Only show when a project is selected */} setShowDeleteDialog(true)} /> diff --git a/app/src/components/views/settings-view/shared/api-provider-config.ts b/app/src/components/views/settings-view/shared/api-provider-config.ts new file mode 100644 index 00000000..79f5b63e --- /dev/null +++ b/app/src/components/views/settings-view/shared/api-provider-config.ts @@ -0,0 +1,148 @@ +import type { Dispatch, SetStateAction } from "react"; +import type { ApiKeys } from "@/store/app-store"; + +export type ProviderKey = "anthropic" | "google" | "openai"; + +export interface ProviderConfig { + key: ProviderKey; + label: string; + inputId: string; + placeholder: string; + value: string; + setValue: Dispatch>; + showValue: boolean; + setShowValue: Dispatch>; + hasStoredKey: string | null | undefined; + inputTestId: string; + toggleTestId: string; + testButton: { + onClick: () => Promise | void; + disabled: boolean; + loading: boolean; + testId: string; + }; + result: { success: boolean; message: string } | null; + resultTestId: string; + resultMessageTestId: string; + descriptionPrefix: string; + descriptionLinkHref: string; + descriptionLinkText: string; + descriptionSuffix?: string; +} + +export interface ProviderConfigParams { + apiKeys: ApiKeys; + anthropic: { + value: string; + setValue: Dispatch>; + show: boolean; + setShow: Dispatch>; + testing: boolean; + onTest: () => Promise; + result: { success: boolean; message: string } | null; + }; + google: { + value: string; + setValue: Dispatch>; + show: boolean; + setShow: Dispatch>; + testing: boolean; + onTest: () => Promise; + result: { success: boolean; message: string } | null; + }; + openai: { + value: string; + setValue: Dispatch>; + show: boolean; + setShow: Dispatch>; + testing: boolean; + onTest: () => Promise; + result: { success: boolean; message: string } | null; + }; +} + +export const buildProviderConfigs = ({ + apiKeys, + anthropic, + google, + openai, +}: ProviderConfigParams): ProviderConfig[] => [ + { + key: "anthropic", + label: "Anthropic API Key (Claude)", + inputId: "anthropic-key", + placeholder: "sk-ant-...", + value: anthropic.value, + setValue: anthropic.setValue, + showValue: anthropic.show, + setShowValue: anthropic.setShow, + hasStoredKey: apiKeys.anthropic, + inputTestId: "anthropic-api-key-input", + toggleTestId: "toggle-anthropic-visibility", + testButton: { + onClick: anthropic.onTest, + disabled: !anthropic.value || anthropic.testing, + loading: anthropic.testing, + testId: "test-claude-connection", + }, + result: anthropic.result, + resultTestId: "test-connection-result", + resultMessageTestId: "test-connection-message", + descriptionPrefix: "Used for Claude AI features. Get your key at", + descriptionLinkHref: "https://console.anthropic.com/account/keys", + descriptionLinkText: "console.anthropic.com", + descriptionSuffix: + ". Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment variable can be used.", + }, + { + key: "google", + label: "Google API Key (Gemini)", + inputId: "google-key", + placeholder: "AIza...", + value: google.value, + setValue: google.setValue, + showValue: google.show, + setShowValue: google.setShow, + hasStoredKey: apiKeys.google, + inputTestId: "google-api-key-input", + toggleTestId: "toggle-google-visibility", + testButton: { + onClick: google.onTest, + disabled: !google.value || google.testing, + loading: google.testing, + testId: "test-gemini-connection", + }, + result: google.result, + resultTestId: "gemini-test-connection-result", + resultMessageTestId: "gemini-test-connection-message", + descriptionPrefix: + "Used for Gemini AI features (including image/design prompts). Get your key at", + descriptionLinkHref: "https://makersuite.google.com/app/apikey", + descriptionLinkText: "makersuite.google.com", + }, + { + key: "openai", + label: "OpenAI API Key (Codex/GPT)", + inputId: "openai-key", + placeholder: "sk-...", + value: openai.value, + setValue: openai.setValue, + showValue: openai.show, + setShowValue: openai.setShow, + hasStoredKey: apiKeys.openai, + inputTestId: "openai-api-key-input", + toggleTestId: "toggle-openai-visibility", + testButton: { + onClick: openai.onTest, + disabled: !openai.value || openai.testing, + loading: openai.testing, + testId: "test-openai-connection", + }, + result: openai.result, + resultTestId: "openai-test-connection-result", + resultMessageTestId: "openai-test-connection-message", + descriptionPrefix: "Used for OpenAI Codex CLI and GPT models. Get your key at", + descriptionLinkHref: "https://platform.openai.com/api-keys", + descriptionLinkText: "platform.openai.com", + }, +]; From 772b0e9e5c5802b788335c18672ab58252e99e84 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 01:10:36 +0100 Subject: [PATCH 21/24] refactor: move configs and hooks to global locations for reusability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move previously nested configs and hooks to global src/ folders to make them reusable across the application, reduce nesting, and establish clearer organization patterns. **New Global Structure:** - src/config/theme-options.ts (moved from appearance/config/) - src/config/api-providers.ts (moved from api-keys/config/) - src/hooks/use-scroll-tracking.ts (moved from settings-view/hooks/) **Changes:** - Move theme-options.ts to src/config/ - app-wide theme configuration - Move api-provider-config.ts to src/config/api-providers.ts - global API config - Move use-scroll-tracking.ts to src/hooks/ - reusable scroll navigation hook - Make useScrollTracking generic and more flexible with options object - Update all imports across settings-view components - Remove duplicate api-provider-config.ts from shared/ folder - Remove empty config/ folders (appearance/config, api-keys/config) **Benefits:** ✅ Single source of truth for themes and API providers ✅ Reusable scroll tracking hook available globally ✅ Cleaner structure with less nesting ✅ Better discoverability for developers ✅ No duplicate configuration files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/src/components/views/settings-view.tsx | 8 +- .../settings-view/api-keys/api-key-field.tsx | 2 +- .../api-keys/api-keys-section.tsx | 2 +- .../api-keys/hooks/use-api-key-management.ts | 2 +- .../appearance/appearance-section.tsx | 2 +- .../shared/api-provider-config.ts | 148 ------------------ .../api-providers.ts} | 0 .../appearance => }/config/theme-options.ts | 2 +- .../hooks/use-scroll-tracking.ts | 70 ++++++--- 9 files changed, 56 insertions(+), 180 deletions(-) delete mode 100644 app/src/components/views/settings-view/shared/api-provider-config.ts rename app/src/{components/views/settings-view/api-keys/config/api-provider-config.ts => config/api-providers.ts} (100%) rename app/src/{components/views/settings-view/appearance => }/config/theme-options.ts (95%) rename app/src/{components/views/settings-view => }/hooks/use-scroll-tracking.ts (54%) diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 8f6806ec..41488a24 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { useAppStore } from "@/store/app-store"; import { Button } from "@/components/ui/button"; import { useCliStatus } from "./settings-view/hooks/use-cli-status"; -import { useScrollTracking } from "./settings-view/hooks/use-scroll-tracking"; +import { useScrollTracking } from "@/hooks/use-scroll-tracking"; import { NAV_ITEMS } from "./settings-view/config/navigation"; import { SettingsHeader } from "./settings-view/components/settings-header"; import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog"; @@ -76,7 +76,11 @@ export function SettingsView() { // Use scroll tracking hook const { activeSection, scrollToSection, scrollContainerRef } = - useScrollTracking(NAV_ITEMS, currentProject); + useScrollTracking({ + items: NAV_ITEMS, + filterFn: (item) => item.id !== "danger" || !!currentProject, + initialSection: "api-keys", + }); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false); diff --git a/app/src/components/views/settings-view/api-keys/api-key-field.tsx b/app/src/components/views/settings-view/api-keys/api-key-field.tsx index b62f4d9a..db281813 100644 --- a/app/src/components/views/settings-view/api-keys/api-key-field.tsx +++ b/app/src/components/views/settings-view/api-keys/api-key-field.tsx @@ -2,7 +2,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { AlertCircle, CheckCircle2, Eye, EyeOff, Loader2, Zap } from "lucide-react"; -import type { ProviderConfig } from "./shared/api-provider-config"; +import type { ProviderConfig } from "@/config/api-providers"; interface ApiKeyFieldProps { config: ProviderConfig; diff --git a/app/src/components/views/settings-view/api-keys/api-keys-section.tsx b/app/src/components/views/settings-view/api-keys/api-keys-section.tsx index 8e6711e5..33c89f8a 100644 --- a/app/src/components/views/settings-view/api-keys/api-keys-section.tsx +++ b/app/src/components/views/settings-view/api-keys/api-keys-section.tsx @@ -3,7 +3,7 @@ import { useSetupStore } from "@/store/setup-store"; import { Button } from "@/components/ui/button"; import { Key, CheckCircle2 } from "lucide-react"; import { ApiKeyField } from "./api-key-field"; -import { buildProviderConfigs } from "./shared/api-provider-config"; +import { buildProviderConfigs } from "@/config/api-providers"; import { AuthenticationStatusDisplay } from "./authentication-status-display"; import { SecurityNotice } from "./security-notice"; import { useApiKeyManagement } from "./hooks/use-api-key-management"; diff --git a/app/src/components/views/settings-view/api-keys/hooks/use-api-key-management.ts b/app/src/components/views/settings-view/api-keys/hooks/use-api-key-management.ts index 6b284540..f45939ed 100644 --- a/app/src/components/views/settings-view/api-keys/hooks/use-api-key-management.ts +++ b/app/src/components/views/settings-view/api-keys/hooks/use-api-key-management.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { useAppStore } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; -import type { ProviderConfigParams } from "../config/api-provider-config"; +import type { ProviderConfigParams } from "@/config/api-providers"; interface TestResult { success: boolean; diff --git a/app/src/components/views/settings-view/appearance/appearance-section.tsx b/app/src/components/views/settings-view/appearance/appearance-section.tsx index e24c355d..90a5c8de 100644 --- a/app/src/components/views/settings-view/appearance/appearance-section.tsx +++ b/app/src/components/views/settings-view/appearance/appearance-section.tsx @@ -1,7 +1,7 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Palette } from "lucide-react"; -import { themeOptions } from "./config/theme-options"; +import { themeOptions } from "@/config/theme-options"; import type { Theme, Project } from "../shared/types"; interface AppearanceSectionProps { diff --git a/app/src/components/views/settings-view/shared/api-provider-config.ts b/app/src/components/views/settings-view/shared/api-provider-config.ts deleted file mode 100644 index 79f5b63e..00000000 --- a/app/src/components/views/settings-view/shared/api-provider-config.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type { Dispatch, SetStateAction } from "react"; -import type { ApiKeys } from "@/store/app-store"; - -export type ProviderKey = "anthropic" | "google" | "openai"; - -export interface ProviderConfig { - key: ProviderKey; - label: string; - inputId: string; - placeholder: string; - value: string; - setValue: Dispatch>; - showValue: boolean; - setShowValue: Dispatch>; - hasStoredKey: string | null | undefined; - inputTestId: string; - toggleTestId: string; - testButton: { - onClick: () => Promise | void; - disabled: boolean; - loading: boolean; - testId: string; - }; - result: { success: boolean; message: string } | null; - resultTestId: string; - resultMessageTestId: string; - descriptionPrefix: string; - descriptionLinkHref: string; - descriptionLinkText: string; - descriptionSuffix?: string; -} - -export interface ProviderConfigParams { - apiKeys: ApiKeys; - anthropic: { - value: string; - setValue: Dispatch>; - show: boolean; - setShow: Dispatch>; - testing: boolean; - onTest: () => Promise; - result: { success: boolean; message: string } | null; - }; - google: { - value: string; - setValue: Dispatch>; - show: boolean; - setShow: Dispatch>; - testing: boolean; - onTest: () => Promise; - result: { success: boolean; message: string } | null; - }; - openai: { - value: string; - setValue: Dispatch>; - show: boolean; - setShow: Dispatch>; - testing: boolean; - onTest: () => Promise; - result: { success: boolean; message: string } | null; - }; -} - -export const buildProviderConfigs = ({ - apiKeys, - anthropic, - google, - openai, -}: ProviderConfigParams): ProviderConfig[] => [ - { - key: "anthropic", - label: "Anthropic API Key (Claude)", - inputId: "anthropic-key", - placeholder: "sk-ant-...", - value: anthropic.value, - setValue: anthropic.setValue, - showValue: anthropic.show, - setShowValue: anthropic.setShow, - hasStoredKey: apiKeys.anthropic, - inputTestId: "anthropic-api-key-input", - toggleTestId: "toggle-anthropic-visibility", - testButton: { - onClick: anthropic.onTest, - disabled: !anthropic.value || anthropic.testing, - loading: anthropic.testing, - testId: "test-claude-connection", - }, - result: anthropic.result, - resultTestId: "test-connection-result", - resultMessageTestId: "test-connection-message", - descriptionPrefix: "Used for Claude AI features. Get your key at", - descriptionLinkHref: "https://console.anthropic.com/account/keys", - descriptionLinkText: "console.anthropic.com", - descriptionSuffix: - ". Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment variable can be used.", - }, - { - key: "google", - label: "Google API Key (Gemini)", - inputId: "google-key", - placeholder: "AIza...", - value: google.value, - setValue: google.setValue, - showValue: google.show, - setShowValue: google.setShow, - hasStoredKey: apiKeys.google, - inputTestId: "google-api-key-input", - toggleTestId: "toggle-google-visibility", - testButton: { - onClick: google.onTest, - disabled: !google.value || google.testing, - loading: google.testing, - testId: "test-gemini-connection", - }, - result: google.result, - resultTestId: "gemini-test-connection-result", - resultMessageTestId: "gemini-test-connection-message", - descriptionPrefix: - "Used for Gemini AI features (including image/design prompts). Get your key at", - descriptionLinkHref: "https://makersuite.google.com/app/apikey", - descriptionLinkText: "makersuite.google.com", - }, - { - key: "openai", - label: "OpenAI API Key (Codex/GPT)", - inputId: "openai-key", - placeholder: "sk-...", - value: openai.value, - setValue: openai.setValue, - showValue: openai.show, - setShowValue: openai.setShow, - hasStoredKey: apiKeys.openai, - inputTestId: "openai-api-key-input", - toggleTestId: "toggle-openai-visibility", - testButton: { - onClick: openai.onTest, - disabled: !openai.value || openai.testing, - loading: openai.testing, - testId: "test-openai-connection", - }, - result: openai.result, - resultTestId: "openai-test-connection-result", - resultMessageTestId: "openai-test-connection-message", - descriptionPrefix: "Used for OpenAI Codex CLI and GPT models. Get your key at", - descriptionLinkHref: "https://platform.openai.com/api-keys", - descriptionLinkText: "platform.openai.com", - }, -]; diff --git a/app/src/components/views/settings-view/api-keys/config/api-provider-config.ts b/app/src/config/api-providers.ts similarity index 100% rename from app/src/components/views/settings-view/api-keys/config/api-provider-config.ts rename to app/src/config/api-providers.ts diff --git a/app/src/components/views/settings-view/appearance/config/theme-options.ts b/app/src/config/theme-options.ts similarity index 95% rename from app/src/components/views/settings-view/appearance/config/theme-options.ts rename to app/src/config/theme-options.ts index 9e52deb0..ac8bc567 100644 --- a/app/src/components/views/settings-view/appearance/config/theme-options.ts +++ b/app/src/config/theme-options.ts @@ -13,7 +13,7 @@ import { Terminal, Trees, } from "lucide-react"; -import { Theme } from "../../shared/types"; +import { Theme } from "@/components/views/settings-view/shared/types"; export interface ThemeOption { value: Theme; diff --git a/app/src/components/views/settings-view/hooks/use-scroll-tracking.ts b/app/src/hooks/use-scroll-tracking.ts similarity index 54% rename from app/src/components/views/settings-view/hooks/use-scroll-tracking.ts rename to app/src/hooks/use-scroll-tracking.ts index 1d76b5a7..25f86a13 100644 --- a/app/src/components/views/settings-view/hooks/use-scroll-tracking.ts +++ b/app/src/hooks/use-scroll-tracking.ts @@ -1,17 +1,34 @@ import { useState, useEffect, useRef, useCallback } from "react"; -import type { Project } from "@/store/app-store"; -import type { NavigationItem } from "../config/navigation"; + +interface ScrollTrackingItem { + id: string; +} + +interface UseScrollTrackingOptions { + /** Navigation items with at least an id property */ + items: T[]; + /** Optional filter function to determine which items should be tracked */ + filterFn?: (item: T) => boolean; + /** Optional initial active section (defaults to first item's id) */ + initialSection?: string; + /** Optional offset from top when scrolling to section (defaults to 24) */ + scrollOffset?: number; +} /** - * Custom hook for managing scroll-based navigation tracking + * Generic custom hook for managing scroll-based navigation tracking * Automatically highlights the active section based on scroll position * and provides smooth scrolling to sections */ -export function useScrollTracking( - navItems: NavigationItem[], - currentProject: Project | null -) { - const [activeSection, setActiveSection] = useState("api-keys"); +export function useScrollTracking({ + items, + filterFn = () => true, + initialSection, + scrollOffset = 24, +}: UseScrollTrackingOptions) { + const [activeSection, setActiveSection] = useState( + initialSection || items[0]?.id || "" + ); const scrollContainerRef = useRef(null); // Track scroll position to highlight active nav item @@ -20,8 +37,8 @@ export function useScrollTracking( if (!container) return; const handleScroll = () => { - const sections = navItems - .filter((item) => item.id !== "danger" || currentProject) + const sections = items + .filter(filterFn) .map((item) => ({ id: item.id, element: document.getElementById(item.id), @@ -57,24 +74,27 @@ export function useScrollTracking( container.addEventListener("scroll", handleScroll); return () => container.removeEventListener("scroll", handleScroll); - }, [currentProject, navItems]); + }, [items, filterFn]); // Scroll to a specific section with smooth animation - const scrollToSection = useCallback((sectionId: string) => { - const element = document.getElementById(sectionId); - if (element && scrollContainerRef.current) { - const container = scrollContainerRef.current; - const containerRect = container.getBoundingClientRect(); - const elementRect = element.getBoundingClientRect(); - const relativeTop = - elementRect.top - containerRect.top + container.scrollTop; + const scrollToSection = useCallback( + (sectionId: string) => { + const element = document.getElementById(sectionId); + if (element && scrollContainerRef.current) { + const container = scrollContainerRef.current; + const containerRect = container.getBoundingClientRect(); + const elementRect = element.getBoundingClientRect(); + const relativeTop = + elementRect.top - containerRect.top + container.scrollTop; - container.scrollTo({ - top: relativeTop - 24, - behavior: "smooth", - }); - } - }, []); + container.scrollTo({ + top: relativeTop - scrollOffset, + behavior: "smooth", + }); + } + }, + [scrollOffset] + ); return { activeSection, From 6ee50853dccfce201a1c3566398a04844fb4c9aa Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 01:22:03 +0100 Subject: [PATCH 22/24] refactor(settings): remove unused back-to-home button from SettingsView - Eliminate the Back to Home button and its associated JSX from SettingsView component - Clean up code by removing unnecessary imports and comments - Enhance readability and maintainability of the SettingsView component --- app/src/components/views/settings-view.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 41488a24..a9dd2ed9 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { useAppStore } from "@/store/app-store"; -import { Button } from "@/components/ui/button"; import { useCliStatus } from "./settings-view/hooks/use-cli-status"; import { useScrollTracking } from "@/hooks/use-scroll-tracking"; import { NAV_ITEMS } from "./settings-view/config/navigation"; @@ -23,7 +22,6 @@ import type { Project as ElectronProject } from "@/lib/electron"; export function SettingsView() { const { - setCurrentView, theme, setTheme, setProjectTheme, @@ -160,18 +158,6 @@ export function SettingsView() { project={settingsProject} onDeleteClick={() => setShowDeleteDialog(true)} /> - - {/* Save Button */} -
- -
From 08de89344c84e2b3d947ece169b8cf9db7450b72 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 01:26:10 +0100 Subject: [PATCH 23/24] chore: add trailing newlines for consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/electron/services/codex-config-manager.js | 1 + app/electron/services/mcp-server-stdio.js | 1 + app/src/hooks/use-window-state.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/app/electron/services/codex-config-manager.js b/app/electron/services/codex-config-manager.js index 37832f61..9ee0f175 100644 --- a/app/electron/services/codex-config-manager.js +++ b/app/electron/services/codex-config-manager.js @@ -349,3 +349,4 @@ class CodexConfigManager { } module.exports = new CodexConfigManager(); + diff --git a/app/electron/services/mcp-server-stdio.js b/app/electron/services/mcp-server-stdio.js index b7f5c1db..b34c3a82 100644 --- a/app/electron/services/mcp-server-stdio.js +++ b/app/electron/services/mcp-server-stdio.js @@ -345,3 +345,4 @@ process.on('SIGINT', () => { console.error('[McpServerStdio] Starting MCP server for automaker-tools'); console.error(`[McpServerStdio] Project path: ${projectPath}`); console.error(`[McpServerStdio] IPC channel: ${ipcChannel}`); + diff --git a/app/src/hooks/use-window-state.ts b/app/src/hooks/use-window-state.ts index 8a4bc332..3647b892 100644 --- a/app/src/hooks/use-window-state.ts +++ b/app/src/hooks/use-window-state.ts @@ -52,3 +52,4 @@ export function useWindowState(): WindowState { return windowState; } + From 43c90adbc09933d43e1603796e2d933c5858d7a5 Mon Sep 17 00:00:00 2001 From: Kacper Date: Thu, 11 Dec 2025 02:07:15 +0100 Subject: [PATCH 24/24] fix: implement copilot suggestions --- app/electron/services/codex-config-manager.js | 1 + app/electron/services/mcp-server-stdio.js | 1 + app/src/components/ui/keyboard-map.tsx | 3 +-- .../keyboard-shortcuts-section.tsx | 1 - app/src/components/views/setup-view.tsx | 24 +++++++++++++------ app/src/hooks/use-window-state.ts | 1 + app/src/store/app-store.ts | 21 ++++++++++------ 7 files changed, 35 insertions(+), 17 deletions(-) diff --git a/app/electron/services/codex-config-manager.js b/app/electron/services/codex-config-manager.js index 9ee0f175..ed324917 100644 --- a/app/electron/services/codex-config-manager.js +++ b/app/electron/services/codex-config-manager.js @@ -350,3 +350,4 @@ class CodexConfigManager { module.exports = new CodexConfigManager(); + diff --git a/app/electron/services/mcp-server-stdio.js b/app/electron/services/mcp-server-stdio.js index f29dbca4..44cdd6b4 100644 --- a/app/electron/services/mcp-server-stdio.js +++ b/app/electron/services/mcp-server-stdio.js @@ -346,3 +346,4 @@ console.error('[McpServerStdio] Starting MCP server for automaker-tools'); console.error(`[McpServerStdio] Project path: ${projectPath}`); console.error(`[McpServerStdio] IPC channel: ${ipcChannel}`); + diff --git a/app/src/components/ui/keyboard-map.tsx b/app/src/components/ui/keyboard-map.tsx index 86051949..76f71d3a 100644 --- a/app/src/components/ui/keyboard-map.tsx +++ b/app/src/components/ui/keyboard-map.tsx @@ -181,7 +181,6 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap const isModified = shortcuts.some( (s) => keyboardShortcuts[s] !== DEFAULT_KEYBOARD_SHORTCUTS[s] ); - const hasModifierShortcuts = shortcutInfos.some(s => s.hasModifiers); // Get category for coloring (use first shortcut's category if multiple) const category = shortcuts.length > 0 ? SHORTCUT_CATEGORIES[shortcuts[0]] : null; @@ -193,7 +192,7 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap onClick={() => onKeySelect?.(keyDef.key)} className={cn( "relative flex flex-col items-center justify-center rounded-lg border transition-all", - "h-12 min-w-[2.75rem] py-1", + "h-12 min-w-11 py-1", keyDef.width > 1 && `w-[${keyDef.width * 2.75}rem]`, // Base styles !isBound && "bg-sidebar-accent/10 border-sidebar-border hover:bg-sidebar-accent/20", diff --git a/app/src/components/views/settings-view/keyboard-shortcuts/keyboard-shortcuts-section.tsx b/app/src/components/views/settings-view/keyboard-shortcuts/keyboard-shortcuts-section.tsx index 2baf9320..a1fa5e34 100644 --- a/app/src/components/views/settings-view/keyboard-shortcuts/keyboard-shortcuts-section.tsx +++ b/app/src/components/views/settings-view/keyboard-shortcuts/keyboard-shortcuts-section.tsx @@ -1,5 +1,4 @@ import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; import { Settings2, Keyboard } from "lucide-react"; interface KeyboardShortcutsSectionProps { diff --git a/app/src/components/views/setup-view.tsx b/app/src/components/views/setup-view.tsx index bcad97f4..cac5e1f1 100644 --- a/app/src/components/views/setup-view.tsx +++ b/app/src/components/views/setup-view.tsx @@ -780,6 +780,22 @@ function CodexSetupStep({ const [apiKey, setApiKey] = useState(""); const [isSavingKey, setIsSavingKey] = useState(false); + // Normalize CLI auth method strings to our store-friendly values + const mapAuthMethod = (method?: string): CodexAuthStatus["method"] => { + switch (method) { + case "cli_verified": + return "cli_verified"; + case "cli_tokens": + return "cli_tokens"; + case "auth_file": + return "api_key"; + case "env_var": + return "env"; + default: + return "none"; + } + }; + const checkStatus = useCallback(async () => { console.log("[Codex Setup] Starting status check..."); setIsChecking(true); @@ -805,13 +821,7 @@ function CodexSetupStep({ setCodexCliStatus(cliStatus); if (result.auth) { - const method = result.auth.method === "cli_verified" || result.auth.method === "cli_tokens" - ? (result.auth.method === "cli_verified" ? "cli_verified" : "cli_tokens") - : result.auth.method === "auth_file" - ? "api_key" - : result.auth.method === "env_var" - ? "env" - : "none"; + const method = mapAuthMethod(result.auth.method); const authStatus: CodexAuthStatus = { authenticated: result.auth.authenticated, diff --git a/app/src/hooks/use-window-state.ts b/app/src/hooks/use-window-state.ts index 3647b892..a54bafd7 100644 --- a/app/src/hooks/use-window-state.ts +++ b/app/src/hooks/use-window-state.ts @@ -53,3 +53,4 @@ export function useWindowState(): WindowState { } + diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index acaf0bc5..21d88563 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -50,6 +50,7 @@ export function parseShortcut(shortcut: string): ShortcutKey { const parts = shortcut.split("+").map(p => p.trim()); const result: ShortcutKey = { key: parts[parts.length - 1] }; + // Normalize common OS-specific modifiers (Cmd/Ctrl/Win/Super symbols) into cmdCtrl for (let i = 0; i < parts.length - 1; i++) { const modifier = parts[i].toLowerCase(); if (modifier === "shift") result.shift = true; @@ -65,13 +66,19 @@ export function formatShortcut(shortcut: string, forDisplay = false): string { const parsed = parseShortcut(shortcut); const parts: string[] = []; - // Improved OS detection - let platform: 'darwin' | 'win32' | 'linux' = 'linux'; - if (typeof navigator !== 'undefined') { - const p = navigator.platform.toLowerCase(); - if (p.includes('mac')) platform = 'darwin'; - else if (p.includes('win')) platform = 'win32'; - } + // Prefer User-Agent Client Hints when available; fall back to legacy + const platform: 'darwin' | 'win32' | 'linux' = (() => { + if (typeof navigator === 'undefined') return 'linux'; + + const uaPlatform = (navigator as Navigator & { userAgentData?: { platform?: string } }) + .userAgentData?.platform?.toLowerCase?.(); + const legacyPlatform = navigator.platform?.toLowerCase?.(); + const platformString = uaPlatform || legacyPlatform || ''; + + if (platformString.includes('mac')) return 'darwin'; + if (platformString.includes('win')) return 'win32'; + return 'linux'; + })(); // Primary modifier - OS-specific if (parsed.cmdCtrl) {