summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-12 10:45:02 -0600
committerAdam <[email protected]>2025-12-12 10:45:34 -0600
commit9629f7464b3a1a6077e7c6b04923c4c13993ac08 (patch)
treed803cca8c47b65c199c00d68c406a80452831d10 /packages
parent9017d103031fa252f5b3fce23df651d1b4ef5bc5 (diff)
downloadopencode-9629f7464b3a1a6077e7c6b04923c4c13993ac08.tar.gz
opencode-9629f7464b3a1a6077e7c6b04923c4c13993ac08.zip
chore: cleanup
Diffstat (limited to 'packages')
-rw-r--r--packages/desktop/src/components/header.tsx113
-rw-r--r--packages/desktop/src/context/global-sync.tsx62
-rw-r--r--packages/desktop/src/pages/layout.tsx101
3 files changed, 154 insertions, 122 deletions
diff --git a/packages/desktop/src/components/header.tsx b/packages/desktop/src/components/header.tsx
new file mode 100644
index 000000000..cc4d01816
--- /dev/null
+++ b/packages/desktop/src/components/header.tsx
@@ -0,0 +1,113 @@
+import { useGlobalSync } from "@/context/global-sync"
+import { useLayout } from "@/context/layout"
+import { Session } from "@opencode-ai/sdk/v2/client"
+import { Button } from "@opencode-ai/ui/button"
+import { Icon } from "@opencode-ai/ui/icon"
+import { Mark } from "@opencode-ai/ui/logo"
+import { Select } from "@opencode-ai/ui/select"
+import { Tooltip } from "@opencode-ai/ui/tooltip"
+import { base64Decode } from "@opencode-ai/util/encode"
+import { getFilename } from "@opencode-ai/util/path"
+import { A, useParams } from "@solidjs/router"
+import { createMemo, Show } from "solid-js"
+
+export function Header(props: {
+ navigateToProject: (directory: string) => void
+ navigateToSession: (session: Session | undefined) => void
+}) {
+ const globalSync = useGlobalSync()
+ const layout = useLayout()
+ const params = useParams()
+ const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
+ const store = createMemo(() => globalSync.child(currentDirectory())[0])
+ const sessions = createMemo(() => store().session ?? [])
+ const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
+
+ return (
+ <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
+ <A
+ href="/"
+ classList={{
+ "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,
+ }}
+ style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
+ data-tauri-drag-region
+ >
+ <Mark class="shrink-0" />
+ </A>
+ <div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
+ <Show when={params.dir && layout.projects.list().length > 0}>
+ <div class="flex items-center gap-3">
+ <div class="flex items-center gap-2">
+ <Select
+ options={layout.projects.list().map((project) => project.worktree)}
+ current={currentDirectory()}
+ label={(x) => getFilename(x)}
+ onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
+ class="text-14-regular text-text-base"
+ variant="ghost"
+ >
+ {/* @ts-ignore */}
+ {(i) => (
+ <div class="flex items-center gap-2">
+ <Icon name="folder" size="small" />
+ <div class="text-text-strong">{getFilename(i)}</div>
+ </div>
+ )}
+ </Select>
+ <div class="text-text-weaker">/</div>
+ <Select
+ options={sessions()}
+ current={currentSession()}
+ placeholder="New session"
+ label={(x) => x.title}
+ value={(x) => x.id}
+ onSelect={props.navigateToSession}
+ class="text-14-regular text-text-base max-w-md"
+ variant="ghost"
+ />
+ </div>
+ <Show when={currentSession()}>
+ <Button as={A} href={`/${params.dir}/session`} icon="plus-small">
+ New session
+ </Button>
+ </Show>
+ </div>
+ <div class="flex items-center gap-4">
+ <Tooltip
+ class="shrink-0"
+ value={
+ <div class="flex items-center gap-2">
+ <span>Toggle terminal</span>
+ <span class="text-icon-base text-12-medium">Ctrl `</span>
+ </div>
+ }
+ >
+ <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
+ <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
+ <Icon
+ size="small"
+ name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
+ class="group-hover/terminal-toggle:hidden"
+ />
+ <Icon
+ size="small"
+ name="layout-bottom-partial"
+ class="hidden group-hover/terminal-toggle:inline-block"
+ />
+ <Icon
+ size="small"
+ name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
+ class="hidden group-active/terminal-toggle:inline-block"
+ />
+ </div>
+ </Button>
+ </Tooltip>
+ </div>
+ </Show>
+ </div>
+ </header>
+ )
+}
diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx
index 4c461a4e9..8151a2c6f 100644
--- a/packages/desktop/src/context/global-sync.tsx
+++ b/packages/desktop/src/context/global-sync.tsx
@@ -69,6 +69,32 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
children: {},
})
+ const children: Record<string, ReturnType<typeof createStore<State>>> = {}
+ function child(directory: string) {
+ if (!children[directory]) {
+ setGlobalStore("children", directory, {
+ project: "",
+ provider: { all: [], connected: [], default: {} },
+ config: {},
+ path: { state: "", config: "", worktree: "", directory: "", home: "" },
+ ready: false,
+ agent: [],
+ session: [],
+ session_status: {},
+ session_diff: {},
+ todo: {},
+ limit: 5,
+ message: {},
+ part: {},
+ node: [],
+ changes: [],
+ })
+ children[directory] = createStore(globalStore.children[directory])
+ bootstrapInstance(directory)
+ }
+ return children[directory]
+ }
+
async function loadSessions(directory: string) {
globalSDK.client.session.list({ directory }).then((x) => {
const sessions = (x.data ?? [])
@@ -76,7 +102,8 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
.filter((s) => !s.time.archived)
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, 5)
- setGlobalStore("children", directory, "session", sessions)
+ const [, setStore] = child(directory)
+ setStore("session", sessions)
})
}
@@ -100,32 +127,6 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
}
- const children: Record<string, ReturnType<typeof createStore<State>>> = {}
- function child(directory: string) {
- if (!children[directory]) {
- setGlobalStore("children", directory, {
- project: "",
- provider: { all: [], connected: [], default: {} },
- config: {},
- path: { state: "", config: "", worktree: "", directory: "", home: "" },
- ready: false,
- agent: [],
- session: [],
- session_status: {},
- session_diff: {},
- todo: {},
- limit: 5,
- message: {},
- part: {},
- node: [],
- changes: [],
- })
- children[directory] = createStore(globalStore.children[directory])
- bootstrapInstance(directory)
- }
- return children[directory]
- }
-
globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
@@ -164,7 +165,12 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (event.properties.info.time.archived) {
if (result.found) {
- setStore("session", (s) => s.filter((x) => x.id !== event.properties.info.id))
+ setStore(
+ "session",
+ produce((draft) => {
+ draft.splice(result.index, 1)
+ }),
+ )
}
break
}
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index d27006835..41b1977c8 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -16,7 +16,6 @@ import { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout, getAvatarColors } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
-import { Mark } from "@opencode-ai/ui/logo"
import { Avatar } from "@opencode-ai/ui/avatar"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
@@ -27,7 +26,6 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { getFilename } from "@opencode-ai/util/path"
-import { Select } from "@opencode-ai/ui/select"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
@@ -56,6 +54,7 @@ import { useGlobalSDK } from "@/context/global-sdk"
import { Spinner } from "@opencode-ai/ui/spinner"
import { useNotification } from "@/context/notification"
import { Binary } from "@opencode-ai/util/binary"
+import { Header } from "@/components/header"
export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
@@ -70,9 +69,6 @@ export default function Layout(props: ParentProps) {
const platform = usePlatform()
const notification = useNotification()
const navigate = useNavigate()
- const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
- const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
- const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const providers = useProviders()
function navigateToProject(directory: string | undefined) {
@@ -226,6 +222,7 @@ export default function Layout(props: ParentProps) {
const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
const name = createMemo(() => getFilename(props.project.worktree))
+ const current = createMemo(() => base64Decode(params.dir ?? ""))
return (
<Switch>
<Match when={layout.sidebar.opened()}>
@@ -246,7 +243,7 @@ export default function Layout(props: ParentProps) {
variant="ghost"
size="large"
class="flex items-center justify-center p-0 aspect-square border-none rounded-lg"
- data-selected={props.project.worktree === currentDirectory()}
+ data-selected={props.project.worktree === current()}
onClick={() => navigateToProject(props.project.worktree)}
>
<ProjectAvatar project={props.project} notify />
@@ -259,10 +256,10 @@ export default function Layout(props: ParentProps) {
const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
const notification = useNotification()
const sortable = createSortable(props.project.worktree)
- const [projectStore, setProjectStore] = globalSync.child(props.project.worktree)
- const sessions = createMemo(() => projectStore.session.filter((s) => !s.time.archived))
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
+ const [store, setStore] = globalSync.child(props.project.worktree)
+ const sessions = createMemo(() => store.session ?? [])
const [expanded, setExpanded] = createSignal(true)
return (
// @ts-ignore
@@ -313,7 +310,7 @@ export default function Layout(props: ParentProps) {
sessionID: session.id,
time: { archived: Date.now() },
})
- setProjectStore(
+ setStore(
produce((draft) => {
const match = Binary.search(draft.session, session.id, (s) => s.id)
if (match.found) draft.session.splice(match.index, 1)
@@ -427,91 +424,7 @@ export default function Layout(props: ParentProps) {
return (
<div class="relative flex-1 flex flex-col">
- <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
- <A
- href="/"
- classList={{
- "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,
- }}
- style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
- data-tauri-drag-region
- >
- <Mark class="shrink-0" />
- </A>
- <div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
- <Show when={params.dir && layout.projects.list().length > 0}>
- <div class="flex items-center gap-3">
- <div class="flex items-center gap-2">
- <Select
- options={layout.projects.list().map((project) => project.worktree)}
- current={currentDirectory()}
- label={(x) => getFilename(x)}
- onSelect={(x) => (x ? navigateToProject(x) : undefined)}
- class="text-14-regular text-text-base"
- variant="ghost"
- >
- {/* @ts-ignore */}
- {(i) => (
- <div class="flex items-center gap-2">
- <Icon name="folder" size="small" />
- <div class="text-text-strong">{getFilename(i)}</div>
- </div>
- )}
- </Select>
- <div class="text-text-weaker">/</div>
- <Select
- options={sessions()}
- current={currentSession()}
- placeholder="New session"
- label={(x) => x.title}
- value={(x) => x.id}
- onSelect={navigateToSession}
- class="text-14-regular text-text-base max-w-md"
- variant="ghost"
- />
- </div>
- <Show when={currentSession()}>
- <Button as={A} href={`/${params.dir}/session`} icon="plus-small">
- New session
- </Button>
- </Show>
- </div>
- <div class="flex items-center gap-4">
- <Tooltip
- class="shrink-0"
- value={
- <div class="flex items-center gap-2">
- <span>Toggle terminal</span>
- <span class="text-icon-base text-12-medium">Ctrl `</span>
- </div>
- }
- >
- <Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
- <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
- <Icon
- size="small"
- name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
- class="group-hover/terminal-toggle:hidden"
- />
- <Icon
- size="small"
- name="layout-bottom-partial"
- class="hidden group-hover/terminal-toggle:inline-block"
- />
- <Icon
- size="small"
- name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
- class="hidden group-active/terminal-toggle:inline-block"
- />
- </div>
- </Button>
- </Tooltip>
- </div>
- </Show>
- </div>
- </header>
+ <Header navigateToProject={navigateToProject} navigateToSession={navigateToSession} />
<div class="h-[calc(100%-3rem)] flex">
<div
classList={{