diff --git a/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts b/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts index 7f08d489..eb2a68a0 100644 --- a/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/claude-code-handlers.ts @@ -216,10 +216,12 @@ async function scanClaudeInstallations(activePath: string | null): Promise { - // Check cache first - if (cachedLatestVersion && Date.now() - cachedLatestVersion.timestamp < CACHE_DURATION_MS) { +async function fetchLatestVersion(currentInstalled?: string | null, forceRefresh?: boolean): Promise { + // 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> => { + async (_event, forceRefresh?: boolean): Promise> => { 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); diff --git a/apps/frontend/src/main/task-state-manager.ts b/apps/frontend/src/main/task-state-manager.ts index e4a979f5..13355e87 100644 --- a/apps/frontend/src/main/task-state-manager.ts +++ b/apps/frontend/src/main/task-state-manager.ts @@ -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; diff --git a/apps/frontend/src/preload/api/modules/claude-code-api.ts b/apps/frontend/src/preload/api/modules/claude-code-api.ts index a9a4c633..a5a7ee3f 100644 --- a/apps/frontend/src/preload/api/modules/claude-code-api.ts +++ b/apps/frontend/src/preload/api/modules/claude-code-api.ts @@ -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; + checkClaudeCodeVersion: (forceRefresh?: boolean) => Promise; /** * 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 => - invokeIpc(IPC_CHANNELS.CLAUDE_CODE_CHECK_VERSION), + checkClaudeCodeVersion: (forceRefresh?: boolean): Promise => + invokeIpc(IPC_CHANNELS.CLAUDE_CODE_CHECK_VERSION, forceRefresh), installClaudeCode: (): Promise => invokeIpc(IPC_CHANNELS.CLAUDE_CODE_INSTALL), diff --git a/apps/frontend/src/renderer/components/ClaudeCodeStatusBadge.tsx b/apps/frontend/src/renderer/components/ClaudeCodeStatusBadge.tsx index 6789c854..b82a4f8c 100644 --- a/apps/frontend/src/renderer/components/ClaudeCodeStatusBadge.tsx +++ b/apps/frontend/src/renderer/components/ClaudeCodeStatusBadge.tsx @@ -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"} > diff --git a/apps/frontend/src/renderer/components/PhaseProgressIndicator.tsx b/apps/frontend/src/renderer/components/PhaseProgressIndicator.tsx index 90d4791f..ad72791b 100644 --- a/apps/frontend/src/renderer/components/PhaseProgressIndicator.tsx +++ b/apps/frontend/src/renderer/components/PhaseProgressIndicator.tsx @@ -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({ {isStuck ? t('execution.labels.interrupted') : showSubtaskProgress ? t('execution.labels.progress') : phaseLabel} - {/* 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) && ( {activeEntries} {activeEntries === 1 ? t('execution.labels.entry') : t('execution.labels.entries')} - ) : 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 + ) : isCodingWithNoProgress && (phaseProgress ?? 0) > 0 ? ( + // Coding phase with subtasks but none completed - show phaseProgress + ) : shouldAnimate && isIndeterminatePhase ? ( // Indeterminate animated progress for planning/validation (only when visible) { + 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)