diff --git a/apps/frontend/src/renderer/stores/roadmap-store.ts b/apps/frontend/src/renderer/stores/roadmap-store.ts index 107de1f0..84054fa2 100644 --- a/apps/frontend/src/renderer/stores/roadmap-store.ts +++ b/apps/frontend/src/renderer/stores/roadmap-store.ts @@ -204,18 +204,40 @@ export const useRoadmapStore = create((set) => ({ // Map the incoming status phase to an XState event let event: RoadmapGenerationEvent | null = null; switch (status.phase) { - case 'analyzing': - // If idle, start generation; otherwise it's a progress update - if (String(actor.getSnapshot().value) === 'idle') { + case 'analyzing': { + const currentState = String(actor.getSnapshot().value); + if (currentState === 'idle') { + event = { type: 'START_GENERATION' }; + } else if (currentState === 'complete' || currentState === 'error') { + actor.send({ type: 'RESET' }); event = { type: 'START_GENERATION' }; } break; - case 'discovering': + } + case 'discovering': { + const cs = String(actor.getSnapshot().value); + if (cs === 'idle') { + actor.send({ type: 'START_GENERATION' }); + } else if (cs === 'complete' || cs === 'error') { + actor.send({ type: 'RESET' }); + actor.send({ type: 'START_GENERATION' }); + } event = { type: 'DISCOVERY_STARTED' }; break; - case 'generating': + } + case 'generating': { + const cs = String(actor.getSnapshot().value); + if (cs === 'idle') { + actor.send({ type: 'START_GENERATION' }); + actor.send({ type: 'DISCOVERY_STARTED' }); + } else if (cs === 'complete' || cs === 'error') { + actor.send({ type: 'RESET' }); + actor.send({ type: 'START_GENERATION' }); + actor.send({ type: 'DISCOVERY_STARTED' }); + } event = { type: 'GENERATION_STARTED' }; break; + } case 'complete': event = { type: 'GENERATION_COMPLETE' }; break; @@ -253,6 +275,10 @@ export const useRoadmapStore = create((set) => ({ setCurrentProjectId: (projectId) => set({ currentProjectId: projectId }), updateFeatureStatus: (featureId, status) => { + // NOTE: getState() is called outside set() because XState actors are external + // side effects that cannot run inside Zustand's synchronous updater. The feature + // lookup and actor state restoration use this snapshot, with the actual state + // write deferred to the set() call below. This is intentional architecture. const state = useRoadmapStore.getState(); if (!state.roadmap) return; diff --git a/apps/frontend/src/shared/state-machines/roadmap-generation-machine.ts b/apps/frontend/src/shared/state-machines/roadmap-generation-machine.ts index fec7d48a..4e351566 100644 --- a/apps/frontend/src/shared/state-machines/roadmap-generation-machine.ts +++ b/apps/frontend/src/shared/state-machines/roadmap-generation-machine.ts @@ -86,7 +86,7 @@ export const roadmapGenerationMachine = createMachine( }), updateProgress: assign({ progress: ({ event }) => - event.type === 'PROGRESS_UPDATE' ? event.progress : 0, + event.type === 'PROGRESS_UPDATE' ? Math.min(100, Math.max(0, event.progress)) : 0, message: ({ event }) => event.type === 'PROGRESS_UPDATE' ? event.message : undefined, }), @@ -97,6 +97,7 @@ export const roadmapGenerationMachine = createMachine( setError: assign({ error: ({ event }) => event.type === 'GENERATION_ERROR' ? event.error : undefined, + progress: () => 0, }), resetContext: assign({ progress: () => 0, diff --git a/apps/frontend/src/shared/state-machines/roadmap-state-utils.ts b/apps/frontend/src/shared/state-machines/roadmap-state-utils.ts index bdb3a16d..2e3678f1 100644 --- a/apps/frontend/src/shared/state-machines/roadmap-state-utils.ts +++ b/apps/frontend/src/shared/state-machines/roadmap-state-utils.ts @@ -35,6 +35,10 @@ export type FeatureStateName = typeof FEATURE_STATE_NAMES[number]; * Generation states where the machine has settled — the generation is * complete or has errored. Stale progress events should NOT overwrite * these states, as XState is the source of truth. + * + * NOTE: Exported for future consumer use (e.g., UI components that need to + * check if generation is settled before allowing user actions). Currently + * unused but intentionally retained as public API for roadmap state checking. */ export const GENERATION_SETTLED_STATES: ReadonlySet = new Set([ 'complete', 'error' @@ -43,6 +47,10 @@ export const GENERATION_SETTLED_STATES: ReadonlySet = new Set = new Set([ 'done'