diff options
| author | adamelmore <[email protected]> | 2026-01-27 11:34:30 -0600 |
|---|---|---|
| committer | adamelmore <[email protected]> | 2026-01-27 11:40:39 -0600 |
| commit | 7de42ca242af2045fdfc4bbbe1ada33069d2769e (patch) | |
| tree | 2df894cba88a9fb6c098853b8f0e9d0c7fa64c6e /packages/app/src/pages | |
| parent | e2c57735b43d4bbca3b8de1ea0d2fe706f3e2799 (diff) | |
| download | opencode-7de42ca242af2045fdfc4bbbe1ada33069d2769e.tar.gz opencode-7de42ca242af2045fdfc4bbbe1ada33069d2769e.zip | |
feat(app): improved layout
Diffstat (limited to 'packages/app/src/pages')
| -rw-r--r-- | packages/app/src/pages/session.tsx | 314 |
1 files changed, 104 insertions, 210 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index a75b1169a..146450293 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -23,7 +23,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { DiffChanges } from "@opencode-ai/ui/diff-changes" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Tabs } from "@opencode-ai/ui/tabs" import { useCodeComponent } from "@opencode-ai/ui/context/code" @@ -433,7 +432,7 @@ export default function Page() { expanded: {} as Record<string, boolean>, messageId: undefined as string | undefined, turnStart: 0, - mobileTab: "session" as "session" | "review", + mobileTab: "session" as "session" | "changes", newSessionWorktree: "main", promptHeight: 0, }) @@ -693,12 +692,12 @@ export default function Page() { onSelect: () => view().terminal.toggle(), }, { - id: "review.toggle", - title: language.t("command.review.toggle"), + id: "fileTree.toggle", + title: language.t("command.fileTree.toggle"), description: "", category: language.t("command.category.view"), keybind: "mod+shift+r", - onSelect: () => view().reviewPanel.toggle(), + onSelect: () => layout.fileTree.toggle(), }, { id: "terminal.new", @@ -822,7 +821,7 @@ export default function Page() { const sessionID = params.id if (!sessionID) return if (status()?.type !== "idle") { - await sdk.client.session.abort({ sessionID }).catch(() => {}) + await sdk.client.session.abort({ sessionID }).catch(() => { }) } const revert = info()?.revert?.messageID // Find the last user message that's not already reverted @@ -905,69 +904,69 @@ export default function Page() { }, ...(sync.data.config.share !== "disabled" ? [ - { - id: "session.share", - title: language.t("command.session.share"), - description: language.t("command.session.share.description"), - category: language.t("command.category.session"), - slash: "share", - disabled: !params.id || !!info()?.share?.url, - onSelect: async () => { - if (!params.id) return - await sdk.client.session - .share({ sessionID: params.id }) - .then((res) => { - navigator.clipboard.writeText(res.data!.share!.url).catch(() => - showToast({ - title: language.t("toast.session.share.copyFailed.title"), - variant: "error", - }), - ) - }) - .then(() => + { + id: "session.share", + title: language.t("command.session.share"), + description: language.t("command.session.share.description"), + category: language.t("command.category.session"), + slash: "share", + disabled: !params.id || !!info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .share({ sessionID: params.id }) + .then((res) => { + navigator.clipboard.writeText(res.data!.share!.url).catch(() => showToast({ - title: language.t("toast.session.share.success.title"), - description: language.t("toast.session.share.success.description"), - variant: "success", - }), - ) - .catch(() => - showToast({ - title: language.t("toast.session.share.failed.title"), - description: language.t("toast.session.share.failed.description"), + title: language.t("toast.session.share.copyFailed.title"), variant: "error", }), ) - }, + }) + .then(() => + showToast({ + title: language.t("toast.session.share.success.title"), + description: language.t("toast.session.share.success.description"), + variant: "success", + }), + ) + .catch(() => + showToast({ + title: language.t("toast.session.share.failed.title"), + description: language.t("toast.session.share.failed.description"), + variant: "error", + }), + ) }, - { - id: "session.unshare", - title: language.t("command.session.unshare"), - description: language.t("command.session.unshare.description"), - category: language.t("command.category.session"), - slash: "unshare", - disabled: !params.id || !info()?.share?.url, - onSelect: async () => { - if (!params.id) return - await sdk.client.session - .unshare({ sessionID: params.id }) - .then(() => - showToast({ - title: language.t("toast.session.unshare.success.title"), - description: language.t("toast.session.unshare.success.description"), - variant: "success", - }), - ) - .catch(() => - showToast({ - title: language.t("toast.session.unshare.failed.title"), - description: language.t("toast.session.unshare.failed.description"), - variant: "error", - }), - ) - }, + }, + { + id: "session.unshare", + title: language.t("command.session.unshare"), + description: language.t("command.session.unshare.description"), + category: language.t("command.category.session"), + slash: "unshare", + disabled: !params.id || !info()?.share?.url, + onSelect: async () => { + if (!params.id) return + await sdk.client.session + .unshare({ sessionID: params.id }) + .then(() => + showToast({ + title: language.t("toast.session.unshare.success.title"), + description: language.t("toast.session.unshare.success.description"), + variant: "success", + }), + ) + .catch(() => + showToast({ + title: language.t("toast.session.unshare.failed.title"), + description: language.t("toast.session.unshare.failed.description"), + variant: "error", + }), + ) }, - ] + }, + ] : []), ]) @@ -1067,40 +1066,31 @@ export default function Page() { .filter((tab) => tab !== "context"), ) - const mobileReview = createMemo(() => !isDesktop() && view().reviewPanel.opened() && store.mobileTab === "review") + const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") - const showTabs = createMemo(() => view().reviewPanel.opened()) + const fileTreeTab = () => layout.fileTree.tab() + const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value) const [tree, setTree] = createStore({ - fileTreeTab: "changes" as "changes" | "all", reviewScroll: undefined as HTMLDivElement | undefined, pendingDiff: undefined as string | undefined, }) - const fileTreeTab = () => tree.fileTreeTab - const setFileTreeTab = (value: "changes" | "all") => setTree("fileTreeTab", value) const reviewScroll = () => tree.reviewScroll const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value) const pendingDiff = () => tree.pendingDiff const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value) const showAllFiles = () => { - if (!layout.fileTree.opened()) return if (fileTreeTab() !== "changes") return setFileTreeTab("all") } - createEffect(() => { - if (!layout.fileTree.opened()) return - setFileTreeTab("changes") - }) - createEffect( on( () => tabs().active(), (active) => { if (!active) return - if (!layout.fileTree.opened()) return if (fileTreeTab() !== "changes") return if (!file.pathFromTab(active)) return showAllFiles() @@ -1197,49 +1187,30 @@ export default function Page() { const activeTab = createMemo(() => { const active = tabs().active() - if (layout.fileTree.opened() && fileTreeTab() === "all") { - if (active && active !== "review" && active !== "context") return normalizeTab(active) - - const first = openedTabs()[0] - if (first) return first - return "review" - } - if (active) return normalizeTab(active) - if (hasReview()) return "review" + if (active === "context") return "context" + if (active && file.pathFromTab(active)) return normalizeTab(active) const first = openedTabs()[0] if (first) return first if (contextOpen()) return "context" - return "review" + return "empty" }) createEffect(() => { if (!layout.ready()) return if (tabs().active()) return - if (!hasReview() && openedTabs().length === 0 && !contextOpen()) return - tabs().setActive(activeTab()) - }) - - createEffect(() => { - if (!layout.fileTree.opened()) return - if (fileTreeTab() !== "all") return - - const first = openedTabs()[0] - if (!first) return + if (openedTabs().length === 0 && !contextOpen()) return - const active = tabs().active() - if (active && active !== "review" && active !== "context") return - tabs().setActive(first) + const next = activeTab() + if (next === "empty") return + tabs().setActive(next) }) createEffect(() => { const id = params.id if (!id) return - const wants = isDesktop() - ? view().reviewPanel.opened() && - (layout.fileTree.opened() ? fileTreeTab() === "changes" : activeTab() === "review") - : view().reviewPanel.opened() && store.mobileTab === "review" + const wants = isDesktop() ? fileTreeTab() === "changes" : store.mobileTab === "changes" if (!wants) return if (sync.data.session_diff[id] !== undefined) return @@ -1654,8 +1625,8 @@ export default function Page() { <div class="relative bg-background-base size-full overflow-hidden flex flex-col"> <SessionHeader /> <div class="flex-1 min-h-0 flex flex-col md:flex-row"> - {/* Mobile tab bar - only shown on mobile when user opened review */} - <Show when={!isDesktop() && view().reviewPanel.opened()}> + {/* Mobile tab bar */} + <Show when={!isDesktop() && params.id}> <Tabs class="h-auto"> <Tabs.List> <Tabs.Trigger @@ -1667,16 +1638,16 @@ export default function Page() { {language.t("session.tab.session")} </Tabs.Trigger> <Tabs.Trigger - value="review" + value="changes" class="w-1/2 !border-r-0" classes={{ button: "w-full" }} - onClick={() => setStore("mobileTab", "review")} + onClick={() => setStore("mobileTab", "changes")} > <Switch> <Match when={hasReview()}> {language.t("session.review.filesChanged", { count: reviewCount() })} </Match> - <Match when={true}>{language.t("session.tab.review")}</Match> + <Match when={true}>{language.t("session.review.change.other")}</Match> </Switch> </Tabs.Trigger> </Tabs.List> @@ -1690,7 +1661,7 @@ export default function Page() { "flex-1 md:flex-none pt-6 md:pt-3": true, }} style={{ - width: isDesktop() && showTabs() ? `${layout.session.width()}px` : "100%", + width: isDesktop() ? `${layout.session.width()}px` : "100%", "--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined, }} > @@ -1699,7 +1670,7 @@ export default function Page() { <Match when={params.id}> <Show when={activeMessage()}> <Show - when={!mobileReview()} + when={!mobileChanges()} fallback={ <div class="relative h-full overflow-hidden"> <Switch> @@ -1789,7 +1760,6 @@ export default function Page() { "sticky top-0 z-30 bg-background-stronger": true, "w-full": true, "px-4 md:px-6": true, - "md:max-w-200 md:mx-auto": !showTabs(), }} > <div class="h-10 flex items-center gap-1"> @@ -1814,13 +1784,7 @@ export default function Page() { <div ref={autoScroll.contentRef} role="log" - class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]" - classList={{ - "w-full": true, - "md:max-w-200 md:mx-auto": !showTabs(), - "mt-0.5": !showTabs(), - "mt-0": showTabs(), - }} + class="flex flex-col gap-32 items-start justify-start w-full mt-0 pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]" > <Show when={store.turnStart > 0}> <div class="w-full flex justify-center"> @@ -1868,10 +1832,7 @@ export default function Page() { <div id={anchor(message.id)} data-message-id={message.id} - classList={{ - "min-w-0 w-full max-w-full": true, - "md:max-w-200": !showTabs(), - }} + class="min-w-0 w-full max-w-full" > <SessionTurn sessionID={params.id!} @@ -1924,12 +1885,7 @@ export default function Page() { ref={(el) => (promptDock = el)} class="absolute inset-x-0 bottom-0 pt-12 pb-4 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none" > - <div - classList={{ - "w-full px-4 pointer-events-auto": true, - "md:max-w-200": !showTabs(), - }} - > + <div class="w-full px-4 pointer-events-auto"> <Show when={request()} keyed> {(perm) => ( <div data-component="tool-part-wrapper" data-permission="true" class="mb-3"> @@ -2000,7 +1956,7 @@ export default function Page() { </div> </div> - <Show when={isDesktop() && showTabs()}> + <Show when={isDesktop()}> <ResizeHandle direction="horizontal" size={layout.session.width()} @@ -2011,8 +1967,8 @@ export default function Page() { </Show> </div> - {/* Desktop tabs panel (Review + Context + Files) - hidden on mobile */} - <Show when={isDesktop() && showTabs()}> + {/* Desktop side panel - hidden on mobile */} + <Show when={isDesktop()}> <aside id="review-panel" aria-label={language.t("session.panel.reviewAndFiles")} @@ -2020,7 +1976,7 @@ export default function Page() { > <div class="flex-1 min-w-0 h-full"> <Show - when={layout.fileTree.opened() && fileTreeTab() === "changes"} + when={fileTreeTab() === "changes"} fallback={ <DragDropProvider onDragStart={handleDragStart} @@ -2033,24 +1989,7 @@ export default function Page() { <Tabs value={activeTab()} onChange={openTab}> <div class="sticky top-0 shrink-0 flex"> <Tabs.List> - <Show when={!layout.fileTree.opened()}> - <Tabs.Trigger value="review" classes={{ button: "!pl-6" }}> - <div class="flex items-center gap-3"> - <Show when={diffs()}> - <DiffChanges changes={diffs()} variant="bars" /> - </Show> - <div class="flex items-center gap-1.5"> - <div>{language.t("session.tab.review")}</div> - <Show when={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"> - {info()?.summary?.files ?? 0} - </div> - </Show> - </div> - </div> - </Tabs.Trigger> - </Show> - <Show when={!layout.fileTree.opened() && contextOpen()}> + <Show when={contextOpen()}> <Tabs.Trigger value="context" closeButton={ @@ -2097,69 +2036,20 @@ export default function Page() { </StickyAddButton> </Tabs.List> </div> - <Show when={!layout.fileTree.opened()}> - <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict"> - <Show when={activeTab() === "review"}> - <div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> - <Switch> - <Match when={hasReview()}> - <Show - when={diffsReady()} - fallback={ - <div class="px-6 py-4 text-text-weak"> - {language.t("session.review.loadingChanges")} - </div> - } - > - <SessionReviewTab - diffs={diffs} - view={view} - diffStyle={layout.review.diffStyle()} - onDiffStyleChange={layout.review.setDiffStyle} - onScrollRef={setReviewScroll} - onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })} - comments={comments.all()} - focusedComment={comments.focus()} - onFocusedCommentChange={comments.setFocus} - onViewFile={(path) => { - showAllFiles() - const value = file.tab(path) - tabs().open(value) - file.load(path) - }} - /> - </Show> - </Match> - <Match when={true}> - <div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6"> - <Mark class="w-14 opacity-10" /> - <div class="text-14-regular text-text-weak max-w-56"> - {language.t("session.review.empty")} - </div> - </div> - </Match> - </Switch> - </div> - </Show> - </Tabs.Content> - </Show> - - <Show when={layout.fileTree.opened() && fileTreeTab() === "all" && openedTabs().length === 0}> - <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict"> - <Show when={activeTab() === "review"}> - <div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> - <div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6"> - <Mark class="w-14 opacity-10" /> - <div class="text-14-regular text-text-weak max-w-56"> - {language.t("session.files.selectToOpen")} - </div> + <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict"> + <Show when={activeTab() === "empty"}> + <div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> + <div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6"> + <Mark class="w-14 opacity-10" /> + <div class="text-14-regular text-text-weak max-w-56"> + {language.t("session.files.selectToOpen")} </div> </div> - </Show> - </Tabs.Content> - </Show> + </div> + </Show> + </Tabs.Content> - <Show when={!layout.fileTree.opened() && contextOpen()}> + <Show when={contextOpen()}> <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict"> <Show when={activeTab() === "context"}> <div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> @@ -2725,7 +2615,11 @@ export default function Page() { </div> <Show when={layout.fileTree.opened()}> - <div class="relative shrink-0 h-full" style={{ width: `${layout.fileTree.width()}px` }}> + <div + id="file-tree-panel" + class="relative shrink-0 h-full" + style={{ width: `${layout.fileTree.width()}px` }} + > <div class="h-full border-l border-border-weak-base flex flex-col overflow-hidden group/filetree"> <Tabs variant="pill" |
