Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29d6c8cb14 | |||
| 01295eff99 | |||
| a41e5a0eba | |||
| 5ed85e86ab | |||
| 071379a109 |
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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