summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'packages/desktop/src/pages')
-rw-r--r--packages/desktop/src/pages/directory-layout.tsx25
-rw-r--r--packages/desktop/src/pages/home.tsx20
-rw-r--r--packages/desktop/src/pages/layout.tsx266
-rw-r--r--packages/desktop/src/pages/session-layout.tsx24
-rw-r--r--packages/desktop/src/pages/session.tsx162
5 files changed, 274 insertions, 223 deletions
diff --git a/packages/desktop/src/pages/directory-layout.tsx b/packages/desktop/src/pages/directory-layout.tsx
new file mode 100644
index 000000000..fe35f20f9
--- /dev/null
+++ b/packages/desktop/src/pages/directory-layout.tsx
@@ -0,0 +1,25 @@
+import { createMemo, Show, type ParentProps } from "solid-js"
+import { useParams } from "@solidjs/router"
+import { SDKProvider } from "@/context/sdk"
+import { SyncProvider } from "@/context/sync"
+import { LocalProvider } from "@/context/local"
+import { useGlobalSync } from "@/context/global-sync"
+import { base64Decode } from "@/utils"
+
+export default function Layout(props: ParentProps) {
+ const params = useParams()
+ const sync = useGlobalSync()
+ const directory = createMemo(() => {
+ const decoded = base64Decode(params.dir)
+ return sync.data.projects.find((x) => x.worktree === decoded)?.worktree ?? "/"
+ })
+ return (
+ <Show when={params.id || true} keyed>
+ <SDKProvider directory={directory()}>
+ <SyncProvider>
+ <LocalProvider>{props.children}</LocalProvider>
+ </SyncProvider>
+ </SDKProvider>
+ </Show>
+ )
+}
diff --git a/packages/desktop/src/pages/home.tsx b/packages/desktop/src/pages/home.tsx
new file mode 100644
index 000000000..e2f99afa8
--- /dev/null
+++ b/packages/desktop/src/pages/home.tsx
@@ -0,0 +1,20 @@
+import { useGlobalSync } from "@/context/global-sync"
+import { base64Encode, getFilename } from "@/utils"
+import { For } from "solid-js"
+import { A } from "@solidjs/router"
+import { Button } from "@opencode-ai/ui"
+
+export default function Home() {
+ const sync = useGlobalSync()
+ return (
+ <div class="flex flex-col gap-3">
+ <For each={sync.data.projects}>
+ {(project) => (
+ <Button as={A} href={base64Encode(project.worktree)}>
+ {getFilename(project.worktree)}
+ </Button>
+ )}
+ </For>
+ </div>
+ )
+}
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index 6e0078a16..33cf7b66f 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -1,19 +1,25 @@
-import { Button, Tooltip, DiffChanges, IconButton, Mark, Icon } from "@opencode-ai/ui"
-import { createMemo, For, Match, ParentProps, Show, Switch } from "solid-js"
+import { Button, Tooltip, DiffChanges, IconButton, Mark, Icon, Collapsible } from "@opencode-ai/ui"
+import { createMemo, For, ParentProps, Show } from "solid-js"
import { DateTime } from "luxon"
-import { useSync } from "@/context/sync"
import { A, useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
+import { useGlobalSync } from "@/context/global-sync"
+import { base64Encode, getFilename } from "@/utils"
export default function Layout(props: ParentProps) {
const params = useParams()
- const sync = useSync()
+ const globalSync = useGlobalSync()
const layout = useLayout()
+ const handleOpenProject = async () => {
+ // layout.projects.open(dir.)
+ }
+
return (
<div class="relative h-screen flex flex-col">
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base">
- <div
+ <A
+ href="/"
classList={{
"w-12 shrink-0 px-4 py-3.5": true,
"flex items-center justify-start self-stretch": true,
@@ -22,7 +28,7 @@ export default function Layout(props: ParentProps) {
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
>
<Mark class="shrink-0" />
- </div>
+ </A>
</header>
<div class="h-[calc(100vh-3rem)] flex">
<div
@@ -33,136 +39,154 @@ export default function Layout(props: ParentProps) {
}}
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="grow flex flex-col items-start self-stretch gap-4 p-2 min-h-0">
+ <Tooltip class="shrink-0" placement="right" value="Toggle sidebar" inactive={layout.sidebar.opened()}>
+ <Button
+ variant="ghost"
+ size="large"
+ class="group/sidebar-toggle shrink-0 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.sidebar.opened() ? "layout-left" : "layout-right"}
+ size="small"
+ class="group-hover/sidebar-toggle:hidden"
+ />
+ <Icon
+ name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
+ size="small"
+ class="hidden group-hover/sidebar-toggle:inline-block"
+ />
+ <Icon
+ name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
+ size="small"
+ class="hidden group-active/sidebar-toggle:inline-block"
+ />
+ </div>
+ <Show when={layout.sidebar.opened()}>
<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
+ </Show>
</Button>
- <Tooltip placement="right" value="New session">
- <IconButton as={A} href="/session" icon="edit-small-2" size="large" class="@[4rem]:hidden" />
- </Tooltip>
- </div>
- <div class="hidden @[4rem]:flex size-full overflow-y-auto no-scrollbar flex-col flex-1 px-3">
- <nav class="w-full">
- <For each={sync.data.session}>
- {(session) => {
- const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
+ </Tooltip>
+ <div class="flex flex-col justify-center items-start gap-4 self-stretch min-h-0">
+ <div class="hidden @[4rem]:flex size-full flex-col grow overflow-y-auto no-scrollbar">
+ <For each={layout.projects.list()}>
+ {(project) => {
+ const [store] = globalSync.child(project.directory)
+ const slug = createMemo(() => base64Encode(project.directory))
return (
- <A
- data-active={session.id === params.id}
- href={`/session/${session.id}`}
- class="group/session focus:outline-none cursor-default"
- >
- <Tooltip placement="right" value={session.title}>
- <div
- class="w-full mb-1.5 px-3 py-1 rounded-md
- group-data-[active=true]/session:bg-surface-raised-base-hover
- group-hover/session:bg-surface-raised-base-hover
- group-focus/session:bg-surface-raised-base-hover"
- >
- <div class="flex items-center self-stretch gap-6 justify-between">
- <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
- {session.title}
- </span>
- <span class="text-12-regular text-text-weak text-right whitespace-nowrap">
- {Math.abs(updated().diffNow().as("seconds")) < 60
- ? "Now"
- : updated()
- .toRelative({ style: "short", unit: ["days", "hours", "minutes"] })
- ?.replace(" ago", "")
- ?.replace(/ days?/, "d")
- ?.replace(" min.", "m")
- ?.replace(" hr.", "h")}
- </span>
- </div>
- <div class="flex justify-between items-center self-stretch">
- <span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
- <Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
- </div>
- </div>
- </Tooltip>
- </A>
+ <Collapsible variant="ghost" defaultOpen class="gap-2">
+ <Button
+ as={"div"}
+ variant="ghost"
+ class="flex items-center justify-between gap-3 w-full h-8 pl-2 pr-2.25 self-stretch"
+ >
+ <Collapsible.Trigger class="p-0 text-left text-14-medium text-text-strong grow min-w-0 truncate">
+ {getFilename(project.directory)}
+ </Collapsible.Trigger>
+ <IconButton as={A} href={`${slug()}/session`} icon="plus-small" size="normal" />
+ </Button>
+ <Collapsible.Content>
+ <nav class="w-full flex flex-col gap-1.5">
+ <For each={store.session}>
+ {(session) => {
+ const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
+ return (
+ <A
+ data-active={session.id === params.id}
+ href={`${slug()}/session/${session.id}`}
+ class="group/session focus:outline-none cursor-default"
+ >
+ <Tooltip placement="right" value={session.title}>
+ <div
+ class="w-full px-2 py-1 rounded-md
+ group-data-[active=true]/session:bg-surface-raised-base-hover
+ group-hover/session:bg-surface-raised-base-hover
+ group-focus/session:bg-surface-raised-base-hover"
+ >
+ <div class="flex items-center self-stretch gap-6 justify-between">
+ <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
+ {session.title}
+ </span>
+ <span class="text-12-regular text-text-weak text-right whitespace-nowrap">
+ {Math.abs(updated().diffNow().as("seconds")) < 60
+ ? "Now"
+ : updated()
+ .toRelative({ style: "short", unit: ["days", "hours", "minutes"] })
+ ?.replace(" ago", "")
+ ?.replace(/ days?/, "d")
+ ?.replace(" min.", "m")
+ ?.replace(" hr.", "h")}
+ </span>
+ </div>
+ <div class="hidden flex justify-between items-center self-stretch">
+ <span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
+ <Show when={session.summary}>
+ {(summary) => <DiffChanges changes={summary()} />}
+ </Show>
+ </div>
+ </div>
+ </Tooltip>
+ </A>
+ )
+ }}
+ </For>
+ </nav>
+ {/* <Show when={sync.session.more()}> */}
+ {/* <button */}
+ {/* class="shrink-0 self-start p-3 text-12-medium text-text-weak hover:text-text-strong" */}
+ {/* onClick={() => sync.session.fetch()} */}
+ {/* > */}
+ {/* Show more */}
+ {/* </button> */}
+ {/* </Show> */}
+ </Collapsible.Content>
+ </Collapsible>
)
}}
</For>
- </nav>
- <Show when={sync.session.more()}>
- <button
- class="shrink-0 self-start p-3 text-12-medium text-text-weak hover:text-text-strong"
- onClick={() => sync.session.fetch()}
- >
- Show more
- </button>
- </Show>
+ </div>
</div>
</div>
- <div class="flex flex-col items-start shrink-0 px-3 py-1 mx-auto @[4rem]:mx-0">
- <Button
- as={"a"}
- href="https://opencode.ai/desktop-feedback"
- target="_blank"
- class="hidden @[4rem]:flex w-full text-12-medium text-text-base stroke-[1.5px]"
- variant="ghost"
- icon="speech-bubble"
- >
- Share feedback
- </Button>
- <Tooltip placement="right" value="Share feedback">
- <IconButton
+ <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
+ <Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
+ <Button
+ disabled
+ class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
+ variant="ghost"
+ size="large"
+ icon="folder-add-left"
+ onClick={handleOpenProject}
+ >
+ <Show when={layout.sidebar.opened()}>Open project</Show>
+ </Button>
+ </Tooltip>
+ <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
+ <Button
+ disabled
+ class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
+ variant="ghost"
+ size="large"
+ icon="settings-gear"
+ >
+ <Show when={layout.sidebar.opened()}>Settings</Show>
+ </Button>
+ </Tooltip>
+ <Tooltip placement="right" value="Share feedback" inactive={layout.sidebar.opened()}>
+ <Button
as={"a"}
href="https://opencode.ai/desktop-feedback"
target="_blank"
- icon="speech-bubble"
+ class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
variant="ghost"
size="large"
- class="@[4rem]:hidden stroke-[1.5px]"
- />
+ icon="bubble-5"
+ >
+ <Show when={layout.sidebar.opened()}>Share feedback</Show>
+ </Button>
</Tooltip>
</div>
</div>
diff --git a/packages/desktop/src/pages/session-layout.tsx b/packages/desktop/src/pages/session-layout.tsx
deleted file mode 100644
index 7f355c9bc..000000000
--- a/packages/desktop/src/pages/session-layout.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Show, type ParentProps } from "solid-js"
-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}>
- {(() => {
- 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 884d789b1..01d32a672 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -294,7 +294,7 @@ export default function Page() {
<Tabs.List>
<Tabs.Trigger value="chat">
<div class="flex gap-x-[17px] items-center">
- <div>Chat</div>
+ <div>Session</div>
<Tooltip
value={`${new Intl.NumberFormat("en-US", {
notation: "compact",
@@ -364,88 +364,94 @@ export default function Page() {
}}
>
<Show when={session.messages.user().length > 1}>
- <ul
- 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": layout.review.state() === "tab",
- "mt-3": layout.review.state() === "pane",
- }}
- >
- <For each={session.messages.user()}>
- {(message) => {
- const assistantMessages = createMemo(() => {
- if (!session.id) return []
- return sync.data.message[session.id]?.filter(
- (m) => m.role === "assistant" && m.parentID == message.id,
- ) as AssistantMessageType[]
- })
- const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
- const working = createMemo(() => !message.summary?.body && !error())
+ {(_) => {
+ const expanded = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
- const handleClick = () => session.messages.setActive(message.id)
+ return (
+ <ul
+ 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": expanded(),
+ "mt-3": !expanded(),
+ }}
+ >
+ <For each={session.messages.user()}>
+ {(message) => {
+ const assistantMessages = createMemo(() => {
+ if (!session.id) return []
+ return sync.data.message[session.id]?.filter(
+ (m) => m.role === "assistant" && m.parentID == message.id,
+ ) as AssistantMessageType[]
+ })
+ const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
+ const working = createMemo(() => !message.summary?.body && !error())
- return (
- <li
- classList={{
- "group/li flex items-center self-stretch justify-end": true,
- "@7xl:justify-start": layout.review.state() === "tab",
- }}
- >
- <Tooltip
- placement="right"
- gutter={8}
- value={
- <div class="flex items-center gap-2">
- <DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
- {message.summary?.title}
- </div>
- }
- >
- <button
- data-active={session.messages.active()?.id === message.id}
- onClick={handleClick}
- 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": 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" />
- </button>
- </Tooltip>
- <button
- classList={{
- "hidden items-center self-stretch w-full gap-x-2 cursor-default": true,
- "@7xl:flex": layout.review.state() === "tab",
- }}
- onClick={handleClick}
- >
- <Switch>
- <Match when={working()}>
- <Spinner class="text-text-base shrink-0 w-[18px] aspect-square" />
- </Match>
- <Match when={true}>
- <DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
- </Match>
- </Switch>
- <div
- data-active={session.messages.active()?.id === message.id}
+ const handleClick = () => session.messages.setActive(message.id)
+
+ return (
+ <li
classList={{
- "text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
- "text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
+ "group/li flex items-center self-stretch justify-end": true,
+ "@7xl:justify-start": expanded(),
}}
>
- <Show when={message.summary?.title} fallback="New message">
- {message.summary?.title}
- </Show>
- </div>
- </button>
- </li>
- )
- }}
- </For>
- </ul>
+ <Tooltip
+ placement="right"
+ gutter={8}
+ value={
+ <div class="flex items-center gap-2">
+ <DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
+ {message.summary?.title}
+ </div>
+ }
+ >
+ <button
+ data-active={session.messages.active()?.id === message.id}
+ onClick={handleClick}
+ 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": expanded(),
+ }}
+ >
+ <div class="h-px w-5 bg-icon-base group-hover/tick:w-full group-hover/tick:bg-icon-strong-base" />
+ </button>
+ </Tooltip>
+ <button
+ classList={{
+ "hidden items-center self-stretch w-full gap-x-2 cursor-default": true,
+ "@7xl:flex": expanded(),
+ }}
+ onClick={handleClick}
+ >
+ <Switch>
+ <Match when={working()}>
+ <Spinner class="text-text-base shrink-0 w-[18px] aspect-square" />
+ </Match>
+ <Match when={true}>
+ <DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
+ </Match>
+ </Switch>
+ <div
+ data-active={session.messages.active()?.id === message.id}
+ classList={{
+ "text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
+ "text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
+ }}
+ >
+ <Show when={message.summary?.title} fallback="New message">
+ {message.summary?.title}
+ </Show>
+ </div>
+ </button>
+ </li>
+ )
+ }}
+ </For>
+ </ul>
+ )
+ }}
</Show>
<div ref={messageScrollElement} class="grow size-full min-w-0 overflow-y-auto no-scrollbar">
<For each={session.messages.user()}>