import { Button, Icon, List, SelectDialog, Tooltip } from "@opencode-ai/ui" import { FileIcon } from "@/ui" import FileTree from "@/components/file-tree" import EditorPane from "@/components/editor-pane" import { For, onCleanup, onMount, Show } from "solid-js" import { useSync, useSDK, useLocal } from "@/context" import type { LocalFile, TextSelection } from "@/context/local" import SessionTimeline from "@/components/session-timeline" import { createStore } from "solid-js/store" import { getDirectory, getFilename } from "@/utils" import { ContentPart, PromptInput } from "@/components/prompt-input" import { DateTime } from "luxon" export default function Page() { const local = useLocal() const sync = useSync() const sdk = useSDK() const [store, setStore] = createStore({ clickTimer: undefined as number | undefined, modelSelectOpen: false, fileSelectOpen: false, }) let inputRef!: HTMLDivElement 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() // TODO: command palette 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") { 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 handlePromptSubmit = async (parts: ContentPart[]) => { const existingSession = local.session.active() let session = existingSession if (!session) { const created = await sdk.session.create() session = created.data ?? undefined } if (!session) return local.session.setActive(session.id) interface SubmissionAttachment { path: string selection?: TextSelection label: string } const createAttachmentKey = (path: string, selection?: TextSelection) => { if (!selection) return path return `${path}:${selection.startLine}:${selection.startChar}:${selection.endLine}:${selection.endChar}` } const formatAttachmentLabel = (path: string, selection?: TextSelection) => { if (!selection) return getFilename(path) return `${getFilename(path)} (${selection.startLine}-${selection.endLine})` } const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path)) const text = parts.map((part) => part.content).join("") const attachments = new Map() const registerAttachment = (path: string, selection: TextSelection | undefined, label?: string) => { if (!path) return const key = createAttachmentKey(path, selection) if (attachments.has(key)) return attachments.set(key, { path, selection, label: label ?? formatAttachmentLabel(path, selection), }) } const promptAttachments = parts.filter((part) => part.type === "file") for (const part of promptAttachments) { registerAttachment(part.path, part.selection, part.content) } // 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 = Array.from(attachments.values()).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.label}`, start: 0, end: 0, }, path: absolute, }, } }) await sdk.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() } return (
{sync.data.path.directory}
x.id} onSelect={(s) => local.session.setActive(s?.id)} onHover={(s) => (!!s ? sync.session.sync(s?.id) : undefined)} > {(session) => (
{session.title} {DateTime.fromMillis(session.time.updated).toRelative()}
2 files changed
+43 -2
)}
{(activeSession) => }
{ inputRef = el }} onSubmit={handlePromptSubmit} />
} >
    {(path) => (
  • )}
x} onOpenChange={(open) => setStore("fileSelectOpen", open)} onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)} > {(i) => (
{getFilename(i)} {getDirectory(i)}
)}
) }