import { Button, List, SelectDialog, Tooltip, IconButton, Tabs, Icon, Accordion, Diff, Collapsible, Part, DiffChanges, ProgressCircle, Message, Typewriter, } from "@opencode-ai/ui" import { FileIcon } from "@/ui" import FileTree from "@/components/file-tree" import { For, onCleanup, onMount, Show, Match, Switch, createSignal, createEffect, createMemo } from "solid-js" import { useLocal, type LocalFile } from "@/context/local" import { createStore } from "solid-js/store" import { getDirectory, getFilename } from "@/utils" import { ContentPart, PromptInput } from "@/components/prompt-input" import { DateTime } from "luxon" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter, createSortable, useDragDropContext, } from "@thisbeyond/solid-dnd" import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd" import type { JSX } from "solid-js" import { Code } from "@/components/code" import { useSync } from "@/context/sync" import { useSDK } from "@/context/sdk" import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk" import { Markdown } from "@opencode-ai/ui" export default function Page() { const local = useLocal() const sync = useSync() const sdk = useSDK() const [store, setStore] = createStore({ clickTimer: undefined as number | undefined, fileSelectOpen: false, }) let inputRef!: HTMLDivElement let messageScrollElement!: HTMLDivElement const [activeItem, setActiveItem] = createSignal(undefined) createEffect(() => { // Set first message as active if none selected const userMessages = local.session.userMessages() if (userMessages.length > 0 && !local.session.activeMessage()) { local.session.setActiveMessage(userMessages[0].id) } }) const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control" onMount(() => { document.addEventListener("keydown", handleKeyDown) }) onCleanup(() => { document.removeEventListener("keydown", handleKeyDown) }) const handleKeyDown = (event: KeyboardEvent) => { if (event.getModifierState(MOD) && event.shiftKey && event.key.toLowerCase() === "p") { event.preventDefault() return } if (event.getModifierState(MOD) && event.key.toLowerCase() === "p") { event.preventDefault() setStore("fileSelectOpen", true) return } const focused = document.activeElement === inputRef if (focused) { if (event.key === "Escape") { inputRef?.blur() } return } if (local.file.active()) { const active = local.file.active()! if (event.key === "Enter" && active.selection) { local.context.add({ type: "file", path: active.path, selection: { ...active.selection }, }) return } if (event.getModifierState(MOD)) { if (event.key.toLowerCase() === "a") { return } if (event.key.toLowerCase() === "c") { return } } } if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { inputRef?.focus() } } const resetClickTimer = () => { if (!store.clickTimer) return clearTimeout(store.clickTimer) setStore("clickTimer", undefined) } const startClickTimer = () => { const newClickTimer = setTimeout(() => { setStore("clickTimer", undefined) }, 300) setStore("clickTimer", newClickTimer as unknown as number) } const handleFileClick = async (file: LocalFile) => { if (store.clickTimer) { resetClickTimer() local.file.update(file.path, { ...file, pinned: true }) } else { local.file.open(file.path) startClickTimer() } } const navigateChange = (dir: 1 | -1) => { const active = local.file.active() if (!active) return const current = local.file.changeIndex(active.path) const next = current === undefined ? (dir === 1 ? 0 : -1) : current + dir local.file.setChangeIndex(active.path, next) } const handleTabChange = (path: string) => { if (path === "chat" || path === "review") return local.file.open(path) } const handleTabClose = (file: LocalFile) => { local.file.close(file.path) } const handleDragStart = (event: unknown) => { const id = getDraggableId(event) if (!id) return setActiveItem(id) } const handleDragOver = (event: DragEvent) => { const { draggable, droppable } = event if (draggable && droppable) { const currentFiles = local.file.opened().map((file) => file.path) const fromIndex = currentFiles.indexOf(draggable.id.toString()) const toIndex = currentFiles.indexOf(droppable.id.toString()) if (fromIndex !== toIndex) { local.file.move(draggable.id.toString(), toIndex) } } } const handleDragEnd = () => { setActiveItem(undefined) } const scrollDiffItem = (element: HTMLElement) => { element.scrollIntoView({ block: "start", behavior: "instant" }) } const handleDiffTriggerClick = (event: MouseEvent) => { // disabling scroll to diff for now return const target = event.currentTarget as HTMLElement queueMicrotask(() => { if (target.getAttribute("aria-expanded") !== "true") return const item = target.closest('[data-slot="accordion-item"]') as HTMLElement | null if (!item) return scrollDiffItem(item) }) } const handlePromptSubmit = async (parts: ContentPart[]) => { const existingSession = local.session.active() let session = existingSession if (!session) { const created = await sdk.client.session.create() session = created.data ?? undefined } if (!session) return local.session.setActive(session.id) const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path)) const text = parts.map((part) => part.content).join("") const attachments = parts.filter((part) => part.type === "file") // const activeFile = local.context.active() // if (activeFile) { // registerAttachment( // activeFile.path, // activeFile.selection, // activeFile.name ?? formatAttachmentLabel(activeFile.path, activeFile.selection), // ) // } // for (const contextFile of local.context.all()) { // registerAttachment( // contextFile.path, // contextFile.selection, // formatAttachmentLabel(contextFile.path, contextFile.selection), // ) // } const attachmentParts = attachments.map((attachment) => { const absolute = toAbsolutePath(attachment.path) const query = attachment.selection ? `?start=${attachment.selection.startLine}&end=${attachment.selection.endLine}` : "" return { type: "file" as const, mime: "text/plain", url: `file://${absolute}${query}`, filename: getFilename(attachment.path), source: { type: "file" as const, text: { value: attachment.content, start: attachment.start, end: attachment.end, }, path: absolute, }, } }) await sdk.client.session.prompt({ path: { id: session.id }, body: { agent: local.agent.current()!.name, model: { modelID: local.model.current()!.id, providerID: local.model.current()!.provider.id, }, parts: [ { type: "text", text, }, ...attachmentParts, ], }, }) } const handleNewSession = () => { local.session.setActive(undefined) inputRef?.focus() } const TabVisual = (props: { file: LocalFile }): JSX.Element => { return (
{props.file.name}
) } const SortableTab = (props: { file: LocalFile onTabClick: (file: LocalFile) => void onTabClose: (file: LocalFile) => void }): JSX.Element => { const sortable = createSortable(props.file.path) return ( // @ts-ignore
props.onTabClick(props.file)} > props.onTabClose(props.file)} />
) } const ConstrainDragYAxis = (): JSX.Element => { const context = useDragDropContext() if (!context) return <> const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context const transformer: Transformer = { id: "constrain-y-axis", order: 100, callback: (transform) => ({ ...transform, y: 0 }), } onDragStart((event) => { const id = getDraggableId(event) if (!id) return addTransformer("draggables", id, transformer) }) onDragEnd((event) => { const id = getDraggableId(event) if (!id) return removeTransformer("draggables", id, transformer.id) }) return <> } const getDraggableId = (event: unknown): string | undefined => { if (typeof event !== "object" || event === null) return undefined if (!("draggable" in event)) return undefined const draggable = (event as { draggable?: { id?: unknown } }).draggable if (!draggable) return undefined return typeof draggable.id === "string" ? draggable.id : undefined } return (
{getFilename(sync.data.path.directory)}
x.id} current={local.session.active()} onSelect={(s) => local.session.setActive(s?.id)} onHover={(s) => (!!s ? sync.session.sync(s?.id) : undefined)} > {(session) => { const diffs = createMemo(() => session.summary?.diffs ?? []) const filesChanged = createMemo(() => diffs().length) const updated = DateTime.fromMillis(session.time.updated) return (
{session.title} {Math.abs(updated.diffNow().as("seconds")) < 60 ? "Now" : updated .toRelative({ style: "short", unit: ["days", "hours", "minutes"] }) ?.replace(" ago", "") ?.replace(/ days?/, "d") ?.replace(" min.", "m") ?.replace(" hr.", "h")}
{`${filesChanged() || "No"} file${filesChanged() !== 1 ? "s" : ""} changed`}
) }}
Chat
{local.session.context() ?? 0}%
{/* Review */} file.path)}> {(file) => }
setStore("fileSelectOpen", true)} />
New session
{getDirectory(sync.data.path.directory)} {getFilename(sync.data.path.directory)}
Last modified  {DateTime.fromMillis(sync.data.project.time.created).toRelative()}
} > {(activeSession) => (
1}>
{(message) => { const isActive = createMemo(() => local.session.activeMessage()?.id === message.id) const [initialized, setInitialized] = createSignal(!!message.summary?.title) const [expanded, setExpanded] = createSignal(false) const parts = createMemo(() => sync.data.part[message.id]) const title = createMemo(() => message.summary?.title) const summary = createMemo(() => message.summary?.body) const assistantMessages = createMemo(() => { return sync.data.message[activeSession().id]?.filter( (m) => m.role === "assistant" && m.parentID == message.id, ) as AssistantMessageType[] }) const working = createMemo(() => !summary()) createEffect(() => { setTimeout(() => setInitialized(!!title()), 10_000) }) return (
{/* Title */}
}>

{title()}

{/* Summary */}

Summary

{(diff) => (
{getDirectory(diff.file)}‎ {getFilename(diff.file)}
)}
{/* Response */}
{(_) => { const items = createMemo(() => assistantMessages().flatMap((m) => sync.data.part[m.id]), ) const finishedItems = createMemo(() => items().filter( (p) => (p?.type === "text" && p.time?.end) || (p?.type === "reasoning" && p.time?.end) || (p?.type === "tool" && p.state.status === "completed"), ), ) const MINIMUM_DELAY = 800 const [visibleCount, setVisibleCount] = createSignal(1) createEffect(() => { const total = finishedItems().length if (total > visibleCount()) { const timer = setTimeout(() => { setVisibleCount((prev) => prev + 1) }, MINIMUM_DELAY) onCleanup(() => clearTimeout(timer)) } else if (total < visibleCount()) { setVisibleCount(total) } }) const translateY = createMemo(() => { const total = visibleCount() if (total < 2) return "0px" return `-${(total - 2) * 48 - 8}px` }) return (
{(part) => { const message = createMemo(() => sync.data.message[part.sessionID].find( (m) => m.id === part.messageID, ), ) return (
{(p) => (
)} {(p) => } {(p) => }
) }}
) }}
Hide steps Show steps
{(assistantMessage) => { const parts = createMemo( () => sync.data.part[assistantMessage.id], ) return }}
) }}
)}
{/* */} {(file) => ( {(() => { const view = local.file.view(file.path) const showRaw = view === "raw" || !file.content?.diff const code = showRaw ? (file.content?.content ?? "") : (file.content?.diff ?? "") return })()} )}
{(() => { const id = activeItem() if (!id) return null const draggedFile = local.file.node(id) if (!draggedFile) return null return (
) })()}
{ inputRef = el }} onSubmit={handlePromptSubmit} />
} >
    {(path) => (
  • )}
x} onOpenChange={(open) => setStore("fileSelectOpen", open)} onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)} > {(i) => (
{getDirectory(i)} {getFilename(i)}
)}
) }