refactor(settings): extract CLI status into custom hook

- 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 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-11 00:44:41 +01:00
parent 2d937bc47f
commit 6bbcc36409
2 changed files with 180 additions and 146 deletions

View File

@@ -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<HTMLDivElement>(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 (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"

View File

@@ -0,0 +1,169 @@
import { useState, useEffect, useCallback } from "react";
import { useSetupStore } from "@/store/setup-store";
import { getElectronAPI } from "@/lib/electron";
interface CliStatusResult {
success: boolean;
status?: string;
method?: string;
version?: string;
path?: string;
recommendation?: string;
installCommands?: {
macos?: string;
windows?: string;
linux?: string;
npm?: string;
};
error?: string;
}
interface CodexCliStatusResult extends CliStatusResult {
hasApiKey?: boolean;
}
/**
* Custom hook for managing Claude and Codex CLI status
* Handles checking CLI installation, authentication, and refresh functionality
*/
export function useCliStatus() {
const { setClaudeAuthStatus, setCodexAuthStatus } = useSetupStore();
const [claudeCliStatus, setClaudeCliStatus] =
useState<CliStatusResult | null>(null);
const [codexCliStatus, setCodexCliStatus] =
useState<CodexCliStatusResult | null>(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,
};
}