






















@@ -44,6 +44,64 @@ const loadSessionsMock = vi.hoisted(() =>
4444}
4545}),
4646);
47+const buildChatItemsMock = vi.hoisted(() =>
48+vi.fn(
49+(props: { messages: unknown[]; stream: string | null; streamStartedAt: number | null }) => {
50+if (
51+props.messages.some(
52+(message) =>
53+typeof message === "object" &&
54+message !== null &&
55+(message as { __testDivider?: unknown })["__testDivider"] === true,
56+)
57+) {
58+return [
59+{
60+kind: "divider",
61+key: "divider:compaction:test",
62+label: "Compacted history",
63+description:
64+"The compacted transcript is preserved as a checkpoint. Open session checkpoints to branch or restore from that compacted view.",
65+action: {
66+kind: "session-checkpoints",
67+label: "Open checkpoints",
68+},
69+timestamp: 1,
70+},
71+];
72+}
73+if (props.messages.length > 0) {
74+return [
75+{
76+kind: "group",
77+key: "group:assistant:test",
78+role: "assistant",
79+messages: props.messages.map((message, index) => ({
80+key: `message:${index}`,
81+ message,
82+})),
83+timestamp: 1,
84+isStreaming: false,
85+},
86+];
87+}
88+if (props.stream !== null) {
89+return props.stream
90+ ? [
91+{
92+kind: "stream",
93+key: "stream:test",
94+text: props.stream,
95+startedAt: props.streamStartedAt ?? 1,
96+isStreaming: true,
97+},
98+]
99+ : [{ kind: "reading-indicator", key: "reading:test" }];
100+}
101+return [];
102+},
103+),
104+);
4710548106function requireFirstAttachmentsChange(
49107onAttachmentsChange: ReturnType<typeof vi.fn>,
@@ -64,64 +122,7 @@ vi.mock("../icons.ts", () => ({
64122}));
6512366124vi.mock("../chat/build-chat-items.ts", () => ({
67-buildChatItems: (props: {
68-messages: unknown[];
69-stream: string | null;
70-streamStartedAt: number | null;
71-}) => {
72-if (
73-props.messages.some(
74-(message) =>
75-typeof message === "object" &&
76-message !== null &&
77-(message as { __testDivider?: unknown })["__testDivider"] === true,
78-)
79-) {
80-return [
81-{
82-kind: "divider",
83-key: "divider:compaction:test",
84-label: "Compacted history",
85-description:
86-"The compacted transcript is preserved as a checkpoint. Open session checkpoints to branch or restore from that compacted view.",
87-action: {
88-kind: "session-checkpoints",
89-label: "Open checkpoints",
90-},
91-timestamp: 1,
92-},
93-];
94-}
95-if (props.messages.length > 0) {
96-return [
97-{
98-kind: "group",
99-key: "group:assistant:test",
100-role: "assistant",
101-messages: props.messages.map((message, index) => ({
102-key: `message:${index}`,
103- message,
104-})),
105-timestamp: 1,
106-isStreaming: false,
107-},
108-];
109-}
110-if (props.stream !== null) {
111-return props.stream
112- ? [
113-{
114-kind: "stream",
115-key: "stream:test",
116-text: props.stream,
117-startedAt: props.streamStartedAt ?? 1,
118-isStreaming: true,
119-},
120-]
121- : [{ kind: "reading-indicator", key: "reading:test" }];
122-}
123-return [];
124-},
125+buildChatItems: buildChatItemsMock,
125126}));
126127127128vi.mock("../chat/grouped-render.ts", () => ({
@@ -673,13 +674,52 @@ describe("chat composer workbench", () => {
673674674675afterEach(() => {
675676vi.useRealTimers();
677+buildChatItemsMock.mockClear();
676678loadSessionsMock.mockClear();
677679refreshVisibleToolsEffectiveForCurrentSessionMock.mockClear();
678680resetChatViewState();
679681resetChatAttachmentPayloadStoreForTest();
680682vi.unstubAllGlobals();
681683});
682684685+describe("chat transcript rendering cache", () => {
686+it("does not rebuild transcript items for draft-only rerenders", () => {
687+const messages = [{ role: "assistant", content: "ready" }];
688+const toolMessages: unknown[] = [];
689+const streamSegments: Array<{ text: string; ts: number }> = [];
690+const queue: ChatQueueItem[] = [];
691+692+renderChatView({ messages, toolMessages, streamSegments, queue, draft: "" });
693+renderChatView({ messages, toolMessages, streamSegments, queue, draft: "h" });
694+renderChatView({ messages, toolMessages, streamSegments, queue, draft: "hello" });
695+696+expect(buildChatItemsMock).toHaveBeenCalledTimes(1);
697+});
698+699+it("rebuilds transcript items when the transcript reference changes", () => {
700+const toolMessages: unknown[] = [];
701+const streamSegments: Array<{ text: string; ts: number }> = [];
702+const queue: ChatQueueItem[] = [];
703+704+renderChatView({
705+messages: [{ role: "assistant", content: "ready" }],
706+ toolMessages,
707+ streamSegments,
708+ queue,
709+draft: "",
710+});
711+renderChatView({
712+messages: [{ role: "assistant", content: "new reply" }],
713+ toolMessages,
714+ streamSegments,
715+ queue,
716+draft: "",
717+});
718+719+expect(buildChatItemsMock).toHaveBeenCalledTimes(2);
720+});
721+});
722+683723describe("chat loading skeleton", () => {
684724it("renders realtime Talk transcript as ordered voice turns", () => {
685725const container = renderChatView({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。