fix(rectification): one stem on screen — a replaced focus draws nothing and the live one takes its slot (BUG-1135)

- GET attach: a live hanging focus replaces a superseded-unanswered focus
  linked to the last assistant turn instead of falling to a standalone copy.
- Client placement: a dead question on the latest message is replaceable,
  and the stem the body still ends with is stripped (stripQuestionSentences).
- Chat view: a dead question no longer turns the gap into 「没有拿到下一个问题」
  while the case has a different live question.
- Message entry: a superseded or replaced unanswered choice draws nothing
  (no stem without options); the current question keeps its stem while busy.
- Copy text skips a dead question.
- Three DOM tests reproduce 「stem, stem」 and 「dead stem + unavailable」, red then green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-01 10:40:29 +08:00
co-authored by Claude Opus 5.5
parent b9215d21df
commit bdf4c31bc4
5 changed files with 119 additions and 7 deletions
@@ -27,6 +27,7 @@ import {
} from "@/lib/rectification-agentic/v9/choice-card";
import {
questionIsAnswered,
questionIsDeadUnanswered,
type TurnQuestion,
} from "@/lib/rectification-agentic/v9/turn-question";
import { RECTIFICATION_STOPPED_NOTICE } from "@/lib/rectification-surface-state";
@@ -152,7 +153,16 @@ function RectificationMessageEntryView({
const embeddedCard = question && !unansweredDeadChoice
? choiceCardFromQuestion(question, liveQuestion ? liveChoiceCard : null)
: null;
const afterAnswer = question && displayedMessage.state === "settled"
// BUG-1135 (D1, supersedes BUG-917's 「stem without options」): a choice
// nobody can answer any more draws nothing on its message — only the live
// focus prints a stem, so the same stem never shows twice.
const replacedQuestion = Boolean(
question
&& unansweredDeadChoice
&& (questionIsDeadUnanswered(question)
|| (currentQuestionFocusId !== null && currentQuestionFocusId !== question.focus_id)),
);
const afterAnswer = question && displayedMessage.state === "settled" && !replacedQuestion
? (
<div className="rectification-message-question">
<p className="rectification-message-question__prompt">{question.prompt}</p>