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`.
|
**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
|
## 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
|
* @param currentInstalled - Optional currently installed version. If provided and newer than
|
||||||
* cached latest, cache will be invalidated and fresh data fetched.
|
* cached latest, cache will be invalidated and fresh data fetched.
|
||||||
* This handles the case where CLI was updated while app was running.
|
* 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> {
|
async function fetchLatestVersion(currentInstalled?: string | null, forceRefresh?: boolean): Promise<string> {
|
||||||
// Check cache first
|
// Check cache first (unless force refresh is requested)
|
||||||
if (cachedLatestVersion && Date.now() - cachedLatestVersion.timestamp < CACHE_DURATION_MS) {
|
if (!forceRefresh && cachedLatestVersion && Date.now() - cachedLatestVersion.timestamp < CACHE_DURATION_MS) {
|
||||||
const cachedVersion = cachedLatestVersion.version;
|
const cachedVersion = cachedLatestVersion.version;
|
||||||
|
|
||||||
// Invalidate cache if installed version is newer than cached latest
|
// Invalidate cache if installed version is newer than cached latest
|
||||||
@@ -958,9 +960,9 @@ export function registerClaudeCodeHandlers(): void {
|
|||||||
// Check Claude Code version
|
// Check Claude Code version
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.CLAUDE_CODE_CHECK_VERSION,
|
IPC_CHANNELS.CLAUDE_CODE_CHECK_VERSION,
|
||||||
async (): Promise<IPCResult<ClaudeCodeVersionInfo>> => {
|
async (_event, forceRefresh?: boolean): Promise<IPCResult<ClaudeCodeVersionInfo>> => {
|
||||||
try {
|
try {
|
||||||
console.warn('[Claude Code] Checking version...');
|
console.warn('[Claude Code] Checking version...', forceRefresh ? '(force refresh)' : '');
|
||||||
|
|
||||||
// Get installed version via cli-tool-manager
|
// Get installed version via cli-tool-manager
|
||||||
let detectionResult;
|
let detectionResult;
|
||||||
@@ -977,10 +979,11 @@ export function registerClaudeCodeHandlers(): void {
|
|||||||
|
|
||||||
// Fetch latest version from npm
|
// Fetch latest version from npm
|
||||||
// Pass installed version to invalidate cache if installed > cached (handles CLI update while app running)
|
// 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;
|
let latest: string;
|
||||||
try {
|
try {
|
||||||
console.warn('[Claude Code] Fetching latest version from npm...');
|
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);
|
console.warn('[Claude Code] Latest version:', latest);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Claude Code] Failed to fetch latest version, continuing with unknown:', 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 { v4 as uuidv4 } from 'uuid';
|
||||||
import type { Project, ProjectSettings, Task, TaskStatus, TaskMetadata, ImplementationPlan, ReviewReason, PlanSubtask, KanbanPreferences, ExecutionPhase } from '../shared/types';
|
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 { 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 { getAutoBuildPath, isInitialized } from './project-initializer';
|
||||||
import { getTaskWorktreeDir } from './worktree-paths';
|
import { getTaskWorktreeDir } from './worktree-paths';
|
||||||
import { findAllSpecPaths } from './utils/spec-path-helpers';
|
import { findAllSpecPaths } from './utils/spec-path-helpers';
|
||||||
@@ -341,8 +342,6 @@ export class ProjectStore {
|
|||||||
const newIsMain = task.location === 'main';
|
const newIsMain = task.location === 'main';
|
||||||
|
|
||||||
if (existingIsMain && !newIsMain) {
|
if (existingIsMain && !newIsMain) {
|
||||||
// Main wins, keep existing
|
|
||||||
continue;
|
|
||||||
} else if (!existingIsMain && newIsMain) {
|
} else if (!existingIsMain && newIsMain) {
|
||||||
// New is main, replace existing worktree
|
// New is main, replace existing worktree
|
||||||
taskMap.set(task.id, task);
|
taskMap.set(task.id, task);
|
||||||
@@ -527,15 +526,33 @@ export class ProjectStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use persisted executionPhase (from text parser) or xstateState for exact restoration
|
// Use xstateState as the authoritative source for execution phase when available.
|
||||||
// Priority: executionPhase > xstateState > inferred from status
|
// 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 persistedPhase = (plan as { executionPhase?: string } | null)?.executionPhase as ExecutionPhase | undefined;
|
||||||
const xstateState = (plan as { xstateState?: string } | null)?.xstateState;
|
const xstateState = (plan as { xstateState?: string } | null)?.xstateState;
|
||||||
const executionProgress = persistedPhase
|
|
||||||
? { phase: persistedPhase, phaseProgress: 50, overallProgress: 50 }
|
// XState settled states take priority - these override any stale persisted phase
|
||||||
: xstateState
|
// because the task is no longer actively running
|
||||||
? this.inferExecutionProgressFromXState(xstateState)
|
const isSettledState = xstateState && XSTATE_SETTLED_STATES.has(xstateState);
|
||||||
: this.inferExecutionProgress(plan?.status);
|
|
||||||
|
// 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({
|
tasks.push({
|
||||||
id: dir.name, // Use spec directory name as ID
|
id: dir.name, // Use spec directory name as ID
|
||||||
|
|||||||
@@ -123,7 +123,14 @@ export class TaskStateManager {
|
|||||||
} else if (!currentState && task.reviewReason === 'plan_review') {
|
} else if (!currentState && task.reviewReason === 'plan_review') {
|
||||||
// Fallback: No actor exists (e.g., after app restart), use task data
|
// Fallback: No actor exists (e.g., after app restart), use task data
|
||||||
this.handleUiEvent(taskId, { type: 'PLAN_APPROVED' }, task, project);
|
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 {
|
} 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);
|
this.handleUiEvent(taskId, { type: 'USER_RESUMED' }, task, project);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -80,8 +80,9 @@ export interface ClaudeCodeAPI {
|
|||||||
/**
|
/**
|
||||||
* Check Claude Code CLI version status
|
* Check Claude Code CLI version status
|
||||||
* Returns installed version, latest version, and whether update is available
|
* 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
|
* Install or update Claude Code CLI
|
||||||
@@ -118,8 +119,8 @@ export interface ClaudeCodeAPI {
|
|||||||
* Creates the Claude Code API implementation
|
* Creates the Claude Code API implementation
|
||||||
*/
|
*/
|
||||||
export const createClaudeCodeAPI = (): ClaudeCodeAPI => ({
|
export const createClaudeCodeAPI = (): ClaudeCodeAPI => ({
|
||||||
checkClaudeCodeVersion: (): Promise<ClaudeCodeVersionResult> =>
|
checkClaudeCodeVersion: (forceRefresh?: boolean): Promise<ClaudeCodeVersionResult> =>
|
||||||
invokeIpc(IPC_CHANNELS.CLAUDE_CODE_CHECK_VERSION),
|
invokeIpc(IPC_CHANNELS.CLAUDE_CODE_CHECK_VERSION, forceRefresh),
|
||||||
|
|
||||||
installClaudeCode: (): Promise<ClaudeCodeInstallResult> =>
|
installClaudeCode: (): Promise<ClaudeCodeInstallResult> =>
|
||||||
invokeIpc(IPC_CHANNELS.CLAUDE_CODE_INSTALL),
|
invokeIpc(IPC_CHANNELS.CLAUDE_CODE_INSTALL),
|
||||||
|
|||||||
@@ -74,14 +74,14 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
|
|||||||
const [showPathChangeWarning, setShowPathChangeWarning] = useState(false);
|
const [showPathChangeWarning, setShowPathChangeWarning] = useState(false);
|
||||||
|
|
||||||
// Check Claude Code version
|
// Check Claude Code version
|
||||||
const checkVersion = useCallback(async () => {
|
const checkVersion = useCallback(async (forceRefresh = false) => {
|
||||||
try {
|
try {
|
||||||
if (!window.electronAPI?.checkClaudeCodeVersion) {
|
if (!window.electronAPI?.checkClaudeCodeVersion) {
|
||||||
setStatus("error");
|
setStatus("error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await window.electronAPI.checkClaudeCodeVersion();
|
const result = await window.electronAPI.checkClaudeCodeVersion(forceRefresh);
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setVersionInfo(result.data);
|
setVersionInfo(result.data);
|
||||||
@@ -486,7 +486,7 @@ export function ClaudeCodeStatusBadge({ className }: ClaudeCodeStatusBadgeProps)
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="gap-1"
|
className="gap-1"
|
||||||
onClick={() => checkVersion()}
|
onClick={() => checkVersion(true)}
|
||||||
disabled={status === "loading"}
|
disabled={status === "loading"}
|
||||||
>
|
>
|
||||||
<RefreshCw className={cn("h-3 w-3", status === "loading" && "animate-spin")} />
|
<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
|
// Determine if we should show indeterminate (activity) vs determinate (%) progress
|
||||||
const isIndeterminatePhase = phase === 'planning' || phase === 'qa_review' || phase === 'qa_fixing';
|
const isIndeterminatePhase = phase === 'planning' || phase === 'qa_review' || phase === 'qa_fixing';
|
||||||
// Show subtask progress whenever subtasks exist (stops pulsing animation when spec completes)
|
// During coding phase with subtasks but none completed yet, prefer phaseProgress over 0%
|
||||||
const showSubtaskProgress = totalSubtasks > 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 colors = PHASE_COLORS[phase] || PHASE_COLORS.idle;
|
||||||
const phaseLabel = t(PHASE_LABEL_KEYS[phase] || PHASE_LABEL_KEYS.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">
|
<span className="text-xs text-muted-foreground">
|
||||||
{isStuck ? t('execution.labels.interrupted') : showSubtaskProgress ? t('execution.labels.progress') : phaseLabel}
|
{isStuck ? t('execution.labels.interrupted') : showSubtaskProgress ? t('execution.labels.progress') : phaseLabel}
|
||||||
</span>
|
</span>
|
||||||
{/* Activity indicator dot for non-coding phases - only animate when visible */}
|
{/* Activity indicator dot - shows for planning/QA and early coding phases */}
|
||||||
{isRunning && !isStuck && isIndeterminatePhase && (
|
{isRunning && !isStuck && (isIndeterminatePhase || isCodingWithNoProgress) && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={cn('h-1.5 w-1.5 rounded-full', colors.color)}
|
className={cn('h-1.5 w-1.5 rounded-full', colors.color)}
|
||||||
animate={shouldAnimate ? {
|
animate={shouldAnimate ? {
|
||||||
@@ -147,7 +150,7 @@ export const PhaseProgressIndicator = memo(function PhaseProgressIndicator({
|
|||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{activeEntries} {activeEntries === 1 ? t('execution.labels.entry') : t('execution.labels.entries')}
|
{activeEntries} {activeEntries === 1 ? t('execution.labels.entry') : t('execution.labels.entries')}
|
||||||
</span>
|
</span>
|
||||||
) : isRunning && isIndeterminatePhase && (phaseProgress ?? 0) > 0 ? (
|
) : isRunning && (isIndeterminatePhase || isCodingWithNoProgress) && (phaseProgress ?? 0) > 0 ? (
|
||||||
`${Math.round(Math.min(phaseProgress!, 100))}%`
|
`${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}
|
transition={isVisible ? { duration: 2, repeat: Infinity, ease: 'easeInOut' } : undefined}
|
||||||
/>
|
/>
|
||||||
) : showSubtaskProgress ? (
|
) : showSubtaskProgress ? (
|
||||||
// Determinate progress for coding phase
|
// Determinate progress for coding phase with completed subtasks
|
||||||
<motion.div
|
<motion.div
|
||||||
key="determinate"
|
key="determinate"
|
||||||
className={cn('h-full rounded-full', colors.color)}
|
className={cn('h-full rounded-full', colors.color)}
|
||||||
@@ -181,6 +184,15 @@ export const PhaseProgressIndicator = memo(function PhaseProgressIndicator({
|
|||||||
animate={{ width: `${subtaskProgress}%` }}
|
animate={{ width: `${subtaskProgress}%` }}
|
||||||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
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 ? (
|
) : shouldAnimate && isIndeterminatePhase ? (
|
||||||
// Indeterminate animated progress for planning/validation (only when visible)
|
// Indeterminate animated progress for planning/validation (only when visible)
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -138,7 +138,9 @@ export const TaskCard = memo(function TaskCard({
|
|||||||
|
|
||||||
const isRunning = task.status === 'in_progress';
|
const isRunning = task.status === 'in_progress';
|
||||||
const executionPhase = task.executionProgress?.phase;
|
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)
|
// Check if task is in human_review but has no completed subtasks (crashed/incomplete)
|
||||||
const isIncomplete = isIncompleteHumanReview(task);
|
const isIncomplete = isIncompleteHumanReview(task);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function ClaudeCodeStep({ onNext, onBack, onSkip }: ClaudeCodeStepProps)
|
|||||||
const [installSuccess, setInstallSuccess] = useState(false);
|
const [installSuccess, setInstallSuccess] = useState(false);
|
||||||
|
|
||||||
// Check Claude Code version on mount
|
// Check Claude Code version on mount
|
||||||
const checkVersion = useCallback(async () => {
|
const checkVersion = useCallback(async (forceRefresh = false) => {
|
||||||
setStatus('loading');
|
setStatus('loading');
|
||||||
setError(null);
|
setError(null);
|
||||||
setInstallSuccess(false);
|
setInstallSuccess(false);
|
||||||
@@ -41,7 +41,7 @@ export function ClaudeCodeStep({ onNext, onBack, onSkip }: ClaudeCodeStepProps)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await window.electronAPI.checkClaudeCodeVersion();
|
const result = await window.electronAPI.checkClaudeCodeVersion(forceRefresh);
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setVersionInfo(result.data);
|
setVersionInfo(result.data);
|
||||||
@@ -217,7 +217,7 @@ export function ClaudeCodeStep({ onNext, onBack, onSkip }: ClaudeCodeStepProps)
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={checkVersion}
|
onClick={() => checkVersion(true)}
|
||||||
disabled={status === 'loading' || isInstalling}
|
disabled={status === 'loading' || isInstalling}
|
||||||
>
|
>
|
||||||
<RefreshCw className={`h-4 w-4 ${status === 'loading' ? 'animate-spin' : ''}`} />
|
<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 { arrayMove } from '@dnd-kit/sortable';
|
||||||
import type { Task, TaskStatus, SubtaskStatus, ImplementationPlan, Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase, ReviewReason, TaskDraft, ImageAttachment, TaskOrderState } from '../../shared/types';
|
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 { debugLog } from '../../shared/utils/debug-logger';
|
||||||
|
import { useProjectStore } from './project-store';
|
||||||
|
|
||||||
interface TaskState {
|
interface TaskState {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
@@ -659,8 +660,37 @@ export async function createTask(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a task
|
* 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);
|
window.electronAPI.startTask(taskId, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -887,7 +887,8 @@ export interface ElectronAPI {
|
|||||||
github: import('../../preload/api/modules/github-api').GitHubAPI;
|
github: import('../../preload/api/modules/github-api').GitHubAPI;
|
||||||
|
|
||||||
// Claude Code CLI operations
|
// 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 }>>;
|
installClaudeCode: () => Promise<IPCResult<{ command: string }>>;
|
||||||
getClaudeCodeVersions: () => Promise<IPCResult<import('./cli').ClaudeCodeVersionList>>;
|
getClaudeCodeVersions: () => Promise<IPCResult<import('./cli').ClaudeCodeVersionList>>;
|
||||||
installClaudeCodeVersion: (version: string) => Promise<IPCResult<{ command: string; version: string }>>;
|
installClaudeCodeVersion: (version: string) => Promise<IPCResult<{ command: string; version: string }>>;
|
||||||
|
|||||||
Reference in New Issue
Block a user