Skip to content

Commit 5431653

Browse files
committed
fix(tui): 避免继续输出时历史内容闪烁
1 parent 2d190b5 commit 5431653

2 files changed

Lines changed: 39 additions & 7 deletions

File tree

‎packages/tui/src/routes/session/index.tsx‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
untrack,
1515
useContext,
1616
} from "solid-js"
17+
import { createStore, reconcile } from "solid-js/store"
1718
import path from "node:path"
1819
import { mkdir, writeFile } from "node:fs/promises"
1920
import { useRoute, useRouteData } from "../../context/route"
@@ -76,7 +77,7 @@ import { DialogExportOptions } from "../../ui/dialog-export-options"
7677
import * as Model from "../../util/model"
7778
import { formatTranscript } from "../../util/transcript"
7879
import { sessionEpilogue } from "../../util/presentation"
79-
import { projectContinuationMessages } from "../../util/session-continuation"
80+
import { projectContinuationMessages, type MessageWithParts } from "../../util/session-continuation"
8081
import { setPreLayoutSiblingMargin } from "../../util/layout"
8182
import { useTuiConfig } from "../../config"
8283
import { useClipboard } from "../../context/clipboard"
@@ -269,11 +270,16 @@ export function Session() {
269270
.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
270271
})
271272
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
272-
const displayMessages = createMemo(() =>
273-
projectContinuationMessages(
274-
messages().map((message) => ({ info: message, parts: sync.data.part[message.id] ?? [] })),
275-
),
276-
)
273+
const [displayMessages, setDisplayMessages] = createStore<Array<MessageWithParts & { id: string }>>([])
274+
createEffect(() => {
275+
setDisplayMessages(
276+
reconcile(
277+
projectContinuationMessages(
278+
messages().map((message) => ({ info: message, parts: sync.data.part[message.id] ?? [] })),
279+
).map((message) => ({ id: message.info.id, ...message })),
280+
),
281+
)
282+
})
277283
const hasUserMessage = createMemo(() => messages().some((message) => message.role === "user"))
278284
const canContinue = createMemo(() => {
279285
const status = sync.data.session_status[route.sessionID]
@@ -1446,7 +1452,7 @@ export function Session() {
14461452
scrollAcceleration={scrollAcceleration()}
14471453
>
14481454
<box height={1} />
1449-
<For each={displayMessages()}>
1455+
<For each={displayMessages}>
14501456
{(entry, index) => {
14511457
const message = entry.info
14521458
return (

‎packages/tui/test/util/session-continuation.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, test } from "bun:test"
22
import type { AssistantMessage, Part } from "@miaopan/sdk/v2"
3+
import { createStore, reconcile } from "solid-js/store"
34
import { projectContinuationMessages, type MessageWithParts } from "../../src/util/session-continuation"
45

56
function assistant(id: string, parentID: string, error?: AssistantMessage["error"]): AssistantMessage {
@@ -93,6 +94,31 @@ describe("projectContinuationMessages", () => {
9394
expect(input[1]?.parts).toEqual([text("prt_2", "msg_2", "continued "), text("prt_3", "msg_2", "answer")])
9495
})
9596

97+
test("preserves projected message and part identity while continued output streams", () => {
98+
const project = (value: string) =>
99+
projectContinuationMessages([
100+
{
101+
info: assistant("msg_1", "msg_user", {
102+
name: "MessageAbortedError",
103+
data: { message: "aborted" },
104+
}),
105+
parts: [text("prt_1", "msg_1", "partial ")],
106+
},
107+
{ info: assistant("msg_2", "msg_user"), parts: [text("prt_2", "msg_2", value)] },
108+
]).map((message) => ({ id: message.info.id, ...message }))
109+
const [messages, setMessages] = createStore(project("answer"))
110+
const message = messages[0]
111+
const part = messages[0]?.parts[0]
112+
113+
setMessages(reconcile(project("answer continues")))
114+
115+
expect(messages[0]).toBe(message)
116+
expect(messages[0]?.parts[0]).toBe(part)
117+
const next = messages[0]?.parts[0]
118+
expect(next?.type).toBe("text")
119+
if (next?.type === "text") expect(next.text).toBe("partial answer continues")
120+
})
121+
96122
test("keeps the latest failure visible until another assistant supersedes it", () => {
97123
const input: MessageWithParts[] = [
98124
{

0 commit comments

Comments
 (0)