diff --git a/packages/ui/src/components/message-nav.css b/packages/ui/src/components/message-nav.css
new file mode 100644
index 000000000..6e9d96a26
--- /dev/null
+++ b/packages/ui/src/components/message-nav.css
@@ -0,0 +1,95 @@
+[data-component="message-nav"] {
+ /* margin-right: 32px; */
+ /* margin-top: 12px; */
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding-left: 0;
+ list-style: none;
+
+ &[data-size="normal"] {
+ position: absolute;
+ right: 100%;
+ width: 240px;
+ /* margin-top: 12px; */
+
+ @media (min-width: 80rem) {
+ gap: 8px;
+ /* margin-top: 4px; */
+ }
+ }
+}
+
+[data-slot="message-nav-item"] {
+ display: flex;
+ align-items: center;
+ align-self: stretch;
+ justify-content: flex-end;
+
+ [data-component="message-nav"][data-size="normal"] & {
+ justify-content: flex-start;
+ }
+}
+
+[data-slot="message-nav-tick-button"] {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ height: 8px;
+ width: 32px;
+ /* margin-right: -12px; */
+ cursor: pointer;
+ border: none;
+ background: none;
+ padding: 0;
+
+ &[data-active] [data-slot="message-nav-tick-line"] {
+ background-color: var(--icon-strong-base);
+ width: 100%;
+ }
+}
+
+[data-slot="message-nav-tick-line"] {
+ height: 1px;
+ width: 20px;
+ background-color: var(--icon-base);
+ transition:
+ width 0.2s,
+ background-color 0.2s;
+}
+
+[data-slot="message-nav-tick-button"]:hover [data-slot="message-nav-tick-line"] {
+ width: 100%;
+ background-color: var(--icon-strong-base);
+}
+
+[data-slot="message-nav-message-button"] {
+ display: flex;
+ align-items: center;
+ align-self: stretch;
+ width: 100%;
+ column-gap: 8px;
+ cursor: default;
+ border: none;
+ background: none;
+ padding: 0;
+}
+
+[data-slot="message-nav-title-preview"] {
+ font-size: 14px; /* text-14-regular */
+ color: var(--text-weak);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+ text-align: left;
+
+ &[data-active] {
+ color: var(--text-strong);
+ }
+}
+
+[data-slot="message-nav-item"]:hover [data-slot="message-nav-title-preview"] {
+ color: var(--text-base);
+}
diff --git a/packages/ui/src/components/message-nav.tsx b/packages/ui/src/components/message-nav.tsx
new file mode 100644
index 000000000..8475c3206
--- /dev/null
+++ b/packages/ui/src/components/message-nav.tsx
@@ -0,0 +1,66 @@
+import { UserMessage } from "@opencode-ai/sdk"
+import { ComponentProps, createMemo, For, Match, Show, splitProps, Switch } from "solid-js"
+import { DiffChanges } from "./diff-changes"
+import { Spinner } from "./spinner"
+
+export function MessageNav(
+ props: ComponentProps<"ul"> & {
+ messages: UserMessage[]
+ current?: UserMessage
+ size: "normal" | "compact"
+ working?: boolean
+ onMessageSelect: (message: UserMessage) => void
+ },
+) {
+ const [local, others] = splitProps(props, ["messages", "current", "size", "working", "onMessageSelect"])
+ const lastUserMessage = createMemo(() => {
+ return local.messages?.at(0)
+ })
+
+ return (
+
+
+ {(message) => {
+ const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && local.working)
+ const handleClick = () => local.onMessageSelect(message)
+
+ return (
+ -
+
+
+
+
+
+
+
+
+
+ )
+ }}
+
+
+ )
+}
diff --git a/packages/ui/src/components/message-progress.tsx b/packages/ui/src/components/message-progress.tsx
index 48a234535..ca42d26ec 100644
--- a/packages/ui/src/components/message-progress.tsx
+++ b/packages/ui/src/components/message-progress.tsx
@@ -3,7 +3,6 @@ import { Part } from "./message-part"
import { Spinner } from "./spinner"
import { useData } from "../context/data"
import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk"
-import "./message-progress.css"
export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
const data = useData()
diff --git a/packages/ui/src/components/session-timeline.css b/packages/ui/src/components/session-timeline.css
deleted file mode 100644
index e86e80e6e..000000000
--- a/packages/ui/src/components/session-timeline.css
+++ /dev/null
@@ -1,324 +0,0 @@
-[data-component="session-timeline"] {
- /* flex: 1; */
- min-height: 0;
- display: flex;
- align-items: flex-start;
- justify-content: flex-start;
-
- [data-slot="session-timeline-timeline-list"] {
- margin-right: 32px;
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- margin-top: 12px;
-
- &[data-expanded="true"] {
- position: absolute;
- right: 100%;
- width: 240px;
- margin-top: 12px;
-
- @media (min-width: 80rem) {
- gap: 8px;
- margin-top: 4px;
- }
- }
- }
-
- [data-slot="session-timeline-timeline-item"] {
- display: flex;
- align-items: center;
- align-self: stretch;
- justify-content: flex-end;
-
- &[data-expanded="true"] {
- @media (min-width: 80rem) {
- justify-content: flex-start;
- }
- }
- }
-
- [data-slot="session-timeline-tick-button"] {
- display: flex;
- align-items: center;
- justify-content: flex-start;
- height: 8px;
- width: 32px;
- margin-right: -12px;
- cursor: pointer;
- border: none;
- background: none;
- padding: 0;
-
- &[data-active="true"] [data-slot="session-timeline-tick-line"] {
- background-color: var(--icon-strong-base);
- width: 100%;
- }
-
- &[data-expanded="true"] {
- @media (min-width: 80rem) {
- display: none;
- }
- }
- }
-
- [data-slot="session-timeline-tick-line"] {
- height: 1px;
- width: 20px;
- background-color: var(--icon-base);
- transition:
- width 0.2s,
- background-color 0.2s;
- }
-
- [data-slot="session-timeline-tick-button"]:hover [data-slot="session-timeline-tick-line"] {
- width: 100%;
- background-color: var(--icon-strong-base);
- }
-
- [data-slot="session-timeline-message-button"] {
- display: none;
- align-items: center;
- align-self: stretch;
- width: 100%;
- column-gap: 8px;
- cursor: default;
- border: none;
- background: none;
- padding: 0;
-
- &[data-expanded="true"] {
- @media (min-width: 80rem) {
- display: flex;
- }
- }
- }
-
- [data-slot="session-timeline-message-title-preview"] {
- font-size: 14px; /* text-14-regular */
- color: var(--text-weak);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- text-align: left;
-
- &[data-active="true"] {
- color: var(--text-strong);
- }
- }
-
- [data-slot="session-timeline-timeline-item"]:hover [data-slot="session-timeline-message-title-preview"] {
- color: var(--text-base);
- }
-
- [data-slot="session-timeline-content"] {
- flex-grow: 1;
- width: 100%;
- height: 100%;
- min-width: 0;
- overflow-y: auto;
- scrollbar-width: none;
- }
-
- [data-slot="session-timeline-content"]::-webkit-scrollbar {
- display: none;
- }
-
- [data-slot="session-timeline-message-container"] {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- align-self: stretch;
- gap: 32px;
- }
-
- [data-slot="session-timeline-message-header"] {
- display: flex;
- align-items: center;
- gap: 8px;
- align-self: stretch;
- position: sticky;
- top: 0;
- background-color: var(--background-stronger);
- z-index: 20;
- height: 32px;
- }
-
- [data-slot="session-timeline-message-content"] {
- margin-top: -24px;
- }
-
- [data-slot="session-timeline-message-title"] {
- width: 100%;
- font-size: 14px; /* text-14-medium */
- font-weight: 500;
- color: var(--text-strong);
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- white-space: nowrap;
- }
-
- [data-slot="session-timeline-message-title"] h1 {
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- white-space: nowrap;
- font-size: inherit;
- font-weight: inherit;
- }
-
- [data-slot="session-timeline-typewriter"] {
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- white-space: nowrap;
- }
-
- [data-slot="session-timeline-summary-section"] {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 24px;
- align-items: flex-start;
- align-self: stretch;
- }
-
- [data-slot="session-timeline-summary-header"] {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 4px;
- align-self: stretch;
- }
-
- [data-slot="session-timeline-summary-title"] {
- font-size: 12px; /* text-12-medium */
- font-weight: 500;
- color: var(--text-weak);
- }
-
- [data-slot="session-timeline-markdown"] {
- &[data-diffs="true"] {
- font-size: 14px; /* text-14-regular */
- }
-
- &[data-fade="true"] > * {
- animation: fade-up-text 0.3s ease-out forwards;
- }
- }
-
- [data-slot="session-timeline-accordion"] {
- width: 100%;
- }
-
- [data-component="sticky-accordion-header"] {
- top: 40px;
-
- &[data-expanded]::before {
- top: -40px;
- }
- }
-
- [data-slot="session-timeline-accordion-trigger-content"] {
- display: flex;
- align-items: center;
- justify-content: space-between;
- width: 100%;
- gap: 20px;
- }
-
- [data-slot="session-timeline-file-info"] {
- flex-grow: 1;
- display: flex;
- align-items: center;
- gap: 20px;
- min-width: 0;
- }
-
- [data-slot="session-timeline-file-icon"] {
- flex-shrink: 0;
- width: 16px;
- height: 16px;
- }
-
- [data-slot="session-timeline-file-path"] {
- display: flex;
- flex-grow: 1;
- min-width: 0;
- }
-
- [data-slot="session-timeline-directory"] {
- color: var(--text-base);
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- direction: rtl;
- text-align: left;
- }
-
- [data-slot="session-timeline-filename"] {
- color: var(--text-strong);
- flex-shrink: 0;
- }
-
- [data-slot="session-timeline-accordion-actions"] {
- flex-shrink: 0;
- display: flex;
- gap: 16px;
- align-items: center;
- justify-content: flex-end;
- }
-
- [data-slot="session-timeline-accordion-content"] {
- max-height: 240px; /* max-h-60 */
- overflow-y: auto;
- scrollbar-width: none;
- }
-
- [data-slot="session-timeline-accordion-content"]::-webkit-scrollbar {
- display: none;
- }
-
- [data-slot="session-timeline-response-section"] {
- width: 100%;
- }
-
- [data-slot="session-timeline-collapsible-trigger-content"] {
- color: var(--text-weak);
- cursor: pointer;
- background: none;
- border: none;
- padding: 0;
- display: flex;
- align-items: center;
-
- &:hover {
- color: var(--text-strong);
- }
- display: flex;
- align-items: center;
- gap: 4px;
- align-self: stretch;
- }
-
- [data-slot="session-timeline-details-text"] {
- font-size: 12px; /* text-12-medium */
- font-weight: 500;
- }
-
- .error-card {
- color: var(--text-on-critical-base);
- }
-
- [data-slot="session-timeline-collapsible-content-inner"] {
- width: 100%;
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- align-self: stretch;
- gap: 12px;
- }
-}
diff --git a/packages/ui/src/components/session-timeline.tsx b/packages/ui/src/components/session-timeline.tsx
deleted file mode 100644
index 5d451b39d..000000000
--- a/packages/ui/src/components/session-timeline.tsx
+++ /dev/null
@@ -1,289 +0,0 @@
-import { AssistantMessage } from "@opencode-ai/sdk"
-import { useData } from "../context"
-import { Binary } from "@opencode-ai/util/binary"
-import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { createEffect, createMemo, createSignal, For, Match, ParentProps, Show, Switch } from "solid-js"
-import { createStore } from "solid-js/store"
-import { DiffChanges } from "./diff-changes"
-import { Spinner } from "./spinner"
-import { Typewriter } from "./typewriter"
-import { Message } from "./message-part"
-import { Markdown } from "./markdown"
-import { Accordion } from "./accordion"
-import { StickyAccordionHeader } from "./sticky-accordion-header"
-import { FileIcon } from "./file-icon"
-import { Icon } from "./icon"
-import { Diff } from "./diff"
-import { Card } from "./card"
-import { MessageProgress } from "./message-progress"
-import { Collapsible } from "./collapsible"
-
-export function SessionTimeline(
- props: ParentProps<{
- sessionID: string
- classes?: {
- root?: string
- content?: string
- container?: string
- }
- expanded?: boolean
- }>,
-) {
- const data = useData()
- const [store, setStore] = createStore({
- messageId: undefined as string | undefined,
- })
- const match = Binary.search(data.session, props.sessionID, (s) => s.id)
- if (!match.found) throw new Error(`Session ${props.sessionID} not found`)
-
- // const info = createMemo(() => data.session[match.index])
- const messages = createMemo(() => (props.sessionID ? (data.message[props.sessionID] ?? []) : []))
- const userMessages = createMemo(() =>
- messages()
- .filter((m) => m.role === "user")
- .sort((a, b) => b.id.localeCompare(a.id)),
- )
- const lastUserMessage = createMemo(() => {
- return userMessages()?.at(0)
- })
- const activeMessage = createMemo(() => {
- if (!store.messageId) return lastUserMessage()
- return userMessages()?.find((m) => m.id === store.messageId)
- })
- const status = createMemo(
- () =>
- data.session_status[props.sessionID] ?? {
- type: "idle",
- },
- )
- const working = createMemo(() => status()?.type !== "idle")
-
- return (
-
-
1}>
-
-
- {(message) => {
- const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && working())
- const handleClick = () => setStore("messageId", message.id)
-
- return (
- -
-
-
-
- )
- }}
-
-
-
-
-
- {(message) => {
- const isActive = createMemo(() => activeMessage()?.id === message.id)
- const titleSeen = createMemo(() => true)
- const contentSeen = createMemo(() => true)
- {
- /* const titleSeen = createSeen(`message-title-${message.id}`) */
- }
- {
- /* const contentSeen = createSeen(`message-content-${message.id}`) */
- }
- const [titled, setTitled] = createSignal(titleSeen())
- const assistantMessages = createMemo(() => {
- return messages()?.filter((m) => m.role === "assistant" && m.parentID == message.id) as AssistantMessage[]
- })
- const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
- const [detailsExpanded, setDetailsExpanded] = createSignal(false)
- const parts = createMemo(() => data.part[message.id])
- const hasToolPart = createMemo(() =>
- assistantMessages()
- ?.flatMap((m) => data.part[m.id])
- .some((p) => p?.type === "tool"),
- )
- const messageWorking = createMemo(() => message.id === lastUserMessage()?.id && working())
- const initialCompleted = !(message.id === lastUserMessage()?.id && working())
- const [completed, setCompleted] = createSignal(initialCompleted)
-
- // allowing time for the animations to finish
- createEffect(() => {
- if (titleSeen()) return
- const title = message.summary?.title
- if (title) setTimeout(() => setTitled(true), 10_000)
- })
- createEffect(() => {
- const completed = !messageWorking()
- setTimeout(() => setCompleted(completed), 1200)
- })
-
- return (
-
-
- {/* Title */}
-
-
-
- }
- >
-
{message.summary?.title}
-
-
-
-
-
-
- {/* Summary */}
-
-
-
-
-
- Summary
- Response
-
-
-
- {(summary) => (
-
- )}
-
-
-
-
- {(diff) => (
-
-
-
-
-
-
-
-
-
- {getDirectory(diff.file)}
-
-
- {getFilename(diff.file)}
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
-
-
- {error()?.data?.message as string}
-
-
- {/* Response */}
-
-
-
-
-
-
-
-
-
-
-
- Hide details
- Show details
-
-
-
-
-
-
-
-
- {(assistantMessage) => {
- const parts = createMemo(() => data.part[assistantMessage.id])
- return
- }}
-
-
-
- {error()?.data?.message as string}
-
-
-
-
-
-
-
-
-
-
- )
- }}
-
- {props.children}
-
-
- )
-}
diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css
new file mode 100644
index 000000000..1dfb54c56
--- /dev/null
+++ b/packages/ui/src/components/session-turn.css
@@ -0,0 +1,220 @@
+[data-component="session-turn"] {
+ /* flex: 1; */
+ height: 100%;
+ min-height: 0;
+ min-width: 0;
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-start;
+
+ [data-slot="session-turn-content"] {
+ flex-grow: 1;
+ width: 100%;
+ height: 100%;
+ min-width: 0;
+ overflow-y: auto;
+ scrollbar-width: none;
+ }
+
+ [data-slot="session-turn-content"]::-webkit-scrollbar {
+ display: none;
+ }
+
+ [data-slot="session-turn-message-container"] {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ align-self: stretch;
+ min-width: 0;
+ gap: 32px;
+ }
+
+ [data-slot="session-turn-message-header"] {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ align-self: stretch;
+ position: sticky;
+ top: 0;
+ background-color: var(--background-stronger);
+ z-index: 20;
+ height: 32px;
+ }
+
+ [data-slot="session-turn-message-content"] {
+ margin-top: -24px;
+ }
+
+ [data-slot="session-turn-message-title"] {
+ width: 100%;
+ font-size: 14px; /* text-14-medium */
+ font-weight: 500;
+ color: var(--text-strong);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+ white-space: nowrap;
+ }
+
+ [data-slot="session-turn-message-title"] h1 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+ white-space: nowrap;
+ font-size: inherit;
+ font-weight: inherit;
+ }
+
+ [data-slot="session-turn-typewriter"] {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+ white-space: nowrap;
+ }
+
+ [data-slot="session-turn-summary-section"] {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ align-items: flex-start;
+ align-self: stretch;
+ }
+
+ [data-slot="session-turn-summary-header"] {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ align-self: stretch;
+ }
+
+ [data-slot="session-turn-summary-title"] {
+ font-size: 12px; /* text-12-medium */
+ font-weight: 500;
+ color: var(--text-weak);
+ }
+
+ [data-slot="session-turn-markdown"] {
+ &[data-diffs="true"] {
+ font-size: 14px; /* text-14-regular */
+ }
+
+ &[data-fade="true"] > * {
+ animation: fade-up-text 0.3s ease-out forwards;
+ }
+ }
+
+ [data-slot="session-turn-accordion"] {
+ width: 100%;
+ }
+
+ [data-component="sticky-accordion-header"] {
+ top: 40px;
+
+ &[data-expanded]::before {
+ top: -40px;
+ }
+ }
+
+ [data-slot="session-turn-accordion-trigger-content"] {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ gap: 20px;
+ }
+
+ [data-slot="session-turn-file-info"] {
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ min-width: 0;
+ }
+
+ [data-slot="session-turn-file-icon"] {
+ flex-shrink: 0;
+ width: 16px;
+ height: 16px;
+ }
+
+ [data-slot="session-turn-file-path"] {
+ display: flex;
+ flex-grow: 1;
+ min-width: 0;
+ }
+
+ [data-slot="session-turn-directory"] {
+ color: var(--text-base);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ direction: rtl;
+ text-align: left;
+ }
+
+ [data-slot="session-turn-filename"] {
+ color: var(--text-strong);
+ flex-shrink: 0;
+ }
+
+ [data-slot="session-turn-accordion-actions"] {
+ flex-shrink: 0;
+ display: flex;
+ gap: 16px;
+ align-items: center;
+ justify-content: flex-end;
+ }
+
+ [data-slot="session-turn-accordion-content"] {
+ max-height: 240px; /* max-h-60 */
+ overflow-y: auto;
+ scrollbar-width: none;
+ }
+
+ [data-slot="session-turn-accordion-content"]::-webkit-scrollbar {
+ display: none;
+ }
+
+ [data-slot="session-turn-response-section"] {
+ width: 100%;
+ min-width: 0;
+ }
+
+ [data-slot="session-turn-collapsible-trigger-content"] {
+ color: var(--text-weak);
+ cursor: pointer;
+ background: none;
+ border: none;
+ padding: 0;
+ display: flex;
+ align-items: center;
+
+ &:hover {
+ color: var(--text-strong);
+ }
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ align-self: stretch;
+ }
+
+ [data-slot="session-turn-details-text"] {
+ font-size: 12px; /* text-12-medium */
+ font-weight: 500;
+ }
+
+ .error-card {
+ color: var(--text-on-critical-base);
+ }
+
+ [data-slot="session-turn-collapsible-content-inner"] {
+ width: 100%;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ align-self: stretch;
+ gap: 12px;
+ }
+}
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
new file mode 100644
index 000000000..f92089d00
--- /dev/null
+++ b/packages/ui/src/components/session-turn.tsx
@@ -0,0 +1,220 @@
+import { AssistantMessage } from "@opencode-ai/sdk"
+import { useData } from "../context"
+import { Binary } from "@opencode-ai/util/binary"
+import { getDirectory, getFilename } from "@opencode-ai/util/path"
+import { createEffect, createMemo, createSignal, For, Match, ParentProps, Show, Switch } from "solid-js"
+import { DiffChanges } from "./diff-changes"
+import { Typewriter } from "./typewriter"
+import { Message } from "./message-part"
+import { Markdown } from "./markdown"
+import { Accordion } from "./accordion"
+import { StickyAccordionHeader } from "./sticky-accordion-header"
+import { FileIcon } from "./file-icon"
+import { Icon } from "./icon"
+import { Diff } from "./diff"
+import { Card } from "./card"
+import { MessageProgress } from "./message-progress"
+import { Collapsible } from "./collapsible"
+
+export function SessionTurn(
+ props: ParentProps<{
+ sessionID: string
+ messageID: string
+ classes?: {
+ root?: string
+ content?: string
+ container?: string
+ }
+ }>,
+) {
+ const data = useData()
+ const match = Binary.search(data.session, props.sessionID, (s) => s.id)
+ if (!match.found) throw new Error(`Session ${props.sessionID} not found`)
+
+ const messages = createMemo(() => (props.sessionID ? (data.message[props.sessionID] ?? []) : []))
+ const userMessages = createMemo(() =>
+ messages()
+ .filter((m) => m.role === "user")
+ .sort((a, b) => b.id.localeCompare(a.id)),
+ )
+ const lastUserMessage = createMemo(() => {
+ return userMessages()?.at(0)
+ })
+ const message = createMemo(() => userMessages()?.find((m) => m.id === props.messageID))
+
+ const status = createMemo(
+ () =>
+ data.session_status[props.sessionID] ?? {
+ type: "idle",
+ },
+ )
+ const working = createMemo(() => status()?.type !== "idle")
+
+ return (
+
+
+
+ {(msg) => {
+ const titleSeen = createMemo(() => true)
+ const contentSeen = createMemo(() => true)
+
+ const [titled, setTitled] = createSignal(titleSeen())
+ const assistantMessages = createMemo(() => {
+ return messages()?.filter((m) => m.role === "assistant" && m.parentID == msg().id) as AssistantMessage[]
+ })
+ const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
+ const [detailsExpanded, setDetailsExpanded] = createSignal(false)
+ const parts = createMemo(() => data.part[msg().id])
+ const hasToolPart = createMemo(() =>
+ assistantMessages()
+ ?.flatMap((m) => data.part[m.id])
+ .some((p) => p?.type === "tool"),
+ )
+ const messageWorking = createMemo(() => msg().id === lastUserMessage()?.id && working())
+ const initialCompleted = !(msg().id === lastUserMessage()?.id && working())
+ const [completed, setCompleted] = createSignal(initialCompleted)
+
+ // allowing time for the animations to finish
+ createEffect(() => {
+ if (titleSeen()) return
+ const title = msg().summary?.title
+ if (title) setTimeout(() => setTitled(true), 10_000)
+ })
+ createEffect(() => {
+ const completed = !messageWorking()
+ setTimeout(() => setCompleted(completed), 1200)
+ })
+
+ return (
+
+ {/* Title */}
+
+
+ }
+ >
+
{msg().summary?.title}
+
+
+
+
+
+
+ {/* Summary */}
+
+
+
+
+
+ Summary
+ Response
+
+
+
+ {(summary) => (
+
+ )}
+
+
+
+
+ {(diff) => (
+
+
+
+
+
+
+
+
+ {getDirectory(diff.file)}
+
+ {getFilename(diff.file)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ {error()?.data?.message as string}
+
+
+ {/* Response */}
+
+
+
+
+
+
+
+
+
+
+
+ Hide details
+ Show details
+
+
+
+
+
+
+
+
+ {(assistantMessage) => {
+ const parts = createMemo(() => data.part[assistantMessage.id])
+ return
+ }}
+
+
+
+ {error()?.data?.message as string}
+
+
+
+
+
+
+
+
+
+ )
+ }}
+
+ {props.children}
+
+
+ )
+}
diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css
index 3c4ccbeb9..e29d8e33b 100644
--- a/packages/ui/src/styles/index.css
+++ b/packages/ui/src/styles/index.css
@@ -22,12 +22,14 @@
@import "../components/logo.css" layer(components);
@import "../components/markdown.css" layer(components);
@import "../components/message-part.css" layer(components);
+@import "../components/message-progress.css" layer(components);
+@import "../components/message-nav.css" layer(components);
@import "../components/progress-circle.css" layer(components);
@import "../components/select.css" layer(components);
@import "../components/select-dialog.css" layer(components);
@import "../components/spinner.css" layer(components);
@import "../components/session-review.css" layer(components);
-@import "../components/session-timeline.css" layer(components);
+@import "../components/session-turn.css" layer(components);
@import "../components/sticky-accordion-header.css" layer(components);
@import "../components/tabs.css" layer(components);
@import "../components/tooltip.css" layer(components);