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 (
)
}
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}
)}
>
)}
)
}