Răsfoiți Sursa

feat(app): improved layout

adamelmore 3 săptămâni în urmă
părinte
comite
7de42ca242

+ 2 - 3
packages/app/src/components/dialog-select-file.tsx

@@ -36,7 +36,6 @@ 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 = [
@@ -45,7 +44,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
     "session.previous",
     "session.next",
     "terminal.toggle",
-    "review.toggle",
+    "fileTree.toggle",
   ]
   const limit = 5
 
@@ -163,7 +162,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
     const value = file.tab(path)
     tabs().open(value)
     file.load(path)
-    view().reviewPanel.open()
+    layout.fileTree.setTab("all")
     props.onOpenFile?.(path)
   }
 

+ 4 - 10
packages/app/src/components/prompt-input.tsx

@@ -171,7 +171,6 @@ 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
@@ -187,20 +186,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
     const focus = { file: item.path, id: item.commentID }
     comments.setActive(focus)
-    view().reviewPanel.open()
 
-    if (item.commentOrigin === "review") {
-      tabs().open("review")
-      requestAnimationFrame(() => comments.setFocus(focus))
-      return
-    }
-
-    if (item.commentOrigin !== "file" && commentInReview(item.path)) {
-      tabs().open("review")
+    const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
+    if (wantsReview) {
+      layout.fileTree.setTab("changes")
       requestAnimationFrame(() => comments.setFocus(focus))
       return
     }
 
+    layout.fileTree.setTab("all")
     const tab = files.tab(item.path)
     tabs().open(tab)
     files.load(item.path)

+ 1 - 2
packages/app/src/components/session-context-usage.tsx

@@ -23,7 +23,6 @@ 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(
@@ -58,7 +57,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
 
   const openContext = () => {
     if (!params.id) return
-    view().reviewPanel.open()
+    layout.fileTree.setTab("all")
     tabs().open("context")
     tabs().setActive("context")
   }

+ 14 - 39
packages/app/src/components/session/session-header.tsx

@@ -45,7 +45,6 @@ export function SessionHeader() {
   const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
   const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
   const showShare = createMemo(() => shareEnabled() && !!currentSession())
-  const showReview = createMemo(() => !!currentSession())
   const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
   const view = createMemo(() => layout.view(sessionKey))
 
@@ -281,62 +280,38 @@ export function SessionHeader() {
                 </TooltipKeybind>
               </div>
               <div class="hidden md:block shrink-0">
-                <TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
+                <TooltipKeybind
+                  title={language.t("command.fileTree.toggle")}
+                  keybind={command.keybind("fileTree.toggle")}
+                >
                   <Button
                     variant="ghost"
-                    class="group/review-toggle size-6 p-0"
-                    onClick={() => view().reviewPanel.toggle()}
-                    aria-label={language.t("command.review.toggle")}
-                    aria-expanded={view().reviewPanel.opened()}
-                    aria-controls="review-panel"
-                    tabIndex={showReview() ? 0 : -1}
+                    class="group/file-tree-toggle size-6 p-0"
+                    onClick={() => 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 [&>*]:absolute [&>*]:inset-0">
                       <Icon
                         size="small"
-                        name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
-                        class="group-hover/review-toggle:hidden"
+                        name={layout.fileTree.opened() ? "layout-right-full" : "layout-right"}
+                        class="group-hover/file-tree-toggle:hidden"
                       />
                       <Icon
                         size="small"
                         name="layout-right-partial"
-                        class="hidden group-hover/review-toggle:inline-block"
+                        class="hidden group-hover/file-tree-toggle:inline-block"
                       />
                       <Icon
                         size="small"
-                        name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
-                        class="hidden group-active/review-toggle:inline-block"
+                        name={layout.fileTree.opened() ? "layout-right" : "layout-right-full"}
+                        class="hidden group-active/file-tree-toggle:inline-block"
                       />
                     </div>
                   </Button>
                 </TooltipKeybind>
               </div>
-              <div class="hidden md:block shrink-0">
-                <Tooltip value="Toggle file tree" placement="bottom">
-                  <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="Toggle file tree"
-                    aria-expanded={layout.fileTree.opened()}
-                  >
-                    <div class="relative flex items-center justify-center size-4">
-                      <Icon
-                        size="small"
-                        name="bullet-list"
-                        classList={{
-                          "text-icon-strong": layout.fileTree.opened(),
-                          "text-icon-weak": !layout.fileTree.opened(),
-                        }}
-                      />
-                    </div>
-                  </Button>
-                </Tooltip>
-              </div>
             </div>
           </Portal>
         )}

+ 51 - 53
packages/app/src/context/layout.tsx

@@ -51,16 +51,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
 
     const migrate = (value: unknown) => {
       if (!isRecord(value)) return value
+
       const sidebar = value.sidebar
-      if (!isRecord(sidebar)) return value
-      if (typeof sidebar.workspaces !== "boolean") return value
-      return {
-        ...value,
-        sidebar: {
+      const migratedSidebar = (() => {
+        if (!isRecord(sidebar)) return sidebar
+        if (typeof sidebar.workspaces !== "boolean") return sidebar
+        return {
           ...sidebar,
           workspaces: {},
           workspacesDefault: sidebar.workspaces,
-        },
+        }
+      })()
+
+      const fileTree = value.fileTree
+      const migratedFileTree = (() => {
+        if (!isRecord(fileTree)) return fileTree
+        if (fileTree.tab === "changes" || fileTree.tab === "all") return fileTree
+
+        const width = typeof fileTree.width === "number" ? fileTree.width : 344
+        return {
+          ...fileTree,
+          opened: true,
+          width: width === 260 ? 344 : width,
+          tab: "changes",
+        }
+      })()
+
+      if (migratedSidebar === sidebar && migratedFileTree === fileTree) return value
+      return {
+        ...value,
+        sidebar: migratedSidebar,
+        fileTree: migratedFileTree,
       }
     }
 
@@ -80,11 +101,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         },
         review: {
           diffStyle: "split" as ReviewDiffStyle,
-          panelOpened: true,
         },
         fileTree: {
-          opened: false,
-          width: 260,
+          opened: true,
+          width: 344,
+          tab: "changes" as "changes" | "all",
         },
         session: {
           width: 600,
@@ -454,32 +475,40 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         },
       },
       fileTree: {
-        opened: createMemo(() => store.fileTree?.opened ?? false),
-        width: createMemo(() => store.fileTree?.width ?? 260),
+        opened: createMemo(() => store.fileTree?.opened ?? true),
+        width: createMemo(() => store.fileTree?.width ?? 344),
+        tab: createMemo(() => store.fileTree?.tab ?? "changes"),
+        setTab(tab: "changes" | "all") {
+          if (!store.fileTree) {
+            setStore("fileTree", { opened: true, width: 344, tab })
+            return
+          }
+          setStore("fileTree", "tab", tab)
+        },
         open() {
           if (!store.fileTree) {
-            setStore("fileTree", { opened: true, width: 260 })
+            setStore("fileTree", { opened: true, width: 344, tab: "changes" })
             return
           }
           setStore("fileTree", "opened", true)
         },
         close() {
           if (!store.fileTree) {
-            setStore("fileTree", { opened: false, width: 260 })
+            setStore("fileTree", { opened: false, width: 344, tab: "changes" })
             return
           }
           setStore("fileTree", "opened", false)
         },
         toggle() {
           if (!store.fileTree) {
-            setStore("fileTree", { opened: true, width: 260 })
+            setStore("fileTree", { opened: true, width: 344, tab: "changes" })
             return
           }
           setStore("fileTree", "opened", (x) => !x)
         },
         resize(width: number) {
           if (!store.fileTree) {
-            setStore("fileTree", { opened: true, width })
+            setStore("fileTree", { opened: true, width, tab: "changes" })
             return
           }
           setStore("fileTree", "width", width)
@@ -526,7 +555,6 @@ 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
@@ -540,18 +568,6 @@ 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)
@@ -571,18 +587,6 @@ 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[]) {
@@ -620,10 +624,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
         return {
           tabs,
-          active: createMemo(() => tabs().active),
-          all: createMemo(() => tabs().all),
+          active: createMemo(() => (tabs().active === "review" ? undefined : 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 {
@@ -632,25 +637,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           },
           setAll(all: string[]) {
             const session = key()
+            const next = all.filter((tab) => tab !== "review")
             if (!store.sessionTabs[session]) {
-              setStore("sessionTabs", session, { all, active: undefined })
+              setStore("sessionTabs", session, { all: next, active: undefined })
             } else {
-              setStore("sessionTabs", session, "all", all)
+              setStore("sessionTabs", session, "all", next)
             }
           },
           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: [], 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]) {

+ 104 - 210
packages/app/src/pages/session.tsx

@@ -23,7 +23,6 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Button } from "@opencode-ai/ui/button"
 import { Icon } from "@opencode-ai/ui/icon"
 import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
-import { DiffChanges } from "@opencode-ai/ui/diff-changes"
 import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { useCodeComponent } from "@opencode-ai/ui/context/code"
@@ -433,7 +432,7 @@ export default function Page() {
     expanded: {} as Record<string, boolean>,
     messageId: undefined as string | undefined,
     turnStart: 0,
-    mobileTab: "session" as "session" | "review",
+    mobileTab: "session" as "session" | "changes",
     newSessionWorktree: "main",
     promptHeight: 0,
   })
@@ -693,12 +692,12 @@ export default function Page() {
       onSelect: () => view().terminal.toggle(),
     },
     {
-      id: "review.toggle",
-      title: language.t("command.review.toggle"),
+      id: "fileTree.toggle",
+      title: language.t("command.fileTree.toggle"),
       description: "",
       category: language.t("command.category.view"),
       keybind: "mod+shift+r",
-      onSelect: () => view().reviewPanel.toggle(),
+      onSelect: () => layout.fileTree.toggle(),
     },
     {
       id: "terminal.new",
@@ -822,7 +821,7 @@ export default function Page() {
         const sessionID = params.id
         if (!sessionID) return
         if (status()?.type !== "idle") {
-          await sdk.client.session.abort({ sessionID }).catch(() => {})
+          await sdk.client.session.abort({ sessionID }).catch(() => { })
         }
         const revert = info()?.revert?.messageID
         // Find the last user message that's not already reverted
@@ -905,69 +904,69 @@ export default function Page() {
     },
     ...(sync.data.config.share !== "disabled"
       ? [
-          {
-            id: "session.share",
-            title: language.t("command.session.share"),
-            description: language.t("command.session.share.description"),
-            category: language.t("command.category.session"),
-            slash: "share",
-            disabled: !params.id || !!info()?.share?.url,
-            onSelect: async () => {
-              if (!params.id) return
-              await sdk.client.session
-                .share({ sessionID: params.id })
-                .then((res) => {
-                  navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
-                    showToast({
-                      title: language.t("toast.session.share.copyFailed.title"),
-                      variant: "error",
-                    }),
-                  )
-                })
-                .then(() =>
+        {
+          id: "session.share",
+          title: language.t("command.session.share"),
+          description: language.t("command.session.share.description"),
+          category: language.t("command.category.session"),
+          slash: "share",
+          disabled: !params.id || !!info()?.share?.url,
+          onSelect: async () => {
+            if (!params.id) return
+            await sdk.client.session
+              .share({ sessionID: params.id })
+              .then((res) => {
+                navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
                   showToast({
-                    title: language.t("toast.session.share.success.title"),
-                    description: language.t("toast.session.share.success.description"),
-                    variant: "success",
-                  }),
-                )
-                .catch(() =>
-                  showToast({
-                    title: language.t("toast.session.share.failed.title"),
-                    description: language.t("toast.session.share.failed.description"),
+                    title: language.t("toast.session.share.copyFailed.title"),
                     variant: "error",
                   }),
                 )
-            },
+              })
+              .then(() =>
+                showToast({
+                  title: language.t("toast.session.share.success.title"),
+                  description: language.t("toast.session.share.success.description"),
+                  variant: "success",
+                }),
+              )
+              .catch(() =>
+                showToast({
+                  title: language.t("toast.session.share.failed.title"),
+                  description: language.t("toast.session.share.failed.description"),
+                  variant: "error",
+                }),
+              )
           },
-          {
-            id: "session.unshare",
-            title: language.t("command.session.unshare"),
-            description: language.t("command.session.unshare.description"),
-            category: language.t("command.category.session"),
-            slash: "unshare",
-            disabled: !params.id || !info()?.share?.url,
-            onSelect: async () => {
-              if (!params.id) return
-              await sdk.client.session
-                .unshare({ sessionID: params.id })
-                .then(() =>
-                  showToast({
-                    title: language.t("toast.session.unshare.success.title"),
-                    description: language.t("toast.session.unshare.success.description"),
-                    variant: "success",
-                  }),
-                )
-                .catch(() =>
-                  showToast({
-                    title: language.t("toast.session.unshare.failed.title"),
-                    description: language.t("toast.session.unshare.failed.description"),
-                    variant: "error",
-                  }),
-                )
-            },
+        },
+        {
+          id: "session.unshare",
+          title: language.t("command.session.unshare"),
+          description: language.t("command.session.unshare.description"),
+          category: language.t("command.category.session"),
+          slash: "unshare",
+          disabled: !params.id || !info()?.share?.url,
+          onSelect: async () => {
+            if (!params.id) return
+            await sdk.client.session
+              .unshare({ sessionID: params.id })
+              .then(() =>
+                showToast({
+                  title: language.t("toast.session.unshare.success.title"),
+                  description: language.t("toast.session.unshare.success.description"),
+                  variant: "success",
+                }),
+              )
+              .catch(() =>
+                showToast({
+                  title: language.t("toast.session.unshare.failed.title"),
+                  description: language.t("toast.session.unshare.failed.description"),
+                  variant: "error",
+                }),
+              )
           },
-        ]
+        },
+      ]
       : []),
   ])
 
@@ -1067,40 +1066,31 @@ export default function Page() {
       .filter((tab) => tab !== "context"),
   )
 
-  const mobileReview = createMemo(() => !isDesktop() && view().reviewPanel.opened() && store.mobileTab === "review")
+  const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
 
-  const showTabs = createMemo(() => view().reviewPanel.opened())
+  const fileTreeTab = () => layout.fileTree.tab()
+  const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
 
   const [tree, setTree] = createStore({
-    fileTreeTab: "changes" as "changes" | "all",
     reviewScroll: undefined as HTMLDivElement | undefined,
     pendingDiff: undefined as string | undefined,
   })
 
-  const fileTreeTab = () => tree.fileTreeTab
-  const setFileTreeTab = (value: "changes" | "all") => setTree("fileTreeTab", value)
   const reviewScroll = () => tree.reviewScroll
   const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value)
   const pendingDiff = () => tree.pendingDiff
   const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value)
 
   const showAllFiles = () => {
-    if (!layout.fileTree.opened()) return
     if (fileTreeTab() !== "changes") return
     setFileTreeTab("all")
   }
 
-  createEffect(() => {
-    if (!layout.fileTree.opened()) return
-    setFileTreeTab("changes")
-  })
-
   createEffect(
     on(
       () => tabs().active(),
       (active) => {
         if (!active) return
-        if (!layout.fileTree.opened()) return
         if (fileTreeTab() !== "changes") return
         if (!file.pathFromTab(active)) return
         showAllFiles()
@@ -1197,49 +1187,30 @@ export default function Page() {
 
   const activeTab = createMemo(() => {
     const active = tabs().active()
-    if (layout.fileTree.opened() && fileTreeTab() === "all") {
-      if (active && active !== "review" && active !== "context") return normalizeTab(active)
-
-      const first = openedTabs()[0]
-      if (first) return first
-      return "review"
-    }
-    if (active) return normalizeTab(active)
-    if (hasReview()) return "review"
+    if (active === "context") return "context"
+    if (active && file.pathFromTab(active)) return normalizeTab(active)
 
     const first = openedTabs()[0]
     if (first) return first
     if (contextOpen()) return "context"
-    return "review"
+    return "empty"
   })
 
   createEffect(() => {
     if (!layout.ready()) return
     if (tabs().active()) return
-    if (!hasReview() && openedTabs().length === 0 && !contextOpen()) return
-    tabs().setActive(activeTab())
-  })
-
-  createEffect(() => {
-    if (!layout.fileTree.opened()) return
-    if (fileTreeTab() !== "all") return
-
-    const first = openedTabs()[0]
-    if (!first) return
+    if (openedTabs().length === 0 && !contextOpen()) return
 
-    const active = tabs().active()
-    if (active && active !== "review" && active !== "context") return
-    tabs().setActive(first)
+    const next = activeTab()
+    if (next === "empty") return
+    tabs().setActive(next)
   })
 
   createEffect(() => {
     const id = params.id
     if (!id) return
 
-    const wants = isDesktop()
-      ? view().reviewPanel.opened() &&
-        (layout.fileTree.opened() ? fileTreeTab() === "changes" : activeTab() === "review")
-      : view().reviewPanel.opened() && store.mobileTab === "review"
+    const wants = isDesktop() ? fileTreeTab() === "changes" : store.mobileTab === "changes"
     if (!wants) return
     if (sync.data.session_diff[id] !== undefined) return
 
@@ -1654,8 +1625,8 @@ export default function Page() {
     <div class="relative bg-background-base size-full overflow-hidden flex flex-col">
       <SessionHeader />
       <div class="flex-1 min-h-0 flex flex-col md:flex-row">
-        {/* Mobile tab bar - only shown on mobile when user opened review */}
-        <Show when={!isDesktop() && view().reviewPanel.opened()}>
+        {/* Mobile tab bar */}
+        <Show when={!isDesktop() && params.id}>
           <Tabs class="h-auto">
             <Tabs.List>
               <Tabs.Trigger
@@ -1667,16 +1638,16 @@ export default function Page() {
                 {language.t("session.tab.session")}
               </Tabs.Trigger>
               <Tabs.Trigger
-                value="review"
+                value="changes"
                 class="w-1/2 !border-r-0"
                 classes={{ button: "w-full" }}
-                onClick={() => setStore("mobileTab", "review")}
+                onClick={() => setStore("mobileTab", "changes")}
               >
                 <Switch>
                   <Match when={hasReview()}>
                     {language.t("session.review.filesChanged", { count: reviewCount() })}
                   </Match>
-                  <Match when={true}>{language.t("session.tab.review")}</Match>
+                  <Match when={true}>{language.t("session.review.change.other")}</Match>
                 </Switch>
               </Tabs.Trigger>
             </Tabs.List>
@@ -1690,7 +1661,7 @@ export default function Page() {
             "flex-1 md:flex-none pt-6 md:pt-3": true,
           }}
           style={{
-            width: isDesktop() && showTabs() ? `${layout.session.width()}px` : "100%",
+            width: isDesktop() ? `${layout.session.width()}px` : "100%",
             "--prompt-height": store.promptHeight ? `${store.promptHeight}px` : undefined,
           }}
         >
@@ -1699,7 +1670,7 @@ export default function Page() {
               <Match when={params.id}>
                 <Show when={activeMessage()}>
                   <Show
-                    when={!mobileReview()}
+                    when={!mobileChanges()}
                     fallback={
                       <div class="relative h-full overflow-hidden">
                         <Switch>
@@ -1789,7 +1760,6 @@ export default function Page() {
                               "sticky top-0 z-30 bg-background-stronger": true,
                               "w-full": true,
                               "px-4 md:px-6": true,
-                              "md:max-w-200 md:mx-auto": !showTabs(),
                             }}
                           >
                             <div class="h-10 flex items-center gap-1">
@@ -1814,13 +1784,7 @@ export default function Page() {
                         <div
                           ref={autoScroll.contentRef}
                           role="log"
-                          class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
-                          classList={{
-                            "w-full": true,
-                            "md:max-w-200 md:mx-auto": !showTabs(),
-                            "mt-0.5": !showTabs(),
-                            "mt-0": showTabs(),
-                          }}
+                          class="flex flex-col gap-32 items-start justify-start w-full mt-0 pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
                         >
                           <Show when={store.turnStart > 0}>
                             <div class="w-full flex justify-center">
@@ -1868,10 +1832,7 @@ export default function Page() {
                                 <div
                                   id={anchor(message.id)}
                                   data-message-id={message.id}
-                                  classList={{
-                                    "min-w-0 w-full max-w-full": true,
-                                    "md:max-w-200": !showTabs(),
-                                  }}
+                                  class="min-w-0 w-full max-w-full"
                                 >
                                   <SessionTurn
                                     sessionID={params.id!}
@@ -1924,12 +1885,7 @@ export default function Page() {
             ref={(el) => (promptDock = el)}
             class="absolute inset-x-0 bottom-0 pt-12 pb-4 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none"
           >
-            <div
-              classList={{
-                "w-full px-4 pointer-events-auto": true,
-                "md:max-w-200": !showTabs(),
-              }}
-            >
+            <div class="w-full px-4 pointer-events-auto">
               <Show when={request()} keyed>
                 {(perm) => (
                   <div data-component="tool-part-wrapper" data-permission="true" class="mb-3">
@@ -2000,7 +1956,7 @@ export default function Page() {
             </div>
           </div>
 
-          <Show when={isDesktop() && showTabs()}>
+          <Show when={isDesktop()}>
             <ResizeHandle
               direction="horizontal"
               size={layout.session.width()}
@@ -2011,8 +1967,8 @@ export default function Page() {
           </Show>
         </div>
 
-        {/* Desktop tabs panel (Review + Context + Files) - hidden on mobile */}
-        <Show when={isDesktop() && showTabs()}>
+        {/* Desktop side panel - hidden on mobile */}
+        <Show when={isDesktop()}>
           <aside
             id="review-panel"
             aria-label={language.t("session.panel.reviewAndFiles")}
@@ -2020,7 +1976,7 @@ export default function Page() {
           >
             <div class="flex-1 min-w-0 h-full">
               <Show
-                when={layout.fileTree.opened() && fileTreeTab() === "changes"}
+                when={fileTreeTab() === "changes"}
                 fallback={
                   <DragDropProvider
                     onDragStart={handleDragStart}
@@ -2033,24 +1989,7 @@ export default function Page() {
                     <Tabs value={activeTab()} onChange={openTab}>
                       <div class="sticky top-0 shrink-0 flex">
                         <Tabs.List>
-                          <Show when={!layout.fileTree.opened()}>
-                            <Tabs.Trigger value="review" classes={{ button: "!pl-6" }}>
-                              <div class="flex items-center gap-3">
-                                <Show when={diffs()}>
-                                  <DiffChanges changes={diffs()} variant="bars" />
-                                </Show>
-                                <div class="flex items-center gap-1.5">
-                                  <div>{language.t("session.tab.review")}</div>
-                                  <Show when={info()?.summary?.files}>
-                                    <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
-                                      {info()?.summary?.files ?? 0}
-                                    </div>
-                                  </Show>
-                                </div>
-                              </div>
-                            </Tabs.Trigger>
-                          </Show>
-                          <Show when={!layout.fileTree.opened() && contextOpen()}>
+                          <Show when={contextOpen()}>
                             <Tabs.Trigger
                               value="context"
                               closeButton={
@@ -2097,69 +2036,20 @@ export default function Page() {
                           </StickyAddButton>
                         </Tabs.List>
                       </div>
-                      <Show when={!layout.fileTree.opened()}>
-                        <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
-                          <Show when={activeTab() === "review"}>
-                            <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
-                              <Switch>
-                                <Match when={hasReview()}>
-                                  <Show
-                                    when={diffsReady()}
-                                    fallback={
-                                      <div class="px-6 py-4 text-text-weak">
-                                        {language.t("session.review.loadingChanges")}
-                                      </div>
-                                    }
-                                  >
-                                    <SessionReviewTab
-                                      diffs={diffs}
-                                      view={view}
-                                      diffStyle={layout.review.diffStyle()}
-                                      onDiffStyleChange={layout.review.setDiffStyle}
-                                      onScrollRef={setReviewScroll}
-                                      onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
-                                      comments={comments.all()}
-                                      focusedComment={comments.focus()}
-                                      onFocusedCommentChange={comments.setFocus}
-                                      onViewFile={(path) => {
-                                        showAllFiles()
-                                        const value = file.tab(path)
-                                        tabs().open(value)
-                                        file.load(path)
-                                      }}
-                                    />
-                                  </Show>
-                                </Match>
-                                <Match when={true}>
-                                  <div class="h-full px-6 pb-30 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">
-                                      {language.t("session.review.empty")}
-                                    </div>
-                                  </div>
-                                </Match>
-                              </Switch>
-                            </div>
-                          </Show>
-                        </Tabs.Content>
-                      </Show>
-
-                      <Show when={layout.fileTree.opened() && fileTreeTab() === "all" && openedTabs().length === 0}>
-                        <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
-                          <Show when={activeTab() === "review"}>
-                            <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">
-                                  {language.t("session.files.selectToOpen")}
-                                </div>
+                      <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">
+                            <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">
+                                {language.t("session.files.selectToOpen")}
                               </div>
                             </div>
-                          </Show>
-                        </Tabs.Content>
-                      </Show>
+                          </div>
+                        </Show>
+                      </Tabs.Content>
 
-                      <Show when={!layout.fileTree.opened() && contextOpen()}>
+                      <Show when={contextOpen()}>
                         <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
                           <Show when={activeTab() === "context"}>
                             <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
@@ -2725,7 +2615,11 @@ export default function Page() {
             </div>
 
             <Show when={layout.fileTree.opened()}>
-              <div class="relative shrink-0 h-full" style={{ width: `${layout.fileTree.width()}px` }}>
+              <div
+                id="file-tree-panel"
+                class="relative shrink-0 h-full"
+                style={{ width: `${layout.fileTree.width()}px` }}
+              >
                 <div class="h-full border-l border-border-weak-base flex flex-col overflow-hidden group/filetree">
                   <Tabs
                     variant="pill"