mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 11:53:10 +08:00
fix(desktop): keep AskUserQuestion hook order stable
The "no questions" early return sat above two useMemo calls, so any mounted instance whose question count crossed zero threw "Rendered fewer/more hooks than expected" and took the surrounding message list down with it. `input` is not fixed for the lifetime of the instance — chatStore rebuilds tool_use messages from the transcript under a stable id — so both directions are reachable. Move the early return below every hook. The two new tests assert on rendering rather than hook counts, and both go red against the old ordering.
This commit is contained in:
@@ -78,6 +78,37 @@ describe('AskUserQuestion', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Regression: the "no questions" early return used to sit above two useMemo calls,
|
||||
// so a mounted instance whose question count crossed zero threw "Rendered fewer/more
|
||||
// hooks than expected" and took the surrounding message list down with it. `input` is
|
||||
// not stable for the lifetime of the instance — chatStore rebuilds tool_use messages
|
||||
// from the transcript under a stable id — so both directions are reachable.
|
||||
//
|
||||
// These assert on rendering rather than on hook counts, which is what a reader can
|
||||
// check: if the early return moves back above a hook, React throws during rerender.
|
||||
describe('hook order across a changing question count', () => {
|
||||
const ONE_QUESTION = { question: 'Ship it?', options: [{ label: 'Yes' }, { label: 'No' }] }
|
||||
|
||||
it('survives input gaining questions after rendering with none', () => {
|
||||
const { rerender } = render(<AskUserQuestion toolUseId="tool-1" input={{}} />)
|
||||
|
||||
rerender(<AskUserQuestion toolUseId="tool-1" input={ONE_QUESTION} />)
|
||||
|
||||
expect(screen.getByText('Ship it?')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('survives input losing its questions while mounted', () => {
|
||||
const { container, rerender } = render(
|
||||
<AskUserQuestion toolUseId="tool-1" input={ONE_QUESTION} />,
|
||||
)
|
||||
expect(screen.getByText('Ship it?')).toBeTruthy()
|
||||
|
||||
rerender(<AskUserQuestion toolUseId="tool-1" input={{}} />)
|
||||
|
||||
expect(container.textContent).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
it('submits answers through permission_response updatedInput instead of sending a chat message', () => {
|
||||
render(
|
||||
<AskUserQuestion
|
||||
|
||||
@@ -82,10 +82,6 @@ export function AskUserQuestion({ sessionId, toolUseId, input, result }: Props)
|
||||
const [hasRequestedChat, setHasRequestedChat] = useState(false)
|
||||
const composingRef = useRef(false)
|
||||
|
||||
if (questions.length === 0) return null
|
||||
const safeActiveTab = Math.min(activeTab, questions.length - 1)
|
||||
const activeQuestion = questions[safeActiveTab]
|
||||
|
||||
const resultAnswers = useMemo(() => {
|
||||
if (!result || typeof result !== 'object') return {}
|
||||
const answers = (result as { answers?: unknown }).answers
|
||||
@@ -110,6 +106,17 @@ export function AskUserQuestion({ sessionId, toolUseId, input, result }: Props)
|
||||
.filter(Boolean)
|
||||
.join('; ')
|
||||
}, [freeTexts, hasStructuredAnswers, questions, resultAnswers, resultText, selections])
|
||||
|
||||
// Every hook above this line runs unconditionally, and it has to stay that way.
|
||||
// `input` is not fixed for the lifetime of the instance: chatStore rebuilds tool_use
|
||||
// messages from the transcript under a stable id (`${messageId}-block-${index}`), so
|
||||
// the same mounted component can see its question count cross zero in either
|
||||
// direction. With the early return above the useMemo calls, that transition threw
|
||||
// "Rendered fewer/more hooks than expected" and took the whole message list down.
|
||||
if (questions.length === 0) return null
|
||||
const safeActiveTab = Math.min(activeTab, questions.length - 1)
|
||||
const activeQuestion = questions[safeActiveTab]
|
||||
|
||||
const submitted = hasTerminalResult || hasSubmitted || hasRequestedChat
|
||||
const terminalWithoutAnswers = submitted && !hasStructuredAnswers && resultText.length > 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user