diff options
| author | Adam <[email protected]> | 2025-11-06 15:13:02 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-11-06 15:13:06 -0600 |
| commit | 96c57418f39bbf10e4538a6e0652baff7f0932fb (patch) | |
| tree | 7709020729579f1ec84ce230726504a810d01421 /packages/desktop/src/pages | |
| parent | b8c51e307f1e26c4cbc962fca742338fba3c9fe9 (diff) | |
| download | opencode-96c57418f39bbf10e4538a6e0652baff7f0932fb.tar.gz opencode-96c57418f39bbf10e4538a6e0652baff7f0932fb.zip | |
feat(desktop): review flow
Diffstat (limited to 'packages/desktop/src/pages')
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 4 | ||||
| -rw-r--r-- | packages/desktop/src/pages/session.tsx | 299 |
2 files changed, 252 insertions, 51 deletions
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index c5957a2d2..d88564007 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -80,7 +80,7 @@ export default function Layout(props: ParentProps) { }} </For> </nav> - <Show when={sync.data.more}> + <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()} @@ -95,7 +95,7 @@ export default function Layout(props: ParentProps) { as={"a"} href="https://opencode.ai/desktop-feedback" target="_blank" - class="hidden @[4rem]:flex w-full gap-2 text-12-medium text-text-base stroke-[1.5px]" + class="hidden @[4rem]:flex w-full text-12-medium text-text-base stroke-[1.5px]" variant="ghost" icon="speech-bubble" > diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx index 36eed718a..9f72191bc 100644 --- a/packages/desktop/src/pages/session.tsx +++ b/packages/desktop/src/pages/session.tsx @@ -13,6 +13,7 @@ import { Code, Tooltip, ProgressCircle, + Button, } from "@opencode-ai/ui" import { FileIcon } from "@/ui" import { MessageProgress } from "@/components/message-progress" @@ -221,9 +222,9 @@ export default function Page() { </Switch> <IconButton icon="close" - class="mt-0.5 opacity-0 text-text-muted/60 group-data-[selected]/tab:opacity-100 - group-data-[selected]/tab:text-text group-data-[selected]/tab:hover:bg-border-subtle - hover:opacity-100 group-hover/tab:opacity-100" + 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)} /> @@ -289,56 +290,101 @@ export default function Page() { <div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div> </Tooltip> </Tabs.Trigger> - {/* <Tabs.Trigger value="review">Review</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> + </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> + </Tabs.Trigger> + </Show> <SortableProvider ids={session.layout.tabs.opened ?? []}> <For each={session.layout.tabs.opened ?? []}> {(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={session.layout.closeTab} />} </For> </SortableProvider> <div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3"> - <IconButton - icon="plus-small" - variant="ghost" - iconSize="large" - onClick={() => setStore("fileSelectOpen", true)} - /> + <Tooltip value="Open file" class="flex items-center"> + <IconButton + icon="plus-small" + variant="ghost" + iconSize="large" + onClick={() => setStore("fileSelectOpen", true)} + /> + </Tooltip> </div> </Tabs.List> </div> <Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden"> - <div class="relative px-6 pt-12 max-w-2xl w-full mx-auto flex flex-col flex-1 min-h-0"> - <Show - when={session.id} - fallback={ - <div class="flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch"> - <div class="text-20-medium text-text-weaker">New session</div> - <div class="flex justify-center items-center gap-3"> - <Icon name="folder" size="small" /> - <div class="text-12-medium text-text-weak"> - {getDirectory(sync.data.path.directory)} - <span class="text-text-strong">{getFilename(sync.data.path.directory)}</span> - </div> + <Show + when={session.id} + fallback={ + <div class="size-full max-w-2xl mx-auto flex flex-col pb-45 px-6 justify-end items-start gap-4 flex-[1_0_0] self-stretch"> + <div class="text-20-medium text-text-weaker">New session</div> + <div class="flex justify-center items-center gap-3"> + <Icon name="folder" size="small" /> + <div class="text-12-medium text-text-weak"> + {getDirectory(sync.data.path.directory)} + <span class="text-text-strong">{getFilename(sync.data.path.directory)}</span> </div> - <div class="flex justify-center items-center gap-3"> - <Icon name="pencil-line" size="small" /> - <div class="text-12-medium text-text-weak"> - Last modified - <span class="text-text-strong"> - {DateTime.fromMillis(sync.data.project.time.created).toRelative()} - </span> - </div> + </div> + <div class="flex justify-center items-center gap-3"> + <Icon name="pencil-line" size="small" /> + <div class="text-12-medium text-text-weak"> + Last modified + <span class="text-text-strong"> + {DateTime.fromMillis(sync.data.project.time.created).toRelative()} + </span> </div> </div> - } - > - {(_) => { - return ( - <div class="pt-3 flex flex-col flex-1 min-h-0"> - <div class="flex-1 min-h-0"> + </div> + } + > + {(_) => { + return ( + <div + classList={{ + "w-full grid flex-1 _gap-6 min-h-0": true, + "grid-cols-2": local.layout.review.state() === "open", + "max-w-2xl mx-auto": local.layout.review.state() !== "open", + }} + > + <div class="relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0"> + <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": true, + "flex items-start justify-start": local.layout.review.state() === "open", + }} + > <Show when={session.messages.user().length > 1}> <ul role="list" - class="absolute right-full mr-8 hidden w-60 shrink-0 @7xl:flex flex-col items-start gap-1" + classList={{ + "mr-8 shrink-0 flex flex-col items-start": true, + "absolute right-full w-60 @7xl:gap-2": local.layout.review.state() !== "open", + "": local.layout.review.state() === "open", + }} > <For each={session.messages.user()}> {(message) => { @@ -351,11 +397,43 @@ export default function Page() { const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error) const working = createMemo(() => !message.summary?.body && !error()) + const handleClick = () => session.messages.setActive(message.id) + return ( - <li class="group/li flex items-center self-stretch"> + <li + classList={{ + "group/li flex items-center self-stretch justify-end": true, + "@7xl:justify-start": local.layout.review.state() !== "open", + }} + > + <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": local.layout.review.state() !== "open", + }} + > + <div class="h-px w-5 bg-icon-base group-hover/tick:w-full group-hover/tick:bg-icon-strong-base" /> + </button> + </Tooltip> <button - class="flex items-center self-stretch w-full gap-x-2 py-1 cursor-default" - onClick={() => session.messages.setActive(message.id)} + classList={{ + "hidden items-center self-stretch w-full gap-x-2 cursor-default": true, + "@7xl:flex": local.layout.review.state() !== "open", + }} + onClick={handleClick} > <Switch> <Match when={working()}> @@ -383,7 +461,7 @@ export default function Page() { </For> </ul> </Show> - <div ref={messageScrollElement} class="grow min-w-0 h-full overflow-y-auto no-scrollbar"> + <div ref={messageScrollElement} class="grow w-full min-w-0 h-full overflow-y-auto no-scrollbar"> <For each={session.messages.user()}> {(message) => { const isActive = createMemo(() => session.messages.active()?.id === message.id) @@ -423,7 +501,7 @@ export default function Page() { class="flex flex-col items-start self-stretch gap-8 pb-50" > {/* Title */} - <div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10"> + <div class="flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10 pb-1"> <div class="w-full text-14-medium text-text-strong"> <Show when={titled()} @@ -441,7 +519,7 @@ export default function Page() { </Show> </div> </div> - <div class="-mt-8"> + <div class="-mt-9"> <Message message={message} parts={parts()} /> </div> {/* Summary */} @@ -496,7 +574,7 @@ export default function Page() { </div> </Accordion.Trigger> </Accordion.Header> - <Accordion.Content class="max-h-[300px] overflow-y-auto no-scrollbar"> + <Accordion.Content class="max-h-[360px] overflow-y-auto no-scrollbar"> <Diff before={{ name: diff.file!, @@ -569,12 +647,135 @@ export default function Page() { </div> </div> </div> - ) - }} - </Show> - </div> + <Show when={local.layout.review.state() === "open"}> + <div + classList={{ + "relative px-6 py-2 w-full flex flex-col gap-6 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> + <Accordion.Header> + <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> + </Accordion.Header> + <Accordion.Content> + <Diff + before={{ + name: diff.file!, + contents: diff.before!, + }} + after={{ + name: diff.file!, + contents: diff.after!, + }} + /> + </Accordion.Content> + </Accordion.Item> + )} + </For> + </Accordion> + </div> + </div> + </Show> + </div> + ) + }} + </Show> </Tabs.Content> - {/* <Tabs.Content value="review" class="select-text"></Tabs.Content> */} + <Show when={local.layout.review.state() === "tab" && session.diffs().length}> + <Tabs.Content value="review" class="select-text"> + <div + classList={{ + "relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0": true, + }} + > + <div class="h-8 w-full flex items-center justify-between shrink-0 self-stretch"> + <div class="flex items-center gap-x-3"></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> + <Accordion.Header> + <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> + </Accordion.Header> + <Accordion.Content> + <Diff + before={{ + name: diff.file!, + contents: diff.before!, + }} + after={{ + name: diff.file!, + contents: diff.after!, + }} + /> + </Accordion.Content> + </Accordion.Item> + )} + </For> + </Accordion> + </div> + </div> + </Tabs.Content> + </Show> <For each={session.layout.tabs.opened}> {(tab) => { const [file] = createResource( |
