summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-11-14 12:38:52 -0600
committeropencode <[email protected]>2025-11-18 17:07:34 +0000
commit4069999b782cc00d4e707f5eca32082bdfad45bc (patch)
treed5c13cdd361fc79b14250b4b426a0a1195f7a01c /packages/desktop
parent5ba9b47b3c87dfb044c30857e56959c8eff0c8c1 (diff)
downloadopencode-4069999b782cc00d4e707f5eca32082bdfad45bc.tar.gz
opencode-4069999b782cc00d4e707f5eca32082bdfad45bc.zip
wip(desktop): new layout work
Diffstat (limited to 'packages/desktop')
-rw-r--r--packages/desktop/src/context/layout.tsx52
-rw-r--r--packages/desktop/src/context/local.tsx47
-rw-r--r--packages/desktop/src/context/sdk.tsx15
-rw-r--r--packages/desktop/src/context/session.tsx6
-rw-r--r--packages/desktop/src/context/sync.tsx2
-rw-r--r--packages/desktop/src/index.tsx6
-rw-r--r--packages/desktop/src/pages/layout.tsx82
-rw-r--r--packages/desktop/src/pages/session-layout.tsx16
-rw-r--r--packages/desktop/src/pages/session.tsx49
-rw-r--r--packages/desktop/src/ui/file-icon.tsx3
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,
}}