Skip to content

Commit f6848fa

Browse files
jesseturner21claude
andcommitted
fix: prevent infinite loop when pressing Escape on policy generation error
When the policy generation API returned an error, pressing Escape on the review step would loop back to the loading step and re-trigger the API call, creating an infinite loop. The root cause was the double goBack() pattern (one immediate, one via setTimeout) suffering from stale closures — both calls saw the same step, so the second never reached the description step, while the first landed on loading and re-fired the useEffect. The fix uses a skipGeneration ref: when navigating back from review, the ref is set to true and a single goBack() moves to the loading step. The useEffect detects the ref, resets it, and calls goBack() again (now with the correct step in scope) to reach the description step — without ever starting generation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0efcf5d commit f6848fa

File tree

1 file changed

+13
-5
lines changed

1 file changed

+13
-5
lines changed

src/cli/tui/screens/policy/AddPolicyScreen.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { POLICY_SOURCE_METHOD_OPTIONS, POLICY_STEP_LABELS, VALIDATION_MODE_OPTIO
1111
import { useAddPolicyWizard } from './useAddPolicyWizard';
1212
import { Box, Text } from 'ink';
1313
import Spinner from 'ink-spinner';
14-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
14+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1515

1616
interface AddPolicyScreenProps {
1717
onComplete: (config: AddPolicyConfig) => void;
@@ -37,6 +37,7 @@ export function AddPolicyScreen({
3737
// Generation state
3838
const [generatedPolicy, setGeneratedPolicy] = useState<string | null>(null);
3939
const [generationError, setGenerationError] = useState<string | null>(null);
40+
const skipGeneration = useRef(false);
4041

4142
const engineItems: SelectableItem[] = useMemo(
4243
() =>
@@ -147,9 +148,8 @@ export function AddPolicyScreen({
147148
} else {
148149
setGeneratedPolicy(null);
149150
setGenerationError(null);
151+
skipGeneration.current = true;
150152
wizard.goBack();
151-
// Go back twice — past loading to description
152-
setTimeout(() => wizard.goBack(), 0);
153153
}
154154
},
155155
[generatedPolicy, wizard]
@@ -161,15 +161,23 @@ export function AddPolicyScreen({
161161
onExit: () => {
162162
setGeneratedPolicy(null);
163163
setGenerationError(null);
164+
skipGeneration.current = true;
164165
wizard.goBack();
165-
setTimeout(() => wizard.goBack(), 0);
166166
},
167167
isActive: isGenerateReviewStep,
168168
});
169169

170170
// Real policy generation when entering the loading step
171171
useEffect(() => {
172172
if (!isGenerateLoadingStep) return undefined;
173+
if (skipGeneration.current) {
174+
skipGeneration.current = false;
175+
// Navigate back past the loading step to the description step.
176+
// This runs after React re-rendered with the loading step active,
177+
// so goBack() correctly sees 'source-generate-loading' as current step.
178+
wizard.goBack();
179+
return undefined;
180+
}
173181

174182
let cancelled = false;
175183

@@ -318,7 +326,7 @@ export function AddPolicyScreen({
318326
<TextInput
319327
key="generate-description"
320328
prompt="Describe your policy in natural language"
321-
initialValue=""
329+
initialValue={wizard.config.naturalLanguageDescription}
322330
expandable
323331
onSubmit={wizard.setNaturalLanguageDescription}
324332
onCancel={goBackOrExit}

0 commit comments

Comments
 (0)