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:
@@ -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 =>
|
||||
|
||||
@@ -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 }>>;
|
||||
|
||||
Reference in New Issue
Block a user