diff options
| author | Adam <[email protected]> | 2025-11-13 14:47:25 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-11-13 14:47:29 -0600 |
| commit | a8836c5615f620321cc7aa2670767e0c9586d27b (patch) | |
| tree | 4af1b0f7397d2dceac6e0c9ba734049f41d7c326 /packages/desktop/src | |
| parent | 779a27693a9d37ae212c27af353414ff4de07ea4 (diff) | |
| download | opencode-a8836c5615f620321cc7aa2670767e0c9586d27b.tar.gz opencode-a8836c5615f620321cc7aa2670767e0c9586d27b.zip | |
wip(desktop): layout improvements
Diffstat (limited to 'packages/desktop/src')
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 12 | ||||
| -rw-r--r-- | packages/desktop/src/components/session-review.tsx | 104 | ||||
| -rw-r--r-- | packages/desktop/src/components/sticky-accordion-header.tsx | 17 | ||||
| -rw-r--r-- | packages/desktop/src/context/local.tsx | 11 | ||||
| -rw-r--r-- | packages/desktop/src/pages/session.tsx | 252 |
5 files changed, 199 insertions, 197 deletions
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index b2022cf30..2ae5f87fc 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -347,7 +347,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { <Show when={store.popoverIsOpen}> <div class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-[252px] min-h-10 - overflow-auto no-scrollbar flex flex-col p-2 pb-0 rounded-2xl + overflow-auto no-scrollbar flex flex-col p-2 pb-0 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md" > <Show when={flat().length > 0} fallback={<div class="text-text-weak px-2">No matching files</div>}> @@ -382,7 +382,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { onSubmit={handleSubmit} classList={{ "bg-surface-raised-stronger-non-alpha border border-border-strong-base": true, - "rounded-2xl overflow-clip focus-within:border-transparent focus-within:shadow-xs-border-select": true, + "rounded-md overflow-clip focus-within:border-transparent focus-within:shadow-xs-border-select": true, [props.class ?? ""]: !!props.class, }} > @@ -396,17 +396,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => { onInput={handleInput} onKeyDown={handleKeyDown} classList={{ - "w-full p-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, + "w-full px-5 py-3 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, "[&>[data-type=file]]:text-icon-info-active": true, }} /> <Show when={!session.prompt.dirty()}> - <div class="absolute top-0 left-0 p-3 text-14-regular text-text-weak pointer-events-none"> + <div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none"> Plan and build anything </div> </Show> </div> - <div class="p-3 flex items-center justify-between"> + <div class="relative p-3 flex items-center justify-between"> <div class="flex items-center justify-start gap-1"> <Select options={local.agent.list().map((agent) => agent.name)} @@ -489,7 +489,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { disabled={!session.prompt.dirty() && !session.working()} icon={session.working() ? "stop" : "arrow-up"} variant="primary" - class="rounded-full" + class="h-10 w-8 absolute right-2 bottom-2" /> </Tooltip> </div> diff --git a/packages/desktop/src/components/session-review.tsx b/packages/desktop/src/components/session-review.tsx new file mode 100644 index 000000000..8aefbbd46 --- /dev/null +++ b/packages/desktop/src/components/session-review.tsx @@ -0,0 +1,104 @@ +import { useLocal } from "@/context/local" +import { useSession } from "@/context/session" +import { FileIcon } from "@/ui" +import { getDirectory, getFilename } from "@/utils" +import { Accordion, Button, Diff, DiffChanges, Icon, IconButton, Tooltip } from "@opencode-ai/ui" +import { For, Match, Show, Switch } from "solid-js" +import { StickyAccordionHeader } from "./sticky-accordion-header" +import { createStore } from "solid-js/store" + +export const SessionReview = (props: { split?: boolean; class?: string; hideExpand?: boolean }) => { + const local = useLocal() + const session = useSession() + const [store, setStore] = createStore({ + open: session.diffs().map((d) => d.file), + }) + + const handleChange = (open: string[]) => { + setStore("open", open) + } + + const handleExpandOrCollapseAll = () => { + if (store.open.length > 0) { + setStore("open", []) + } else { + setStore( + "open", + session.diffs().map((d) => d.file), + ) + } + } + + return ( + <div + classList={{ + "flex flex-col gap-3 h-full overflow-y-auto no-scrollbar": true, + [props.class ?? ""]: !!props.class, + }} + > + <div class="sticky top-0 z-20 bg-background-stronger h-8 shrink-0 flex justify-between items-center self-stretch"> + <div class="text-14-medium text-text-strong">Session changes</div> + <div class="flex items-center gap-x-4 pr-px"> + <Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}> + <Switch> + <Match when={store.open.length > 0}>Collapse all</Match> + <Match when={true}>Expand all</Match> + </Switch> + </Button> + <Show when={!props.hideExpand}> + <Tooltip value="Open in tab"> + <IconButton + icon="expand" + variant="ghost" + onClick={() => { + local.layout.review.tab() + session.layout.setActiveTab("review") + }} + /> + </Tooltip> + </Show> + </div> + </div> + <Accordion multiple value={store.open} onChange={handleChange}> + <For each={session.diffs()}> + {(diff) => ( + <Accordion.Item value={diff.file}> + <StickyAccordionHeader class="top-11 data-expanded:before:-top-11"> + <Accordion.Trigger class="bg-background-stronger"> + <div class="flex items-center justify-between w-full gap-5"> + <div class="grow flex items-center gap-5 min-w-0"> + <FileIcon node={{ path: diff.file, type: "file" }} class="shrink-0 size-4" /> + <div class="flex grow min-w-0"> + <Show when={diff.file.includes("/")}> + <span class="text-text-base truncate-start">{getDirectory(diff.file)}‎</span> + </Show> + <span class="text-text-strong shrink-0">{getFilename(diff.file)}</span> + </div> + </div> + <div class="shrink-0 flex gap-4 items-center justify-end"> + <DiffChanges changes={diff} /> + <Icon name="chevron-grabber-vertical" size="small" /> + </div> + </div> + </Accordion.Trigger> + </StickyAccordionHeader> + <Accordion.Content> + <Diff + diffStyle={props.split ? "split" : "unified"} + before={{ + name: diff.file!, + contents: diff.before!, + }} + after={{ + name: diff.file!, + contents: diff.after!, + }} + /> + </Accordion.Content> + </Accordion.Item> + )} + </For> + </Accordion> + </div> + ) +} diff --git a/packages/desktop/src/components/sticky-accordion-header.tsx b/packages/desktop/src/components/sticky-accordion-header.tsx new file mode 100644 index 000000000..cb3f0d5eb --- /dev/null +++ b/packages/desktop/src/components/sticky-accordion-header.tsx @@ -0,0 +1,17 @@ +import { Accordion } from "@opencode-ai/ui" +import { ParentProps } from "solid-js" + +export function StickyAccordionHeader(props: ParentProps<{ class?: string }>) { + return ( + <Accordion.Header + classList={{ + "sticky top-0 data-expanded:z-10": true, + "data-expanded:before:content-[''] data-expanded:before:z-[-10]": true, + "data-expanded:before:absolute data-expanded:before:inset-0 data-expanded:before:bg-background-stronger": true, + [props.class ?? ""]: !!props.class, + }} + > + {props.children} + </Accordion.Header> + ) +} diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 1785bdf0c..1cef1c9f1 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -465,11 +465,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ width: 240, }, review: { - state: "closed" as "open" | "closed" | "tab", + state: "pane" as "pane" | "tab", }, }), { - name: "default-layout", + name: "_default-layout", }, ) @@ -492,11 +492,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }, review: { state: createMemo(() => store.review?.state ?? "closed"), - open() { - setStore("review", "state", "open") - }, - close() { - setStore("review", "state", "closed") + pane() { + setStore("review", "state", "pane") }, tab() { setStore("review", "state", "tab") diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index f16729cc2..0e5eb1f3d 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -44,12 +44,14 @@ import { useDragDropContext, } from "@thisbeyond/solid-dnd" import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd" -import type { JSX, ParentProps } from "solid-js" +import type { JSX } from "solid-js" import { useSync } from "@/context/sync" import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk" import { Markdown } from "@opencode-ai/ui" import { Spinner } from "@/components/spinner" import { useSession } from "@/context/session" +import { StickyAccordionHeader } from "@/components/sticky-accordion-header" +import { SessionReview } from "@/components/session-review" export default function Page() { const local = useLocal() @@ -83,6 +85,15 @@ export default function Page() { setStore("fileSelectOpen", true) return } + if (event.ctrlKey && event.key.toLowerCase() === "t") { + event.preventDefault() + const currentTheme = localStorage.getItem("theme") ?? "oc-1" + const themes = ["oc-1", "oc-2-paper"] + const nextTheme = themes[(themes.indexOf(currentTheme) + 1) % themes.length] + localStorage.setItem("theme", nextTheme) + document.documentElement.setAttribute("data-theme", nextTheme) + return + } const focused = document.activeElement === inputRef if (focused) { @@ -216,18 +227,15 @@ export default function Page() { // @ts-ignore <div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}> <div class="relative h-full"> - <Tabs.Trigger value={props.tab} class="group/tab pl-3 pr-1" onClick={() => props.onTabClick(props.tab)}> + <Tabs.Trigger + value={props.tab} + closeButton={<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />} + hideCloseButton + onClick={() => props.onTabClick(props.tab)} + > <Switch> <Match when={file()}>{(f) => <FileVisual file={f()} />}</Match> </Switch> - <IconButton - icon="close" - class="mt-0.5 opacity-0 group-data-[selected]/tab:opacity-100 - hover:bg-transparent - hover:opacity-100 group-hover/tab:opacity-100" - variant="ghost" - onClick={() => props.onTabClose(props.tab)} - /> </Tabs.Trigger> </div> </div> @@ -277,38 +285,40 @@ export default function Page() { <Tabs value={session.layout.tabs.active ?? "chat"} onChange={session.layout.openTab}> <div class="sticky top-0 shrink-0 flex"> <Tabs.List> - <Tabs.Trigger value="chat" class="flex gap-x-4 items-center"> - <div>Chat</div> - <Tooltip - value={`${new Intl.NumberFormat("en-US", { - notation: "compact", - compactDisplay: "short", - }).format(session.usage.tokens() ?? 0)} Tokens`} - class="flex items-center gap-1.5" - > - <ProgressCircle percentage={session.usage.context() ?? 0} /> - <div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div> - </Tooltip> + <Tabs.Trigger value="chat"> + <div class="flex gap-x-[17px] items-center"> + <div>Chat</div> + <Tooltip + value={`${new Intl.NumberFormat("en-US", { + notation: "compact", + compactDisplay: "short", + }).format(session.usage.tokens() ?? 0)} Tokens`} + class="flex items-center gap-1.5" + > + <ProgressCircle percentage={session.usage.context() ?? 0} /> + <div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div> + </Tooltip> + </div> </Tabs.Trigger> <Show when={local.layout.review.state() === "tab" && session.diffs().length}> - <Tabs.Trigger value="review" class="flex gap-3 items-center group/tab pr-1"> - <Show when={session.diffs()}> - <DiffChanges changes={session.diffs()} variant="bars" /> - </Show> - <div class="flex items-center gap-1.5"> - <div>Review</div> - <Show when={session.info()?.summary?.files}> - <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base"> - {session.info()?.summary?.files ?? 0} - </div> + <Tabs.Trigger + value="review" + closeButton={ + <IconButton icon="collapse" size="normal" variant="ghost" onClick={local.layout.review.pane} /> + } + > + <div class="flex items-center gap-3"> + <Show when={session.diffs()}> + <DiffChanges changes={session.diffs()} variant="bars" /> </Show> - <IconButton - icon="close" - class="mt-0.5 -ml-1 opacity-0 group-data-[selected]/tab:opacity-100 - hover:bg-transparent hover:opacity-100 group-hover/tab:opacity-100" - variant="ghost" - onClick={local.layout.review.close} - /> + <div class="flex items-center gap-1.5"> + <div>Review</div> + <Show when={session.info()?.summary?.files}> + <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base"> + {session.info()?.summary?.files ?? 0} + </div> + </Show> + </div> </div> </Tabs.Trigger> </Show> @@ -333,24 +343,17 @@ export default function Page() { <div classList={{ "w-full flex-1 min-h-0": true, - grid: local.layout.review.state() !== "open", - flex: local.layout.review.state() === "open", + grid: local.layout.review.state() === "tab", + flex: local.layout.review.state() === "pane", }} > - <div class="relative shrink-0 px-6 py-2 flex flex-col gap-6 flex-1 min-h-0 w-full max-w-xl mx-auto"> + <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"> <Switch> <Match when={session.id}> - <div class="h-8 flex shrink-0 self-stretch items-center justify-end"> - <Show when={local.layout.review.state() === "closed" && session.diffs().length}> - <Button icon="layout-right" onClick={local.layout.review.open}> - Review - </Button> - </Show> - </div> <div classList={{ "flex-1 min-h-0 pb-20": true, - "flex items-start justify-start": local.layout.review.state() === "open", + "flex items-start justify-start": local.layout.review.state() === "pane", }} > <Show when={session.messages.user().length > 1}> @@ -358,8 +361,8 @@ export default function Page() { role="list" classList={{ "mr-8 shrink-0 flex flex-col items-start": true, - "absolute right-full w-60 @7xl:gap-2": local.layout.review.state() !== "open", - "mt-1": local.layout.review.state() === "open", + "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", }} > <For each={session.messages.user()}> @@ -379,7 +382,7 @@ export default function Page() { <li classList={{ "group/li flex items-center self-stretch justify-end": true, - "@7xl:justify-start": local.layout.review.state() !== "open", + "@7xl:justify-start": local.layout.review.state() === "tab", }} > <Tooltip @@ -398,7 +401,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() !== "open", + "@7xl:hidden": local.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" /> @@ -407,7 +410,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() !== "open", + "@7xl:flex": local.layout.review.state() === "tab", }} onClick={handleClick} > @@ -477,7 +480,7 @@ export default function Page() { class="flex flex-col items-start self-stretch gap-8 pb-20" > {/* Title */} - <div class="flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-20 pb-1"> + <div class="flex items-center gap-2 self-stretch sticky top-0 bg-background-stronger z-20 h-8"> <div class="w-full text-14-medium text-text-strong"> <Show when={titled()} @@ -495,9 +498,7 @@ export default function Page() { </Show> </div> </div> - <div class="-mt-9"> - <Message message={message} parts={parts()} /> - </div> + <Message message={message} parts={parts()} /> {/* Summary */} <Show when={completed()}> <div class="w-full flex flex-col gap-6 items-start self-stretch"> @@ -524,7 +525,7 @@ export default function Page() { <For each={message.summary?.diffs ?? []}> {(diff) => ( <Accordion.Item value={diff.file}> - <StickyAccordionHeader class="top-10 data-expanded:before:-top-10 "> + <StickyAccordionHeader class="top-10 data-expanded:before:-top-10"> <Accordion.Trigger> <div class="flex items-center justify-between w-full gap-5"> <div class="grow flex items-center gap-5 min-w-0"> @@ -653,127 +654,25 @@ export default function Page() { /> </div> </div> - <Show when={local.layout.review.state() === "open"}> + <Show when={local.layout.review.state() === "pane" && session.diffs().length}> <div classList={{ - "relative grow px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0 border-l border-border-weak-base": true, + "relative grow px-6 py-3 flex-1 min-h-0 border-l border-border-weak-base": true, }} > - <div class="h-8 w-full flex items-center justify-between shrink-0 self-stretch"> - <div class="flex items-center gap-x-3"> - <Tooltip value="Close"> - <IconButton icon="align-right" variant="ghost" onClick={local.layout.review.close} /> - </Tooltip> - <Tooltip value="Open in tab"> - <IconButton - icon="expand" - variant="ghost" - onClick={() => { - local.layout.review.tab() - session.layout.setActiveTab("review") - }} - /> - </Tooltip> - </div> - </div> - <div class="text-14-medium text-text-strong">All changes</div> - <div class="h-full pb-40 overflow-y-auto no-scrollbar"> - <Accordion class="w-full" multiple> - <For each={session.diffs()}> - {(diff) => ( - <Accordion.Item value={diff.file} defaultOpen> - <StickyAccordionHeader> - <Accordion.Trigger> - <div class="flex items-center justify-between w-full gap-5"> - <div class="grow flex items-center gap-5 min-w-0"> - <FileIcon node={{ path: diff.file, type: "file" }} class="shrink-0 size-4" /> - <div class="flex grow min-w-0"> - <Show when={diff.file.includes("/")}> - <span class="text-text-base truncate-start"> - {getDirectory(diff.file)}‎ - </span> - </Show> - <span class="text-text-strong shrink-0">{getFilename(diff.file)}</span> - </div> - </div> - <div class="shrink-0 flex gap-4 items-center justify-end"> - <DiffChanges changes={diff} /> - <Icon name="chevron-grabber-vertical" size="small" /> - </div> - </div> - </Accordion.Trigger> - </StickyAccordionHeader> - <Accordion.Content> - <Diff - before={{ - name: diff.file!, - contents: diff.before!, - }} - after={{ - name: diff.file!, - contents: diff.after!, - }} - /> - </Accordion.Content> - </Accordion.Item> - )} - </For> - </Accordion> - </div> + <SessionReview /> </div> </Show> </div> </Tabs.Content> <Show when={local.layout.review.state() === "tab" && session.diffs().length}> - <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden mt-8"> + <Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden"> <div classList={{ - "relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0 overflow-hidden": true, + "relative px-6 py-3 flex-1 min-h-0 overflow-hidden": true, }} > - <div class="text-14-medium text-text-strong shrink-0">All changes</div> - <div class="flex-1 min-h-0 pb-40 overflow-y-auto no-scrollbar"> - <Accordion class="w-full" multiple> - <For each={session.diffs()}> - {(diff) => ( - <Accordion.Item value={diff.file} defaultOpen> - <StickyAccordionHeader> - <Accordion.Trigger> - <div class="flex items-center justify-between w-full gap-5"> - <div class="grow flex items-center gap-5 min-w-0"> - <FileIcon node={{ path: diff.file, type: "file" }} class="shrink-0 size-4" /> - <div class="flex grow min-w-0"> - <Show when={diff.file.includes("/")}> - <span class="text-text-base truncate-start">{getDirectory(diff.file)}‎</span> - </Show> - <span class="text-text-strong shrink-0">{getFilename(diff.file)}</span> - </div> - </div> - <div class="shrink-0 flex gap-4 items-center justify-end"> - <DiffChanges changes={diff} /> - <Icon name="chevron-grabber-vertical" size="small" /> - </div> - </div> - </Accordion.Trigger> - </StickyAccordionHeader> - <Accordion.Content> - <Diff - diffStyle="split" - before={{ - name: diff.file!, - contents: diff.before!, - }} - after={{ - name: diff.file!, - contents: diff.after!, - }} - /> - </Accordion.Content> - </Accordion.Item> - )} - </For> - </Accordion> - </div> + <SessionReview split hideExpand class="pb-40" /> </div> </Tabs.Content> </Show> @@ -828,7 +727,7 @@ export default function Page() { </DragOverlay> </DragDropProvider> <Show when={session.layout.tabs.active}> - <div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8"> + <div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-6"> <PromptInput ref={(el) => { inputRef = el @@ -895,18 +794,3 @@ export default function Page() { </div> ) } - -function StickyAccordionHeader(props: ParentProps<{ class?: string }>) { - return ( - <Accordion.Header - classList={{ - "sticky top-0 data-expanded:z-10": true, - "data-expanded:before:content-[''] data-expanded:before:z-[-10]": true, - "data-expanded:before:absolute data-expanded:before:inset-0 data-expanded:before:bg-background-stronger": true, - [props.class ?? ""]: !!props.class, - }} - > - {props.children} - </Accordion.Header> - ) -} |
