fix: preserve roadmap generation state when switching projects
Previously, roadmap generation would stop or appear stopped when users navigated between projects. This fix ensures generation continues in the background and the UI properly reflects the generation state. Changes: - Add ROADMAP_GET_STATUS IPC endpoint to query if generation is running - Update loadRoadmap() to query backend status when switching projects - Restore generation UI state when returning to a project with active gen - Remove aggressive stopRoadmap() call that killed generation on switch The fix allows users to start roadmap generation, navigate to other projects, and return to see the correct progress/completion state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -160,6 +160,16 @@ export function registerRoadmapHandlers(
|
||||
}
|
||||
);
|
||||
|
||||
// Get roadmap generation status - allows frontend to query if generation is running
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.ROADMAP_GET_STATUS,
|
||||
async (_, projectId: string): Promise<IPCResult<{ isRunning: boolean }>> => {
|
||||
const isRunning = agentManager.isRoadmapRunning(projectId);
|
||||
debugLog('[Roadmap Handler] Get status:', { projectId, isRunning });
|
||||
return { success: true, data: { isRunning } };
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
IPC_CHANNELS.ROADMAP_GENERATE,
|
||||
(_, projectId: string, enableCompetitorAnalysis?: boolean) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createIpcListener, invokeIpc, sendIpc, IpcListenerCleanup } from './ipc
|
||||
export interface RoadmapAPI {
|
||||
// Operations
|
||||
getRoadmap: (projectId: string) => Promise<IPCResult<Roadmap | null>>;
|
||||
getRoadmapStatus: (projectId: string) => Promise<IPCResult<{ isRunning: boolean }>>;
|
||||
saveRoadmap: (projectId: string, roadmap: Roadmap) => Promise<IPCResult>;
|
||||
generateRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void;
|
||||
refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void;
|
||||
@@ -51,6 +52,9 @@ export const createRoadmapAPI = (): RoadmapAPI => ({
|
||||
getRoadmap: (projectId: string): Promise<IPCResult<Roadmap | null>> =>
|
||||
invokeIpc(IPC_CHANNELS.ROADMAP_GET, projectId),
|
||||
|
||||
getRoadmapStatus: (projectId: string): Promise<IPCResult<{ isRunning: boolean }>> =>
|
||||
invokeIpc(IPC_CHANNELS.ROADMAP_GET_STATUS, projectId),
|
||||
|
||||
saveRoadmap: (projectId: string, roadmap: Roadmap): Promise<IPCResult> =>
|
||||
invokeIpc(IPC_CHANNELS.ROADMAP_SAVE, projectId, roadmap),
|
||||
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRoadmapStore, loadRoadmap, generateRoadmap, refreshRoadmap, stopRoadmap } from '../../stores/roadmap-store';
|
||||
import { useTaskStore } from '../../stores/task-store';
|
||||
import type { RoadmapFeature } from '../../../shared/types';
|
||||
|
||||
/**
|
||||
* Hook to manage roadmap data and loading
|
||||
*
|
||||
* When the projectId changes, this hook:
|
||||
* 1. Loads the new project's roadmap data
|
||||
* 2. Queries the backend to check if generation is running for this project
|
||||
* 3. Restores the generation status UI state accordingly
|
||||
*
|
||||
* NOTE: Generation continues in the background when switching projects.
|
||||
* The loadRoadmap function queries the backend to restore the correct UI state.
|
||||
*/
|
||||
export function useRoadmapData(projectId: string) {
|
||||
const roadmap = useRoadmapStore((state) => state.roadmap);
|
||||
@@ -11,6 +20,9 @@ export function useRoadmapData(projectId: string) {
|
||||
const generationStatus = useRoadmapStore((state) => state.generationStatus);
|
||||
|
||||
useEffect(() => {
|
||||
// Load roadmap data and query generation status for this project
|
||||
// The loadRoadmap function handles checking if generation is running
|
||||
// and restores the UI state accordingly
|
||||
loadRoadmap(projectId);
|
||||
}, [projectId]);
|
||||
|
||||
@@ -26,6 +38,7 @@ export function useRoadmapData(projectId: string) {
|
||||
*/
|
||||
export function useFeatureActions() {
|
||||
const updateFeatureLinkedSpec = useRoadmapStore((state) => state.updateFeatureLinkedSpec);
|
||||
const addTask = useTaskStore((state) => state.addTask);
|
||||
|
||||
const convertFeatureToSpec = async (
|
||||
projectId: string,
|
||||
@@ -35,6 +48,10 @@ export function useFeatureActions() {
|
||||
) => {
|
||||
const result = await window.electronAPI.convertFeatureToSpec(projectId, feature.id);
|
||||
if (result.success && result.data) {
|
||||
// Add the created task to the task store so it appears in the kanban immediately
|
||||
addTask(result.data);
|
||||
|
||||
// Update the roadmap feature with the linked spec
|
||||
updateFeatureLinkedSpec(feature.id, result.data.specId);
|
||||
if (selectedFeature?.id === feature.id) {
|
||||
setSelectedFeature({
|
||||
|
||||
@@ -53,6 +53,11 @@ const browserMockAPI: ElectronAPI = {
|
||||
data: null
|
||||
}),
|
||||
|
||||
getRoadmapStatus: async () => ({
|
||||
success: true,
|
||||
data: { isRunning: false }
|
||||
}),
|
||||
|
||||
saveRoadmap: async () => ({
|
||||
success: true
|
||||
}),
|
||||
|
||||
@@ -12,11 +12,13 @@ interface RoadmapState {
|
||||
roadmap: Roadmap | null;
|
||||
competitorAnalysis: CompetitorAnalysis | null;
|
||||
generationStatus: RoadmapGenerationStatus;
|
||||
currentProjectId: string | null; // Track which project we're viewing/generating for
|
||||
|
||||
// Actions
|
||||
setRoadmap: (roadmap: Roadmap | null) => void;
|
||||
setCompetitorAnalysis: (analysis: CompetitorAnalysis | null) => void;
|
||||
setGenerationStatus: (status: RoadmapGenerationStatus) => void;
|
||||
setCurrentProjectId: (projectId: string | null) => void;
|
||||
updateFeatureStatus: (featureId: string, status: RoadmapFeatureStatus) => void;
|
||||
updateFeatureLinkedSpec: (featureId: string, specId: string) => void;
|
||||
clearRoadmap: () => void;
|
||||
@@ -37,6 +39,7 @@ export const useRoadmapStore = create<RoadmapState>((set) => ({
|
||||
roadmap: null,
|
||||
competitorAnalysis: null,
|
||||
generationStatus: initialGenerationStatus,
|
||||
currentProjectId: null,
|
||||
|
||||
// Actions
|
||||
setRoadmap: (roadmap) => set({ roadmap }),
|
||||
@@ -45,6 +48,8 @@ export const useRoadmapStore = create<RoadmapState>((set) => ({
|
||||
|
||||
setGenerationStatus: (status) => set({ generationStatus: status }),
|
||||
|
||||
setCurrentProjectId: (projectId) => set({ currentProjectId: projectId }),
|
||||
|
||||
updateFeatureStatus: (featureId, status) =>
|
||||
set((state) => {
|
||||
if (!state.roadmap) return state;
|
||||
@@ -85,7 +90,8 @@ export const useRoadmapStore = create<RoadmapState>((set) => ({
|
||||
set({
|
||||
roadmap: null,
|
||||
competitorAnalysis: null,
|
||||
generationStatus: initialGenerationStatus
|
||||
generationStatus: initialGenerationStatus,
|
||||
currentProjectId: null
|
||||
}),
|
||||
|
||||
// Reorder features within a phase
|
||||
@@ -159,9 +165,34 @@ export const useRoadmapStore = create<RoadmapState>((set) => ({
|
||||
|
||||
// Helper functions for loading roadmap
|
||||
export async function loadRoadmap(projectId: string): Promise<void> {
|
||||
const store = useRoadmapStore.getState();
|
||||
|
||||
// Always set current project ID first - this ensures event handlers
|
||||
// only process events for the currently viewed project
|
||||
store.setCurrentProjectId(projectId);
|
||||
|
||||
// Query if roadmap generation is currently running for this project
|
||||
// This restores the generation status when switching back to a project
|
||||
const statusResult = await window.electronAPI.getRoadmapStatus(projectId);
|
||||
if (statusResult.success && statusResult.data?.isRunning) {
|
||||
// Generation is running - restore the UI state to show progress
|
||||
// The actual progress will be updated by incoming events
|
||||
store.setGenerationStatus({
|
||||
phase: 'analyzing',
|
||||
progress: 0,
|
||||
message: 'Roadmap generation in progress...'
|
||||
});
|
||||
} else {
|
||||
// Generation is not running - reset to idle
|
||||
store.setGenerationStatus({
|
||||
phase: 'idle',
|
||||
progress: 0,
|
||||
message: ''
|
||||
});
|
||||
}
|
||||
|
||||
const result = await window.electronAPI.getRoadmap(projectId);
|
||||
if (result.success && result.data) {
|
||||
const store = useRoadmapStore.getState();
|
||||
store.setRoadmap(result.data);
|
||||
// Extract and set competitor analysis separately if present
|
||||
if (result.data.competitorAnalysis) {
|
||||
@@ -170,7 +201,6 @@ export async function loadRoadmap(projectId: string): Promise<void> {
|
||||
store.setCompetitorAnalysis(null);
|
||||
}
|
||||
} else {
|
||||
const store = useRoadmapStore.getState();
|
||||
store.setRoadmap(null);
|
||||
store.setCompetitorAnalysis(null);
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ export const IPC_CHANNELS = {
|
||||
|
||||
// Roadmap operations
|
||||
ROADMAP_GET: 'roadmap:get',
|
||||
ROADMAP_GET_STATUS: 'roadmap:getStatus',
|
||||
ROADMAP_SAVE: 'roadmap:save',
|
||||
ROADMAP_GENERATE: 'roadmap:generate',
|
||||
ROADMAP_GENERATE_WITH_COMPETITOR: 'roadmap:generateWithCompetitor',
|
||||
|
||||
@@ -237,6 +237,7 @@ export interface ElectronAPI {
|
||||
|
||||
// Roadmap operations
|
||||
getRoadmap: (projectId: string) => Promise<IPCResult<Roadmap | null>>;
|
||||
getRoadmapStatus: (projectId: string) => Promise<IPCResult<{ isRunning: boolean }>>;
|
||||
saveRoadmap: (projectId: string, roadmap: Roadmap) => Promise<IPCResult>;
|
||||
generateRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void;
|
||||
refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void;
|
||||
|
||||
Reference in New Issue
Block a user