fix(app): toggle file tree and review panel better ux (#12481)

This commit is contained in:
Rahul A Mistry
2026-02-07 16:32:40 +05:30
committed by GitHub
parent 4abf8049c9
commit b5b93aea42
5 changed files with 180 additions and 161 deletions

View File

@@ -544,11 +544,7 @@ export function SessionHeader() {
<Button <Button
variant="ghost" variant="ghost"
class="group/file-tree-toggle size-6 p-0" class="group/file-tree-toggle size-6 p-0"
onClick={() => { onClick={() => layout.fileTree.toggle()}
const opening = !layout.fileTree.opened()
if (opening && !view().reviewPanel.opened()) view().reviewPanel.open()
layout.fileTree.toggle()
}}
aria-label={language.t("command.fileTree.toggle")} aria-label={language.t("command.fileTree.toggle")}
aria-expanded={layout.fileTree.opened()} aria-expanded={layout.fileTree.opened()}
aria-controls="file-tree-panel" aria-controls="file-tree-panel"

View File

@@ -44,7 +44,7 @@ function groupFor(id: string): KeybindGroup {
if (id === PALETTE_ID) return "General" if (id === PALETTE_ID) return "General"
if (id.startsWith("terminal.")) return "Terminal" if (id.startsWith("terminal.")) return "Terminal"
if (id.startsWith("model.") || id.startsWith("agent.") || id.startsWith("mcp.")) return "Model and agent" 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("prompt.")) return "Prompt"
if ( if (
id.startsWith("session.") || id.startsWith("session.") ||

View File

@@ -233,7 +233,15 @@ export default function Page() {
} }
const isDesktop = createMediaQuery("(min-width: 768px)") 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) { function normalizeTab(tab: string) {
if (!tab.startsWith("file://")) return tab if (!tab.startsWith("file://")) return tab
@@ -252,12 +260,18 @@ export default function Page() {
return next return next
} }
const openReviewPanel = () => {
if (!view().reviewPanel.opened()) view().reviewPanel.open()
}
const openTab = (value: string) => { const openTab = (value: string) => {
const next = normalizeTab(value) const next = normalizeTab(value)
tabs().open(next) tabs().open(next)
const path = file.pathFromTab(next) const path = file.pathFromTab(next)
if (path) file.load(path) if (!path) return
file.load(path)
openReviewPanel()
} }
createEffect(() => { createEffect(() => {
@@ -1085,6 +1099,7 @@ export default function Page() {
} }
const focusReviewDiff = (path: string) => { const focusReviewDiff = (path: string) => {
openReviewPanel()
const current = view().review.open() ?? [] const current = view().review.open() ?? []
if (!current.includes(path)) view().review.setOpen([...current, path]) if (!current.includes(path)) view().review.setOpen([...current, path])
setTree({ activeDiff: path, pendingDiff: path }) setTree({ activeDiff: path, pendingDiff: path })
@@ -1203,7 +1218,7 @@ export default function Page() {
if (!id) return if (!id) return
const wants = isDesktop() const wants = isDesktop()
? view().reviewPanel.opened() && (layout.fileTree.opened() || activeTab() === "review") ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
: store.mobileTab === "changes" : store.mobileTab === "changes"
if (!wants) return if (!wants) return
if (sync.data.session_diff[id] !== undefined) return if (sync.data.session_diff[id] !== undefined) return
@@ -1216,7 +1231,6 @@ export default function Page() {
createEffect(() => { createEffect(() => {
const dir = sdk.directory const dir = sdk.directory
if (!isDesktop()) return if (!isDesktop()) return
if (!view().reviewPanel.opened()) return
if (!layout.fileTree.opened()) return if (!layout.fileTree.opened()) return
if (sync.status === "loading") return if (sync.status === "loading") return
@@ -1533,10 +1547,10 @@ export default function Page() {
classList={{ classList={{
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true, "@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true,
"flex-1 pt-2 md:pt-3": true, "flex-1 pt-2 md:pt-3": true,
"md:flex-none": view().reviewPanel.opened(), "md:flex-none": desktopSidePanelOpen(),
}} }}
style={{ style={{
width: isDesktop() && view().reviewPanel.opened() ? `${layout.session.width()}px` : "100%", width: sessionPanelWidth(),
"--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined, "--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined,
}} }}
> >
@@ -1663,7 +1677,7 @@ export default function Page() {
setPromptDockRef={(el) => (promptDock = el)} setPromptDockRef={(el) => (promptDock = el)}
/> />
<Show when={isDesktop() && view().reviewPanel.opened()}> <Show when={desktopReviewOpen()}>
<ResizeHandle <ResizeHandle
direction="horizontal" direction="horizontal"
size={layout.session.width()} size={layout.session.width()}
@@ -1675,7 +1689,8 @@ export default function Page() {
</div> </div>
<SessionSidePanel <SessionSidePanel
open={isDesktop() && view().reviewPanel.opened()} open={desktopSidePanelOpen()}
reviewOpen={desktopReviewOpen()}
language={language} language={language}
layout={layout} layout={layout}
command={command} command={command}

View File

@@ -24,6 +24,7 @@ import { useSync } from "@/context/sync"
export function SessionSidePanel(props: { export function SessionSidePanel(props: {
open: boolean open: boolean
reviewOpen: boolean
language: ReturnType<typeof useLanguage> language: ReturnType<typeof useLanguage>
layout: ReturnType<typeof useLayout> layout: ReturnType<typeof useLayout>
command: ReturnType<typeof useCommand> command: ReturnType<typeof useCommand>
@@ -72,157 +73,164 @@ export function SessionSidePanel(props: {
<aside <aside
id="review-panel" id="review-panel"
aria-label={props.language.t("session.panel.reviewAndFiles")} 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.reviewOpen}>
<Show <div class="flex-1 min-w-0 h-full">
when={props.layout.fileTree.opened() && props.fileTreeTab() === "changes"} <Show
fallback={ when={props.layout.fileTree.opened() && props.fileTreeTab() === "changes"}
<DragDropProvider fallback={
onDragStart={props.onDragStart} <DragDropProvider
onDragEnd={props.onDragEnd} onDragStart={props.onDragStart}
onDragOver={props.onDragOver} onDragEnd={props.onDragEnd}
collisionDetector={closestCenter} onDragOver={props.onDragOver}
> collisionDetector={closestCenter}
<DragDropSensors /> >
<ConstrainDragYAxis /> <DragDropSensors />
<Tabs value={props.activeTab()} onChange={props.openTab}> <ConstrainDragYAxis />
<div class="sticky top-0 shrink-0 flex"> <Tabs value={props.activeTab()} onChange={props.openTab}>
<Tabs.List <div class="sticky top-0 shrink-0 flex">
ref={(el: HTMLDivElement) => { <Tabs.List
const stop = createFileTabListSync({ el, contextOpen: props.contextOpen }) ref={(el: HTMLDivElement) => {
onCleanup(stop) const stop = createFileTabListSync({ el, contextOpen: props.contextOpen })
}} onCleanup(stop)
> }}
<Show when={props.reviewTab}> >
<Tabs.Trigger value="review" classes={{ button: "!pl-6" }}> <Show when={props.reviewTab}>
<div class="flex items-center gap-1.5"> <Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
<div>{props.language.t("session.tab.review")}</div> <div class="flex items-center gap-1.5">
<Show when={props.hasReview}> <div>{props.language.t("session.tab.review")}</div>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base"> <Show when={props.hasReview}>
{props.reviewCount} <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
</div> {props.reviewCount}
</Show> </div>
</div> </Show>
</Tabs.Trigger> </div>
</Show> </Tabs.Trigger>
<Show when={props.contextOpen()}> </Show>
<Tabs.Trigger <Show when={props.contextOpen()}>
value="context" <Tabs.Trigger
closeButton={ value="context"
<Tooltip value={props.language.t("common.closeTab")} placement="bottom"> closeButton={
<IconButton <Tooltip value={props.language.t("common.closeTab")} placement="bottom">
icon="close-small" <IconButton
variant="ghost" icon="close-small"
class="h-5 w-5" variant="ghost"
onClick={() => props.tabs().close("context")} class="h-5 w-5"
aria-label={props.language.t("common.closeTab")} onClick={() => props.tabs().close("context")}
/> aria-label={props.language.t("common.closeTab")}
</Tooltip> />
} </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} />)
} }
aria-label={props.language.t("command.file.open")} hideCloseButton
/> onMiddleClick={() => props.tabs().close("context")}
</TooltipKeybind> >
</StickyAddButton> <div class="flex items-center gap-2">
</Tabs.List> <SessionContextUsage variant="indicator" />
</div> <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}> <Show when={props.reviewTab}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict"> <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={props.activeTab() === "review"}>{props.reviewPanel()}</Show> <Show when={props.activeTab() === "review"}>{props.reviewPanel()}</Show>
</Tabs.Content> </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> </Show>
</Tabs.Content>
<Show when={props.contextOpen()}> <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict"> <Show when={props.activeTab() === "empty"}>
<Show when={props.activeTab() === "context"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionContextTab <div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
messages={props.messages as never} <Mark class="w-14 opacity-10" />
visibleUserMessages={props.visibleUserMessages as never} <div class="text-14-regular text-text-weak max-w-56">
view={props.view as never} {props.language.t("session.files.selectToOpen")}
info={props.info as never} </div>
/> </div>
</div> </div>
</Show> </Show>
</Tabs.Content> </Tabs.Content>
</Show>
<Show when={props.activeFileTab()} keyed> <Show when={props.contextOpen()}>
{(tab) => ( <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
<FileTabContent <Show when={props.activeTab() === "context"}>
tab={tab} <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
activeTab={props.activeTab} <SessionContextTab
tabs={props.tabs} messages={props.messages as never}
view={props.view} visibleUserMessages={props.visibleUserMessages as never}
handoffFiles={props.handoffFiles} view={props.view as never}
file={props.file} info={props.info as never}
comments={props.comments} />
language={props.language} </div>
codeComponent={props.codeComponent} </Show>
addCommentToContext={props.addCommentToContext} </Tabs.Content>
/> </Show>
)}
</Show> <Show when={props.activeFileTab()} keyed>
</Tabs> {(tab) => (
<DragOverlay> <FileTabContent
<Show when={props.activeDraggable()}> tab={tab}
{(tab) => { activeTab={props.activeTab}
const path = createMemo(() => props.file.pathFromTab(tab())) tabs={props.tabs}
return ( view={props.view}
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent"> handoffFiles={props.handoffFiles}
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show> file={props.file}
</div> comments={props.comments}
) language={props.language}
}} codeComponent={props.codeComponent}
</Show> addCommentToContext={props.addCommentToContext}
</DragOverlay> />
</DragDropProvider> )}
} </Show>
> </Tabs>
{props.reviewPanel()} <DragOverlay>
</Show> <Show when={props.activeDraggable()}>
</div> {(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()}> <Show when={props.layout.fileTree.opened()}>
<div <div
@@ -230,7 +238,10 @@ export function SessionSidePanel(props: {
class="relative shrink-0 h-full" class="relative shrink-0 h-full"
style={{ width: `${props.layout.fileTree.width()}px` }} 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 <Tabs
variant="pill" variant="pill"
value={props.fileTreeTab()} value={props.fileTreeTab()}

View File

@@ -139,11 +139,8 @@ export const useSessionCommands = (input: {
title: input.language.t("command.fileTree.toggle"), title: input.language.t("command.fileTree.toggle"),
description: "", description: "",
category: input.language.t("command.category.view"), category: input.language.t("command.category.view"),
onSelect: () => { keybind: "mod+\\",
const opening = !input.layout.fileTree.opened() onSelect: () => input.layout.fileTree.toggle(),
if (opening && !input.view().reviewPanel.opened()) input.view().reviewPanel.open()
input.layout.fileTree.toggle()
},
}, },
{ {
id: "terminal.new", id: "terminal.new",