fix(rectification): show natal houses and lagna changes in a live right rail

The board was live data sitting under chat, so each new event looked like nothing changed. Keep candidate cards in the transcript and group 换升 by minute for the side panel.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-20 14:24:05 +08:00
co-authored by Cursor
parent 19de0bab36
commit e07d1bada4
10 changed files with 985 additions and 130 deletions
@@ -24,6 +24,14 @@ const agent = readFileSync(
"utf8",
);
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const board = readFileSync(
new URL("../src/components/rectification-board.tsx", import.meta.url),
"utf8",
);
const houseTable = readFileSync(
new URL("../src/components/rectification-house-table.tsx", import.meta.url),
"utf8",
);
const completedActivityReceipt = readFileSync(
new URL("../src/components/completed-activity-receipt.tsx", import.meta.url),
"utf8",
@@ -224,7 +232,7 @@ test("rectification uses one case-level entitlement and the session-pinned model
test("Agentic rectification scrolls the conversation container as streamed messages grow", () => {
assert.match(chat, /const conversation = useRef<HTMLElement>\(null\)/);
assert.match(chat, /<section ref=\{conversation\} className="conversation"/);
assert.match(chat, /<section ref=\{conversation\} className="conversation is-rectification"/);
assert.match(chat, /const container = conversation\.current/);
assert.match(chat, /top: container\.scrollHeight/);
assert.match(chat, /\}, \[busy, candidateResult, error, messages, savedTime\]\);/);
@@ -245,7 +253,7 @@ test("rectification keeps receipts for the varga sentence and hides Activity fro
assert.match(chat, /showActivity=\{displayedMessage\.state === "thinking"\}/);
assert.match(chat, /vargaSentenceFromMethods/);
assert.match(chat, /vargaSentence=\{vargaSentence\}/);
assert.match(chat, /RectificationHouseTableView/);
assert.match(board, /RectificationHouseTableView/);
assert.doesNotMatch(chat, /activeActivity/);
assert.doesNotMatch(chat, /正在读取校正记录/);
assert.doesNotMatch(chat, /<CompletedActivityReceipt/);
@@ -259,9 +267,8 @@ test("rectification keeps receipts for the varga sentence and hides Activity fro
chat.indexOf("{savedTime &&"),
);
const replyIndex = messageRender.indexOf("<ChatMessageRow");
const houseIndex = messageRender.indexOf("<RectificationHouseTableView");
assert.ok(replyIndex >= 0);
assert.ok(houseIndex > replyIndex);
assert.doesNotMatch(messageRender, /<RectificationHouseTableView/);
assert.match(completedActivityReceipt, /<details className=/);
assert.doesNotMatch(completedActivityReceipt, /<details[^>]*\sopen/);
assert.match(completedActivityReceipt, /本轮完成 · \$\{stepLabels\.length\} 个步骤 · \$\{receipt\.methods\.length\} 项计算依据/);
@@ -314,34 +321,33 @@ test("candidate state renders from the snapshot API and never from sentinels", (
assert.match(chat, /isRecommendedRectificationCandidate/);
assert.match(chat, /已采用/);
assert.match(chat, /正在采用…/);
assert.match(chat, /RectificationHouseTableView/);
assert.match(chat, /className="rectification-snapshot"/);
assert.match(chat, /RectificationBoard/);
assert.match(board, /className="rectification-snapshot"/);
assert.match(styles, /\.rectification-snapshot \{/);
assert.match(styles, /\.rectification-house-table \{[^}]*border:\s*1px solid var\(--color-border\)/);
assert.match(styles, /\.rectification-house-table \{[^}]*background:\s*var\(--color-canvas-soft\)/);
assert.match(styles, /\.message-list \{[\s\S]*--assistant-content-inset: calc\(32px \+ var\(--space-3\)\)/);
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*width: calc\(100% - var\(--assistant-content-inset\)\)/);
assert.match(styles, /\.rectification-message-wrap \.rectification-candidates \{[\s\S]*margin-inline-start: var\(--assistant-content-inset\)/);
assert.match(styles, /\.rectification-snapshot \{[\s\S]*width: calc\(100% - var\(--assistant-content-inset\)\)/);
assert.match(styles, /\.rectification-snapshot \{[\s\S]*margin-inline-start: var\(--assistant-content-inset\)/);
assert.match(styles, /\.rectification-workspace \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) minmax\(18rem, 22\.5rem\)/);
assert.match(styles, /\.rectification-workspace\.is-compact \{[\s\S]*grid-template-columns: minmax\(0, 1fr\)/);
assert.doesNotMatch(styles, /\.rectification-snapshot \{[^}]*--assistant-content-inset/);
assert.doesNotMatch(styles, /\.rectification-house-table \{[^}]*padding-inline-start:\s*var\(--assistant-content-inset\)/);
assert.doesNotMatch(styles, /\.rectification-house-table \{[^}]*--assistant-content-inset/);
assert.doesNotMatch(page, /rectification-workspace/);
assert.match(page, /chat-panel\$\{rectificationSurfaceOpen \? " is-rectification" : ""\}/);
});
test("the natal house table is a live snapshot card outside the agent bubble", () => {
const houseTable = readFileSync(
new URL("../src/components/rectification-house-table.tsx", import.meta.url),
"utf8",
);
test("the natal house table is a live board beside the chat, not a message", () => {
const messageLoop = chat.slice(
chat.indexOf("{messages.map((message) => {"),
chat.indexOf("{candidateResult?.houseTable &&"),
);
const snapshot = chat.slice(
chat.indexOf("{candidateResult?.houseTable &&"),
chat.indexOf("{savedTime &&"),
);
const snapshot = board.slice(
board.indexOf("className=\"rectification-snapshot\""),
board.indexOf("{scoringMinutes.length > 0 || displayMinutes.length > 0"),
);
const rowIndex = messageLoop.indexOf("<ChatMessageRow");
const snapshotIndex = snapshot.indexOf("className=\"rectification-snapshot\"");
const houseIndex = snapshot.indexOf("<RectificationHouseTableView");
@@ -349,8 +355,15 @@ test("the natal house table is a live snapshot card outside the agent bubble", (
assert.ok(snapshotIndex >= 0 && houseIndex > snapshotIndex);
assert.doesNotMatch(snapshot, /<RectificationCandidateCards/);
assert.doesNotMatch(snapshot, /当前可能的出生时间/);
assert.doesNotMatch(messageLoop, /className="rectification-snapshot"/);
assert.doesNotMatch(messageLoop, /<RectificationHouseTableView/);
assert.match(chat, /className="conversation is-rectification"/);
assert.match(chat, /<RectificationBoard/);
assert.match(chat, /await loadCandidate\(\)\.then\(\(result\) => \{/);
assert.match(houseTable, /aria-live="polite"/);
assert.match(board, /aria-live="polite"/);
assert.match(board, /groupWindowTransitions/);
assert.match(board, /<dialog/);
assert.match(chat, /RectificationBoardPeek/);
assert.match(houseTable, /当前本命宫位/);
assert.match(houseTable, /补充经历后会按新线索重算/);
assert.doesNotMatch(houseTable, /<ChatMessageRow/);
@@ -359,7 +372,7 @@ test("the natal house table is a live snapshot card outside the agent bubble", (
test("time-selection cards appear under the latest settled agent bubble only after offer-candidates", () => {
const messageLoop = chat.slice(
chat.indexOf("{messages.map((message) => {"),
chat.indexOf("{candidateResult?.houseTable &&"),
chat.indexOf("{savedTime &&"),
);
const actionsIndex = messageLoop.indexOf("<ChatMessageActions");
const cardsIndex = messageLoop.indexOf("<RectificationCandidateCards");
@@ -437,8 +450,8 @@ test("the Agent prompt cannot offer candidates while asking for more evidence",
test("adopted time offers a consultation handoff without unique-minute copy", () => {
assert.match(chat, /用这个时间看盘/);
assert.match(chat, /onStartConsultation/);
assert.match(chat, /换升时刻/);
assert.match(chat, /经历与大运对照/);
assert.match(board, /换升时刻/);
assert.match(board, /经历与大运对照/);
assert.match(chat, /盘外对照/);
assert.match(page, /startConsultationAfterRectification/);
assert.match(page, /createSession\(modelCatalog\.defaultModelId\)/);