diff --git a/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts
index c4f78c2f75..97e6c190b6 100644
--- a/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts
+++ b/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts
@@ -9,6 +9,7 @@ import { fileWatcher } from '../../file-watcher';
import { findTaskAndProject } from './shared';
import { checkGitStatus } from '../../project-initializer';
import { getClaudeProfileManager } from '../../claude-profile-manager';
+import { taskLogService } from '../../task-log-service';
/**
* Helper function to check subtask completion status
@@ -185,6 +186,20 @@ export function registerTaskExecutionHandlers(
agentManager.killTask(taskId);
fileWatcher.unwatch(taskId);
+ // Update phase logs to mark active phases as stopped
+ // Wrapped in try-catch to ensure task stopping still works even if log update fails
+ try {
+ const { task, project } = findTaskAndProject(taskId);
+ if (task && project) {
+ // Mark any active phases as stopped in the log files
+ const specsBaseDir = getSpecsDir(project.autoBuildPath);
+ taskLogService.markActivePhasesStopped(task.specId, project.path, specsBaseDir);
+ }
+ } catch (error) {
+ // Log error but don't prevent task from stopping
+ console.error('[TASK_STOP] Failed to update phase logs:', error);
+ }
+
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.webContents.send(
diff --git a/apps/frontend/src/main/task-log-service.ts b/apps/frontend/src/main/task-log-service.ts
index 9ad2569649..16bfedb377 100644
--- a/apps/frontend/src/main/task-log-service.ts
+++ b/apps/frontend/src/main/task-log-service.ts
@@ -1,5 +1,5 @@
import path from 'path';
-import { existsSync, readFileSync, watchFile } from 'fs';
+import { existsSync, readFileSync, writeFileSync, watchFile, renameSync, unlinkSync } from 'fs';
import { EventEmitter } from 'events';
import type { TaskLogs, TaskLogPhase, TaskLogStreamChunk, TaskPhaseLog } from '../shared/types';
@@ -376,6 +376,106 @@ export class TaskLogService extends EventEmitter {
const logFile = path.join(specDir, 'task_logs.json');
return existsSync(logFile);
}
+
+ /**
+ * Mark all active phases as stopped when a task is interrupted
+ * Updates both main and worktree log files if they exist
+ *
+ * @param specId - The spec ID
+ * @param projectPath - Project root path (needed to find worktree)
+ * @param specsRelPath - Relative path to specs (e.g., ".auto-claude/specs")
+ */
+ markActivePhasesStopped(specId: string, projectPath: string, specsRelPath: string): void {
+ // Validate specId to prevent path traversal attacks
+ if (!this.isValidSpecId(specId)) {
+ console.error(`[TaskLogService] Invalid specId format: ${specId}`);
+ return;
+ }
+
+ // Get watched paths for this spec
+ const watchedInfo = this.watchedPaths.get(specId);
+
+ // Build paths for main and worktree spec directories
+ const mainSpecDir = watchedInfo?.mainSpecDir || path.join(projectPath, specsRelPath, specId);
+ const worktreeSpecDir = watchedInfo?.worktreeSpecDir || path.join(projectPath, '.worktrees', specId, specsRelPath, specId);
+
+ // Update main spec dir logs
+ this.updateLogFilePhaseStatus(path.join(mainSpecDir, 'task_logs.json'));
+
+ // Update worktree spec dir logs if it exists
+ if (existsSync(worktreeSpecDir)) {
+ this.updateLogFilePhaseStatus(path.join(worktreeSpecDir, 'task_logs.json'));
+ }
+
+ // Reload and emit updated logs
+ const updatedLogs = this.loadLogs(mainSpecDir, projectPath, specsRelPath, specId);
+ if (updatedLogs) {
+ this.logCache.set(mainSpecDir, updatedLogs);
+ this.emit('logs-changed', specId, updatedLogs);
+ }
+ }
+
+ /**
+ * Update a single log file to mark active phases as stopped
+ * Uses atomic write (temp file + rename) to prevent corruption
+ */
+ private updateLogFilePhaseStatus(logFilePath: string): void {
+ try {
+ // Read file - if it doesn't exist, readFileSync will throw and we'll catch it
+ const content = readFileSync(logFilePath, 'utf-8');
+ const logs = JSON.parse(content) as TaskLogs;
+
+ let hasChanges = false;
+ const phases: TaskLogPhase[] = ['planning', 'coding', 'validation'];
+
+ for (const phase of phases) {
+ if (logs.phases[phase]?.status === 'active') {
+ // Mark as stopped since the task was stopped by the user
+ logs.phases[phase].status = 'stopped';
+ logs.phases[phase].completed_at = new Date().toISOString();
+ hasChanges = true;
+ }
+ }
+
+ // Only write if we made changes
+ if (hasChanges) {
+ logs.updated_at = new Date().toISOString();
+
+ // Use atomic write: temp file + rename to prevent corruption
+ const tempFilePath = `${logFilePath}.tmp`;
+ try {
+ writeFileSync(tempFilePath, JSON.stringify(logs, null, 2), 'utf-8');
+ renameSync(tempFilePath, logFilePath);
+ console.log(`[TaskLogService] Marked active phases as stopped in ${logFilePath}`);
+ } catch (writeError) {
+ // Clean up temp file if it exists
+ if (existsSync(tempFilePath)) {
+ unlinkSync(tempFilePath);
+ }
+ throw writeError;
+ }
+ }
+ } catch (error) {
+ // File doesn't exist, is corrupted, or write failed - log and continue
+ console.error(`[TaskLogService] Failed to update log file ${logFilePath}:`, error);
+ }
+ }
+
+ /**
+ * Validate specId format to prevent path traversal attacks
+ * Valid format: 3 digits followed by dash and alphanumeric characters with dashes
+ * Example: "001-feature-name" or "123-bug-fix"
+ */
+ private isValidSpecId(specId: string): boolean {
+ // Check for path traversal patterns
+ if (specId.includes('..') || specId.includes('/') || specId.includes('\\')) {
+ return false;
+ }
+
+ // Validate format: XXX-name (e.g., 001-feature, 123-bug-fix)
+ const specIdPattern = /^[0-9]{3}-[a-z0-9-]+$/;
+ return specIdPattern.test(specId);
+ }
}
// Singleton instance
diff --git a/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx b/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx
index 3481b263f0..ce46c0dc12 100644
--- a/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx
+++ b/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx
@@ -1,4 +1,5 @@
import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
import {
Terminal,
Loader2,
@@ -175,6 +176,7 @@ interface PhaseLogSectionProps {
}
function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, phaseConfig }: PhaseLogSectionProps) {
+ const { t } = useTranslation('taskLogs');
const Icon = PHASE_ICONS[phase];
const status = phaseLog?.status || 'pending';
const hasEntries = (phaseLog?.entries.length || 0) > 0;
@@ -186,34 +188,41 @@ function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, p
return (
- Interrupted
+ {t('status.interrupted')}
);
}
return (
- Running
+ {t('status.active')}
);
case 'completed':
return (
- Complete
+ {t('status.completed')}
);
case 'failed':
return (
- Failed
+ {t('status.failed')}
+
+ );
+ case 'stopped':
+ return (
+
+
+ {t('status.stopped')}
);
default:
return (
- Pending
+ {t('status.pending')}
);
}
@@ -232,6 +241,7 @@ function PhaseLogSection({ phase, phaseLog, isExpanded, onToggle, isTaskStuck, p
isInterrupted && 'border-warning/30 bg-warning/5',
status === 'completed' && 'border-success/30 bg-success/5',
status === 'failed' && 'border-destructive/30 bg-destructive/5',
+ status === 'stopped' && 'border-muted-foreground/30 bg-muted/5',
status === 'pending' && 'border-border bg-secondary/30'
)}
>
diff --git a/apps/frontend/src/shared/i18n/index.ts b/apps/frontend/src/shared/i18n/index.ts
index 407d8d4c75..a3a6283142 100644
--- a/apps/frontend/src/shared/i18n/index.ts
+++ b/apps/frontend/src/shared/i18n/index.ts
@@ -10,6 +10,7 @@ import enWelcome from './locales/en/welcome.json';
import enOnboarding from './locales/en/onboarding.json';
import enDialogs from './locales/en/dialogs.json';
import enTaskReview from './locales/en/taskReview.json';
+import enTaskLogs from './locales/en/taskLogs.json';
// Import French translation resources
import frCommon from './locales/fr/common.json';
@@ -20,6 +21,7 @@ import frWelcome from './locales/fr/welcome.json';
import frOnboarding from './locales/fr/onboarding.json';
import frDialogs from './locales/fr/dialogs.json';
import frTaskReview from './locales/fr/taskReview.json';
+import frTaskLogs from './locales/fr/taskLogs.json';
export const defaultNS = 'common';
@@ -32,7 +34,8 @@ export const resources = {
welcome: enWelcome,
onboarding: enOnboarding,
dialogs: enDialogs,
- taskReview: enTaskReview
+ taskReview: enTaskReview,
+ taskLogs: enTaskLogs
},
fr: {
common: frCommon,
@@ -42,7 +45,8 @@ export const resources = {
welcome: frWelcome,
onboarding: frOnboarding,
dialogs: frDialogs,
- taskReview: frTaskReview
+ taskReview: frTaskReview,
+ taskLogs: frTaskLogs
}
} as const;
@@ -53,7 +57,7 @@ i18n
lng: 'en', // Default language (will be overridden by settings)
fallbackLng: 'en',
defaultNS,
- ns: ['common', 'navigation', 'settings', 'tasks', 'welcome', 'onboarding', 'dialogs', 'taskReview'],
+ ns: ['common', 'navigation', 'settings', 'tasks', 'welcome', 'onboarding', 'dialogs', 'taskReview', 'taskLogs'],
interpolation: {
escapeValue: false // React already escapes values
},
diff --git a/apps/frontend/src/shared/i18n/locales/en/taskLogs.json b/apps/frontend/src/shared/i18n/locales/en/taskLogs.json
new file mode 100644
index 0000000000..bd005772af
--- /dev/null
+++ b/apps/frontend/src/shared/i18n/locales/en/taskLogs.json
@@ -0,0 +1,10 @@
+{
+ "status": {
+ "pending": "Pending",
+ "active": "Running",
+ "completed": "Complete",
+ "failed": "Failed",
+ "stopped": "Stopped",
+ "interrupted": "Interrupted"
+ }
+}
diff --git a/apps/frontend/src/shared/i18n/locales/fr/taskLogs.json b/apps/frontend/src/shared/i18n/locales/fr/taskLogs.json
new file mode 100644
index 0000000000..72dc9840b7
--- /dev/null
+++ b/apps/frontend/src/shared/i18n/locales/fr/taskLogs.json
@@ -0,0 +1,10 @@
+{
+ "status": {
+ "pending": "En attente",
+ "active": "En cours",
+ "completed": "Terminé",
+ "failed": "Échoué",
+ "stopped": "Arrêté",
+ "interrupted": "Interrompu"
+ }
+}
diff --git a/apps/frontend/src/shared/types/task.ts b/apps/frontend/src/shared/types/task.ts
index 42c9e61a61..eb0c681c86 100644
--- a/apps/frontend/src/shared/types/task.ts
+++ b/apps/frontend/src/shared/types/task.ts
@@ -58,7 +58,7 @@ export interface QAIssue {
// Task Log Types - for persistent, phase-based logging
export type TaskLogPhase = 'planning' | 'coding' | 'validation';
-export type TaskLogPhaseStatus = 'pending' | 'active' | 'completed' | 'failed';
+export type TaskLogPhaseStatus = 'pending' | 'active' | 'completed' | 'failed' | 'stopped';
export type TaskLogEntryType = 'text' | 'tool_start' | 'tool_end' | 'phase_start' | 'phase_end' | 'error' | 'success' | 'info';
export interface TaskLogEntry {