diff options
| author | Adam <[email protected]> | 2026-02-05 13:51:08 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-05 13:51:08 -0600 |
| commit | 83646e0366c47a3bccb5135d40628176a6776f33 (patch) | |
| tree | cd8525d1a684a659e31a4413301577fac3f8fb63 | |
| parent | c40ce47e92befbe4cb27735e4d870f540e75b646 (diff) | |
| download | opencode-83646e0366c47a3bccb5135d40628176a6776f33.tar.gz opencode-83646e0366c47a3bccb5135d40628176a6776f33.zip | |
fix(app): allow toggling file tree closed independently (#12293)
| -rw-r--r-- | packages/app/src/components/dialog-select-file.tsx | 2 | ||||
| -rw-r--r-- | packages/app/src/components/prompt-input.tsx | 3 | ||||
| -rw-r--r-- | packages/app/src/components/session-context-usage.tsx | 2 | ||||
| -rw-r--r-- | packages/app/src/components/session/session-header.tsx | 46 | ||||
| -rw-r--r-- | packages/app/src/context/layout.tsx | 62 | ||||
| -rw-r--r-- | packages/app/src/pages/session.tsx | 93 |
6 files changed, 185 insertions, 23 deletions
diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 167f21195..36448dd3e 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -47,6 +47,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil const filesOnly = () => props.mode === "files" const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) + const view = createMemo(() => layout.view(sessionKey)) const state = { cleanup: undefined as (() => void) | void, committed: false } const [grouped, setGrouped] = createSignal(false) const common = [ @@ -282,6 +283,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil const value = file.tab(path) tabs().open(value) file.load(path) + if (!view().reviewPanel.opened()) view().reviewPanel.open() layout.fileTree.open() layout.fileTree.setTab("all") props.onOpenFile?.(path) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index b897e394a..f40b61bca 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -172,6 +172,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) + const view = createMemo(() => layout.view(sessionKey)) const commentInReview = (path: string) => { const sessionID = params.id @@ -190,12 +191,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path)) if (wantsReview) { + if (!view().reviewPanel.opened()) view().reviewPanel.open() layout.fileTree.open() layout.fileTree.setTab("changes") requestAnimationFrame(() => comments.setFocus(focus)) return } + if (!view().reviewPanel.opened()) view().reviewPanel.open() layout.fileTree.open() layout.fileTree.setTab("all") const tab = files.tab(item.path) diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index c5de54cf0..c6256395f 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -23,6 +23,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { const variant = createMemo(() => props.variant ?? "button") const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) + const view = createMemo(() => layout.view(sessionKey)) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const usd = createMemo( @@ -57,6 +58,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { const openContext = () => { if (!params.id) return + if (!view().reviewPanel.opened()) view().reviewPanel.open() layout.fileTree.open() layout.fileTree.setTab("all") tabs().open("context") diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 5b00f80c0..f2bfc8d25 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -283,27 +283,57 @@ export function SessionHeader() { <TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}> <Button variant="ghost" - class="group/file-tree-toggle size-6 p-0" - onClick={() => layout.fileTree.toggle()} + class="group/review-toggle size-6 p-0" + onClick={() => view().reviewPanel.toggle()} aria-label={language.t("command.review.toggle")} - aria-expanded={layout.fileTree.opened()} + aria-expanded={view().reviewPanel.opened()} aria-controls="review-panel" > <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0"> <Icon size="small" - name={layout.fileTree.opened() ? "layout-right-full" : "layout-right"} - class="group-hover/file-tree-toggle:hidden" + name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"} + class="group-hover/review-toggle:hidden" /> <Icon size="small" name="layout-right-partial" - class="hidden group-hover/file-tree-toggle:inline-block" + class="hidden group-hover/review-toggle:inline-block" + /> + <Icon + size="small" + name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"} + class="hidden group-active/review-toggle:inline-block" /> + </div> + </Button> + </TooltipKeybind> + </div> + <div class="hidden md:block shrink-0"> + <TooltipKeybind + title={language.t("command.fileTree.toggle")} + keybind={command.keybind("fileTree.toggle")} + > + <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() + }} + aria-label={language.t("command.fileTree.toggle")} + aria-expanded={layout.fileTree.opened()} + aria-controls="file-tree-panel" + > + <div class="relative flex items-center justify-center size-4"> <Icon size="small" - name={layout.fileTree.opened() ? "layout-right" : "layout-right-full"} - class="hidden group-active/file-tree-toggle:inline-block" + name="bullet-list" + classList={{ + "text-icon-strong": layout.fileTree.opened(), + "text-icon-weak": !layout.fileTree.opened(), + }} /> </div> </Button> diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index e2fd0a7f4..95a2006ea 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -71,6 +71,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( } })() + const review = value.review const fileTree = value.fileTree const migratedFileTree = (() => { if (!isRecord(fileTree)) return fileTree @@ -85,10 +86,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( } })() - if (migratedSidebar === sidebar && migratedFileTree === fileTree) return value + const migratedReview = (() => { + if (!isRecord(review)) return review + if (typeof review.panelOpened === "boolean") return review + + const opened = isRecord(fileTree) && typeof fileTree.opened === "boolean" ? fileTree.opened : true + return { + ...review, + panelOpened: opened, + } + })() + + if (migratedSidebar === sidebar && migratedReview === review && migratedFileTree === fileTree) return value return { ...value, sidebar: migratedSidebar, + review: migratedReview, fileTree: migratedFileTree, } } @@ -109,6 +122,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, review: { diffStyle: "split" as ReviewDiffStyle, + panelOpened: true, }, fileTree: { opened: true, @@ -490,7 +504,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( diffStyle: createMemo(() => store.review?.diffStyle ?? "split"), setDiffStyle(diffStyle: ReviewDiffStyle) { if (!store.review) { - setStore("review", { diffStyle }) + setStore("review", { diffStyle, panelOpened: true }) return } setStore("review", "diffStyle", diffStyle) @@ -620,6 +634,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} }) const terminalOpened = createMemo(() => store.terminal?.opened ?? false) + const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true) function setTerminalOpened(next: boolean) { const current = store.terminal @@ -633,6 +648,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("terminal", "opened", next) } + function setReviewPanelOpened(next: boolean) { + const current = store.review + if (!current) { + setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next }) + return + } + + const value = current.panelOpened ?? true + if (value === next) return + setStore("review", "panelOpened", next) + } + return { scroll(tab: string) { return scroll.scroll(key(), tab) @@ -652,6 +679,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setTerminalOpened(!terminalOpened()) }, }, + reviewPanel: { + opened: reviewPanelOpened, + open() { + setReviewPanelOpened(true) + }, + close() { + setReviewPanelOpened(false) + }, + toggle() { + setReviewPanelOpened(!reviewPanelOpened()) + }, + }, review: { open: createMemo(() => s().reviewOpen), setOpen(open: string[]) { @@ -689,11 +728,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] }) return { tabs, - active: createMemo(() => (tabs().active === "review" ? undefined : tabs().active)), + active: createMemo(() => tabs().active), all: createMemo(() => tabs().all.filter((tab) => tab !== "review")), setActive(tab: string | undefined) { const session = key() - if (tab === "review") return if (!store.sessionTabs[session]) { setStore("sessionTabs", session, { all: [], active: tab }) } else { @@ -710,10 +748,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( } }, async open(tab: string) { - if (tab === "review") return const session = key() const current = store.sessionTabs[session] ?? { all: [] } + if (tab === "review") { + if (!store.sessionTabs[session]) { + setStore("sessionTabs", session, { all: current.all.filter((x) => x !== "review"), active: tab }) + return + } + setStore("sessionTabs", session, "active", tab) + return + } + if (tab === "context") { const all = [tab, ...current.all.filter((x) => x !== tab)] if (!store.sessionTabs[session]) { @@ -746,6 +792,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const current = store.sessionTabs[session] if (!current) return + if (tab === "review") { + if (current.active !== tab) return + setStore("sessionTabs", session, "active", current.all[0]) + return + } + const all = current.all.filter((x) => x !== tab) if (current.active !== tab) { setStore("sessionTabs", session, "all", all) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 0bdf5f7f3..b0b955ed1 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -395,7 +395,7 @@ export default function Page() { } const isDesktop = createMediaQuery("(min-width: 768px)") - const centered = createMemo(() => isDesktop() && !layout.fileTree.opened()) + const centered = createMemo(() => isDesktop() && !view().reviewPanel.opened()) function normalizeTab(tab: string) { if (!tab.startsWith("file://")) return tab @@ -1043,7 +1043,18 @@ export default function Page() { description: "", category: language.t("command.category.view"), keybind: "mod+shift+r", - onSelect: () => layout.fileTree.toggle(), + onSelect: () => view().reviewPanel.toggle(), + }, + { + id: "fileTree.toggle", + title: language.t("command.fileTree.toggle"), + description: "", + category: language.t("command.category.view"), + onSelect: () => { + const opening = !layout.fileTree.opened() + if (opening && !view().reviewPanel.opened()) view().reviewPanel.open() + layout.fileTree.toggle() + }, }, { id: "terminal.new", @@ -1409,10 +1420,11 @@ export default function Page() { const openedTabs = createMemo(() => tabs() .all() - .filter((tab) => tab !== "context"), + .filter((tab) => tab !== "context" && tab !== "review"), ) const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") + const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened()) const fileTreeTab = () => layout.fileTree.tab() const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value) @@ -1627,29 +1639,71 @@ export default function Page() { const activeTab = createMemo(() => { const active = tabs().active() if (active === "context") return "context" + if (active === "review" && reviewTab()) return "review" if (active && file.pathFromTab(active)) return normalizeTab(active) const first = openedTabs()[0] if (first) return first if (contextOpen()) return "context" + if (reviewTab() && hasReview()) return "review" return "empty" }) createEffect(() => { if (!layout.ready()) return if (tabs().active()) return - if (openedTabs().length === 0 && !contextOpen()) return + if (openedTabs().length === 0 && !contextOpen() && !(reviewTab() && hasReview())) return const next = activeTab() if (next === "empty") return tabs().setActive(next) }) + createEffect( + on( + () => layout.fileTree.opened(), + (opened, prev) => { + if (prev === undefined) return + if (!isDesktop()) return + + if (opened) { + const active = tabs().active() + const tab = active === "review" || (!active && hasReview()) ? "changes" : "all" + layout.fileTree.setTab(tab) + return + } + + if (fileTreeTab() !== "changes") return + tabs().setActive("review") + }, + { defer: true }, + ), + ) + + createEffect(() => { + if (!isDesktop()) return + if (!layout.fileTree.opened()) return + if (fileTreeTab() !== "all") return + + const active = tabs().active() + if (active && active !== "review") return + + const first = openedTabs()[0] + if (first) { + tabs().setActive(first) + return + } + + if (contextOpen()) tabs().setActive("context") + }) + createEffect(() => { const id = params.id if (!id) return - const wants = isDesktop() ? layout.fileTree.opened() : store.mobileTab === "changes" + const wants = isDesktop() + ? view().reviewPanel.opened() && (layout.fileTree.opened() || activeTab() === "review") + : store.mobileTab === "changes" if (!wants) return if (sync.data.session_diff[id] !== undefined) return if (sync.status === "loading") return @@ -1661,6 +1715,7 @@ 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 @@ -2195,10 +2250,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": layout.fileTree.opened(), + "md:flex-none": view().reviewPanel.opened(), }} style={{ - width: isDesktop() && layout.fileTree.opened() ? `${layout.session.width()}px` : "100%", + width: isDesktop() && view().reviewPanel.opened() ? `${layout.session.width()}px` : "100%", "--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined, }} > @@ -2711,7 +2766,7 @@ export default function Page() { </div> </div> - <Show when={isDesktop() && layout.fileTree.opened()}> + <Show when={isDesktop() && view().reviewPanel.opened()}> <ResizeHandle direction="horizontal" size={layout.session.width()} @@ -2723,7 +2778,7 @@ export default function Page() { </div> {/* Desktop side panel - hidden on mobile */} - <Show when={isDesktop() && layout.fileTree.opened()}> + <Show when={isDesktop() && view().reviewPanel.opened()}> <aside id="review-panel" aria-label={language.t("session.panel.reviewAndFiles")} @@ -2731,7 +2786,7 @@ export default function Page() { > <div class="flex-1 min-w-0 h-full"> <Show - when={fileTreeTab() === "changes"} + when={layout.fileTree.opened() && fileTreeTab() === "changes"} fallback={ <DragDropProvider onDragStart={handleDragStart} @@ -2799,6 +2854,18 @@ export default function Page() { }) }} > + <Show when={reviewTab()}> + <Tabs.Trigger value="review" classes={{ button: "!pl-6" }}> + <div class="flex items-center gap-1.5"> + <div>{language.t("session.tab.review")}</div> + <Show when={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"> + {reviewCount()} + </div> + </Show> + </div> + </Tabs.Trigger> + </Show> <Show when={contextOpen()}> <Tabs.Trigger value="context" @@ -2847,6 +2914,12 @@ export default function Page() { </Tabs.List> </div> + <Show when={reviewTab()}> + <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict"> + <Show when={activeTab() === "review"}>{reviewPanel()}</Show> + </Tabs.Content> + </Show> + <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"> |
