fix(rectification): satisfy React compiler lint for collect stem attach
Independent Staging Quality Gate / validate (push) Successful in 9m21s
Independent Staging Quality Gate / publish (push) Successful in 1m49s

Move the current-question ref off render and persist the collect stem from snapshot/send callbacks so ESLint no longer fails the staging gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-02 17:05:29 +08:00
co-authored by Cursor
parent b1d4796d30
commit 33d55b3a54
3 changed files with 58 additions and 33 deletions
@@ -120,6 +120,25 @@ function currentQuestionFromSnapshot(value: unknown): CurrentQuestionModel | nul
};
}
function collectSpokenPromptFromQuestion(question: CurrentQuestionModel | null): string | null {
return question?.kind === "collect_spoken" ? question.prompt : null;
}
function attachCollectSpokenStem(current: RenderMessage[], prompt: string): RenderMessage[] {
const last = [...current].reverse().find((message) => (
message.role === "assistant"
&& message.state === "settled"
&& !message.failed
&& Boolean(message.text)
));
if (!last) return current;
const combined = composeCollectSpokenAssistantText(last.text, prompt);
if (combined === last.text) return current;
return current.map((item) => (
item.renderKey === last.renderKey ? { ...item, text: combined } : item
));
}
function RectificationCandidateCards({
result,
@@ -324,7 +343,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const runAbort = useRef<AbortController | null>(null);
const choiceActionIds = useRef(new Map<string, string>());
const currentQuestionRef = useRef(currentQuestion);
currentQuestionRef.current = currentQuestion;
const [compactBoard, setCompactBoard] = useState(false);
const [boardOpen, setBoardOpen] = useState(false);
const [boardDiff, setBoardDiff] = useState(() => diffRectificationBoard(null, null));
@@ -335,6 +353,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
// new cards land the viewport on the bottom only while the reader is there.
const conversationAnchor = useConversationScrollAnchor(conversation, true, caseId);
useLayoutEffect(() => {
currentQuestionRef.current = currentQuestion;
}, [currentQuestion]);
useLayoutEffect(() => {
const query = window.matchMedia(`(max-width: ${RECTIFICATION_BOARD_SPLIT_MIN_PX - 1}px)`);
const update = () => {
@@ -399,16 +421,19 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}
}, []);
const loadCaseSnapshot = useCallback(async () => {
const loadCaseSnapshot = useCallback(async (): Promise<CurrentQuestionModel | null | undefined> => {
try {
const response = await fetch(
`/api/rectification/cases/${encodeURIComponent(caseId)}?sessionId=${encodeURIComponent(sessionId)}`,
{ cache: "no-store" },
);
if (!response.ok) return;
applyCaseSnapshot(await response.json().catch(() => null));
if (!response.ok) return undefined;
const payload = await response.json().catch(() => null);
applyCaseSnapshot(payload);
return currentQuestionFromSnapshot(payload?.current_question);
} catch {
// Snapshot refresh is best-effort; the durable Case remains on the server.
return undefined;
}
}, [applyCaseSnapshot, caseId, sessionId]);
@@ -420,7 +445,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
)
.then((response) => (response.ok ? response.json() : null))
.then((payload) => {
if (!controller.signal.aborted) applyCaseSnapshot(payload);
if (controller.signal.aborted) return;
applyCaseSnapshot(payload);
const prompt = collectSpokenPromptFromQuestion(currentQuestionFromSnapshot(payload?.current_question));
if (prompt) setMessages((current) => attachCollectSpokenStem(current, prompt));
})
.catch(() => {
// Snapshot refresh is best-effort; the durable Case remains on the server.
@@ -428,25 +456,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
return () => controller.abort();
}, [applyCaseSnapshot, caseId, sessionId]);
useEffect(() => {
const prompt = currentQuestion?.kind === "collect_spoken" ? currentQuestion.prompt : null;
if (!prompt || busy || regeneratingMessageKey !== null) return;
setMessages((current) => {
const last = [...current].reverse().find((message) => (
message.role === "assistant"
&& message.state === "settled"
&& !message.failed
&& Boolean(message.text)
));
if (!last) return current;
const combined = composeCollectSpokenAssistantText(last.text, prompt);
if (combined === last.text) return current;
return current.map((item) => (
item.renderKey === last.renderKey ? { ...item, text: combined } : item
));
});
}, [busy, currentQuestion, regeneratingMessageKey]);
const send = useCallback(async (action: "opening" | "message" | "read_only", messageText: string) => {
const trimmed = action === "message" ? messageText.trim() : "";
if ((action === "message" && !trimmed) || busy || readonly) return;
@@ -459,8 +468,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const userRenderKey = `v9-user-${turnKey}`;
const assistantRenderKey = `v9-assistant-${turnKey}`;
const pendingCollectPrompt = collectSpokenPromptFromQuestion(currentQuestionRef.current);
setMessages((current) => [
...current,
...(action === "message" && pendingCollectPrompt
? attachCollectSpokenStem(current, pendingCollectPrompt)
: current),
...(action === "message"
? [{ role: "user", text: trimmed, renderKey: userRenderKey, state: "settled" } satisfies RenderMessage]
: []),
@@ -667,9 +679,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const parsed = completed && !streamFailed ? parseAgentReply(raw) : { text: "", title: undefined };
const succeeded = completed && !streamFailed && Boolean(parsed.text);
const collectPrompt = currentQuestionRef.current?.kind === "collect_spoken"
? currentQuestionRef.current.prompt
: null;
const collectPrompt = collectSpokenPromptFromQuestion(currentQuestionRef.current);
const settledText = succeeded && collectPrompt
? composeCollectSpokenAssistantText(parsed.text, collectPrompt)
: parsed.text;
@@ -708,12 +718,20 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
setError((current) => current || userFacingRunFailure("run_failed"));
}
if (succeeded) {
const snapshotPrompt = collectSpokenPromptFromQuestion(await loadCaseSnapshot() ?? null);
const attachedText = snapshotPrompt
? composeCollectSpokenAssistantText(parsed.text, snapshotPrompt)
: settledText;
if (attachedText !== settledText) {
setMessages((current) => current.map((message) => (
message.renderKey === assistantRenderKey ? { ...message, text: attachedText } : message
)));
}
onMessagesChange?.([
...(action === "message" ? [{ role: "user" as const, text: trimmed }] : []),
{ role: "assistant", text: settledText },
{ role: "assistant", text: attachedText },
]);
onCompleted?.();
await loadCaseSnapshot();
}
} catch (caught) {
frames.settle();
@@ -1010,8 +1028,12 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
if (!response.ok || payload?.ok !== true || typeof payload.assistantMessage !== "string") {
throw new Error(payload?.message || payload?.error || "暂时无法重新生成回答");
}
const collectPrompt = collectSpokenPromptFromQuestion(currentQuestion);
const nextText = collectPrompt
? composeCollectSpokenAssistantText(payload.assistantMessage, collectPrompt)
: payload.assistantMessage;
setMessages((current) => current.map((item) => item.renderKey === message.renderKey
? { ...item, text: payload.assistantMessage, state: "settled" }
? { ...item, text: nextText, state: "settled" }
: item));
} catch (caught) {
setMessages((current) => current.map((item) => item.renderKey === message.renderKey