Browse Source

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

Rahul A Mistry 1 tuần trước cách đây
mục cha
commit
b5b93aea42

+ 1 - 5
packages/app/src/components/session/session-header.tsx

@@ -544,11 +544,7 @@ export function SessionHeader() {
                   <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()
-                    }}
+                    onClick={() => layout.fileTree.toggle()}
                     aria-label={language.t("command.fileTree.toggle")}
                     aria-expanded={layout.fileTree.opened()}
                     aria-controls="file-tree-panel"

+ 1 - 1
packages/app/src/components/settings-keybinds.tsx

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

+ 23 - 8
packages/app/src/pages/session.tsx

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

+ 153 - 142
packages/app/src/pages/session/session-side-panel.tsx

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

+ 2 - 5
packages/app/src/pages/session/use-session-commands.tsx

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