Jelajahi Sumber

fix(app): suppress review sidebar hydration motion (#36847)

Luke Parker 1 bulan lalu
induk
melakukan
21f8184c58

+ 2 - 1
packages/app/src/pages/session/v2/review-panel-v2-state.ts

@@ -9,7 +9,7 @@ import { createStore } from "solid-js/store"
 import { Persist, persisted } from "@/utils/persist"
 
 export function createReviewPanelV2State() {
-  const [store, setStore] = persisted(
+  const [store, setStore, , ready] = persisted(
     Persist.global("review-panel-v2"),
     createStore({
       sidebarOpened: true,
@@ -24,6 +24,7 @@ export function createReviewPanelV2State() {
   return {
     sidebarOpened: () => store.sidebarOpened,
     sidebarWidth: () => store.sidebarWidth,
+    sidebarTransition: ready,
     filter,
     setFilter,
     expandMode: () => store.expandMode,

+ 1 - 0
packages/app/src/pages/session/v2/review-panel-v2.tsx

@@ -200,6 +200,7 @@ function ReviewPanelV2Sidebar(props: {
   return (
     <SessionReviewV2Sidebar
       open={props.state.sidebarOpened()}
+      transition={props.state.sidebarTransition()}
       title={props.title}
       stats={<DiffChanges changes={props.diffs()} />}
       filter={props.state.filter()}

+ 2 - 0
packages/app/src/pages/session/v2/session-file-browser-tab.tsx

@@ -19,6 +19,7 @@ const emptyFiles: string[] = []
 export type SessionFileBrowserState = {
   sidebarOpened: () => boolean
   sidebarWidth: () => number
+  sidebarTransition: () => boolean
   resizeSidebar: (width: number) => void
   toggleSidebar: () => void
 }
@@ -96,6 +97,7 @@ export function SessionFileBrowserTab(props: {
       sidebar={
         <SessionReviewV2Sidebar
           open={sidebarOpened()}
+          transition={props.state.sidebarTransition()}
           title={<span class="truncate">{title()}</span>}
           filter={filter()}
           onFilterChange={setFilter}

+ 65 - 0
packages/app/test-browser/review-panel-v2-state.test.ts

@@ -0,0 +1,65 @@
+import { beforeAll, expect, mock, test } from "bun:test"
+import type { AsyncStorage } from "@solid-primitives/storage"
+import { createEffect, createRoot } from "solid-js"
+
+let createReviewPanelV2State: typeof import("@/pages/session/v2/review-panel-v2-state").createReviewPanelV2State
+let read: ((value: string | null) => void) | undefined
+
+const storage: AsyncStorage = {
+  getItem: () => new Promise((resolve) => (read = resolve)),
+  setItem: async () => undefined,
+  removeItem: async () => undefined,
+  clear: async () => undefined,
+  key: async () => null,
+  getLength: async () => 0,
+  length: Promise.resolve(0),
+}
+
+beforeAll(async () => {
+  mock.module("@opencode-ai/session-ui/v2/session-review-v2", () => ({
+    SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT: 240,
+    SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN: 200,
+    SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX: 480,
+  }))
+  mock.module("@/context/platform", () => ({
+    usePlatform: () => ({ platform: "desktop", storage: () => storage }),
+  }))
+
+  createReviewPanelV2State = (await import("@/pages/session/v2/review-panel-v2-state")).createReviewPanelV2State
+})
+
+test("enables sidebar motion only after custom width hydration", async () => {
+  await new Promise<void>((resolve, reject) => {
+    createRoot((dispose) => {
+      const state = createReviewPanelV2State()
+      const transition =
+        "sidebarTransition" in state && typeof state.sidebarTransition === "function"
+          ? (state.sidebarTransition as () => boolean)
+          : undefined
+
+      try {
+        expect(transition).toBeFunction()
+        expect(transition?.()).toBeFalse()
+        expect(state.sidebarWidth()).toBe(240)
+      } catch (error) {
+        dispose()
+        reject(error)
+        return
+      }
+
+      createEffect(() => {
+        if (!transition?.()) return
+        try {
+          expect(state.sidebarWidth()).toBe(360)
+          dispose()
+          resolve()
+        } catch (error) {
+          dispose()
+          reject(error)
+        }
+      })
+
+      read?.(JSON.stringify({ sidebarOpened: true, sidebarWidth: 360, expandMode: "collapse" }))
+    })
+  })
+})

+ 2 - 1
packages/session-ui/src/v2/components/session-review-v2.css

@@ -38,7 +38,8 @@
   border-right-width: 0;
 }
 
-[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar"]:not([data-resizing]) {
+[data-component="session-review-v2-sidebar-root"]
+  [data-slot="session-review-v2-sidebar"][data-transition]:not([data-resizing]) {
   transition:
     width 200ms cubic-bezier(0.22, 1, 0.36, 1),
     border-right-width 200ms cubic-bezier(0.22, 1, 0.36, 1);

+ 2 - 0
packages/session-ui/src/v2/components/session-review-v2.tsx

@@ -39,6 +39,7 @@ export type SessionReviewV2Props = {
 
 export type SessionReviewV2SidebarProps = {
   open: boolean
+  transition: boolean
   title?: JSX.Element
   stats?: JSX.Element
   filter: string
@@ -76,6 +77,7 @@ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) {
       <Show when={props.open}>
         <aside
           data-slot="session-review-v2-sidebar"
+          data-transition={props.transition ? "" : undefined}
           data-resizing={resizing() ? "" : undefined}
           style={{ width: `${width()}px` }}
         >