From d04934359affa7ae6b054f168c0b33252cc2b900 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sat, 24 Jan 2026 15:44:39 +0100 Subject: [PATCH 1/2] fix: Invalidate all features query on pipeline_step_started event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a pipeline step starts, the feature status changes to the pipeline column status. Previously, only the single feature query was invalidated, but the Kanban board uses the all features query for column grouping. This caused the UI to not immediately reflect features moving to custom pipeline columns - updates would only appear after the first pipeline step completed. Fixes #668 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/ui/src/hooks/use-query-invalidation.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/ui/src/hooks/use-query-invalidation.ts b/apps/ui/src/hooks/use-query-invalidation.ts index f331f1d3..ae53d1e0 100644 --- a/apps/ui/src/hooks/use-query-invalidation.ts +++ b/apps/ui/src/hooks/use-query-invalidation.ts @@ -115,13 +115,16 @@ export function useAutoModeQueryInvalidation(projectPath: string | undefined) { // This allows polling to be disabled when WebSocket events are flowing recordGlobalEvent(); - // Invalidate features when agent completes, errors, or receives plan approval + // Invalidate features when agent completes, errors, receives plan approval, or pipeline step changes + // Note: pipeline_step_started is included to ensure Kanban board immediately reflects + // feature moving to custom pipeline columns (fixes GitHub issue #668) if ( event.type === 'auto_mode_feature_complete' || event.type === 'auto_mode_error' || event.type === 'plan_approval_required' || event.type === 'plan_approved' || event.type === 'plan_rejected' || + event.type === 'pipeline_step_started' || event.type === 'pipeline_step_complete' ) { queryClient.invalidateQueries({ @@ -142,11 +145,12 @@ export function useAutoModeQueryInvalidation(projectPath: string | undefined) { } // Invalidate specific feature when it starts or has phase changes + // Note: pipeline_step_started is NOT included here because it already invalidates + // features.all() above, which also invalidates child queries (features.single) if ( (event.type === 'auto_mode_feature_start' || event.type === 'auto_mode_phase' || - event.type === 'auto_mode_phase_complete' || - event.type === 'pipeline_step_started') && + event.type === 'auto_mode_phase_complete') && 'featureId' in event ) { queryClient.invalidateQueries({ From ed92d4fd8033c72da61da5c0b8fb82e9e9eb4707 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sat, 24 Jan 2026 15:56:35 +0100 Subject: [PATCH 2/2] refactor: Extract invalidation events to constants --- apps/ui/src/hooks/use-query-invalidation.ts | 95 +++++++++++++-------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/apps/ui/src/hooks/use-query-invalidation.ts b/apps/ui/src/hooks/use-query-invalidation.ts index ae53d1e0..9966260c 100644 --- a/apps/ui/src/hooks/use-query-invalidation.ts +++ b/apps/ui/src/hooks/use-query-invalidation.ts @@ -22,6 +22,57 @@ import { useEventRecencyStore } from './use-event-recency'; const PROGRESS_DEBOUNCE_WAIT = 150; const PROGRESS_DEBOUNCE_MAX_WAIT = 2000; +/** + * Events that should invalidate the feature list (features.all query) + * Note: pipeline_step_started is included to ensure Kanban board immediately reflects + * feature moving to custom pipeline columns (fixes GitHub issue #668) + */ +const FEATURE_LIST_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [ + 'auto_mode_feature_complete', + 'auto_mode_error', + 'plan_approval_required', + 'plan_approved', + 'plan_rejected', + 'pipeline_step_started', + 'pipeline_step_complete', +]; + +/** + * Events that should invalidate a specific feature (features.single query) + * Note: pipeline_step_started is NOT included here because it already invalidates + * features.all() above, which also invalidates child queries (features.single) + */ +const SINGLE_FEATURE_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [ + 'auto_mode_feature_start', + 'auto_mode_phase', + 'auto_mode_phase_complete', +]; + +/** + * Events that should invalidate running agents status + */ +const RUNNING_AGENTS_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [ + 'auto_mode_feature_start', + 'auto_mode_feature_complete', + 'auto_mode_error', + 'auto_mode_resuming_features', +]; + +/** + * Events that signal a feature is done and debounce cleanup should occur + */ +const FEATURE_CLEANUP_EVENTS: AutoModeEvent['type'][] = [ + 'auto_mode_feature_complete', + 'auto_mode_error', +]; + +/** + * Type guard to check if an event has a featureId property + */ +function hasFeatureId(event: AutoModeEvent): event is AutoModeEvent & { featureId: string } { + return 'featureId' in event && typeof event.featureId === 'string'; +} + /** * Creates a unique key for per-feature debounce tracking */ @@ -115,44 +166,22 @@ export function useAutoModeQueryInvalidation(projectPath: string | undefined) { // This allows polling to be disabled when WebSocket events are flowing recordGlobalEvent(); - // Invalidate features when agent completes, errors, receives plan approval, or pipeline step changes - // Note: pipeline_step_started is included to ensure Kanban board immediately reflects - // feature moving to custom pipeline columns (fixes GitHub issue #668) - if ( - event.type === 'auto_mode_feature_complete' || - event.type === 'auto_mode_error' || - event.type === 'plan_approval_required' || - event.type === 'plan_approved' || - event.type === 'plan_rejected' || - event.type === 'pipeline_step_started' || - event.type === 'pipeline_step_complete' - ) { + // Invalidate feature list for lifecycle events + if (FEATURE_LIST_INVALIDATION_EVENTS.includes(event.type)) { queryClient.invalidateQueries({ queryKey: queryKeys.features.all(currentProjectPath), }); } - // Invalidate running agents on any status change - if ( - event.type === 'auto_mode_feature_start' || - event.type === 'auto_mode_feature_complete' || - event.type === 'auto_mode_error' || - event.type === 'auto_mode_resuming_features' - ) { + // Invalidate running agents on status changes + if (RUNNING_AGENTS_INVALIDATION_EVENTS.includes(event.type)) { queryClient.invalidateQueries({ queryKey: queryKeys.runningAgents.all(), }); } - // Invalidate specific feature when it starts or has phase changes - // Note: pipeline_step_started is NOT included here because it already invalidates - // features.all() above, which also invalidates child queries (features.single) - if ( - (event.type === 'auto_mode_feature_start' || - event.type === 'auto_mode_phase' || - event.type === 'auto_mode_phase_complete') && - 'featureId' in event - ) { + // Invalidate specific feature for phase changes + if (SINGLE_FEATURE_INVALIDATION_EVENTS.includes(event.type) && hasFeatureId(event)) { queryClient.invalidateQueries({ queryKey: queryKeys.features.single(currentProjectPath, event.featureId), }); @@ -160,23 +189,19 @@ export function useAutoModeQueryInvalidation(projectPath: string | undefined) { // Invalidate agent output during progress updates (DEBOUNCED) // Uses per-feature debouncing to batch rapid progress events during streaming - if (event.type === 'auto_mode_progress' && 'featureId' in event) { + if (event.type === 'auto_mode_progress' && hasFeatureId(event)) { const debouncedInvalidation = getDebouncedInvalidation(event.featureId); debouncedInvalidation(); } // Clean up debounced functions when feature completes or errors // This ensures we flush any pending invalidations and free memory - if ( - (event.type === 'auto_mode_feature_complete' || event.type === 'auto_mode_error') && - 'featureId' in event && - event.featureId - ) { + if (FEATURE_CLEANUP_EVENTS.includes(event.type) && hasFeatureId(event)) { cleanupFeatureDebounce(event.featureId); } // Invalidate worktree queries when feature completes (may have created worktree) - if (event.type === 'auto_mode_feature_complete' && 'featureId' in event) { + if (event.type === 'auto_mode_feature_complete' && hasFeatureId(event)) { queryClient.invalidateQueries({ queryKey: queryKeys.worktrees.all(currentProjectPath), });