summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-05 13:51:08 -0600
committerGitHub <[email protected]>2026-02-05 13:51:08 -0600
commit83646e0366c47a3bccb5135d40628176a6776f33 (patch)
treecd8525d1a684a659e31a4413301577fac3f8fb63 /packages/app/src
parentc40ce47e92befbe4cb27735e4d870f540e75b646 (diff)
downloadopencode-83646e0366c47a3bccb5135d40628176a6776f33.tar.gz
opencode-83646e0366c47a3bccb5135d40628176a6776f33.zip
fix(app): allow toggling file tree closed independently (#12293)
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/components/dialog-select-file.tsx2
-rw-r--r--packages/app/src/components/prompt-input.tsx3
-rw-r--r--packages/app/src/components/session-context-usage.tsx2
-rw-r--r--packages/app/src/components/session/session-header.tsx46
-rw-r--r--packages/app/src/context/layout.tsx62
-rw-r--r--packages/app/src/pages/session.tsx93
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">