diff options
| author | Adam <[email protected]> | 2025-11-14 12:38:52 -0600 |
|---|---|---|
| committer | opencode <[email protected]> | 2025-11-18 17:07:34 +0000 |
| commit | 4069999b782cc00d4e707f5eca32082bdfad45bc (patch) | |
| tree | d5c13cdd361fc79b14250b4b426a0a1195f7a01c /packages/desktop | |
| parent | 5ba9b47b3c87dfb044c30857e56959c8eff0c8c1 (diff) | |
| download | opencode-4069999b782cc00d4e707f5eca32082bdfad45bc.tar.gz opencode-4069999b782cc00d4e707f5eca32082bdfad45bc.zip | |
wip(desktop): new layout work
Diffstat (limited to 'packages/desktop')
| -rw-r--r-- | packages/desktop/src/context/layout.tsx | 52 | ||||
| -rw-r--r-- | packages/desktop/src/context/local.tsx | 47 | ||||
| -rw-r--r-- | packages/desktop/src/context/sdk.tsx | 15 | ||||
| -rw-r--r-- | packages/desktop/src/context/session.tsx | 6 | ||||
| -rw-r--r-- | packages/desktop/src/context/sync.tsx | 2 | ||||
| -rw-r--r-- | packages/desktop/src/index.tsx | 6 | ||||
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 82 | ||||
| -rw-r--r-- | packages/desktop/src/pages/session-layout.tsx | 16 | ||||
| -rw-r--r-- | packages/desktop/src/pages/session.tsx | 49 | ||||
| -rw-r--r-- | packages/desktop/src/ui/file-icon.tsx | 3 |
10 files changed, 181 insertions, 97 deletions
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx new file mode 100644 index 000000000..9e4af90aa --- /dev/null +++ b/packages/desktop/src/context/layout.tsx @@ -0,0 +1,52 @@ +import { createStore } from "solid-js/store" +import { createMemo } from "solid-js" +import { createSimpleContext } from "./helper" +import { makePersisted } from "@solid-primitives/storage" + +export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ + name: "Layout", + init: () => { + const [store, setStore] = makePersisted( + createStore({ + sidebar: { + opened: true, + width: 280, + }, + review: { + state: "pane" as "pane" | "tab", + }, + }), + { + name: "__default-layout", + }, + ) + + return { + sidebar: { + opened: createMemo(() => store.sidebar.opened), + open() { + setStore("sidebar", "opened", true) + }, + close() { + setStore("sidebar", "opened", false) + }, + toggle() { + setStore("sidebar", "opened", (x) => !x) + }, + width: createMemo(() => store.sidebar.width), + resize(width: number) { + setStore("sidebar", "width", width) + }, + }, + review: { + state: createMemo(() => store.review?.state ?? "closed"), + pane() { + setStore("review", "state", "pane") + }, + tab() { + setStore("review", "state", "tab") + }, + }, + } + }, +}) diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 1cef1c9f1..cef6c5555 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -5,7 +5,6 @@ import type { FileContent, FileNode, Model, Provider, File as FileStatus } from import { createSimpleContext } from "./helper" import { useSDK } from "./sdk" import { useSync } from "./sync" -import { makePersisted } from "@solid-primitives/storage" export type LocalFile = FileNode & Partial<{ @@ -457,57 +456,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } })() - const layout = (() => { - const [store, setStore] = makePersisted( - createStore({ - sidebar: { - opened: true, - width: 240, - }, - review: { - state: "pane" as "pane" | "tab", - }, - }), - { - name: "_default-layout", - }, - ) - - return { - sidebar: { - opened: createMemo(() => store.sidebar.opened), - open() { - setStore("sidebar", "opened", true) - }, - close() { - setStore("sidebar", "opened", false) - }, - toggle() { - setStore("sidebar", "opened", (x) => !x) - }, - width: createMemo(() => store.sidebar.width), - resize(width: number) { - setStore("sidebar", "width", width) - }, - }, - review: { - state: createMemo(() => store.review?.state ?? "closed"), - pane() { - setStore("review", "state", "pane") - }, - tab() { - setStore("review", "state", "tab") - }, - }, - } - })() - const result = { model, agent, file, context, - layout, } return result }, diff --git a/packages/desktop/src/context/sdk.tsx b/packages/desktop/src/context/sdk.tsx index 8d0cace65..b7b753dbc 100644 --- a/packages/desktop/src/context/sdk.tsx +++ b/packages/desktop/src/context/sdk.tsx @@ -5,12 +5,15 @@ import { onCleanup } from "solid-js" export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", - init: (props: { url: string }) => { + init: (props: { url: string; directory?: string }) => { const abort = new AbortController() - const sdk = createOpencodeClient({ - baseUrl: props.url, - signal: abort.signal, - }) + const sdk = createOpencodeClient( + { + baseUrl: props.url, + signal: abort.signal, + }, + { directory: props.directory }, + ) const emitter = createGlobalEmitter<{ [key in Event["type"]]: Extract<Event, { type: key }> @@ -27,6 +30,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ abort.abort() }) - return { client: sdk, event: emitter } + return { url: props.url, directory: props.directory, client: sdk, event: emitter } }, }) diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx index b2e15a42c..a468f4673 100644 --- a/packages/desktop/src/context/session.tsx +++ b/packages/desktop/src/context/session.tsx @@ -3,7 +3,7 @@ import { createSimpleContext } from "./helper" import { batch, createEffect, createMemo } from "solid-js" import { useSync } from "./sync" import { makePersisted } from "@solid-primitives/storage" -import { TextSelection, useLocal } from "./local" +import { TextSelection } from "./local" import { pipe, sumBy } from "remeda" import { AssistantMessage } from "@opencode-ai/sdk" @@ -11,7 +11,6 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex name: "Session", init: (props: { sessionId?: string }) => { const sync = useSync() - const local = useLocal() const [store, setStore] = makePersisted( createStore<{ @@ -140,9 +139,6 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex setStore("tabs", "active", undefined) return } - if (tab.startsWith("file://")) { - await local.file.open(tab.replace("file://", "")) - } if (tab !== "review") { if (!store.tabs.opened.includes(tab)) { setStore("tabs", "opened", [...store.tabs.opened, tab]) diff --git a/packages/desktop/src/context/sync.tsx b/packages/desktop/src/context/sync.tsx index 3626cf54f..11b2c36b8 100644 --- a/packages/desktop/src/context/sync.tsx +++ b/packages/desktop/src/context/sync.tsx @@ -63,6 +63,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const sdk = useSDK() sdk.event.listen((e) => { + // fetch the child store + // make a set store function that always rights to the child store const event = e.details switch (event.type) { case "session.updated": { diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 63d96ae84..2499e5e85 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -6,10 +6,10 @@ import { MetaProvider } from "@solidjs/meta" import { Fonts, MarkedProvider } from "@opencode-ai/ui" import { SDKProvider } from "./context/sdk" import { SyncProvider } from "./context/sync" -import { LocalProvider } from "./context/local" import Layout from "@/pages/layout" import SessionLayout from "@/pages/session-layout" import Session from "@/pages/session" +import { LayoutProvider } from "./context/layout" const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1" const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096" @@ -32,7 +32,7 @@ render( <MarkedProvider> <SDKProvider url={url}> <SyncProvider> - <LocalProvider> + <LayoutProvider> <MetaProvider> <Fonts /> <Router root={Layout}> @@ -41,7 +41,7 @@ render( </Route> </Router> </MetaProvider> - </LocalProvider> + </LayoutProvider> </SyncProvider> </SDKProvider> </MarkedProvider> diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index d88564007..6e0078a16 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -1,33 +1,85 @@ -import { Button, Tooltip, DiffChanges, IconButton } from "@opencode-ai/ui" -import { createMemo, For, ParentProps, Show } from "solid-js" +import { Button, Tooltip, DiffChanges, IconButton, Mark, Icon } from "@opencode-ai/ui" +import { createMemo, For, Match, ParentProps, Show, Switch } from "solid-js" import { DateTime } from "luxon" import { useSync } from "@/context/sync" import { A, useParams } from "@solidjs/router" -import { useLocal } from "@/context/local" +import { useLayout } from "@/context/layout" export default function Layout(props: ParentProps) { const params = useParams() const sync = useSync() - const local = useLocal() + const layout = useLayout() return ( <div class="relative h-screen flex flex-col"> - <header class="hidden h-12 shrink-0 bg-background-strong border-b border-border-weak-base"></header> - <div class="h-[calc(100vh-0rem)] flex"> + <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base"> <div classList={{ - "@container w-14 pb-4 shrink-0 bg-background-weak": true, - "flex flex-col items-start self-stretch justify-between": true, + "w-12 shrink-0 px-4 py-3.5": true, + "flex items-center justify-start self-stretch": true, "border-r border-border-weak-base": true, - "w-70": local.layout.sidebar.opened(), }} + style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }} > - <div class="flex flex-col justify-center items-start gap-4 self-stretch py-2 overflow-hidden mx-auto @[4rem]:mx-0"> - <div class="h-8 shrink-0 flex items-center self-stretch px-3"> - <Tooltip placement="right" value="Collapse sidebar"> - <IconButton icon="layout-left" variant="ghost" size="large" onClick={local.layout.sidebar.toggle} /> - </Tooltip> - </div> + <Mark class="shrink-0" /> + </div> + </header> + <div class="h-[calc(100vh-3rem)] flex"> + <div + classList={{ + "@container w-12 pb-5 shrink-0 bg-background-base": true, + "flex flex-col gap-5.5 items-start self-stretch justify-between": true, + "border-r border-border-weak-base": true, + }} + style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }} + > + <div class="flex flex-col justify-center items-start gap-4 self-stretch p-2 overflow-hidden mx-auto @[4rem]:mx-0"> + <Switch> + <Match when={layout.sidebar.opened()}> + <Button + variant="ghost" + size="large" + class="group/sidebar-toggle w-full text-left justify-start" + onClick={layout.sidebar.toggle} + > + <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0"> + <Icon name="layout-left" size="small" class="group-hover/sidebar-toggle:hidden" /> + <Icon + name="layout-left-partial" + size="small" + class="hidden group-hover/sidebar-toggle:inline-block" + /> + <Icon + name="layout-left-full" + size="small" + class="hidden group-active/sidebar-toggle:inline-block" + /> + </div> + <div class="hidden group-hover/sidebar-toggle:block group-active/sidebar-toggle:block text-text-base"> + Toggle sidebar + </div> + </Button> + </Match> + <Match when={!layout.sidebar.opened()}> + <Tooltip placement="right" value="Toggle sidebar"> + <Button variant="ghost" size="large" class="group/sidebar-toggle" onClick={layout.sidebar.toggle}> + <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0"> + <Icon name="layout-right" size="small" class="group-hover/sidebar-toggle:hidden" /> + <Icon + name="layout-right-partial" + size="small" + class="hidden group-hover/sidebar-toggle:inline-block" + /> + <Icon + name="layout-right-full" + size="small" + class="hidden group-active/sidebar-toggle:inline-block" + /> + </div> + </Button> + </Tooltip> + </Match> + </Switch> <div class="w-full px-3"> <Button as={A} href="/session" class="hidden @[4rem]:flex w-full" size="large" icon="edit-small-2"> New Session diff --git a/packages/desktop/src/pages/session-layout.tsx b/packages/desktop/src/pages/session-layout.tsx index 9a24608f0..7f355c9bc 100644 --- a/packages/desktop/src/pages/session-layout.tsx +++ b/packages/desktop/src/pages/session-layout.tsx @@ -1,12 +1,24 @@ import { Show, type ParentProps } from "solid-js" -import { SessionProvider } from "@/context/session" +import { SessionProvider, useSession } from "@/context/session" import { useParams } from "@solidjs/router" +import { SDKProvider, useSDK } from "@/context/sdk" +import { LocalProvider } from "@/context/local" export default function Layout(props: ParentProps) { const params = useParams() + const root = useSDK() return ( <Show when={params.id || true} keyed> - <SessionProvider sessionId={params.id}>{props.children}</SessionProvider> + <SessionProvider sessionId={params.id}> + {(() => { + const session = useSession() + return ( + <SDKProvider url={root.url} directory={session.info()?.directory}> + <LocalProvider>{props.children}</LocalProvider> + </SDKProvider> + ) + })()} + </SessionProvider> </Show> ) } diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 0e5eb1f3d..884d789b1 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -13,7 +13,6 @@ import { Code, Tooltip, ProgressCircle, - Button, } from "@opencode-ai/ui" import { FileIcon } from "@/ui" import { MessageProgress } from "@/components/message-progress" @@ -52,8 +51,10 @@ import { Spinner } from "@/components/spinner" import { useSession } from "@/context/session" import { StickyAccordionHeader } from "@/components/sticky-accordion-header" import { SessionReview } from "@/components/session-review" +import { useLayout } from "@/context/layout" export default function Page() { + const layout = useLayout() const local = useLocal() const sync = useSync() const session = useSession() @@ -176,10 +177,16 @@ export default function Page() { setStore("activeDraggable", undefined) } - const FileVisual = (props: { file: LocalFile }): JSX.Element => { + const FileVisual = (props: { file: LocalFile; active?: boolean }): JSX.Element => { return ( <div class="flex items-center gap-x-1.5"> - <FileIcon node={props.file} class="grayscale-100 group-data-[selected]/tab:grayscale-0" /> + <FileIcon + node={props.file} + classList={{ + "grayscale-100 group-data-[selected]/tab:grayscale-0": !props.active, + "grayscale-0": props.active, + }} + /> <span classList={{ "text-14-medium": true, @@ -300,11 +307,11 @@ export default function Page() { </Tooltip> </div> </Tabs.Trigger> - <Show when={local.layout.review.state() === "tab" && session.diffs().length}> + <Show when={layout.review.state() === "tab" && session.diffs().length}> <Tabs.Trigger value="review" closeButton={ - <IconButton icon="collapse" size="normal" variant="ghost" onClick={local.layout.review.pane} /> + <IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} /> } > <div class="flex items-center gap-3"> @@ -343,8 +350,8 @@ export default function Page() { <div classList={{ "w-full flex-1 min-h-0": true, - grid: local.layout.review.state() === "tab", - flex: local.layout.review.state() === "pane", + grid: layout.review.state() === "tab", + flex: layout.review.state() === "pane", }} > <div class="relative shrink-0 px-6 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-xl mx-auto"> @@ -353,7 +360,7 @@ export default function Page() { <div classList={{ "flex-1 min-h-0 pb-20": true, - "flex items-start justify-start": local.layout.review.state() === "pane", + "flex items-start justify-start": layout.review.state() === "pane", }} > <Show when={session.messages.user().length > 1}> @@ -361,8 +368,8 @@ export default function Page() { role="list" classList={{ "mr-8 shrink-0 flex flex-col items-start": true, - "absolute right-full w-60 mt-3 @7xl:gap-2 @7xl:mt-1": local.layout.review.state() === "tab", - "mt-3": local.layout.review.state() === "pane", + "absolute right-full w-60 mt-3 @7xl:gap-2 @7xl:mt-1": layout.review.state() === "tab", + "mt-3": layout.review.state() === "pane", }} > <For each={session.messages.user()}> @@ -382,7 +389,7 @@ export default function Page() { <li classList={{ "group/li flex items-center self-stretch justify-end": true, - "@7xl:justify-start": local.layout.review.state() === "tab", + "@7xl:justify-start": layout.review.state() === "tab", }} > <Tooltip @@ -401,7 +408,7 @@ export default function Page() { classList={{ "group/tick flex items-center justify-start h-2 w-8 -mr-3": true, "data-[active=true]:[&>div]:bg-icon-strong-base data-[active=true]:[&>div]:w-full": true, - "@7xl:hidden": local.layout.review.state() === "tab", + "@7xl:hidden": layout.review.state() === "tab", }} > <div class="h-px w-5 bg-icon-base group-hover/tick:w-full group-hover/tick:bg-icon-strong-base" /> @@ -410,7 +417,7 @@ export default function Page() { <button classList={{ "hidden items-center self-stretch w-full gap-x-2 cursor-default": true, - "@7xl:flex": local.layout.review.state() === "tab", + "@7xl:flex": layout.review.state() === "tab", }} onClick={handleClick} > @@ -654,7 +661,7 @@ export default function Page() { /> </div> </div> - <Show when={local.layout.review.state() === "pane" && session.diffs().length}> + <Show when={layout.review.state() === "pane" && session.diffs().length}> <div classList={{ "relative grow px-6 py-3 flex-1 min-h-0 border-l border-border-weak-base": true, @@ -665,7 +672,7 @@ export default function Page() { </Show> </div> </Tabs.Content> - <Show when={local.layout.review.state() === "tab" && session.diffs().length}> + <Show when={layout.review.state() === "tab" && session.diffs().length}> <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden"> <div classList={{ @@ -718,8 +725,8 @@ export default function Page() { }, ) return ( - <div class="relative px-3 h-10 flex items-center bg-background-base border-x border-border-weak-base border-b border-b-transparent"> - <Show when={file()}>{(f) => <FileVisual file={f()} />}</Show> + <div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent"> + <Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show> </div> ) }} @@ -769,7 +776,13 @@ export default function Page() { items={local.file.searchFiles} key={(x) => x} onOpenChange={(open) => setStore("fileSelectOpen", open)} - onSelect={(x) => (x ? session.layout.openTab("file://" + x) : undefined)} + onSelect={(x) => { + if (x) { + local.file.open(x) + return session.layout.openTab("file://" + x) + } + return undefined + }} > {(i) => ( <div diff --git a/packages/desktop/src/ui/file-icon.tsx b/packages/desktop/src/ui/file-icon.tsx index d31a741e0..53b3c1e69 100644 --- a/packages/desktop/src/ui/file-icon.tsx +++ b/packages/desktop/src/ui/file-icon.tsx @@ -9,12 +9,13 @@ export type FileIconProps = JSX.GSVGAttributes<SVGSVGElement> & { } export const FileIcon: Component<FileIconProps> = (props) => { - const [local, rest] = splitProps(props, ["node", "class", "expanded"]) + const [local, rest] = splitProps(props, ["node", "class", "classList", "expanded"]) const name = createMemo(() => chooseIconName(local.node.path, local.node.type, local.expanded || false)) return ( <svg {...rest} classList={{ + ...(local.classList ?? {}), "shrink-0 size-4": true, [local.class ?? ""]: !!local.class, }} |
