fix(app): restore optimistic timeline state (#38693)

This commit is contained in:
Brendan Allan 2026-07-24 21:20:13 +08:00 committed by GitHub
parent 66495a2a22
commit 909db63265
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 48 additions and 0 deletions

View file

@ -332,6 +332,7 @@ export function MessageTimeline(props: {
const showHeader = createMemo(() => !!(titleValue() || parentID()))
const projection = createTimelineProjection({
messages: sessionMessages,
userMessages: () => props.userMessages,
sessionMessages: projectedMessages,
parts: getMsgParts,
status: sessionStatus,

View file

@ -8,6 +8,7 @@ export { reuseTimelineRows } from "./row-reconciliation"
export function createTimelineProjection(input: {
messages: Accessor<Message[]>
userMessages: Accessor<UserMessage[]>
sessionMessages: Accessor<SessionMessageInfo[]>
parts: (messageID: string) => Part[]
status: Accessor<SessionStatus>
@ -36,6 +37,7 @@ export function createTimelineProjection(input: {
input.showReasoningSummaries(),
input.status().type,
input.inlineComments(),
input.userMessages(),
),
)
const activeMessageID = createMemo(() => projection().activeMessageID)

View file

@ -46,6 +46,7 @@ describe("current session timeline rows", () => {
true,
"busy",
true,
normalized.messages.filter((message) => message.role === "user"),
)
expect(result.activeMessageID).toBe("msg_3")
@ -81,6 +82,7 @@ describe("current session timeline rows", () => {
true,
"idle",
true,
normalized.messages.filter((message) => message.role === "user"),
)
expect(result.activeMessageID).toBe("msg_shell")
@ -121,6 +123,7 @@ describe("current session timeline rows", () => {
true,
"idle",
true,
normalized.messages.filter((message) => message.role === "user"),
)
expect(result.rows.map(TimelineRow.key)).toEqual([
@ -131,4 +134,37 @@ describe("current session timeline rows", () => {
"assistant-part:msg_user_2:msg_assistant_2:text:0",
])
})
test("renders an optimistic user turn and thinking before the protocol message arrives", () => {
const source = [
{ id: "msg_1", type: "user", text: "existing", time: { created: 1 } },
] satisfies SessionMessageInfo[]
const normalized = normalizeSessionMessages("ses_1", source)
const optimistic = {
id: "msg_2",
sessionID: "ses_1",
role: "user" as const,
time: { created: 2 },
agent: "build",
model: { modelID: "model", providerID: "provider" },
}
const result = Timeline.constructSessionMessageRows(
source,
(messageID) =>
messageID === optimistic.id ? optimistic : normalized.messages.find((message) => message.id === messageID),
() => [],
true,
"busy",
true,
[...normalized.messages.filter((message) => message.role === "user"), optimistic],
)
expect(result.activeMessageID).toBe(optimistic.id)
expect(result.rows.map(TimelineRow.key)).toEqual([
"user-message:msg_1",
"turn-gap:msg_2",
"user-message:msg_2",
"thinking:msg_2",
])
})
})

View file

@ -39,6 +39,7 @@ export namespace Timeline {
showReasoning: boolean,
status: SessionStatus["type"],
inlineComments: boolean,
projectedUserMessages: UserMessage[],
) {
const turns: { user: UserMessage; assistants: AssistantMessage[] }[] = []
const turnByUserID = new Map<string, (typeof turns)[number]>()
@ -70,6 +71,14 @@ export namespace Timeline {
turns.push(turn)
turnByUserID.set(user.id, turn)
})
const latestUserMessageID = turns.at(-1)?.user.id
projectedUserMessages.forEach((user) => {
if (turnByUserID.has(user.id)) return
if (latestUserMessageID && user.id < latestUserMessageID) return
const turn = { user, assistants: [] }
turns.push(turn)
turnByUserID.set(user.id, turn)
})
const activeMessageID = turns.at(-1)?.user.id
return {
activeMessageID,