feat: enhance Claude Code version checking with force refresh option

- Updated checkClaudeCodeVersion API to accept an optional forceRefresh parameter, allowing users to bypass the cache and fetch fresh data from npm.
- Modified related components to support the new parameter, enabling a manual refresh of the Claude Code version.
- Improved user experience by providing immediate feedback on version checks, especially when the user explicitly requests a refresh.

This change enhances the flexibility of version management for the Claude Code CLI.
This commit is contained in:
Test User
2026-02-05 20:10:54 +01:00
parent 071379a109
commit 5ed85e86ab
8 changed files with 48 additions and 24 deletions
@@ -216,10 +216,12 @@ async function scanClaudeInstallations(activePath: string | null): Promise<Claud
* @param currentInstalled - Optional currently installed version. If provided and newer than
* cached latest, cache will be invalidated and fresh data fetched.
* This handles the case where CLI was updated while app was running.
* @param forceRefresh - If true, bypasses the cache and fetches fresh data from npm.
* Use this when the user explicitly clicks a "Refresh" button.
*/
async function fetchLatestVersion(currentInstalled?: string | null): Promise<string> {
// Check cache first
if (cachedLatestVersion && Date.now() - cachedLatestVersion.timestamp < CACHE_DURATION_MS) {
async function fetchLatestVersion(currentInstalled?: string | null, forceRefresh?: boolean): Promise<string> {
// Check cache first (unless force refresh is requested)
if (!forceRefresh && cachedLatestVersion && Date.now() - cachedLatestVersion.timestamp < CACHE_DURATION_MS) {
const cachedVersion = cachedLatestVersion.version;
// Invalidate cache if installed version is newer than cached latest
@@ -958,9 +960,9 @@ export function registerClaudeCodeHandlers(): void {
// Check Claude Code version
ipcMain.handle(
IPC_CHANNELS.CLAUDE_CODE_CHECK_VERSION,
async (): Promise<IPCResult<ClaudeCodeVersionInfo>> => {
async (_event, forceRefresh?: boolean): Promise<IPCResult<ClaudeCodeVersionInfo>> => {
try {
console.warn('[Claude Code] Checking version...');
console.warn('[Claude Code] Checking version...', forceRefresh ? '(force refresh)' : '');
// Get installed version via cli-tool-manager
let detectionResult;
@@ -977,10 +979,11 @@ export function registerClaudeCodeHandlers(): void {
// Fetch latest version from npm
// Pass installed version to invalidate cache if installed > cached (handles CLI update while app running)
// Pass forceRefresh to bypass cache when user explicitly clicks Refresh
let latest: string;
try {
console.warn('[Claude Code] Fetching latest version from npm...');
latest = await fetchLatestVersion(installed);
latest = await fetchLatestVersion(installed, forceRefresh);
console.warn('[Claude Code] Latest version:', latest);
} catch (error) {
console.warn('[Claude Code] Failed to fetch latest version, continuing with unknown:', error);
@@ -123,7 +123,14 @@ export class TaskStateManager {
} else if (!currentState && task.reviewReason === 'plan_review') {
// Fallback: No actor exists (e.g., after app restart), use task data
this.handleUiEvent(taskId, { type: 'PLAN_APPROVED' }, task, project);
} else if (currentState === 'backlog' || !currentState) {
// Fresh start from backlog or no actor - send PLANNING_STARTED
// USER_RESUMED only works from human_review/error states
this.handleUiEvent(taskId, { type: 'PLANNING_STARTED' }, task, project);
} else {
// Already in a running state (planning, coding, qa_*) - send USER_RESUMED
// Note: USER_RESUMED may be ignored if state doesn't handle it, but that's OK
// since the task is already running
this.handleUiEvent(taskId, { type: 'USER_RESUMED' }, task, project);
}
return true;
@@ -80,8 +80,9 @@ export interface ClaudeCodeAPI {
/**
* Check Claude Code CLI version status
* Returns installed version, latest version, and whether update is available
* @param forceRefresh - If true, bypasses the 24-hour cache and fetches fresh data from npm
*/
checkClaudeCodeVersion: () => Promise<ClaudeCodeVersionResult>;
checkClaudeCodeVersion: (forceRefresh?: boolean) => Promise<ClaudeCodeVersionResult>;
/**
* Install or update Claude Code CLI
@@ -118,8 +119,8 @@ export interface ClaudeCodeAPI {
* Creates the Claude Code API implementation
*/
export const createClaudeCodeAPI = (): ClaudeCodeAPI => ({
checkClaudeCodeVersion: (): Promise<ClaudeCodeVersionResult> =>
invokeIpc(IPC_CHANNELS.CLAUDE_CODE_CHECK_VERSION),
checkClaudeCodeVersion: (forceRefresh?: boolean): Promise<ClaudeCodeVersionResult> =>
invokeIpc(IPC_CHANNELS.CLAUDE_CODE_CHECK_VERSION, forceRefresh),
installClaudeCode: (): Promise<ClaudeCodeInstallResult> =>
invokeIpc(IPC_CHANNELS.CLAUDE_CODE_INSTALL),
@@ -74,14 +74,14 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
const [showPathChangeWarning, setShowPathChangeWarning] = useState(false);
// Check Claude Code version
const checkVersion = useCallback(async () => {
const checkVersion = useCallback(async (forceRefresh = false) => {
try {
if (!window.electronAPI?.checkClaudeCodeVersion) {
setStatus("error");
return;
}
const result = await window.electronAPI.checkClaudeCodeVersion();
const result = await window.electronAPI.checkClaudeCodeVersion(forceRefresh);
if (result.success && result.data) {
setVersionInfo(result.data);
@@ -486,7 +486,7 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
variant="outline"
size="sm"
className="gap-1"
onClick={() => checkVersion()}
onClick={() => checkVersion(true)}
disabled={status === "loading"}
>
<RefreshCw className={cn("h-3 w-3", status === "loading" && "animate-spin")} />
@@ -109,8 +109,11 @@ export const PhaseProgressIndicator = memo(function PhaseProgressIndicator({
// Determine if we should show indeterminate (activity) vs determinate (%) progress
const isIndeterminatePhase = phase === 'planning' || phase === 'qa_review' || phase === 'qa_fixing';
// Show subtask progress whenever subtasks exist (stops pulsing animation when spec completes)
const showSubtaskProgress = totalSubtasks > 0;
// During coding phase with subtasks but none completed yet, prefer phaseProgress over 0%
// This gives users feedback that work is happening before the first subtask completes
const isCodingWithNoProgress = phase === 'coding' && totalSubtasks > 0 && completedSubtasks === 0;
// Show subtask progress when subtasks exist AND at least one is completed (or not actively coding)
const showSubtaskProgress = totalSubtasks > 0 && !isCodingWithNoProgress;
const colors = PHASE_COLORS[phase] || PHASE_COLORS.idle;
const phaseLabel = t(PHASE_LABEL_KEYS[phase] || PHASE_LABEL_KEYS.idle);
@@ -124,8 +127,8 @@ export const PhaseProgressIndicator = memo(function PhaseProgressIndicator({
<span className="text-xs text-muted-foreground">
{isStuck ? t('execution.labels.interrupted') : showSubtaskProgress ? t('execution.labels.progress') : phaseLabel}
</span>
{/* Activity indicator dot for non-coding phases - only animate when visible */}
{isRunning && !isStuck && isIndeterminatePhase && (
{/* Activity indicator dot - shows for planning/QA and early coding phases */}
{isRunning && !isStuck && (isIndeterminatePhase || isCodingWithNoProgress) && (
<motion.div
className={cn('h-1.5 w-1.5 rounded-full', colors.color)}
animate={shouldAnimate ? {
@@ -147,7 +150,7 @@ export const PhaseProgressIndicator = memo(function PhaseProgressIndicator({
<span className="text-muted-foreground">
{activeEntries} {activeEntries === 1 ? t('execution.labels.entry') : t('execution.labels.entries')}
</span>
) : isRunning && isIndeterminatePhase && (phaseProgress ?? 0) > 0 ? (
) : isRunning && (isIndeterminatePhase || isCodingWithNoProgress) && (phaseProgress ?? 0) > 0 ? (
`${Math.round(Math.min(phaseProgress!, 100))}%`
) : (
'—'
@@ -173,7 +176,7 @@ export const PhaseProgressIndicator = memo(function PhaseProgressIndicator({
transition={isVisible ? { duration: 2, repeat: Infinity, ease: 'easeInOut' } : undefined}
/>
) : showSubtaskProgress ? (
// Determinate progress for coding phase
// Determinate progress for coding phase with completed subtasks
<motion.div
key="determinate"
className={cn('h-full rounded-full', colors.color)}
@@ -181,6 +184,15 @@ export const PhaseProgressIndicator = memo(function PhaseProgressIndicator({
animate={{ width: `${subtaskProgress}%` }}
transition={{ duration: 0.5, ease: 'easeOut' }}
/>
) : isCodingWithNoProgress && (phaseProgress ?? 0) > 0 ? (
// Coding phase with subtasks but none completed - show phaseProgress
<motion.div
key="coding-phase-progress"
className={cn('h-full rounded-full', colors.color)}
initial={{ width: 0 }}
animate={{ width: `${Math.min(phaseProgress!, 100)}%` }}
transition={{ duration: 0.5, ease: 'easeOut' }}
/>
) : shouldAnimate && isIndeterminatePhase ? (
// Indeterminate animated progress for planning/validation (only when visible)
<motion.div
@@ -28,7 +28,7 @@ export function ClaudeCodeStep({ onNext, onBack, onSkip }: ClaudeCodeStepProps)
const [installSuccess, setInstallSuccess] = useState(false);
// Check Claude Code version on mount
const checkVersion = useCallback(async () => {
const checkVersion = useCallback(async (forceRefresh = false) => {
setStatus('loading');
setError(null);
setInstallSuccess(false);
@@ -41,7 +41,7 @@ export function ClaudeCodeStep({ onNext, onBack, onSkip }: ClaudeCodeStepProps)
return;
}
const result = await window.electronAPI.checkClaudeCodeVersion();
const result = await window.electronAPI.checkClaudeCodeVersion(forceRefresh);
if (result.success && result.data) {
setVersionInfo(result.data);
@@ -217,7 +217,7 @@ export function ClaudeCodeStep({ onNext, onBack, onSkip }: ClaudeCodeStepProps)
<Button
variant="ghost"
size="sm"
onClick={checkVersion}
onClick={() => checkVersion(true)}
disabled={status === 'loading' || isInstalling}
>
<RefreshCw className={`h-4 w-4 ${status === 'loading' ? 'animate-spin' : ''}`} />
@@ -674,9 +674,9 @@ export async function startTask(taskId: string, options?: { parallel?: boolean;
return;
}
// Get project and maxParallelTasks setting
// Get project and maxParallelTasks setting (capped at 10)
const project = projectStore.projects.find(p => p.id === task.projectId);
const maxParallelTasks = project?.settings?.maxParallelTasks ?? 3;
const maxParallelTasks = Math.min(project?.settings?.maxParallelTasks ?? 3, 10);
// Count current in-progress tasks (excluding archived)
const inProgressCount = store.tasks.filter(t =>
+2 -1
View File
@@ -887,7 +887,8 @@ export interface ElectronAPI {
github: import('../../preload/api/modules/github-api').GitHubAPI;
// Claude Code CLI operations
checkClaudeCodeVersion: () => Promise<IPCResult<import('./cli').ClaudeCodeVersionInfo>>;
/** Check Claude Code CLI version. Pass forceRefresh=true to bypass the 24-hour cache. */
checkClaudeCodeVersion: (forceRefresh?: boolean) => Promise<IPCResult<import('./cli').ClaudeCodeVersionInfo>>;
installClaudeCode: () => Promise<IPCResult<{ command: string }>>;
getClaudeCodeVersions: () => Promise<IPCResult<import('./cli').ClaudeCodeVersionList>>;
installClaudeCodeVersion: (version: string) => Promise<IPCResult<{ command: string; version: string }>>;