summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorRahul A Mistry <[email protected]>2026-02-07 16:32:40 +0530
committerGitHub <[email protected]>2026-02-07 05:02:40 -0600
commitb5b93aea425d44a0f49d37eb22d29b98616b7392 (patch)
treeab79d1e5decfb854b661fadfa302ee8a54936d14 /packages
parent4abf8049c99b6adc0f130e28d0126f59b3869e49 (diff)
downloadopencode-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.tsx6
-rw-r--r--packages/app/src/components/settings-keybinds.tsx2
-rw-r--r--packages/app/src/pages/session.tsx31
-rw-r--r--packages/app/src/pages/session/session-side-panel.tsx295
-rw-r--r--packages/app/src/pages/session/use-session-commands.tsx7
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",