import { useLocal, useSync } from "@/context" import { Icon } from "@opencode-ai/ui" import { Collapsible } from "@/ui" import type { Part, ToolPart } from "@opencode-ai/sdk" import { DateTime } from "luxon" import { createSignal, onMount, For, Match, splitProps, Switch, type ComponentProps, type ParentProps, createEffect, createMemo, Show, } from "solid-js" import { getFilename } from "@/utils" import { Markdown } from "./markdown" import { Code } from "./code" import { createElementSize } from "@solid-primitives/resize-observer" import { createScrollPosition } from "@solid-primitives/scroll" function Part(props: ParentProps & ComponentProps<"div">) { const [local, others] = splitProps(props, ["class", "classList", "children"]) return (

{local.children}

) } function CollapsiblePart(props: { title: ParentProps["children"] } & ParentProps & ComponentProps) { return ( {props.title}

{props.children}

) } function ReadToolPart(props: { part: ToolPart }) { const sync = useSync() const local = useLocal() return ( Reading file... {(state) => { const path = state().input["filePath"] as string return ( local.file.open(path)}> Read {getFilename(path)} ) }} {(state) => (
Read {getFilename(state().input["filePath"] as string)}
{sync.sanitize(state().error)}
)}
) } function EditToolPart(props: { part: ToolPart }) { const sync = useSync() return ( Preparing edit... {(state) => ( Edit {getFilename(state().input["filePath"] as string)} } > )} {(state) => ( Edit {getFilename(state().input["filePath"] as string)} } >
{sync.sanitize(state().error)}
)}
) } function WriteToolPart(props: { part: ToolPart }) { const sync = useSync() return ( Preparing write... {(state) => ( Write {getFilename(state().input["filePath"] as string)} } >
)}
{(state) => (
Write {getFilename(state().input["filePath"] as string)}
{sync.sanitize(state().error)}
)}
) } function BashToolPart(props: { part: ToolPart }) { const sync = useSync() return ( Writing shell command... {(state) => ( Run command: {state().input["command"]} } > )} {(state) => ( Shell {state().input["command"]} } >
{sync.sanitize(state().error)}
)}
) } function ToolPart(props: { part: ToolPart }) { // read // edit // write // bash // ls // glob // grep // todowrite // todoread // webfetch // websearch // patch // task return (
{props.part.type}:{props.part.tool} } >
) } export default function SessionTimeline(props: { session: string; class?: string }) { const sync = useSync() const [scrollElement, setScrollElement] = createSignal(undefined) const [root, setRoot] = createSignal(undefined) const [tail, setTail] = createSignal(true) const size = createElementSize(root) const scroll = createScrollPosition(scrollElement) onMount(() => sync.session.sync(props.session)) const session = createMemo(() => sync.session.get(props.session)) const messages = createMemo(() => sync.data.message[props.session] ?? []) const working = createMemo(() => { const last = messages()[messages().length - 1] if (!last) return false if (last.role === "user") return true return !last.time.completed }) const getScrollParent = (el: HTMLElement | null): HTMLElement | undefined => { let p = el?.parentElement while (p && p !== document.body) { const s = getComputedStyle(p) if (s.overflowY === "auto" || s.overflowY === "scroll") return p p = p.parentElement } return undefined } createEffect(() => { if (!root()) return setScrollElement(getScrollParent(root()!)) }) const scrollToBottom = () => { const element = scrollElement() if (!element) return element.scrollTop = element.scrollHeight } createEffect(() => { size.height if (tail()) scrollToBottom() }) createEffect(() => { if (working()) { setTail(true) scrollToBottom() } }) let lastScrollY = 0 createEffect(() => { if (scroll.y < lastScrollY) { setTail(false) } lastScrollY = scroll.y }) const valid = (part: Part) => { if (!part) return false switch (part.type) { case "step-start": case "step-finish": case "file": case "patch": return false case "text": return !part.synthetic case "reasoning": return part.text.trim() default: return true } } const duration = (part: Part) => { switch (part.type) { default: if ( "time" in part && part.time && "start" in part.time && part.time.start && "end" in part.time && part.time.end ) { const start = DateTime.fromMillis(part.time.start) const end = DateTime.fromMillis(part.time.end) return end.diff(start).toFormat("s") } return "" } } return (
    {(message) => ( {(part) => (
  • {part.type}
}> {(part) => (

{part().text}

{DateTime.fromMillis(message.time.created).toRelative()} ยท{" "} {sync.data.config.username ?? "user"}

)}
{(part) => ( Thinking}> Thought for {duration(part())}s } > )} {(part) => } )} )}
Raw Session Data
  • session
  • {(message) => ( <>
  • {message.role === "user" ? "user" : "assistant"}
  • {(part) => (
  • {part.type}
  • )}
    )}
) }