import { Button, List, SelectDialog, Tooltip, IconButton, Tabs, Icon, Accordion, Diff, Collapsible, } 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 { ProgressCircle } from "@/components/progress-circle" import { AssistantMessage, Part } from "@/components/assistant-message" import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk" import { DiffChanges } from "@/components/diff-changes" 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(() => { if (!local.session.activeMessage()) return if (!messageScrollElement) return const element = messageScrollElement.querySelector(`[data-message="${local.session.activeMessage()?.id}"]`) element?.scrollIntoView({ block: "start", behavior: "instant" }) }) 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 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, ], }, }) local.session.setActive(session.id) } 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) return (
{session.title} {DateTime.fromMillis(session.time.updated).toRelative()}
{`${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 countLines = (text: string) => { if (!text) return 0 return text.split("\n").length } const additions = createMemo( () => message.summary?.diffs.reduce((acc, diff) => acc + (diff.additions ?? 0), 0) ?? 0, ) const deletions = createMemo( () => message.summary?.diffs.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0) ?? 0, ) const totalBeforeLines = createMemo( () => message.summary?.diffs.reduce((acc, diff) => acc + countLines(diff.before), 0) ?? 0, ) const blockCounts = createMemo(() => { const TOTAL_BLOCKS = 5 const adds = additions() const dels = deletions() const unchanged = Math.max(0, totalBeforeLines() - dels) const totalActivity = unchanged + adds + dels if (totalActivity === 0) { return { added: 0, deleted: 0, neutral: TOTAL_BLOCKS } } const percentAdded = adds / totalActivity const percentDeleted = dels / totalActivity const added_raw = percentAdded * TOTAL_BLOCKS const deleted_raw = percentDeleted * TOTAL_BLOCKS let added = adds > 0 ? Math.ceil(added_raw) : 0 let deleted = dels > 0 ? Math.ceil(deleted_raw) : 0 let total_allocated = added + deleted if (total_allocated > TOTAL_BLOCKS) { if (added_raw < deleted_raw) { added = Math.floor(added_raw) } else { deleted = Math.floor(deleted_raw) } total_allocated = added + deleted if (total_allocated > TOTAL_BLOCKS) { if (added_raw < deleted_raw) { deleted = Math.floor(deleted_raw) } else { added = Math.floor(added_raw) } } } const neutral = Math.max(0, TOTAL_BLOCKS - added - deleted) return { added, deleted, neutral } }) const ADD_COLOR = "var(--icon-diff-add-base)" const DELETE_COLOR = "var(--icon-diff-delete-base)" const NEUTRAL_COLOR = "var(--icon-weak-base)" const visibleBlocks = createMemo(() => { const counts = blockCounts() const blocks = [ ...Array(counts.added).fill(ADD_COLOR), ...Array(counts.deleted).fill(DELETE_COLOR), ...Array(counts.neutral).fill(NEUTRAL_COLOR), ] return blocks.slice(0, 5) }) return (
  • local.session.setActiveMessage(message.id)} >
    {(color, i) => ( )}
    {message.summary?.title ?? local.session.getMessageText(message)}
  • ) }}
{(message) => { const [expanded, setExpanded] = createSignal(false) const title = createMemo(() => message.summary?.title) const prompt = createMemo(() => local.session.getMessageText(message)) 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(() => { const last = assistantMessages()[assistantMessages().length - 1] if (!last) return false return !last.time.completed }) return (
{/* Title */}

{title() ?? prompt()}

{prompt()}
{/* Response */}

Hide steps Show steps

{(assistantMessage) => { const parts = createMemo(() => sync.data.part[assistantMessage.id]) return }}
{(_) => { const lastMessageWithText = createMemo(() => assistantMessages().findLast((m) => { const parts = sync.data.part[m.id] return parts?.find((p) => p.type === "text") }), ) const lastMessageWithReasoning = createMemo(() => assistantMessages().findLast((m) => { const parts = sync.data.part[m.id] return parts?.find((p) => p.type === "reasoning") }), ) const lastMessageWithTool = createMemo(() => assistantMessages().findLast((m) => { const parts = sync.data.part[m.id] return parts?.find( (p) => p.type === "tool" && p.state.status === "completed", ) }), ) return (
{(last) => { const lastTextPart = createMemo(() => sync.data.part[last().id].findLast((p) => p.type === "text"), ) return }} {(last) => { const lastReasoningPart = createMemo(() => sync.data.part[last().id].findLast( (p) => p.type === "reasoning", ), ) return ( ) }} {(last) => { const lastToolPart = createMemo(() => sync.data.part[last().id].findLast( (p) => p.type === "tool" && p.state.status === "completed", ), ) return }}
) }}
{/* Summary */}

Summary

{summary()}
{(diff) => (
{getDirectory(diff.file)}‎ {getFilename(diff.file)}
)}
) }}
)}
{/* */} {(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)}
)}
) }