import { For, onCleanup, onMount, Show, type JSX } from "solid-js" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { InlineInput } from "@opencode-ai/ui/inline-input" import { Tooltip } from "@opencode-ai/ui/tooltip" import { SessionTurn } from "@opencode-ai/ui/session-turn" import type { UserMessage } from "@opencode-ai/sdk/v2" import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" export function MessageTimeline(props: { mobileChanges: boolean mobileFallback: JSX.Element scroll: { overflow: boolean; bottom: boolean } onResumeScroll: () => void setScrollRef: (el: HTMLDivElement | undefined) => void onScheduleScrollState: (el: HTMLDivElement) => void onAutoScrollHandleScroll: () => void onMarkScrollGesture: (target?: EventTarget | null) => void hasScrollGesture: () => boolean isDesktop: boolean onScrollSpyScroll: () => void onAutoScrollInteraction: (event: MouseEvent) => void showHeader: boolean centered: boolean title?: string parentID?: string openTitleEditor: () => void closeTitleEditor: () => void saveTitleEditor: () => void | Promise titleRef: (el: HTMLInputElement) => void titleState: { draft: string editing: boolean saving: boolean menuOpen: boolean pendingRename: boolean } onTitleDraft: (value: string) => void onTitleMenuOpen: (open: boolean) => void onTitlePendingRename: (value: boolean) => void onNavigateParent: () => void sessionID: string onArchiveSession: (sessionID: string) => void onDeleteSession: (sessionID: string) => void t: (key: string, vars?: Record) => string setContentRef: (el: HTMLDivElement) => void turnStart: number onRenderEarlier: () => void historyMore: boolean historyLoading: boolean onLoadEarlier: () => void renderedUserMessages: UserMessage[] anchor: (id: string) => string onRegisterMessage: (el: HTMLDivElement, id: string) => void onUnregisterMessage: (id: string) => void onFirstTurnMount?: () => void lastUserMessageID?: string expanded: Record onToggleExpanded: (id: string) => void }) { let touchGesture: number | undefined return ( {props.mobileFallback}} >
{ const root = e.currentTarget const target = e.target instanceof Element ? e.target : undefined const nested = target?.closest("[data-scrollable]") if (!nested || nested === root) { props.onMarkScrollGesture(root) return } if (!(nested instanceof HTMLElement)) { props.onMarkScrollGesture(root) return } const delta = normalizeWheelDelta({ deltaY: e.deltaY, deltaMode: e.deltaMode, rootHeight: root.clientHeight, }) if (!delta) return if ( shouldMarkBoundaryGesture({ delta, scrollTop: nested.scrollTop, scrollHeight: nested.scrollHeight, clientHeight: nested.clientHeight, }) ) { props.onMarkScrollGesture(root) } }} onTouchStart={(e) => { touchGesture = e.touches[0]?.clientY }} onTouchMove={(e) => { const next = e.touches[0]?.clientY const prev = touchGesture touchGesture = next if (next === undefined || prev === undefined) return const delta = prev - next if (!delta) return const root = e.currentTarget const target = e.target instanceof Element ? e.target : undefined const nested = target?.closest("[data-scrollable]") if (!nested || nested === root) { props.onMarkScrollGesture(root) return } if (!(nested instanceof HTMLElement)) { props.onMarkScrollGesture(root) return } if ( shouldMarkBoundaryGesture({ delta, scrollTop: nested.scrollTop, scrollHeight: nested.scrollHeight, clientHeight: nested.clientHeight, }) ) { props.onMarkScrollGesture(root) } }} onTouchEnd={() => { touchGesture = undefined }} onTouchCancel={() => { touchGesture = undefined }} onPointerDown={(e) => { if (e.target !== e.currentTarget) return props.onMarkScrollGesture(e.currentTarget) }} onScroll={(e) => { props.onScheduleScrollState(e.currentTarget) if (!props.hasScrollGesture()) return props.onAutoScrollHandleScroll() props.onMarkScrollGesture(e.currentTarget) if (props.isDesktop) props.onScrollSpyScroll() }} onClick={props.onAutoScrollInteraction} class="relative min-w-0 w-full h-full overflow-y-auto session-scroller" style={{ "--session-title-height": props.showHeader ? "40px" : "0px" }} >
{props.title} } > props.onTitleDraft(event.currentTarget.value)} onKeyDown={(event) => { event.stopPropagation() if (event.key === "Enter") { event.preventDefault() void props.saveTitleEditor() return } if (event.key === "Escape") { event.preventDefault() props.closeTitleEditor() } }} onBlur={props.closeTitleEditor} />
{(id) => (
{ if (!props.titleState.pendingRename) return event.preventDefault() props.onTitlePendingRename(false) props.openTitleEditor() }} > { props.onTitlePendingRename(true) props.onTitleMenuOpen(false) }} > {props.t("common.rename")} props.onArchiveSession(id())}> {props.t("common.archive")} props.onDeleteSession(id())}> {props.t("common.delete")}
)}
0}>
{(message) => { if (import.meta.env.DEV && props.onFirstTurnMount) { onMount(() => props.onFirstTurnMount?.()) } return (
{ props.onRegisterMessage(el, message.id) onCleanup(() => props.onUnregisterMessage(message.id)) }} classList={{ "min-w-0 w-full max-w-full": true, "md:max-w-200 3xl:max-w-[1200px]": props.centered, }} > props.onToggleExpanded(message.id)} classes={{ root: "min-w-0 w-full relative", content: "flex flex-col justify-between !overflow-visible", container: "w-full px-4 md:px-6", }} />
) }}
) }