diff options
Diffstat (limited to 'packages/desktop/src/pages')
| -rw-r--r-- | packages/desktop/src/pages/directory-layout.tsx | 25 | ||||
| -rw-r--r-- | packages/desktop/src/pages/home.tsx | 20 | ||||
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 266 | ||||
| -rw-r--r-- | packages/desktop/src/pages/session-layout.tsx | 24 | ||||
| -rw-r--r-- | packages/desktop/src/pages/session.tsx | 162 |
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()}> |
