diff options
| author | Adam <[email protected]> | 2025-10-23 15:27:31 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-10-24 12:16:32 -0500 |
| commit | 3eb2db98ed0a9c266e1bf00544e460cb0633b368 (patch) | |
| tree | eb04fea563b3a3b74a3d89ca9500e92cf4b908c8 /packages/desktop/src/components | |
| parent | 35dec0649db8f46bffd7121af9cd301668e69e8c (diff) | |
| download | opencode-3eb2db98ed0a9c266e1bf00544e460cb0633b368.tar.gz opencode-3eb2db98ed0a9c266e1bf00544e460cb0633b368.zip | |
wip: desktop work
Diffstat (limited to 'packages/desktop/src/components')
| -rw-r--r-- | packages/desktop/src/components/code.tsx | 4 | ||||
| -rw-r--r-- | packages/desktop/src/components/editor-pane.tsx | 252 | ||||
| -rw-r--r-- | packages/desktop/src/components/file-tree.tsx | 3 | ||||
| -rw-r--r-- | packages/desktop/src/components/markdown.tsx | 2 | ||||
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 9 | ||||
| -rw-r--r-- | packages/desktop/src/components/session-timeline.tsx | 7 |
6 files changed, 12 insertions, 265 deletions
diff --git a/packages/desktop/src/components/code.tsx b/packages/desktop/src/components/code.tsx index b4dd216e9..11518e73a 100644 --- a/packages/desktop/src/components/code.tsx +++ b/packages/desktop/src/components/code.tsx @@ -1,8 +1,8 @@ import { bundledLanguages, type BundledLanguage, type ShikiTransformer } from "shiki" import { splitProps, type ComponentProps, createEffect, onMount, onCleanup, createMemo, createResource } from "solid-js" -import { useLocal, useShiki } from "@/context" -import type { TextSelection } from "@/context/local" +import { useLocal, type TextSelection } from "@/context/local" import { getFileExtension, getNodeOffsetInLine, getSelectionInContainer } from "@/utils" +import { useShiki } from "@/context/shiki" type DefinedSelection = Exclude<TextSelection, undefined> diff --git a/packages/desktop/src/components/editor-pane.tsx b/packages/desktop/src/components/editor-pane.tsx deleted file mode 100644 index a97a0ef7f..000000000 --- a/packages/desktop/src/components/editor-pane.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { For, Match, Show, Switch, createSignal, splitProps } from "solid-js" -import { IconButton, Tabs, Tooltip } from "@opencode-ai/ui" -import { FileIcon } from "@/ui" -import { - DragDropProvider, - DragDropSensors, - DragOverlay, - SortableProvider, - closestCenter, - createSortable, - useDragDropContext, -} from "@thisbeyond/solid-dnd" -import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd" -import type { LocalFile } from "@/context/local" -import { Code } from "@/components/code" -import { useLocal } from "@/context" -import type { JSX } from "solid-js" - -interface EditorPaneProps { - onFileClick: (file: LocalFile) => void -} - -export default function EditorPane(props: EditorPaneProps): JSX.Element { - const [localProps] = splitProps(props, ["onFileClick"]) - const local = useLocal() - const [activeItem, setActiveItem] = createSignal<string | undefined>(undefined) - - 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) => { - 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) - } - - return ( - <DragDropProvider - onDragStart={handleDragStart} - onDragEnd={handleDragEnd} - onDragOver={handleDragOver} - collisionDetector={closestCenter} - > - <DragDropSensors /> - <ConstrainDragYAxis /> - <Tabs value={local.file.active()?.path} onChange={handleTabChange}> - <div class="sticky top-0 shrink-0 flex"> - <Tabs.List> - <SortableProvider ids={local.file.opened().map((file) => file.path)}> - <For each={local.file.opened()}> - {(file) => <SortableTab file={file} onTabClick={localProps.onFileClick} onTabClose={handleTabClose} />} - </For> - </SortableProvider> - </Tabs.List> - <div class="hidden shrink-0 h-full _flex items-center gap-1 px-2 border-b border-border-subtle/40"> - <Show when={local.file.active() && local.file.active()!.content?.diff}> - {(() => { - const activeFile = local.file.active()! - const view = local.file.view(activeFile.path) - return ( - <div class="flex items-center gap-1"> - <Show when={view !== "raw"}> - <div class="mr-1 flex items-center gap-1"> - <Tooltip value="Previous change" placement="bottom"> - <IconButton icon="arrow-up" variant="ghost" onClick={() => navigateChange(-1)} /> - </Tooltip> - <Tooltip value="Next change" placement="bottom"> - <IconButton icon="arrow-down" variant="ghost" onClick={() => navigateChange(1)} /> - </Tooltip> - </div> - </Show> - <Tooltip value="Raw" placement="bottom"> - <IconButton - icon="file-text" - variant="ghost" - classList={{ - "text-text": view === "raw", - "text-text-muted/70": view !== "raw", - "bg-background-element": view === "raw", - }} - onClick={() => local.file.setView(activeFile.path, "raw")} - /> - </Tooltip> - <Tooltip value="Unified diff" placement="bottom"> - <IconButton - icon="checklist" - variant="ghost" - classList={{ - "text-text": view === "diff-unified", - "text-text-muted/70": view !== "diff-unified", - "bg-background-element": view === "diff-unified", - }} - onClick={() => local.file.setView(activeFile.path, "diff-unified")} - /> - </Tooltip> - <Tooltip value="Split diff" placement="bottom"> - <IconButton - icon="columns" - variant="ghost" - classList={{ - "text-text": view === "diff-split", - "text-text-muted/70": view !== "diff-split", - "bg-background-element": view === "diff-split", - }} - onClick={() => local.file.setView(activeFile.path, "diff-split")} - /> - </Tooltip> - </div> - ) - })()} - </Show> - </div> - </div> - <For each={local.file.opened()}> - {(file) => ( - <Tabs.Content value={file.path} class="select-text"> - {(() => { - const view = local.file.view(file.path) - const showRaw = view === "raw" || !file.content?.diff - const code = showRaw ? (file.content?.content ?? "") : (file.content?.diff ?? "") - return <Code path={file.path} code={code} class="[&_code]:pb-60" /> - })()} - </Tabs.Content> - )} - </For> - </Tabs> - <DragOverlay> - {(() => { - const id = activeItem() - if (!id) return null - const draggedFile = local.file.node(id) - if (!draggedFile) return null - return ( - <div class="relative px-3 h-8 flex items-center text-sm font-medium text-text whitespace-nowrap shrink-0 bg-background-panel border-x border-border-subtle/40 border-b border-b-transparent"> - <TabVisual file={draggedFile} /> - </div> - ) - })()} - </DragOverlay> - </DragDropProvider> - ) -} - -function TabVisual(props: { file: LocalFile }): JSX.Element { - return ( - <div class="flex items-center gap-x-1.5"> - <FileIcon node={props.file} class="" /> - <span classList={{ "text-xs": true, "text-primary": !!props.file.status?.status, italic: !props.file.pinned }}> - {props.file.name} - </span> - <span class="text-xs opacity-70"> - <Switch> - <Match when={props.file.status?.status === "modified"}> - <span class="text-primary">M</span> - </Match> - <Match when={props.file.status?.status === "added"}> - <span class="text-success">A</span> - </Match> - <Match when={props.file.status?.status === "deleted"}> - <span class="text-error">D</span> - </Match> - </Switch> - </span> - </div> - ) -} - -function SortableTab(props: { - file: LocalFile - onTabClick: (file: LocalFile) => void - onTabClose: (file: LocalFile) => void -}): JSX.Element { - const sortable = createSortable(props.file.path) - - return ( - // @ts-ignore - <div use:sortable classList={{ "opacity-0": sortable.isActiveDraggable }}> - <Tooltip value={props.file.path} placement="bottom"> - <div class="relative"> - <Tabs.Trigger value={props.file.path} class="peer/tab pr-7" onClick={() => props.onTabClick(props.file)}> - <TabVisual file={props.file} /> - </Tabs.Trigger> - <IconButton - icon="close" - class="absolute right-1 top-1.5 opacity-0 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" - variant="ghost" - onClick={() => props.onTabClose(props.file)} - /> - </div> - </Tooltip> - </div> - ) -} - -function 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 -} diff --git a/packages/desktop/src/components/file-tree.tsx b/packages/desktop/src/components/file-tree.tsx index 7e4b1abcc..d10328136 100644 --- a/packages/desktop/src/components/file-tree.tsx +++ b/packages/desktop/src/components/file-tree.tsx @@ -1,5 +1,4 @@ -import { useLocal } from "@/context" -import type { LocalFile } from "@/context/local" +import { useLocal, type LocalFile } from "@/context/local" import { Tooltip } from "@opencode-ai/ui" import { Collapsible, FileIcon } from "@/ui" import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js" diff --git a/packages/desktop/src/components/markdown.tsx b/packages/desktop/src/components/markdown.tsx index 30e3831e3..e0f185f5f 100644 --- a/packages/desktop/src/components/markdown.tsx +++ b/packages/desktop/src/components/markdown.tsx @@ -1,4 +1,4 @@ -import { useMarked } from "@/context" +import { useMarked } from "@/context/marked" import { createResource } from "solid-js" function strip(text: string): string { diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 55a410516..47893f44c 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -1,12 +1,11 @@ -import { useLocal } from "@/context" -import { Button, Icon, IconButton, Select, SelectDialog, Tooltip } from "@opencode-ai/ui" +import { Button, Icon, IconButton, Select, SelectDialog } from "@opencode-ai/ui" import { useFilteredList } from "@opencode-ai/ui/hooks" -import { createEffect, on, Component, createMemo, Show, Switch, Match, For } from "solid-js" +import { createEffect, on, Component, createMemo, Show, For } from "solid-js" import { createStore } from "solid-js/store" import { FileIcon } from "@/ui" import { getDirectory, getFilename } from "@/utils" import { createFocusSignal } from "@solid-primitives/active-element" -import { TextSelection } from "@/context/local" +import { TextSelection, useLocal } from "@/context/local" import { DateTime } from "luxon" interface PartBase { @@ -245,7 +244,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { } return ( - <div class="relative size-full max-w-[640px] _max-h-[320px] flex flex-col gap-3"> + <div class="relative size-full _max-h-[320px] flex flex-col gap-3"> <Show when={store.popoverIsOpen}> <div class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-[252px] min-h-10 overflow-y-auto flex flex-col p-2 pb-0 rounded-2xl border border-border-base bg-surface-raised-stronger-non-alpha shadow-md"> <For each={flat()}> diff --git a/packages/desktop/src/components/session-timeline.tsx b/packages/desktop/src/components/session-timeline.tsx index 0d8a7cd3c..b751f2940 100644 --- a/packages/desktop/src/components/session-timeline.tsx +++ b/packages/desktop/src/components/session-timeline.tsx @@ -1,4 +1,3 @@ -import { useLocal, useSync } from "@/context" import { Icon, Tooltip } from "@opencode-ai/ui" import { Collapsible } from "@/ui" import type { AssistantMessage, Message, Part, ToolPart } from "@opencode-ai/sdk" @@ -22,6 +21,8 @@ import { createElementSize } from "@solid-primitives/resize-observer" import { createScrollPosition } from "@solid-primitives/scroll" import { ProgressCircle } from "./progress-circle" import { pipe, sumBy } from "remeda" +import { useSync } from "@/context/sync" +import { useLocal } from "@/context/local" function Part(props: ParentProps & ComponentProps<"div">) { const [local, others] = splitProps(props, ["class", "classList", "children"]) @@ -394,7 +395,7 @@ export default function SessionTimeline(props: { session: string; class?: string [props.class ?? ""]: !!props.class, }} > - <div class="py-1.5 px-6 flex justify-end items-center self-stretch"> + <div class="flex justify-end items-center self-stretch"> <div class="flex items-center gap-6"> <Tooltip value={`${tokens()} Tokens`} class="flex items-center gap-1.5"> <Show when={context()}> @@ -405,7 +406,7 @@ export default function SessionTimeline(props: { session: string; class?: string <div class="text-14-regular text-text-strong text-right">{cost()}</div> </div> </div> - <ul role="list" class="flex flex-col items-start self-stretch px-6 pt-2 pb-6 gap-1"> + <ul role="list" class="flex flex-col items-start self-stretch"> <For each={messagesWithValidParts()}> {(message) => ( <div |
