diff options
| author | Rahul A Mistry <[email protected]> | 2026-02-07 16:32:40 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-07 05:02:40 -0600 |
| commit | b5b93aea425d44a0f49d37eb22d29b98616b7392 (patch) | |
| tree | ab79d1e5decfb854b661fadfa302ee8a54936d14 /packages | |
| parent | 4abf8049c99b6adc0f130e28d0126f59b3869e49 (diff) | |
| download | opencode-b5b93aea425d44a0f49d37eb22d29b98616b7392.tar.gz opencode-b5b93aea425d44a0f49d37eb22d29b98616b7392.zip | |
fix(app): toggle file tree and review panel better ux (#12481)
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/app/src/components/session/session-header.tsx | 6 | ||||
| -rw-r--r-- | packages/app/src/components/settings-keybinds.tsx | 2 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 31 | ||||
| -rw-r--r-- | packages/app/src/pages/session/session-side-panel.tsx | 295 | ||||
| -rw-r--r-- | packages/app/src/pages/session/use-session-commands.tsx | 7 |
5 files changed, 180 insertions, 161 deletions
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 805e69931..7eaafc854 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -544,11 +544,7 @@ export function SessionHeader() { <Button variant="ghost" class="group/file-tree-toggle size-6 p-0" - onClick={() => { - const opening = !layout.fileTree.opened() - if (opening && !view().reviewPanel.opened()) view().reviewPanel.open() - layout.fileTree.toggle() - }} + onClick={() => layout.fileTree.toggle()} aria-label={language.t("command.fileTree.toggle")} aria-expanded={layout.fileTree.opened()} aria-controls="file-tree-panel" diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index a24db13f5..79e000f37 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -44,7 +44,7 @@ function groupFor(id: string): KeybindGroup { if (id === PALETTE_ID) return "General" if (id.startsWith("terminal.")) return "Terminal" if (id.startsWith("model.") || id.startsWith("agent.") || id.startsWith("mcp.")) return "Model and agent" - if (id.startsWith("file.")) return "Navigation" + if (id.startsWith("file.") || id.startsWith("fileTree.")) return "Navigation" if (id.startsWith("prompt.")) return "Prompt" if ( id.startsWith("session.") || diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index a70d4e8a2..31f9e3fb7 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -233,7 +233,15 @@ export default function Page() { } const isDesktop = createMediaQuery("(min-width: 768px)") - const centered = createMemo(() => isDesktop() && !view().reviewPanel.opened()) + const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) + const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) + const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen()) + const sessionPanelWidth = createMemo(() => { + if (!desktopSidePanelOpen()) return "100%" + if (desktopReviewOpen()) return `${layout.session.width()}px` + return `calc(100% - ${layout.fileTree.width()}px)` + }) + const centered = createMemo(() => isDesktop() && !desktopSidePanelOpen()) function normalizeTab(tab: string) { if (!tab.startsWith("file://")) return tab @@ -252,12 +260,18 @@ export default function Page() { return next } + const openReviewPanel = () => { + if (!view().reviewPanel.opened()) view().reviewPanel.open() + } + const openTab = (value: string) => { const next = normalizeTab(value) tabs().open(next) const path = file.pathFromTab(next) - if (path) file.load(path) + if (!path) return + file.load(path) + openReviewPanel() } createEffect(() => { @@ -1085,6 +1099,7 @@ export default function Page() { } const focusReviewDiff = (path: string) => { + openReviewPanel() const current = view().review.open() ?? [] if (!current.includes(path)) view().review.setOpen([...current, path]) setTree({ activeDiff: path, pendingDiff: path }) @@ -1203,7 +1218,7 @@ export default function Page() { if (!id) return const wants = isDesktop() - ? view().reviewPanel.opened() && (layout.fileTree.opened() || activeTab() === "review") + ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review") : store.mobileTab === "changes" if (!wants) return if (sync.data.session_diff[id] !== undefined) return @@ -1216,7 +1231,6 @@ export default function Page() { createEffect(() => { const dir = sdk.directory if (!isDesktop()) return - if (!view().reviewPanel.opened()) return if (!layout.fileTree.opened()) return if (sync.status === "loading") return @@ -1533,10 +1547,10 @@ export default function Page() { classList={{ "@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true, "flex-1 pt-2 md:pt-3": true, - "md:flex-none": view().reviewPanel.opened(), + "md:flex-none": desktopSidePanelOpen(), }} style={{ - width: isDesktop() && view().reviewPanel.opened() ? `${layout.session.width()}px` : "100%", + width: sessionPanelWidth(), "--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined, }} > @@ -1663,7 +1677,7 @@ export default function Page() { setPromptDockRef={(el) => (promptDock = el)} /> - <Show when={isDesktop() && view().reviewPanel.opened()}> + <Show when={desktopReviewOpen()}> <ResizeHandle direction="horizontal" size={layout.session.width()} @@ -1675,7 +1689,8 @@ export default function Page() { </div> <SessionSidePanel - open={isDesktop() && view().reviewPanel.opened()} + open={desktopSidePanelOpen()} + reviewOpen={desktopReviewOpen()} language={language} layout={layout} command={command} diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 573680dec..1048e17d3 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -24,6 +24,7 @@ import { useSync } from "@/context/sync" export function SessionSidePanel(props: { open: boolean + reviewOpen: boolean language: ReturnType<typeof useLanguage> layout: ReturnType<typeof useLayout> command: ReturnType<typeof useCommand> @@ -72,157 +73,164 @@ export function SessionSidePanel(props: { <aside id="review-panel" aria-label={props.language.t("session.panel.reviewAndFiles")} - class="relative flex-1 min-w-0 h-full border-l border-border-weak-base flex" + class="relative min-w-0 h-full border-l border-border-weak-base flex" + classList={{ + "flex-1": props.reviewOpen, + "shrink-0": !props.reviewOpen, + }} + style={{ width: props.reviewOpen ? undefined : `${props.layout.fileTree.width()}px` }} > - <div class="flex-1 min-w-0 h-full"> - <Show - when={props.layout.fileTree.opened() && props.fileTreeTab() === "changes"} - fallback={ - <DragDropProvider - onDragStart={props.onDragStart} - onDragEnd={props.onDragEnd} - onDragOver={props.onDragOver} - collisionDetector={closestCenter} - > - <DragDropSensors /> - <ConstrainDragYAxis /> - <Tabs value={props.activeTab()} onChange={props.openTab}> - <div class="sticky top-0 shrink-0 flex"> - <Tabs.List - ref={(el: HTMLDivElement) => { - const stop = createFileTabListSync({ el, contextOpen: props.contextOpen }) - onCleanup(stop) - }} - > - <Show when={props.reviewTab}> - <Tabs.Trigger value="review" classes={{ button: "!pl-6" }}> - <div class="flex items-center gap-1.5"> - <div>{props.language.t("session.tab.review")}</div> - <Show when={props.hasReview}> - <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base"> - {props.reviewCount} - </div> - </Show> - </div> - </Tabs.Trigger> - </Show> - <Show when={props.contextOpen()}> - <Tabs.Trigger - value="context" - closeButton={ - <Tooltip value={props.language.t("common.closeTab")} placement="bottom"> - <IconButton - icon="close-small" - variant="ghost" - class="h-5 w-5" - onClick={() => props.tabs().close("context")} - aria-label={props.language.t("common.closeTab")} - /> - </Tooltip> - } - hideCloseButton - onMiddleClick={() => props.tabs().close("context")} - > - <div class="flex items-center gap-2"> - <SessionContextUsage variant="indicator" /> - <div>{props.language.t("session.tab.context")}</div> - </div> - </Tabs.Trigger> - </Show> - <SortableProvider ids={props.openedTabs()}> - <For each={props.openedTabs()}> - {(tab) => <SortableTab tab={tab} onTabClose={props.tabs().close} />} - </For> - </SortableProvider> - <StickyAddButton> - <TooltipKeybind - title={props.language.t("command.file.open")} - keybind={props.command.keybind("file.open")} - class="flex items-center" - > - <IconButton - icon="plus-small" - variant="ghost" - iconSize="large" - onClick={() => - props.dialog.show(() => <DialogSelectFile mode="files" onOpenFile={props.showAllFiles} />) + <Show when={props.reviewOpen}> + <div class="flex-1 min-w-0 h-full"> + <Show + when={props.layout.fileTree.opened() && props.fileTreeTab() === "changes"} + fallback={ + <DragDropProvider + onDragStart={props.onDragStart} + onDragEnd={props.onDragEnd} + onDragOver={props.onDragOver} + collisionDetector={closestCenter} + > + <DragDropSensors /> + <ConstrainDragYAxis /> + <Tabs value={props.activeTab()} onChange={props.openTab}> + <div class="sticky top-0 shrink-0 flex"> + <Tabs.List + ref={(el: HTMLDivElement) => { + const stop = createFileTabListSync({ el, contextOpen: props.contextOpen }) + onCleanup(stop) + }} + > + <Show when={props.reviewTab}> + <Tabs.Trigger value="review" classes={{ button: "!pl-6" }}> + <div class="flex items-center gap-1.5"> + <div>{props.language.t("session.tab.review")}</div> + <Show when={props.hasReview}> + <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base"> + {props.reviewCount} + </div> + </Show> + </div> + </Tabs.Trigger> + </Show> + <Show when={props.contextOpen()}> + <Tabs.Trigger + value="context" + closeButton={ + <Tooltip value={props.language.t("common.closeTab")} placement="bottom"> + <IconButton + icon="close-small" + variant="ghost" + class="h-5 w-5" + onClick={() => props.tabs().close("context")} + aria-label={props.language.t("common.closeTab")} + /> + </Tooltip> } - aria-label={props.language.t("command.file.open")} - /> - </TooltipKeybind> - </StickyAddButton> - </Tabs.List> - </div> + hideCloseButton + onMiddleClick={() => props.tabs().close("context")} + > + <div class="flex items-center gap-2"> + <SessionContextUsage variant="indicator" /> + <div>{props.language.t("session.tab.context")}</div> + </div> + </Tabs.Trigger> + </Show> + <SortableProvider ids={props.openedTabs()}> + <For each={props.openedTabs()}> + {(tab) => <SortableTab tab={tab} onTabClose={props.tabs().close} />} + </For> + </SortableProvider> + <StickyAddButton> + <TooltipKeybind + title={props.language.t("command.file.open")} + keybind={props.command.keybind("file.open")} + class="flex items-center" + > + <IconButton + icon="plus-small" + variant="ghost" + iconSize="large" + onClick={() => + props.dialog.show(() => <DialogSelectFile mode="files" onOpenFile={props.showAllFiles} />) + } + aria-label={props.language.t("command.file.open")} + /> + </TooltipKeybind> + </StickyAddButton> + </Tabs.List> + </div> - <Show when={props.reviewTab}> - <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict"> - <Show when={props.activeTab() === "review"}>{props.reviewPanel()}</Show> - </Tabs.Content> - </Show> - - <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict"> - <Show when={props.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"> - {props.language.t("session.files.selectToOpen")} - </div> - </div> - </div> + <Show when={props.reviewTab}> + <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict"> + <Show when={props.activeTab() === "review"}>{props.reviewPanel()}</Show> + </Tabs.Content> </Show> - </Tabs.Content> - <Show when={props.contextOpen()}> - <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict"> - <Show when={props.activeTab() === "context"}> + <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict"> + <Show when={props.activeTab() === "empty"}> <div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> - <SessionContextTab - messages={props.messages as never} - visibleUserMessages={props.visibleUserMessages as never} - view={props.view as never} - info={props.info as never} - /> + <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"> + {props.language.t("session.files.selectToOpen")} + </div> + </div> </div> </Show> </Tabs.Content> - </Show> - <Show when={props.activeFileTab()} keyed> - {(tab) => ( - <FileTabContent - tab={tab} - activeTab={props.activeTab} - tabs={props.tabs} - view={props.view} - handoffFiles={props.handoffFiles} - file={props.file} - comments={props.comments} - language={props.language} - codeComponent={props.codeComponent} - addCommentToContext={props.addCommentToContext} - /> - )} - </Show> - </Tabs> - <DragOverlay> - <Show when={props.activeDraggable()}> - {(tab) => { - const path = createMemo(() => props.file.pathFromTab(tab())) - return ( - <div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent"> - <Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show> - </div> - ) - }} - </Show> - </DragOverlay> - </DragDropProvider> - } - > - {props.reviewPanel()} - </Show> - </div> + <Show when={props.contextOpen()}> + <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict"> + <Show when={props.activeTab() === "context"}> + <div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> + <SessionContextTab + messages={props.messages as never} + visibleUserMessages={props.visibleUserMessages as never} + view={props.view as never} + info={props.info as never} + /> + </div> + </Show> + </Tabs.Content> + </Show> + + <Show when={props.activeFileTab()} keyed> + {(tab) => ( + <FileTabContent + tab={tab} + activeTab={props.activeTab} + tabs={props.tabs} + view={props.view} + handoffFiles={props.handoffFiles} + file={props.file} + comments={props.comments} + language={props.language} + codeComponent={props.codeComponent} + addCommentToContext={props.addCommentToContext} + /> + )} + </Show> + </Tabs> + <DragOverlay> + <Show when={props.activeDraggable()}> + {(tab) => { + const path = createMemo(() => props.file.pathFromTab(tab())) + return ( + <div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent"> + <Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show> + </div> + ) + }} + </Show> + </DragOverlay> + </DragDropProvider> + } + > + {props.reviewPanel()} + </Show> + </div> + </Show> <Show when={props.layout.fileTree.opened()}> <div @@ -230,7 +238,10 @@ export function SessionSidePanel(props: { class="relative shrink-0 h-full" style={{ width: `${props.layout.fileTree.width()}px` }} > - <div class="h-full border-l border-border-weak-base flex flex-col overflow-hidden group/filetree"> + <div + class="h-full flex flex-col overflow-hidden group/filetree" + classList={{ "border-l border-border-weak-base": props.reviewOpen }} + > <Tabs variant="pill" value={props.fileTreeTab()} diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index ae845a657..d50401d3f 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -139,11 +139,8 @@ export const useSessionCommands = (input: { title: input.language.t("command.fileTree.toggle"), description: "", category: input.language.t("command.category.view"), - onSelect: () => { - const opening = !input.layout.fileTree.opened() - if (opening && !input.view().reviewPanel.opened()) input.view().reviewPanel.open() - input.layout.fileTree.toggle() - }, + keybind: "mod+\\", + onSelect: () => input.layout.fileTree.toggle(), }, { id: "terminal.new", |
