fix(rectification): offer times under the agent bubble
Independent Staging Quality Gate / validate (push) Successful in 11m52s
Independent Staging Quality Gate / publish (push) Successful in 10m10s

Keep the natal house table as a live snapshot outside chat, and show candidate times only after selection is allowed, attached to the latest settled reply.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-19 22:13:48 +08:00
co-authored by Cursor
parent 053e63effa
commit 4d872c68b2
5 changed files with 184 additions and 63 deletions
@@ -54,6 +54,53 @@ type PersistedTurn = Readonly<{
type CandidateResult = RectificationCandidateResult | null;
function RectificationCandidateCards({
result,
acceptingCandidateId,
readonly,
onAccept,
}: Readonly<{
result: RectificationCandidateResult;
acceptingCandidateId: string | null;
readonly: boolean;
onAccept: (candidateId: string) => void;
}>) {
return (
<section className="rectification-candidates" aria-label="生时校正候选时间">
<div className="rectification-candidates-heading">
<strong></strong>
<span></span>
<span></span>
</div>
<div className="rectification-candidate-list">
{result.candidates.map((candidate) => {
const selected = result.selectedTime === candidate.time;
const recommended = isRecommendedRectificationCandidate(result, candidate);
return (
<button
type="button"
className={`rectification-candidate${selected ? " is-selected" : ""}`}
key={candidate.candidateId}
disabled={selected || Boolean(acceptingCandidateId) || readonly}
onClick={() => onAccept(candidate.candidateId)}
>
<span className="rectification-candidate-time">
<strong>{candidate.time}</strong>
{selected && <span className="rectification-candidate-badge"></span>}
{recommended && <span className="rectification-candidate-badge"></span>}
</span>
<span className="rectification-candidate-support"> {candidate.relativeSupport}</span>
<span className="rectification-candidate-action">
{selected ? "已采用" : acceptingCandidateId === candidate.candidateId ? "正在采用…" : result.selectedTime ? "改选为此时间" : "采用此时间"}
</span>
</button>
);
})}
</div>
</section>
);
}
type RectificationAgenticChatProps = Readonly<{
caseId: string;
sessionId: string;
@@ -194,7 +241,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
top: container.scrollHeight,
behavior: busy || reduceMotion ? "auto" : "smooth",
});
}, [busy, error, messages, savedTime]);
}, [busy, candidateResult, error, messages, savedTime]);
// Candidate snapshot comes from the persisted Candidate Snapshot API, never
// from parsing agent text or hidden sentinels.
@@ -498,6 +545,20 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
&& Boolean(message.turnId)
&& Boolean(message.text)
))?.renderKey;
const latestOfferMessageKey = [...messages]
.reverse()
.find((message) => (
message.role === "assistant"
&& message.state === "settled"
&& !message.failed
&& Boolean(message.text)
))?.renderKey;
const showSelectionCards = Boolean(
candidateResult?.selectionAllowed
&& latestOfferMessageKey
&& !busy
&& regeneratingMessageKey === null,
);
const canSend = !busy && !readonly && !regeneratingMessageKey;
return (
@@ -552,45 +613,21 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onRegenerate={() => void regenerateMessage(message)}
/>
)}
{showSelectionCards && message.renderKey === latestOfferMessageKey && candidateResult && (
<RectificationCandidateCards
result={candidateResult}
acceptingCandidateId={acceptingCandidateId}
readonly={readonly}
onAccept={(candidateId) => void acceptCandidate(candidateId)}
/>
)}
</div>
);
})}
{candidateResult?.houseTable && (
<RectificationHouseTableView table={candidateResult.houseTable} />
)}
{candidateResult?.selectionAllowed && (
<section className="rectification-candidates" aria-label="生时校正候选时间">
<div className="rectification-candidates-heading">
<strong></strong>
<span></span>
<span></span>
</div>
<div className="rectification-candidate-list">
{candidateResult.candidates.map((candidate) => {
const selected = candidateResult.selectedTime === candidate.time;
const recommended = isRecommendedRectificationCandidate(candidateResult, candidate);
return (
<button
type="button"
className={`rectification-candidate${selected ? " is-selected" : ""}`}
key={candidate.candidateId}
disabled={selected || Boolean(acceptingCandidateId) || readonly}
onClick={() => void acceptCandidate(candidate.candidateId)}
>
<span className="rectification-candidate-time">
<strong>{candidate.time}</strong>
{selected && <span className="rectification-candidate-badge"></span>}
{recommended && <span className="rectification-candidate-badge"></span>}
</span>
<span className="rectification-candidate-support"> {candidate.relativeSupport}</span>
<span className="rectification-candidate-action">
{selected ? "已采用" : acceptingCandidateId === candidate.candidateId ? "正在采用…" : candidateResult.selectedTime ? "改选为此时间" : "采用此时间"}
</span>
</button>
);
})}
</div>
</section>
<div className="rectification-snapshot">
<RectificationHouseTableView table={candidateResult.houseTable} />
</div>
)}
{savedTime && (
<p className="rectification-saved" role="status">
@@ -6,12 +6,13 @@ export function RectificationHouseTableView({
table,
}: Readonly<{ table: RectificationHouseTable }>) {
return (
<section className="rectification-house-table" aria-label="本命宫位">
<section className="rectification-house-table" aria-label="当前本命宫位" aria-live="polite">
<div className="rectification-house-table__heading">
<strong></strong>
<strong></strong>
<span>
{table.time} {table.lagna}
{table.time} {table.lagna}线
</span>
<span></span>
</div>
<div className="markdown-table">
<table>