Selaa lähdekoodia

fix(app): allow toggling file tree closed independently (#12293)

Adam 2 viikkoa sitten
vanhempi
sitoutus
83646e0366

+ 2 - 0
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)

+ 3 - 0
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)

+ 2 - 0
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")

+ 38 - 8
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>

+ 57 - 5
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)

+ 83 - 10
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">