Compare commits

...

5 Commits

Author SHA1 Message Date
Test User 29d6c8cb14 fix: enhance execution phase handling in ProjectStore
- Added logic to correct stale execution phases when a task reaches a terminal status (human_review, done, pr_created) but the persisted executionPhase is outdated.
- Improved the determination of execution progress to ensure accurate representation of task status, particularly for tasks that have transitioned from running to completed states.

These changes improve the reliability of task status updates and enhance user experience by ensuring accurate visual feedback on task execution phases.
2026-02-05 22:18:43 +01:00
Test User 01295eff99 adjust for claude mem 2026-02-05 22:18:25 +01:00
Test User a41e5a0eba fix: improve execution phase handling in ProjectStore and TaskCard components
- Updated ProjectStore to prioritize xstateState for execution phase determination, ensuring accurate task status representation.
- Enhanced TaskCard to only display execution phase badges for actively running tasks, preventing stale badges from appearing for recovered tasks.

These changes enhance the reliability of task status updates and improve user experience by ensuring accurate visual feedback on task execution phases.
2026-02-05 20:34:56 +01:00
Test User 5ed85e86ab 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.
2026-02-05 20:10:54 +01:00
Test User 071379a109 feat: enhance task management by implementing maxParallelTasks limit in startTask function
- Updated startTask function to respect maxParallelTasks setting, queuing tasks if the limit is reached.
- Integrated project store to retrieve project-specific settings.
- Added logic to count current in-progress tasks and conditionally queue tasks based on the maxParallelTasks configuration.

