fix(rectification): put the collect stem in the assistant bubble
Independent Staging Quality Gate / validate (push) Failing after 8m27s
Independent Staging Quality Gate / publish (push) Has been skipped

The live question slot still rendered the collect prompt below the action icons when the streamed body was only a greeting. Join the GET prompt into that bubble and stop drawing a sibling collect slot.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-02 16:30:26 +08:00
parent 39c30b5597
commit 42c6de142b
3 changed files with 67 additions and 47 deletions
@@ -323,12 +323,13 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const previousBoardResult = useRef<CandidateResult>(null);
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));
const boardId = useId();
const boardTitleId = useId();
const collectSpokenPromptId = useId();
const composerRemainingId = useId();
// The same anchor-and-follow the consultation surface uses: streamed tokens and
// new cards land the viewport on the bottom only while the reader is there.
@@ -427,6 +428,25 @@ 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;
@@ -647,12 +667,18 @@ 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 settledText = succeeded && collectPrompt
? composeCollectSpokenAssistantText(parsed.text, collectPrompt)
: parsed.text;
setMessages((current) => current.flatMap((message): RenderMessage[] => {
if (message.renderKey !== assistantRenderKey) return [message];
if (succeeded) {
return [{
...message,
text: parsed.text,
text: settledText,
activityTrace: completeActivityTrace(activityTrace),
state: "settled",
completedReceipt,
@@ -684,7 +710,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
if (succeeded) {
onMessagesChange?.([
...(action === "message" ? [{ role: "user" as const, text: trimmed }] : []),
{ role: "assistant", text: parsed.text },
{ role: "assistant", text: settledText },
]);
onCompleted?.();
await loadCaseSnapshot();
@@ -1048,20 +1074,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const collectSpokenPrompt = currentQuestion?.kind === "collect_spoken"
? currentQuestion.prompt
: null;
const latestCollectBody = latestSettledAssistant?.text ?? "";
const collectStemAlreadyInLatestBody = Boolean(
collectSpokenPrompt
&& latestSettledAssistant
&& composeCollectSpokenAssistantText(latestCollectBody, collectSpokenPrompt) === latestCollectBody.trim(),
);
const showCollectSpokenPrompt = Boolean(
collectSpokenPrompt
&& !collectStemAlreadyInLatestBody
&& !showLiveChoiceCard
&& !readonly
&& !busy
&& regeneratingMessageKey === null,
);
const resumableCase = caseStatus !== null && isResumableStatus(caseStatus);
const showMissingQuestion = Boolean(
caseSnapshotLoaded
@@ -1081,7 +1093,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
);
const showQuestionSlot = Boolean(
showLiveChoiceCard
|| showCollectSpokenPrompt
|| showMissingQuestion
|| showUnavailableQuestion,
);
@@ -1164,12 +1175,21 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
? vargaSentenceFromMethods(message.completedReceipt?.methods)
: null;
const isLatestMessage = message.renderKey === messages[messages.length - 1]?.renderKey;
const bubbleText = !regenerating
&& isLatestMessage
&& message.role === "assistant"
&& collectSpokenPrompt
? composeCollectSpokenAssistantText(displayedMessage.text, collectSpokenPrompt)
: displayedMessage.text;
const bubbleMessage = bubbleText === displayedMessage.text
? displayedMessage
: { ...displayedMessage, text: bubbleText };
return (
<div key={message.renderKey} className="rectification-message-wrap rectification-message-entry">
{(!message.failed || Boolean(displayedMessage.text) || regenerating) && (
{(!message.failed || Boolean(bubbleMessage.text) || regenerating) && (
<ChatMessageRow
message={displayedMessage}
showActivity={displayedMessage.state !== "settled"}
message={bubbleMessage}
showActivity={bubbleMessage.state !== "settled"}
vargaSentence={vargaSentence}
/>
)}
@@ -1182,7 +1202,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
...current,
[message.renderKey]: toggleRectificationFeedback(current[message.renderKey], requested),
}))}
onCopy={() => void copyMessage(message)}
onCopy={() => void copyMessage({ ...message, text: bubbleText })}
onRegenerate={() => void regenerateMessage(message)}
/>
)}
@@ -1220,11 +1240,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onStop={submitStop}
/>
)}
{showCollectSpokenPrompt && (
<p id={collectSpokenPromptId} className="rectification-question-slot__prompt">
{collectSpokenPrompt}
</p>
)}
{showMissingQuestion && (
<p className="rectification-question-slot__status" role="status">
@@ -1253,11 +1268,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onStop={submitStop}
/>
)}
{showCollectSpokenPrompt && (
<p id={collectSpokenPromptId} className="rectification-question-slot__prompt">
{collectSpokenPrompt}
</p>
)}
{showMissingQuestion && (
<p className="rectification-question-slot__status" role="status">
@@ -1298,9 +1308,6 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
)}
<ChatComposer
inputRef={composer}
describedBy={showCollectSpokenPrompt
? collectSpokenPromptId
: undefined}
value={draft}
remainingId={composerRemainingId}
inputLabel={readonly ? "该校正已结束,只能查看历史" : "继续描述你的经历或回答"}
@@ -1308,7 +1315,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
? "该校正已结束,只能查看历史;需要再次校正请新建。"
: showLiveChoiceCard
? "点上面的选项即可;想补一句细节再写"
: showCollectSpokenPrompt || collectStemAlreadyInLatestBody
: collectSpokenPrompt
? "请回答上面的问题…"
: "继续说你记得的人生经历,或回答刚才的问题…"}
maxLength={RECTIFICATION_COMPOSER_MAX_LENGTH}