diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index f8134976a8..89b3c4a4b0 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, createSignal, For, Index, on, onCleanup, Show, mapArray, type JSX } from "solid-js" +import { createEffect, createMemo, createSignal, For, Index, on, onCleanup, Show, mapArray, type Accessor, type JSX } from "solid-js" import { createStore, produce } from "solid-js/store" import { Dynamic } from "solid-js/web" import { useNavigate } from "@solidjs/router" @@ -74,6 +74,7 @@ type MessageComment = { const emptyMessages: MessageType[] = [] const emptyParts: PartType[] = [] +const emptyTools: ToolPart[] = [] const emptyAssistantMessages: AssistantMessage[] = [] const idle = { type: "idle" as const } @@ -98,6 +99,7 @@ type TimelineRow = | { key: string; type: "bottom-spacer" } type FramedTimelineRow = Exclude +type TimelineRowByType = Extract function sameKeys(a: readonly string[] | undefined, b: readonly string[] | undefined) { if (a === b) return true @@ -1168,46 +1170,75 @@ export function MessageTimeline(props: { const partByRef = (messageID: string, partID: string) => (sync.data.part[messageID] ?? emptyParts).find((part) => part.id === partID) - const renderAssistantPartGroup = (row: Extract) => { - if (row.group.type === "context") { - const parts = row.group.refs - .map((ref) => partByRef(ref.messageID, ref.partID)) - .filter((part): part is ToolPart => part?.type === "tool") + const renderAssistantPartGroup = (row: Accessor>) => { + if (row().group.type === "context") { + const parts = createMemo(() => { + const group = row().group + if (group.type !== "context") return emptyTools + return group.refs + .map((ref) => partByRef(ref.messageID, ref.partID)) + .filter((part): part is ToolPart => part?.type === "tool") + }) - return + return } - const message = messageByID().get(row.group.ref.messageID) - const part = partByRef(row.group.ref.messageID, row.group.ref.partID) - if (!message || !part) return null + const message = createMemo(() => { + const group = row().group + if (group.type !== "part") return + return messageByID().get(group.ref.messageID) + }) + const part = createMemo(() => { + const group = row().group + if (group.type !== "part") return + return partByRef(group.ref.messageID, group.ref.partID) + }) return ( - + + {(message) => ( + + {(part) => ( + + )} + + )} + ) } - function TimelineRowFrame(input: { row: FramedTimelineRow; children: JSX.Element }) { - const anchor = () => input.row.type === "comment-strip" || (input.row.type === "user-message" && input.row.anchor) + function TimelineRowFrame(input: { row: Accessor; children: JSX.Element }) { + const anchor = () => { + const row = input.row() + return row.type === "comment-strip" || (row.type === "user-message" && row.anchor) + } + const previousUserMessage = () => { + const row = input.row() + return (row.type === "comment-strip" || row.type === "user-message") && row.previousUserMessage + } + const previousAssistantPart = () => { + const row = input.row() + return row.type === "assistant-part" && row.previousAssistantPart + } return (
@@ -1217,15 +1248,17 @@ export function MessageTimeline(props: { ) } - const renderTimelineRow = (row: TimelineRow) => { - switch (row.type) { - case "comment-strip": + const renderTimelineRow = (row: Accessor) => { + switch (row().type) { + case "comment-strip": { + const commentStripRow = row as Accessor> + const comments = createMemo(() => messageComments(sync.data.part[commentStripRow().userMessageID] ?? emptyParts)) return ( - +
- + {(commentAccessor: () => MessageComment) => { const comment = createMemo(() => commentAccessor()) return ( @@ -1259,78 +1292,98 @@ export function MessageTimeline(props: {
) + } case "user-message": { - const message = messageByID().get(row.userMessageID) - if (!message || message.role !== "user") return null + const userRow = row as Accessor> + const message = createMemo(() => { + const message = messageByID().get(userRow().userMessageID) + if (message?.role === "user") return message + }) return ( - -
-
- -
-
+ + + {(message) => ( +
+
+ +
+
+ )} +
) } - case "turn-divider": + case "turn-divider": { + const dividerRow = row as Accessor> return ( - +
) - case "assistant-part": + } + case "assistant-part": { + const assistantRow = row as Accessor> return ( - +
-
- {renderAssistantPartGroup(row)} +
+ {renderAssistantPartGroup(assistantRow)}
) - case "thinking": + } + case "thinking": { + const thinkingRow = row as Accessor> return ( - +
) - case "retry": + } + case "retry": { + const retryRow = row as Accessor> return ( - +
- +
) - case "diff-summary": + } + case "diff-summary": { + const diffSummaryRow = row as Accessor> return ( - +
- +
) - case "error": + } + case "error": { + const errorRow = row as Accessor> return ( - +
- {row.text} + {errorRow().text}
) + } case "bottom-spacer": return