import type { Todo } from "@opencode-ai/sdk/v2" import { Checkbox } from "@opencode-ai/ui/checkbox" import { IconButton } from "@opencode-ai/ui/icon-button" import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" function dot(status: Todo["status"]) { if (status !== "in_progress") return undefined return ( ) } export function SessionTodoDock(props: { todos: Todo[]; title: string; collapseLabel: string; expandLabel: string }) { const [store, setStore] = createStore({ collapsed: false, }) const toggle = () => setStore("collapsed", (value) => !value) const summary = createMemo(() => { const total = props.todos.length if (total === 0) return "" const completed = props.todos.filter((todo) => todo.status === "completed").length return `${completed} of ${total} ${props.title.toLowerCase()} completed` }) const active = createMemo( () => props.todos.find((todo) => todo.status === "in_progress") ?? props.todos.find((todo) => todo.status === "pending") ?? props.todos.filter((todo) => todo.status === "completed").at(-1) ?? props.todos[0], ) const preview = createMemo(() => active()?.content ?? "") return (
{ if (event.key !== "Enter" && event.key !== " ") return event.preventDefault() toggle() }} > {summary()}
{preview()}
{ event.preventDefault() event.stopPropagation() }} onClick={(event) => { event.stopPropagation() toggle() }} aria-label={store.collapsed ? props.expandLabel : props.collapseLabel} />
) } function TodoList(props: { todos: Todo[]; open: boolean }) { const [stuck, setStuck] = createSignal(false) const [scrolling, setScrolling] = createSignal(false) let scrollRef!: HTMLDivElement let timer: number | undefined const inProgress = createMemo(() => props.todos.findIndex((todo) => todo.status === "in_progress")) const ensure = () => { if (!props.open) return if (scrolling()) return if (!scrollRef || scrollRef.offsetParent === null) return const el = scrollRef.querySelector("[data-in-progress]") if (!(el instanceof HTMLElement)) return const topFade = 16 const bottomFade = 44 const container = scrollRef.getBoundingClientRect() const rect = el.getBoundingClientRect() const top = rect.top - container.top + scrollRef.scrollTop const bottom = rect.bottom - container.top + scrollRef.scrollTop const viewTop = scrollRef.scrollTop + topFade const viewBottom = scrollRef.scrollTop + scrollRef.clientHeight - bottomFade if (top < viewTop) { scrollRef.scrollTop = Math.max(0, top - topFade) } else if (bottom > viewBottom) { scrollRef.scrollTop = bottom - (scrollRef.clientHeight - bottomFade) } setStuck(scrollRef.scrollTop > 0) } createEffect( on([() => props.open, inProgress], () => { if (!props.open || inProgress() < 0) return requestAnimationFrame(ensure) }), ) onCleanup(() => { if (!timer) return window.clearTimeout(timer) }) return (
{ setStuck(e.currentTarget.scrollTop > 0) setScrolling(true) if (timer) window.clearTimeout(timer) timer = window.setTimeout(() => { setScrolling(false) if (inProgress() < 0) return requestAnimationFrame(ensure) }, 250) }} > {(todo) => ( {todo.content} )}
) }