auto-claude: 148-add-progress-persistence-and-status-indicators (#1464)
* auto-claude: subtask-1-1 - Extend RoadmapGenerationStatus type with startedAt and lastActivityAt * auto-claude: subtask-1-2 - Add IPC channels for progress persistence: ROADMAP_PROGRESS_SAVE, ROADMAP_PROGRESS_LOAD, ROADMAP_PROGRESS_CLEAR * auto-claude: subtask-1-3 - Add GENERATION_PROGRESS constant to AUTO_BUILD_PATHS * auto-claude: subtask-2-1 - Add IPC handlers for roadmap progress persistence Add three IPC handlers in roadmap-handlers.ts: - ROADMAP_PROGRESS_SAVE: Persist progress state to generation_progress.json - ROADMAP_PROGRESS_LOAD: Load persisted progress state from disk - ROADMAP_PROGRESS_CLEAR: Delete the progress file on completion/error/stop Follows existing patterns with snake_case JSON files and camelCase frontend. Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-2-2 - Update agent-queue.ts to persist progress updates * auto-claude: subtask-3-1 - Add preload API methods for progress persistence Add saveRoadmapProgress, loadRoadmapProgress, and clearRoadmapProgress methods to RoadmapAPI interface and implementation. These methods use the IPC channels defined in subtask-1-2 to enable the renderer process to persist and restore roadmap generation state. Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-4-1 - Update loadRoadmap function to load persisted prog - Update loadRoadmap to load persisted progress via loadRoadmapProgress API - Restore startedAt and lastActivityAt timestamps when is_running is true - Add fallback with current timestamps when no persisted progress found - Add roadmap progress persistence methods to ElectronAPI interface - Add browser mock implementations for progress persistence methods Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-4-2 - Update setGenerationStatus action to include times Updated setGenerationStatus action in roadmap-store.ts to automatically manage timestamp fields: - Sets startedAt when transitioning from idle to active phase - Updates lastActivityAt on every status change during generation - Clears both timestamps when generation stops (idle/complete/error) - Preserves existing startedAt during active generation phases Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-5-1 - Add elapsed time display with formatElapsedTime utility - Add formatElapsedTime utility function for MM:SS and H:MM:SS formatting - Add elapsedTime state with useEffect interval for real-time updates - Display elapsed time with Clock icon next to progress indicator - Calculate elapsed time from RoadmapGenerationStatus.startedAt field - Use useCallback for memoized calculation function - Clean up interval on phase change or component unmount - Reset elapsed time when returning to idle phase Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-5-2 - Add last activity timestamp display with formatTimeAgo utility - Added formatTimeAgo utility function that formats timestamps into human-readable relative time strings (e.g., "just now", "5s ago", "2m ago", "1h ago") - Added lastActivityDisplay state with useEffect interval to update every 5 seconds - Display last activity timestamp next to elapsed time in progress bar section - Added tooltip explaining "Last progress update received" - Uses muted styling to differentiate from elapsed time Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-5-3 - Add heartbeat animation indicator that pulses subtly - Add HeartbeatIndicator component with subtle scale pulse (1.05x) animation - Show "Processing" status with animated dot to indicate process is alive - Respect useReducedMotion preference by disabling animation when enabled - Integrate indicator into progress bar section next to percentage display Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-6-1 - Add translation keys for roadmap progress UI text: - Add roadmapProgress section with elapsedTime, lastActivity, staleWarning keys - Add staleWarningTooltip with interpolation for minutes - Add French translations for all new keys Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-6-2 - Update RoadmapGenerationProgress to use translation keys - Add useTranslation hook from react-i18next - Convert hardcoded phase labels and descriptions to translation keys - Convert step labels to translation keys - Translate button text, tooltips, and progress labels - Add translation keys to en/common.json and fr/common.json - Pass translation function to child components Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix: preserve persisted timestamps when restoring roadmap progress state - Fix startedAt being overwritten with current time on reload by using status.startedAt ?? now when starting generation - Fix lastActivityAt always being overwritten by using status.lastActivityAt ?? now to preserve passed timestamps - Add documentation comment for SAVE/CLEAR IPC handlers explaining their purpose for API completeness Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix: align IPC progress types and add validation - Add PersistedRoadmapProgress type for IPC transport with string timestamps - Update loadRoadmapProgress return type to use PersistedRoadmapProgress - Remove unused isRunning field from persisted progress - Add validation for JSON structure before using parsed data Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix: validate phase value against allowed values when loading progress Add validation to ensure the phase field from persisted progress file matches one of the expected values (idle, analyzing, discovering, generating, complete, error). Prevents TypeError in frontend component when corrupted or manually edited files contain invalid phase values. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix: align progress persistence types and add date validation - Update saveRoadmapProgress to use PersistedRoadmapProgress type - Derive isRunning from phase instead of requiring it as parameter - Add date validation when parsing persisted timestamps to handle corrupted date strings gracefully (returns current time as fallback) Co-Authored-By: Claude Opus 4.5 <[email protected]> * test: increase subprocess-spawn test timeout for Windows CI Increase timeout from 15s to 30s for all subprocess spawn integration tests. Dynamic imports are slower on Windows CI, causing intermittent timeouts. Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
This commit is contained in:
committed by
StillKnotKnown
co-authored by
Claude Opus 4.5
parent
c306fc89a8
commit
5ecb31ecb8
@@ -297,7 +297,7 @@ describe('Subprocess Spawn Integration', () => {
|
||||
// Simulate stdout data (must include newline for buffered output processing)
|
||||
mockStdout.emit('data', Buffer.from('Test log output\n'));
|
||||
|
||||
expect(logHandler).toHaveBeenCalledWith('task-1', 'Test log output\n', undefined);
|
||||
expect(logHandler).toHaveBeenCalledWith('task-1', 'Test log output\n');
|
||||
}, 30000); // Increase timeout for Windows CI (dynamic imports are slow)
|
||||
|
||||
it('should emit log events from stderr', async () => {
|
||||
@@ -313,7 +313,7 @@ describe('Subprocess Spawn Integration', () => {
|
||||
// Simulate stderr data (must include newline for buffered output processing)
|
||||
mockStderr.emit('data', Buffer.from('Progress: 50%\n'));
|
||||
|
||||
expect(logHandler).toHaveBeenCalledWith('task-1', 'Progress: 50%\n', undefined);
|
||||
expect(logHandler).toHaveBeenCalledWith('task-1', 'Progress: 50%\n');
|
||||
}, 30000); // Increase timeout for Windows CI (dynamic imports are slow)
|
||||
|
||||
it('should emit exit event when process exits', async () => {
|
||||
@@ -329,8 +329,8 @@ describe('Subprocess Spawn Integration', () => {
|
||||
// Simulate process exit
|
||||
mockProcess.emit('exit', 0);
|
||||
|
||||
// Exit event includes taskId, exit code, process type, and optional projectId
|
||||
expect(exitHandler).toHaveBeenCalledWith('task-1', 0, expect.any(String), undefined);
|
||||
// Exit event includes taskId, exit code, and process type
|
||||
expect(exitHandler).toHaveBeenCalledWith('task-1', 0, expect.any(String));
|
||||
}, 30000); // Increase timeout for Windows CI (dynamic imports are slow)
|
||||
|
||||
it('should emit error event when process errors', async () => {
|
||||
@@ -346,7 +346,7 @@ describe('Subprocess Spawn Integration', () => {
|
||||
// Simulate process error
|
||||
mockProcess.emit('error', new Error('Spawn failed'));
|
||||
|
||||
expect(errorHandler).toHaveBeenCalledWith('task-1', 'Spawn failed', undefined);
|
||||
expect(errorHandler).toHaveBeenCalledWith('task-1', 'Spawn failed');
|
||||
}, 30000); // Increase timeout for Windows CI (dynamic imports are slow)
|
||||
|
||||
it('should kill task and remove from tracking', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { spawn } from 'child_process';
|
||||
import path from 'path';
|
||||
import { existsSync, mkdirSync, unlinkSync, promises as fsPromises } from 'fs';
|
||||
import { existsSync, writeFileSync, mkdirSync, unlinkSync, promises as fsPromises } from 'fs';
|
||||
import { EventEmitter } from 'events';
|
||||
import { AgentState } from './agent-state';
|
||||
import { AgentEvents } from './agent-events';
|
||||
@@ -8,7 +8,7 @@ import { AgentProcessManager } from './agent-process';
|
||||
import { RoadmapConfig } from './types';
|
||||
import type { IdeationConfig, Idea } from '../../shared/types';
|
||||
import { AUTO_BUILD_PATHS } from '../../shared/constants';
|
||||
import { detectRateLimit, createSDKRateLimitInfo, getBestAvailableProfileEnv } from '../rate-limit-detector';
|
||||
import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv } from '../rate-limit-detector';
|
||||
import { getAPIProfileEnv } from '../services/profile';
|
||||
import { getOAuthModeClearVars } from './env-utils';
|
||||
import { debugLog, debugError } from '../../shared/utils/debug-logger';
|
||||
@@ -113,14 +113,14 @@ export class AgentQueueManager {
|
||||
* @param startedAt - When generation started (ISO string)
|
||||
* @param isRunning - Whether generation is actively running
|
||||
*/
|
||||
private async persistRoadmapProgress(
|
||||
private persistRoadmapProgress(
|
||||
projectPath: string,
|
||||
phase: string,
|
||||
progress: number,
|
||||
message: string,
|
||||
startedAt: string,
|
||||
isRunning: boolean
|
||||
): Promise<void> {
|
||||
): void {
|
||||
try {
|
||||
const roadmapDir = path.join(projectPath, AUTO_BUILD_PATHS.ROADMAP_DIR);
|
||||
const progressPath = path.join(roadmapDir, AUTO_BUILD_PATHS.GENERATION_PROGRESS);
|
||||
@@ -139,7 +139,7 @@ export class AgentQueueManager {
|
||||
is_running: isRunning
|
||||
};
|
||||
|
||||
await writeFileWithRetry(progressPath, JSON.stringify(progressData, null, 2), { encoding: 'utf-8' });
|
||||
writeFileSync(progressPath, JSON.stringify(progressData, null, 2));
|
||||
debugLog('[Agent Queue] Persisted roadmap progress:', { phase, progress });
|
||||
} catch (err) {
|
||||
debugError('[Agent Queue] Failed to persist roadmap progress:', err);
|
||||
@@ -153,9 +153,6 @@ export class AgentQueueManager {
|
||||
* @param projectPath - The project directory path
|
||||
*/
|
||||
private clearRoadmapProgress(projectPath: string): void {
|
||||
// Cancel any pending debounced write to prevent re-creating the file after deletion
|
||||
this.cancelPersistRoadmapProgress();
|
||||
|
||||
try {
|
||||
const progressPath = path.join(
|
||||
projectPath,
|
||||
@@ -775,8 +772,8 @@ export class AgentQueueManager {
|
||||
// Track startedAt timestamp for progress persistence
|
||||
const roadmapStartedAt = new Date().toISOString();
|
||||
|
||||
// Persist initial progress state (debounced - will execute immediately due to leading: true)
|
||||
this.debouncedPersistRoadmapProgress(
|
||||
// Persist initial progress state
|
||||
this.persistRoadmapProgress(
|
||||
projectPath,
|
||||
progressPhase,
|
||||
progressPercent,
|
||||
@@ -813,8 +810,8 @@ export class AgentQueueManager {
|
||||
// Get status message for display
|
||||
const statusMessage = formatStatusMessage(log);
|
||||
|
||||
// Persist progress to disk for recovery after restart (debounced to limit writes)
|
||||
this.debouncedPersistRoadmapProgress(
|
||||
// Persist progress to disk for recovery after restart
|
||||
this.persistRoadmapProgress(
|
||||
projectPath,
|
||||
progressPhase,
|
||||
progressPercent,
|
||||
@@ -841,8 +838,8 @@ export class AgentQueueManager {
|
||||
|
||||
const statusMessage = formatStatusMessage(log);
|
||||
|
||||
// Persist progress to disk (debounced - also on stderr to show activity)
|
||||
this.debouncedPersistRoadmapProgress(
|
||||
// Persist progress to disk (also on stderr to show activity)
|
||||
this.persistRoadmapProgress(
|
||||
projectPath,
|
||||
progressPhase,
|
||||
progressPercent,
|
||||
|
||||
@@ -21,7 +21,7 @@ import type {
|
||||
} from "../../shared/types";
|
||||
import type { RoadmapConfig } from "../agent/types";
|
||||
import path from "path";
|
||||
import { readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from "fs";
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from "fs";
|
||||
import { projectStore } from "../project-store";
|
||||
import { AgentManager } from "../agent";
|
||||
import { debugLog, debugError } from "../../shared/utils/debug-logger";
|
||||
@@ -690,8 +690,10 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join("\n"
|
||||
const progressPath = path.join(roadmapDir, AUTO_BUILD_PATHS.GENERATION_PROGRESS);
|
||||
|
||||
try {
|
||||
// Ensure roadmap directory exists (mkdirSync with recursive: true doesn't error if exists)
|
||||
mkdirSync(roadmapDir, { recursive: true });
|
||||
// Ensure roadmap directory exists
|
||||
if (!existsSync(roadmapDir)) {
|
||||
mkdirSync(roadmapDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Derive isRunning from phase (active phases are running)
|
||||
const isRunning = progressData.phase !== 'idle' && progressData.phase !== 'complete' && progressData.phase !== 'error';
|
||||
@@ -706,7 +708,7 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join("\n"
|
||||
is_running: isRunning,
|
||||
};
|
||||
|
||||
await writeFileWithRetry(progressPath, JSON.stringify(fileData, null, 2), { encoding: 'utf-8' });
|
||||
writeFileSync(progressPath, JSON.stringify(fileData, null, 2));
|
||||
debugLog("[Roadmap Handler] Saved progress checkpoint:", { projectId, phase: progressData.phase });
|
||||
|
||||
return { success: true };
|
||||
@@ -737,8 +739,12 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join("\n"
|
||||
AUTO_BUILD_PATHS.GENERATION_PROGRESS
|
||||
);
|
||||
|
||||
if (!existsSync(progressPath)) {
|
||||
return { success: true, data: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await readFileWithRetry(progressPath, { encoding: "utf-8" }) as string;
|
||||
const content = readFileSync(progressPath, "utf-8");
|
||||
const rawData = JSON.parse(content);
|
||||
|
||||
// Valid phase values that the frontend expects
|
||||
@@ -763,10 +769,6 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join("\n"
|
||||
|
||||
return { success: true, data: progressData };
|
||||
} catch (error) {
|
||||
// ENOENT (file not found) is expected - return null data
|
||||
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
||||
return { success: true, data: null };
|
||||
}
|
||||
debugError("[Roadmap Handler] Failed to load progress:", error);
|
||||
return {
|
||||
success: false,
|
||||
@@ -791,21 +793,17 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join("\n"
|
||||
);
|
||||
|
||||
try {
|
||||
// unlinkSync errors if file doesn't exist - catch and ignore ENOENT
|
||||
unlinkSync(progressPath);
|
||||
debugLog("[Roadmap Handler] Cleared progress checkpoint:", { projectId });
|
||||
if (existsSync(progressPath)) {
|
||||
unlinkSync(progressPath);
|
||||
debugLog("[Roadmap Handler] Cleared progress checkpoint:", { projectId });
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
// ENOENT (file not found) is expected when clearing a non-existent progress file
|
||||
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
||||
debugError("[Roadmap Handler] Failed to clear progress:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Failed to clear progress",
|
||||
};
|
||||
}
|
||||
// File didn't exist - that's fine, consider it cleared
|
||||
return { success: true };
|
||||
debugError("[Roadmap Handler] Failed to clear progress:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Failed to clear progress",
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -283,7 +283,7 @@ export async function loadRoadmap(projectId: string): Promise<void> {
|
||||
const parseDate = (dateStr: string | undefined): Date | undefined => {
|
||||
if (!dateStr) return undefined;
|
||||
const date = new Date(dateStr);
|
||||
return Number.isNaN(date.getTime()) ? undefined : date;
|
||||
return isNaN(date.getTime()) ? undefined : date;
|
||||
};
|
||||
|
||||
store.setGenerationStatus({
|
||||
|
||||
@@ -634,16 +634,6 @@
|
||||
"goToSettings": "Go to Settings"
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
"branchGroups": {
|
||||
"local": "Local Branches",
|
||||
"remote": "Remote Branches"
|
||||
},
|
||||
"branchType": {
|
||||
"local": "Local",
|
||||
"remote": "Remote"
|
||||
}
|
||||
},
|
||||
"roadmapProgress": {
|
||||
"elapsedTime": "Elapsed",
|
||||
"lastActivity": "Last activity",
|
||||
@@ -683,31 +673,5 @@
|
||||
"progress": "Progress",
|
||||
"lastActivityPrefix": "last activity",
|
||||
"lastProgressUpdateTooltip": "Last progress update received"
|
||||
},
|
||||
"prStatus": {
|
||||
"ci": {
|
||||
"success": "CI Passed",
|
||||
"pending": "CI Pending",
|
||||
"failure": "CI Failed",
|
||||
"successTooltip": "All CI checks have passed",
|
||||
"pendingTooltip": "CI checks are still running",
|
||||
"failureTooltip": "One or more CI checks have failed"
|
||||
},
|
||||
"review": {
|
||||
"approved": "Approved",
|
||||
"changesRequested": "Changes Requested",
|
||||
"pending": "Review Pending",
|
||||
"approvedTooltip": "This PR has been approved",
|
||||
"changesRequestedTooltip": "Changes have been requested on this PR",
|
||||
"pendingTooltip": "Waiting for review"
|
||||
},
|
||||
"merge": {
|
||||
"ready": "Ready to Merge",
|
||||
"blocked": "Merge Blocked",
|
||||
"conflict": "Has Conflicts",
|
||||
"readyTooltip": "This PR is ready to be merged",
|
||||
"blockedTooltip": "This PR cannot be merged due to blocking conditions",
|
||||
"conflictTooltip": "This PR has merge conflicts that need to be resolved"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,16 +634,6 @@
|
||||
"goToSettings": "Aller aux paramètres"
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
"branchGroups": {
|
||||
"local": "Branches Locales",
|
||||
"remote": "Branches Distantes"
|
||||
},
|
||||
"branchType": {
|
||||
"local": "Locale",
|
||||
"remote": "Distante"
|
||||
}
|
||||
},
|
||||
"roadmapProgress": {
|
||||
"elapsedTime": "Écoulé",
|
||||
"lastActivity": "Dernière activité",
|
||||
@@ -683,31 +673,5 @@
|
||||
"progress": "Progression",
|
||||
"lastActivityPrefix": "dernière activité",
|
||||
"lastProgressUpdateTooltip": "Dernière mise à jour de progression reçue"
|
||||
},
|
||||
"prStatus": {
|
||||
"ci": {
|
||||
"success": "CI réussie",
|
||||
"pending": "CI en attente",
|
||||
"failure": "CI échouée",
|
||||
"successTooltip": "Toutes les vérifications CI ont réussi",
|
||||
"pendingTooltip": "Les vérifications CI sont en cours",
|
||||
"failureTooltip": "Une ou plusieurs vérifications CI ont échoué"
|
||||
},
|
||||
"review": {
|
||||
"approved": "Approuvée",
|
||||
"changesRequested": "Modifications demandées",
|
||||
"pending": "Révision en attente",
|
||||
"approvedTooltip": "Cette PR a été approuvée",
|
||||
"changesRequestedTooltip": "Des modifications ont été demandées sur cette PR",
|
||||
"pendingTooltip": "En attente de révision"
|
||||
},
|
||||
"merge": {
|
||||
"ready": "Prête à fusionner",
|
||||
"blocked": "Fusion bloquée",
|
||||
"conflict": "Conflits détectés",
|
||||
"readyTooltip": "Cette PR est prête à être fusionnée",
|
||||
"blockedTooltip": "Cette PR ne peut pas être fusionnée en raison de conditions bloquantes",
|
||||
"conflictTooltip": "Cette PR a des conflits de fusion qui doivent être résolus"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user