This change improves task handling efficiency and prevents overload during concurrent task execution.
2026-02-05 19:47:34 +01:00
11 changed files with 117 additions and 33 deletions
+11
View File
@@ -40,6 +40,17 @@ Auto Claude is a desktop application (+ CLI) where users describe a goal and AI
**PR target** — Always target the `develop` branch for PRs to AndyMik90/Auto-Claude, NOT `main`.
## Memory (claude-mem)
This project uses claude-mem for persistent memory across sessions. MCP search tools are available — use them proactively:
- **Before modifying code** — Search for past work on the same files/features: `search(query="<file or feature>")`. Check if there are known bugs, decisions, or patterns to follow.
- **When debugging** — Search for prior encounters with the same error or symptom: `search(query="<error message>", type="bugfix")`.
- **When making architectural decisions** — Check for past decisions: `search(query="<topic>", type="decision")`.
- **When resuming work** — Use `timeline(anchor=<recent_id>)` to understand where things left off.
Follow the 3-layer workflow: `search` (cheap index) → `timeline` (context) → `get_observations` (full details only for relevant IDs). Never fetch full details without filtering first.
## Project Structure
```
@@ -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);
+26 -9
View File
@@ -4,6 +4,7 @@ import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import type { Project, ProjectSettings, Task, TaskStatus, TaskMetadata, ImplementationPlan, ReviewReason, PlanSubtask, KanbanPreferences, ExecutionPhase } from '../shared/types';
import { DEFAULT_PROJECT_SETTINGS, AUTO_BUILD_PATHS, getSpecsDir, JSON_ERROR_PREFIX, JSON_ERROR_TITLE_SUFFIX, TASK_STATUS_PRIORITY } from '../shared/constants';
import { XSTATE_SETTLED_STATES } from '../shared/state-machines';
import { getAutoBuildPath, isInitialized } from './project-initializer';
import { getTaskWorktreeDir } from './worktree-paths';
import { findAllSpecPaths } from './utils/spec-path-helpers';
@@ -341,8 +342,6 @@ export class ProjectStore {
const newIsMain = task.location === 'main';
if (existingIsMain && !newIsMain) {
// Main wins, keep existing
continue;
} else if (!existingIsMain && newIsMain) {
// New is main, replace existing worktree
taskMap.set(task.id, task);
@@ -527,15 +526,33 @@ export class ProjectStore {
}
}
// Use persisted executionPhase (from text parser) or xstateState for exact restoration
// Priority: executionPhase > xstateState > inferred from status
// Use xstateState as the authoritative source for execution phase when available.
// Only fall back to persistedPhase for active running states where xstateState
// might not reflect the detailed phase yet (e.g., planning vs coding within in_progress).
// Priority: xstateState (when settled) > persistedPhase (when running) > inferred from status
const persistedPhase = (plan as { executionPhase?: string } | null)?.executionPhase as ExecutionPhase | undefined;
const xstateState = (plan as { xstateState?: string } | null)?.xstateState;
const executionProgress = persistedPhase
? { phase: persistedPhase, phaseProgress: 50, overallProgress: 50 }
: xstateState
? this.inferExecutionProgressFromXState(xstateState)
: this.inferExecutionProgress(plan?.status);
// XState settled states take priority - these override any stale persisted phase
// because the task is no longer actively running
const isSettledState = xstateState && XSTATE_SETTLED_STATES.has(xstateState);
// Status-based override: if the task has reached a terminal status (human_review, done, pr_created)
// but the persisted executionPhase is stale (e.g. still "planning"), correct it.
// This happens when the XState machine didn't persist the final phase before being reset to backlog.
const STATUS_IMPLIES_COMPLETE: ReadonlySet<string> = new Set(['human_review', 'done', 'pr_created']);
const phaseIsStale = persistedPhase && persistedPhase !== 'complete' && persistedPhase !== 'failed'
&& STATUS_IMPLIES_COMPLETE.has(finalStatus);
const executionProgress = isSettledState
? this.inferExecutionProgressFromXState(xstateState) // Use XState for settled states
: phaseIsStale
? { phase: 'complete' as ExecutionPhase, phaseProgress: 100, overallProgress: 100 } // Fix stale phase
: persistedPhase
? { phase: persistedPhase, phaseProgress: 50, overallProgress: 50 } // Use persisted for running
: xstateState
? this.inferExecutionProgressFromXState(xstateState)
: this.inferExecutionProgress(plan?.status);
tasks.push({
id: dir.name, // Use spec directory name as ID
@@ -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
@@ -138,7 +138,9 @@ export const TaskCard = memo(function TaskCard({
const isRunning = task.status === 'in_progress';
const executionPhase = task.executionProgress?.phase;
const hasActiveExecution = executionPhase && executionPhase !== 'idle' && executionPhase !== 'complete' && executionPhase !== 'failed';
// Only show execution phase badge if task is actually running AND has an active phase
// This prevents showing stale "Planning" badges for tasks that were recovered or have inconsistent state
const hasActiveExecution = isRunning && executionPhase && executionPhase !== 'idle' && executionPhase !== 'complete' && executionPhase !== 'failed';
// Check if task is in human_review but has no completed subtasks (crashed/incomplete)
const isIncomplete = isIncompleteHumanReview(task);
@@ -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' : ''}`} />
@@ -2,6 +2,7 @@ import { create } from 'zustand';
import { arrayMove } from '@dnd-kit/sortable';
import type { Task, TaskStatus, SubtaskStatus, ImplementationPlan, Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase, ReviewReason, TaskDraft, ImageAttachment, TaskOrderState } from '../../shared/types';
import { debugLog } from '../../shared/utils/debug-logger';
import { useProjectStore } from './project-store';
interface TaskState {
tasks: Task[];
@@ -659,8 +660,37 @@ export async function createTask(
/**
* Start a task
* Respects maxParallelTasks limit - queues task if at capacity
*/
export function startTask(taskId: string, options?: { parallel?: boolean; workers?: number }): void {
export async function startTask(taskId: string, options?: { parallel?: boolean; workers?: number }): Promise<void> {
const store = useTaskStore.getState();
const projectStore = useProjectStore.getState();
// Find the task to get its projectId
const task = store.tasks.find(t => t.id === taskId || t.specId === taskId);
if (!task) {
console.warn('[startTask] Task not found:', taskId);
window.electronAPI.startTask(taskId, options);
return;
}
// Get project and maxParallelTasks setting (capped at 10)
const project = projectStore.projects.find(p => p.id === task.projectId);
const maxParallelTasks = Math.min(project?.settings?.maxParallelTasks ?? 3, 10);
// Count current in-progress tasks (excluding archived)
const inProgressCount = store.tasks.filter(t =>
t.status === 'in_progress' && !t.metadata?.archivedAt
).length;
// If at capacity, queue the task instead of starting
if (inProgressCount >= maxParallelTasks) {
console.log(`[startTask] In Progress full (${inProgressCount}/${maxParallelTasks}), moving task ${taskId} to Queue`);
await persistTaskStatus(taskId, 'queue');
return;
}
// Below capacity - start the task normally
window.electronAPI.startTask(taskId, options);
}
+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 }>>;