fix(rectification): make every gap a live row with retries, give an empty Case a start, and say why a run stopped

The chat used to show "当前没有可回答的问题,正在等待服务端更新" / "当前问题暂时无法显示"
/ "题目加载失败,请刷新" as bare copy above the composer. The gap between a
settled turn and its next question is now a state: `preparing` renders the
same timeline live row the reply uses ("正在准备下一个问题…") and refetches the
Case on a 2s timer up to two retries; `unavailable` renders one line and a
44px reload button. A snapshot that never arrived at hydration is the same
gap. A resumed Case with no turns and no server-started opening shows
"这段校正还没有开始。" with one 开始提问 action. Stopping a run keeps what
streamed and says so instead of "暂时不可用"; the opening turn's live row
names what is happening; a 402 explains itself for 600ms before leaving.

BUG-505, BUG-507

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
This commit is contained in:
Jesse_Chen
2026-09-03 06:56:17 +00:00
parent 53017d4153
commit 360989ca2c
6 changed files with 169 additions and 50 deletions
@@ -228,6 +228,8 @@ export type RectificationQuestionGapInput = Readonly<{
questionMissing: boolean;
/** The server said the question could not be loaded (`question_source: "unavailable"`). */
questionLoadFailed: boolean;
/** Candidate cards are offered and not yet adopted: the reader, not the server, holds the next move. */
offerAwaitingReader?: boolean;
busy: boolean;
readonly: boolean;
regenerating: boolean;
@@ -251,7 +253,7 @@ export function rectificationQuestionGapState(input: RectificationQuestionGapInp
const retryGate = input.retryAttempts < limit ? "preparing" : "unavailable";
if (!input.snapshotLoaded) return retryGate;
if (!input.resumableCase) return "idle";
if (input.liveQuestionVisible) return "idle";
if (input.liveQuestionVisible || input.offerAwaitingReader) return "idle";
if (input.questionLoadFailed) return "unavailable";
// Either the snapshot names no question, or it names one that no settled
// message carries live yet: both are a gap the next read may close.