summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/pages
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-10-13 06:09:02 -0500
committerAdam <[email protected]>2025-10-14 07:15:08 -0500
commitbb82d4309435f2052e98531e8d955198fb1c55ba (patch)
tree9455ff36a5fec5d147425c2b2fae982a82b94fca /packages/desktop/src/pages
parent2893b6e3a58a25514bb8d75c68f893ea5f2b593a (diff)
downloadopencode-bb82d4309435f2052e98531e8d955198fb1c55ba.tar.gz
opencode-bb82d4309435f2052e98531e8d955198fb1c55ba.zip
wip: desktop work
Diffstat (limited to 'packages/desktop/src/pages')
-rw-r--r--packages/desktop/src/pages/index.tsx313
1 files changed, 226 insertions, 87 deletions
diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx
index 128c6adfd..f344830a4 100644
--- a/packages/desktop/src/pages/index.tsx
+++ b/packages/desktop/src/pages/index.tsx
@@ -1,28 +1,30 @@
import { FileIcon, Icon, IconButton, Tooltip } from "@/ui"
-import { Tabs } from "@/ui/tabs"
+import * as KobalteTabs from "@kobalte/core/tabs"
import FileTree from "@/components/file-tree"
import EditorPane from "@/components/editor-pane"
import { For, Match, onCleanup, onMount, Show, Switch } from "solid-js"
import { SelectDialog } from "@/components/select-dialog"
-import { useLocal } from "@/context"
-import { ResizeableLayout, ResizeablePane } from "@/components/resizeable-pane"
-import type { LocalFile } from "@/context/local"
+import { useSync, useSDK, useLocal } from "@/context"
+import type { LocalFile, TextSelection } from "@/context/local"
import SessionList from "@/components/session-list"
import SessionTimeline from "@/components/session-timeline"
+import PromptForm, { type PromptContentPart, type PromptSubmitValue } from "@/components/prompt-form"
import { createStore } from "solid-js/store"
import { getDirectory, getFilename } from "@/utils"
+import { Select } from "@/components/select"
+import { Tabs } from "@/ui/tabs"
+import { Code } from "@/components/code"
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,
})
- const layoutKey = "workspace"
- const timelinePane = "timeline"
-
let inputRef: HTMLTextAreaElement | undefined = undefined
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
@@ -104,95 +106,231 @@ export default function Page() {
}
}
+ const handlePromptSubmit = async (prompt: PromptSubmitValue) => {
+ 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 attachments = new Map<string, SubmissionAttachment>()
+
+ 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 = prompt.parts.filter(
+ (part): part is Extract<PromptContentPart, { kind: "attachment" }> => part.kind === "attachment",
+ )
+
+ for (const part of promptAttachments) {
+ registerAttachment(part.path, part.selection, part.display)
+ }
+
+ 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: prompt.text,
+ },
+ ...attachmentParts,
+ ],
+ },
+ })
+ }
+
return (
<div class="relative">
- <ResizeableLayout
- id={layoutKey}
- defaults={{
- explorer: { size: 24, visible: true },
- editor: { size: 56, visible: true },
- timeline: { size: 20, visible: false },
- }}
- class="h-screen"
- >
- <ResizeablePane
- id="explorer"
- minSize="150px"
- maxSize="300px"
- class="border-r border-border-subtle/30 bg-background z-10 overflow-hidden"
- >
- <Tabs class="relative flex flex-col h-full" defaultValue="files">
- <div class="sticky top-0 shrink-0 flex">
- <Tabs.List class="grow w-full after:hidden">
- <Tabs.Trigger value="files" class="flex-1 justify-center text-xs">
- Files
- </Tabs.Trigger>
- <Tabs.Trigger value="changes" class="flex-1 justify-center text-xs">
- Changes
- </Tabs.Trigger>
- </Tabs.List>
- </div>
- <Tabs.Content value="files" class="grow min-h-0 py-2 bg-background">
- <FileTree path="" onFileClick={handleFileClick} />
- </Tabs.Content>
- <Tabs.Content value="changes" class="grow min-h-0 py-2 bg-background">
- <Show
- when={local.file.changes().length}
- fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
- >
- <ul class="">
- <For each={local.file.changes()}>
- {(path) => (
- <li>
- <button
- onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
- class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 cursor-pointer hover:bg-background-element"
- >
- <FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
- <span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
- <span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
- {getDirectory(path)}
- </span>
- </button>
- </li>
+ <div class="h-screen flex">
+ <div class="shrink-0 w-56">
+ <SessionList />
+ </div>
+ <div class="grow w-full min-w-0 overflow-y-auto flex justify-center">
+ <Show when={local.session.active()}>
+ {(activeSession) => <SessionTimeline session={activeSession().id} class="max-w-xl" />}
+ </Show>
+ </div>
+ <div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
+ <FileTree path="" onFileClick={handleFileClick} />
+ </div>
+ <div class="hidden shrink-0 w-56 p-2">
+ <Show
+ when={local.file.changes().length}
+ fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
+ >
+ <ul class="">
+ <For each={local.file.changes()}>
+ {(path) => (
+ <li>
+ <button
+ onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
+ class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 cursor-pointer hover:bg-background-element"
+ >
+ <FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
+ <span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
+ <span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
+ {getDirectory(path)}
+ </span>
+ </button>
+ </li>
+ )}
+ </For>
+ </ul>
+ </Show>
+ </div>
+ <div class="hidden grow min-w-0">
+ <EditorPane onFileClick={handleFileClick} />
+ </div>
+ <div class="absolute bottom-4 right-4 border border-border-subtle/60 p-2 rounded-xl bg-background w-xl flex flex-col gap-2 z-50">
+ <div class="flex items-center gap-2">
+ <Select
+ options={sync.data.session}
+ current={local.session.active()}
+ placeholder="New Session"
+ value={(x) => x.id}
+ label={(x) => x.title}
+ onSelect={(s) => local.session.setActive(s?.id)}
+ class="bg-transparent! max-w-48 pl-0! text-text-muted!"
+ />
+ <Show when={local.session.active()}>
+ <>
+ <div>/</div>
+ <Select
+ options={sync.data.message[local.session.active()!.id]?.filter((m) => m.role === "user") ?? []}
+ label={(m) => sync.data.part[m.id].find((p) => p.type === "text")!.text}
+ class="bg-transparent! max-w-48 pl-0! text-text-muted!"
+ />
+ </>
+ </Show>
+ </div>
+ <div class="h-72 text-xs overflow-x-scroll no-scrollbar w-full min-w-0">
+ <Tabs
+ class="relative grow w-full flex flex-col gap-1 h-full"
+ value={local.context.activeFile()?.path}
+ onChange={local.context.setActiveFile}
+ >
+ <div class="sticky top-0 shrink-0 flex items-center gap-1">
+ <IconButton
+ class="text-text-muted/60 peer-data-[selected]/tab:opacity-100 peer-data-[selected]/tab:text-text peer-data-[selected]/tab:hover:bg-border-subtle hover:opacity-100 peer-hover/tab:opacity-100"
+ size="xs"
+ variant="secondary"
+ onClick={() => setStore("fileSelectOpen", true)}
+ >
+ <Icon name="plus" size={12} />
+ </IconButton>
+ <Tabs.List class="grow after:hidden! h-full divide-none! gap-1">
+ <For each={local.context.files()}>
+ {(file) => (
+ <KobalteTabs.Trigger
+ value={file.path}
+ class="h-full"
+ // onClick={() => props.onTabClick(props.file)}
+ >
+ <div class="flex items-center gap-x-1 rounded-md bg-background-panel px-2 h-full">
+ <FileIcon node={file} class="shrink-0 size-3!" />
+ <span class="text-xs text-text whitespace-nowrap">{getFilename(file.path)}</span>
+ </div>
+ </KobalteTabs.Trigger>
)}
</For>
- </ul>
- </Show>
- </Tabs.Content>
- </Tabs>
- </ResizeablePane>
- <ResizeablePane id="editor" minSize={30} maxSize={80} class="bg-background">
- <EditorPane
- layoutKey={layoutKey}
- timelinePane={timelinePane}
- onFileClick={handleFileClick}
+ </Tabs.List>
+ </div>
+ <For each={local.context.files()}>
+ {(file) => (
+ <Tabs.Content value={file.path} class="grow h-full pt-1 select-text rounded-md">
+ <Code path={file.path} code={file.content?.content ?? ""} />
+ </Tabs.Content>
+ )}
+ </For>
+ </Tabs>
+ </div>
+ <PromptForm
+ onSubmit={handlePromptSubmit}
onOpenModelSelect={() => setStore("modelSelectOpen", true)}
- onInputRefChange={(element: HTMLTextAreaElement | null) => {
+ onInputRefChange={(element: HTMLTextAreaElement | undefined) => {
inputRef = element ?? undefined
}}
/>
- </ResizeablePane>
- <ResizeablePane
- id="timeline"
- minSize={20}
- maxSize={40}
- class="border-l border-border-subtle/30 bg-background z-10 overflow-hidden"
- >
- <div class="relative flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
- <Show when={local.session.active()} fallback={<SessionList />}>
+ <div class="hidden relative flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
+ <Show when={local.session.active()}>
{(activeSession) => (
<div class="relative">
<div class="sticky top-0 bg-background z-50 px-2 h-8 border-b border-border-subtle/30">
<div class="h-full flex items-center gap-2">
- <IconButton
- size="xs"
- variant="ghost"
- onClick={() => local.session.clearActive()}
- class="text-text-muted hover:text-text"
- >
- <Icon name="arrow-left" size={14} />
- </IconButton>
<h2 class="text-sm font-medium text-text truncate">
{activeSession().title || "Untitled Session"}
</h2>
@@ -203,8 +341,8 @@ export default function Page() {
)}
</Show>
</div>
- </ResizeablePane>
- </ResizeableLayout>
+ </div>
+ </div>
<Show when={store.modelSelectOpen}>
<SelectDialog
key={(x) => `${x.provider.id}:${x.id}`}
@@ -270,7 +408,8 @@ export default function Page() {
</div>
)}
onClose={() => setStore("fileSelectOpen", false)}
- onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)}
+ onSelect={(x) => (x ? local.context.openFile(x) : undefined)}
+ // onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)}
/>
</Show>
</div>