refactor(app): remove Accessor wrapping from timeline row renders (#28334)

This commit is contained in:
Brendan Allan
2026-05-19 20:22:50 +08:00
committed by GitHub
parent a2ee437a0e
commit 922b2e10eb
@@ -1,19 +1,4 @@
import { import { createEffect, createMemo, createSignal, For, Index, on, onCleanup, Show, mapArray, type JSX } from "solid-js"
createEffect,
createMemo,
createSignal,
For,
Index,
Match,
Switch,
on,
onCleanup,
Show,
mapArray,
untrack,
type Accessor,
type JSX,
} from "solid-js"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
@@ -995,26 +980,26 @@ export function MessageTimeline(props: {
const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID) const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID)
const renderAssistantPartGroup = (row: Accessor<TimelineRowMap["AssistantPart"]>) => { const renderAssistantPartGroup = (row: TimelineRowMap["AssistantPart"]) => {
if (untrack(row).group.type === "context") { if (row.group.type === "context") {
const parts = createMemo(() => { const parts = createMemo(() => {
const group = row().group const group = row.group
if (group.type !== "context") return emptyTools if (group.type !== "context") return emptyTools
return group.refs return group.refs
.map((ref) => getMsgPart(ref.messageID, ref.partID)) .map((ref) => getMsgPart(ref.messageID, ref.partID))
.filter((part): part is ToolPart => part?.type === "tool") .filter((part): part is ToolPart => part?.type === "tool")
}) })
return <ContextToolGroup parts={parts()} busy={workingTurn(row().userMessageID) && row().lastAssistantPart} /> return <ContextToolGroup parts={parts()} busy={workingTurn(row.userMessageID) && row.lastAssistantPart} />
} }
const message = createMemo(() => { const message = createMemo(() => {
const group = row().group const group = row.group
if (group.type !== "part") return if (group.type !== "part") return
return messageByID().get(group.ref.messageID) return messageByID().get(group.ref.messageID)
}) })
const part = createMemo(() => { const part = createMemo(() => {
const group = row().group const group = row.group
if (group.type !== "part") return if (group.type !== "part") return
return getMsgPart(group.ref.messageID, group.ref.partID) return getMsgPart(group.ref.messageID, group.ref.partID)
}) })
@@ -1027,8 +1012,8 @@ export function MessageTimeline(props: {
<MessagePart <MessagePart
part={part()} part={part()}
message={message()} message={message()}
showAssistantCopyPartID={assistantCopyPartID(row().userMessageID)} showAssistantCopyPartID={assistantCopyPartID(row.userMessageID)}
turnDurationMs={turnDurationMs(row().userMessageID)} turnDurationMs={turnDurationMs(row.userMessageID)}
defaultOpen={partDefaultOpen( defaultOpen={partDefaultOpen(
part(), part(),
settings.general.shellToolPartsExpanded(), settings.general.shellToolPartsExpanded(),
@@ -1043,25 +1028,25 @@ export function MessageTimeline(props: {
) )
} }
function TimelineRowFrame(input: { row: Accessor<FramedTimelineRow>; children: JSX.Element }) { function TimelineRowFrame(input: { row: FramedTimelineRow; children: JSX.Element }) {
const anchor = () => { const anchor = () => {
const row = input.row() const row = input.row
return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor) return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor)
} }
const previousUserMessage = () => { const previousUserMessage = () => {
const row = input.row() const row = input.row
return (row._tag === "CommentStrip" || row._tag === "UserMessage") && row.previousUserMessage return (row._tag === "CommentStrip" || row._tag === "UserMessage") && row.previousUserMessage
} }
const previousAssistantPart = () => { const previousAssistantPart = () => {
const row = input.row() const row = input.row
return row._tag === "AssistantPart" && row.previousAssistantPart return row._tag === "AssistantPart" && row.previousAssistantPart
} }
return ( return (
<div <div
id={anchor() ? props.anchor(input.row().userMessageID) : undefined} id={anchor() ? props.anchor(input.row.userMessageID) : undefined}
data-message-id={input.row().userMessageID} data-message-id={input.row.userMessageID}
data-timeline-row={input.row()._tag} data-timeline-row={input.row._tag}
classList={{ classList={{
"min-w-0 w-full max-w-full": true, "min-w-0 w-full max-w-full": true,
"md:max-w-200 2xl:max-w-[1000px]": props.centered, "md:max-w-200 2xl:max-w-[1000px]": props.centered,
@@ -1077,15 +1062,14 @@ export function MessageTimeline(props: {
) )
} }
const renderTimelineRow = (row: Accessor<TimelineRow.TimelineRow>) => { const renderTimelineRow = (row: TimelineRow.TimelineRow) => {
switch (row()._tag) { switch (row._tag) {
case "CommentStrip": { case "CommentStrip": {
const commentStripRow = row as Accessor<TimelineRowByTag<"CommentStrip">>
const comments = createMemo(() => const comments = createMemo(() =>
getMsgParts(commentStripRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []), getMsgParts(row.userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []),
) )
return ( return (
<TimelineRowFrame row={commentStripRow}> <TimelineRowFrame row={row}>
<div class="w-full px-4 md:px-5 pb-2"> <div class="w-full px-4 md:px-5 pb-2">
<div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar"> <div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
<div class="flex w-max min-w-full justify-end gap-2"> <div class="flex w-max min-w-full justify-end gap-2">
@@ -1118,22 +1102,17 @@ export function MessageTimeline(props: {
) )
} }
case "UserMessage": { case "UserMessage": {
const userMessageRow = row as Accessor<TimelineRowByTag<"UserMessage">>
const message = createMemo(() => { const message = createMemo(() => {
const m = messageByID().get(userMessageRow().userMessageID) const m = messageByID().get(row.userMessageID)
if (m?.role === "user") return m if (m?.role === "user") return m
}) })
return ( return (
<TimelineRowFrame row={userMessageRow}> <TimelineRowFrame row={row}>
<Show when={message()}> <Show when={message()}>
{(message) => ( {(message) => (
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<div data-slot="session-turn-message-content" aria-live="off"> <div data-slot="session-turn-message-content" aria-live="off">
<Message <Message message={message()} parts={getMsgParts(row.userMessageID)} actions={props.actions} />
message={message()}
parts={getMsgParts(userMessageRow().userMessageID)}
actions={props.actions}
/>
</div> </div>
</div> </div>
)} )}
@@ -1142,14 +1121,13 @@ export function MessageTimeline(props: {
) )
} }
case "TurnDivider": { case "TurnDivider": {
const turnDividerRow = row as Accessor<TimelineRowByTag<"TurnDivider">>
return ( return (
<TimelineRowFrame row={turnDividerRow}> <TimelineRowFrame row={row}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<div data-slot="session-turn-compaction"> <div data-slot="session-turn-compaction">
<MessageDivider <MessageDivider
label={language.t( label={language.t(
turnDividerRow().label === "compaction" ? "ui.messagePart.compaction" : "ui.message.interrupted", row.label === "compaction" ? "ui.messagePart.compaction" : "ui.message.interrupted",
)} )}
/> />
</div> </div>
@@ -1158,27 +1136,22 @@ export function MessageTimeline(props: {
) )
} }
case "AssistantPart": { case "AssistantPart": {
const assistantPartRow = row as Accessor<TimelineRowByTag<"AssistantPart">>
return ( return (
<TimelineRowFrame row={assistantPartRow}> <TimelineRowFrame row={row}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<div <div data-slot="session-turn-assistant-content" aria-hidden={workingTurn(row.userMessageID)}>
data-slot="session-turn-assistant-content" {renderAssistantPartGroup(row)}
aria-hidden={workingTurn(assistantPartRow().userMessageID)}
>
{renderAssistantPartGroup(assistantPartRow)}
</div> </div>
</div> </div>
</TimelineRowFrame> </TimelineRowFrame>
) )
} }
case "Thinking": { case "Thinking": {
const thinkingRow = row as Accessor<TimelineRowByTag<"Thinking">>
return ( return (
<TimelineRowFrame row={thinkingRow}> <TimelineRowFrame row={row}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<TimelineThinkingRow <TimelineThinkingRow
reasoningHeading={thinkingRow().reasoningHeading} reasoningHeading={row.reasoningHeading}
showReasoningSummaries={settings.general.showReasoningSummaries()} showReasoningSummaries={settings.general.showReasoningSummaries()}
/> />
</div> </div>
@@ -1186,32 +1159,29 @@ export function MessageTimeline(props: {
) )
} }
case "Retry": { case "Retry": {
const retryRow = row as Accessor<TimelineRowByTag<"Retry">>
return ( return (
<TimelineRowFrame row={retryRow}> <TimelineRowFrame row={row}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<SessionRetry status={sessionStatus()} show={activeMessageID() === retryRow().userMessageID} /> <SessionRetry status={sessionStatus()} show={activeMessageID() === row.userMessageID} />
</div> </div>
</TimelineRowFrame> </TimelineRowFrame>
) )
} }
case "DiffSummary": { case "DiffSummary": {
const diffSummaryRow = row as Accessor<TimelineRowByTag<"DiffSummary">>
return ( return (
<TimelineRowFrame row={diffSummaryRow}> <TimelineRowFrame row={row}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<TimelineDiffSummaryRow diffs={diffSummaryRow().diffs} /> <TimelineDiffSummaryRow diffs={row.diffs} />
</div> </div>
</TimelineRowFrame> </TimelineRowFrame>
) )
} }
case "Error": { case "Error": {
const errorRow = row as Accessor<TimelineRowByTag<"Error">>
return ( return (
<TimelineRowFrame row={errorRow}> <TimelineRowFrame row={row}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<Card variant="error" class="error-card"> <Card variant="error" class="error-card">
{errorRow().text} {row.text}
</Card> </Card>
</div> </div>
</TimelineRowFrame> </TimelineRowFrame>
@@ -1223,7 +1193,11 @@ export function MessageTimeline(props: {
} }
function TimelineRowView(props: { rowKey: string }) { function TimelineRowView(props: { rowKey: string }) {
return <Show when={timelineRowByKey().get(props.rowKey)}>{(item) => renderTimelineRow(item)}</Show> return (
<Show when={timelineRowByKey().get(props.rowKey)} keyed>
{(item) => renderTimelineRow(item)}
</Show>
)
} }
return ( return (