Explorar el Código

feat(app): allow toggling tabs layout (#29526)

Brendan Allan hace 3 meses
padre
commit
e1581183ff

+ 19 - 6
packages/app/src/app.tsx

@@ -14,6 +14,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
 import { Effect } from "effect"
 import { Effect } from "effect"
 import {
 import {
   type Component,
   type Component,
+  createEffect,
   createMemo,
   createMemo,
   createResource,
   createResource,
   createSignal,
   createSignal,
@@ -40,7 +41,7 @@ import { NotificationProvider } from "@/context/notification"
 import { PermissionProvider } from "@/context/permission"
 import { PermissionProvider } from "@/context/permission"
 import { PromptProvider } from "@/context/prompt"
 import { PromptProvider } from "@/context/prompt"
 import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
 import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
-import { SettingsProvider } from "@/context/settings"
+import { SettingsProvider, useSettings } from "@/context/settings"
 import { TerminalProvider } from "@/context/terminal"
 import { TerminalProvider } from "@/context/terminal"
 import DirectoryLayout from "@/pages/directory-layout"
 import DirectoryLayout from "@/pages/directory-layout"
 import Layout from "@/pages/layout"
 import Layout from "@/pages/layout"
@@ -48,11 +49,6 @@ import { ErrorPage } from "./pages/error"
 import { useCheckServerHealth } from "./utils/server-health"
 import { useCheckServerHealth } from "./utils/server-health"
 import { ServersProvider } from "./context/servers"
 import { ServersProvider } from "./context/servers"
 
 
-if (import.meta.env.VITE_OPENCODE_CHANNEL !== "prod") {
-  document.body.classList.remove("text-12-regular")
-  document.body.classList.add("font-(family-name:--font-family-text)", "text-[13px]", "font-[440]")
-}
-
 const HomeRoute = lazy(() => import("@/pages/home"))
 const HomeRoute = lazy(() => import("@/pages/home"))
 const Session = lazy(() => import("@/pages/session"))
 const Session = lazy(() => import("@/pages/session"))
 
 
@@ -97,9 +93,26 @@ function QueryProvider(props: ParentProps) {
   return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
   return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
 }
 }
 
 
+function BodyDesignClass() {
+  const settings = useSettings()
+
+  createEffect(() => {
+    if (typeof document === "undefined") return
+
+    const enabled = settings.general.newLayoutDesigns()
+    document.body.classList.toggle("text-12-regular", !enabled)
+    document.body.classList.toggle("font-(family-name:--font-family-text)", enabled)
+    document.body.classList.toggle("text-[13px]", enabled)
+    document.body.classList.toggle("font-[440]", enabled)
+  })
+
+  return null
+}
+
 function AppShellProviders(props: ParentProps) {
 function AppShellProviders(props: ParentProps) {
   return (
   return (
     <SettingsProvider>
     <SettingsProvider>
+      <BodyDesignClass />
       <PermissionProvider>
       <PermissionProvider>
         <LayoutProvider>
         <LayoutProvider>
           <NotificationProvider>
           <NotificationProvider>

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

@@ -79,8 +79,6 @@ import { pathKey } from "@/utils/path-key"
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { displayName } from "@/pages/layout/helpers"
 import { displayName } from "@/pages/layout/helpers"
 
 
-const USE_V2_INPUT = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
-
 interface PromptInputProps {
 interface PromptInputProps {
   class?: string
   class?: string
   variant?: "dock" | "new-session"
   variant?: "dock" | "new-session"
@@ -1456,7 +1454,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
         t={(key) => language.t(key as Parameters<typeof language.t>[0])}
         t={(key) => language.t(key as Parameters<typeof language.t>[0])}
       />
       />
       <Switch>
       <Switch>
-        <Match when={USE_V2_INPUT}>
+        <Match when={settings.general.newLayoutDesigns()}>
           <div class="flex flex-col gap-3">
           <div class="flex flex-col gap-3">
             <DockShellForm
             <DockShellForm
               data-component={newSession() ? "session-new-composer" : "session-composer"}
               data-component={newSession() ? "session-new-composer" : "session-composer"}
@@ -1528,7 +1526,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                     onKeyDown={handleKeyDown}
                     onKeyDown={handleKeyDown}
                     classList={{
                     classList={{
                       "select-text": true,
                       "select-text": true,
-                      "min-h-[52px] w-full px-4 pt-4 pb-2 focus:outline-none whitespace-pre-wrap leading-5 text-[13px] font-[440] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)]": true,
+                      "min-h-[52px] w-full px-4 pt-4 pb-2 focus:outline-none whitespace-pre-wrap leading-5 text-[13px] font-[440] text-v2-text-text-base": true,
                       "[&_[data-type=file]]:text-syntax-property": true,
                       "[&_[data-type=file]]:text-syntax-property": true,
                       "[&_[data-type=agent]]:text-syntax-type": true,
                       "[&_[data-type=agent]]:text-syntax-type": true,
                       "font-mono!": store.mode === "shell",
                       "font-mono!": store.mode === "shell",

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

@@ -45,8 +45,6 @@ const OPEN_APPS = [
   "sublime-text",
   "sublime-text",
 ] as const
 ] as const
 
 
-const USE_V2_TITLEBAR = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
-
 type OpenApp = (typeof OPEN_APPS)[number]
 type OpenApp = (typeof OPEN_APPS)[number]
 type OS = "macos" | "windows" | "linux" | "unknown"
 type OS = "macos" | "windows" | "linux" | "unknown"
 
 
@@ -157,11 +155,11 @@ export function SessionHeader() {
   })
   })
   const hotkey = createMemo(() => command.keybind("file.open"))
   const hotkey = createMemo(() => command.keybind("file.open"))
   const os = createMemo(() => detectOS(platform))
   const os = createMemo(() => detectOS(platform))
-  const isDesktopV2 = platform.platform === "desktop" && USE_V2_TITLEBAR
-  const search = createMemo(() => (isDesktopV2 ? settings.general.showSearch() : true))
-  const tree = createMemo(() => (isDesktopV2 ? settings.general.showFileTree() : true))
-  const term = createMemo(() => (isDesktopV2 ? settings.general.showTerminal() : true))
-  const status = createMemo(() => (isDesktopV2 ? settings.general.showStatus() : true))
+  const isDesktopV2 = createMemo(() => platform.platform === "desktop" && settings.general.newLayoutDesigns())
+  const search = createMemo(() => (isDesktopV2() ? settings.general.showSearch() : true))
+  const tree = createMemo(() => (isDesktopV2() ? settings.general.showFileTree() : true))
+  const term = createMemo(() => (isDesktopV2() ? settings.general.showTerminal() : true))
+  const status = createMemo(() => (isDesktopV2() ? settings.general.showStatus() : true))
 
 
   const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
   const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
     finder: true,
     finder: true,

+ 13 - 1
packages/app/src/components/settings-general.tsx

@@ -399,6 +399,18 @@ export const SettingsGeneral: Component = () => {
             />
             />
           </div>
           </div>
         </SettingsRow>
         </SettingsRow>
+
+        <SettingsRow
+          title={language.t("settings.general.row.newLayoutDesigns.title")}
+          description={language.t("settings.general.row.newLayoutDesigns.description")}
+        >
+          <div data-action="settings-new-layout-designs">
+            <Switch
+              checked={settings.general.newLayoutDesigns()}
+              onChange={(checked) => settings.general.setNewLayoutDesigns(checked)}
+            />
+          </div>
+        </SettingsRow>
       </SettingsList>
       </SettingsList>
     </div>
     </div>
   )
   )
@@ -802,7 +814,7 @@ export const SettingsGeneral: Component = () => {
 
 
         <DisplaySection />
         <DisplaySection />
 
 
-        <Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"}>
+        <Show when={desktop()}>
           <AdvancedSection />
           <AdvancedSection />
         </Show>
         </Show>
       </div>
       </div>

+ 6 - 6
packages/app/src/components/titlebar.tsx

@@ -55,7 +55,6 @@ const legacyTitlebarHeight = 40
 const v2TitlebarHeight = 44
 const v2TitlebarHeight = 44
 const minTitlebarZoom = 0.25
 const minTitlebarZoom = 0.25
 const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
 const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
-const USE_V2_TITLEBAR = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
 
 
 const makeSessionHref = (b64Dir: string, sessionId: string) => `/${b64Dir}/session/${sessionId}`
 const makeSessionHref = (b64Dir: string, sessionId: string) => `/${b64Dir}/session/${sessionId}`
 
 
@@ -75,6 +74,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
   const navigate = useNavigate()
   const navigate = useNavigate()
   const location = useLocation()
   const location = useLocation()
   const params = useParams()
   const params = useParams()
+  const useV2Titlebar = createMemo(() => settings.general.newLayoutDesigns())
 
 
   const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
   const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
   const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
   const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
@@ -85,7 +85,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
   const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
   const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
   const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
   const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
   const minHeight = () => {
   const minHeight = () => {
-    const height = USE_V2_TITLEBAR ? v2TitlebarHeight : legacyTitlebarHeight
+    const height = useV2Titlebar() ? v2TitlebarHeight : legacyTitlebarHeight
     if (mac()) return `${height / zoom()}px`
     if (mac()) return `${height / zoom()}px`
     if (windows()) return `${height / Math.min(titlebarZoom(), 1)}px`
     if (windows()) return `${height / Math.min(titlebarZoom(), 1)}px`
     return undefined
     return undefined
@@ -119,7 +119,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
   const canBack = createMemo(() => history.index > 0)
   const canBack = createMemo(() => history.index > 0)
   const canForward = createMemo(() => history.index < history.stack.length - 1)
   const canForward = createMemo(() => history.index < history.stack.length - 1)
   const hasProjects = createMemo(() => layout.projects.list().length > 0)
   const hasProjects = createMemo(() => layout.projects.list().length > 0)
-  const nav = createMemo(() => (USE_V2_TITLEBAR ? settings.general.showNavigation() : true))
+  const nav = createMemo(() => (useV2Titlebar() ? settings.general.showNavigation() : true))
   const updateState = createMemo<TitlebarUpdatePillState>(() => {
   const updateState = createMemo<TitlebarUpdatePillState>(() => {
     const version = props.update?.version()
     const version = props.update?.version()
     return {
     return {
@@ -222,8 +222,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
     <header
     <header
       classList={{
       classList={{
         "shrink-0 relative overflow-hidden flex flex-row": true,
         "shrink-0 relative overflow-hidden flex flex-row": true,
-        "h-11 bg-v2-background-bg-deep": USE_V2_TITLEBAR,
-        "h-10 bg-background-base": !USE_V2_TITLEBAR,
+        "h-11 bg-v2-background-bg-deep": useV2Titlebar(),
+        "h-10 bg-background-base": !useV2Titlebar(),
       }}
       }}
       style={{
       style={{
         "min-height": minHeight(),
         "min-height": minHeight(),
@@ -239,7 +239,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
       onDblClick={maximize}
       onDblClick={maximize}
     >
     >
       <Switch>
       <Switch>
-        <Match when={USE_V2_TITLEBAR}>
+        <Match when={useV2Titlebar()}>
           {(_) => {
           {(_) => {
             const serverSync = useServerSync()
             const serverSync = useServerSync()
             const navigate = useNavigate()
             const navigate = useNavigate()

+ 6 - 0
packages/app/src/context/settings.tsx

@@ -33,6 +33,7 @@ export interface Settings {
     editToolPartsExpanded: boolean
     editToolPartsExpanded: boolean
     showSessionProgressBar: boolean
     showSessionProgressBar: boolean
     showCustomAgents: boolean
     showCustomAgents: boolean
+    newLayoutDesigns?: boolean
   }
   }
   updates: {
   updates: {
     startup: boolean
     startup: boolean
@@ -54,6 +55,7 @@ export interface Settings {
 export const monoDefault = "System Mono"
 export const monoDefault = "System Mono"
 export const sansDefault = "System Sans"
 export const sansDefault = "System Sans"
 export const terminalDefault = "JetBrainsMono Nerd Font Mono"
 export const terminalDefault = "JetBrainsMono Nerd Font Mono"
+export const newLayoutDesignsDefault = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
 
 
 const monoFallback =
 const monoFallback =
   'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
   'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
@@ -242,6 +244,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
         setShowCustomAgents(value: boolean) {
         setShowCustomAgents(value: boolean) {
           setStore("general", "showCustomAgents", value)
           setStore("general", "showCustomAgents", value)
         },
         },
+        newLayoutDesigns: withFallback(() => store.general?.newLayoutDesigns, newLayoutDesignsDefault),
+        setNewLayoutDesigns(value: boolean) {
+          setStore("general", "newLayoutDesigns", value)
+        },
       },
       },
       updates: {
       updates: {
         startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
         startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),

+ 2 - 0
packages/app/src/i18n/en.ts

@@ -791,6 +791,8 @@ export const dict = {
   "settings.general.row.showSessionProgressBar.title": "Show session progress bar",
   "settings.general.row.showSessionProgressBar.title": "Show session progress bar",
   "settings.general.row.showSessionProgressBar.description":
   "settings.general.row.showSessionProgressBar.description":
     "Display the animated progress bar at the top of the session when the agent is working",
     "Display the animated progress bar at the top of the session when the agent is working",
+  "settings.general.row.newLayoutDesigns.title": "New layout and designs",
+  "settings.general.row.newLayoutDesigns.description": "Enable the redesigned layout, home, composer, and session UI",
   "settings.general.row.pinchZoom.title": "Pinch to zoom",
   "settings.general.row.pinchZoom.title": "Pinch to zoom",
   "settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom",
   "settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom",
 
 

+ 7 - 3
packages/app/src/pages/home.tsx

@@ -31,8 +31,8 @@ import { messageAgentColor } from "@/utils/agent"
 import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
 import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
 import { ServerHealthIndicator } from "@/components/server/server-row"
 import { ServerHealthIndicator } from "@/components/server/server-row"
 import { useServers } from "@/context/servers"
 import { useServers } from "@/context/servers"
+import { useSettings } from "@/context/settings"
 
 
-const USE_HOME_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
 const HOME_SESSION_LIMIT = 15
 const HOME_SESSION_LIMIT = 15
 const HOME_ROW =
 const HOME_ROW =
   "flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] border-0 bg-transparent text-left text-v2-text-text-muted transition-colors duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
   "flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] border-0 bg-transparent text-left text-v2-text-text-muted transition-colors duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
@@ -52,8 +52,12 @@ type HomeSessionGroup = {
 }
 }
 
 
 export default function Home() {
 export default function Home() {
-  if (USE_HOME_DESIGN) return <HomeDesign />
-  return <LegacyHome />
+  const settings = useSettings()
+  return (
+    <Show when={settings.general.newLayoutDesigns()} fallback={<LegacyHome />}>
+      <HomeDesign />
+    </Show>
+  )
 }
 }
 
 
 function HomeDesign() {
 function HomeDesign() {

+ 155 - 155
packages/app/src/pages/layout.tsx

@@ -89,8 +89,6 @@ import {
 import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
 import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
 import { SidebarContent } from "./layout/sidebar-shell"
 import { SidebarContent } from "./layout/sidebar-shell"
 
 
-const USE_NEW_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
-
 export default function Layout(props: ParentProps) {
 export default function Layout(props: ParentProps) {
   const [store, setStore, , ready] = persisted(
   const [store, setStore, , ready] = persisted(
     Persist.global("layout.page", ["layout.page.v1"]),
     Persist.global("layout.page", ["layout.page.v1"]),
@@ -129,6 +127,7 @@ export default function Layout(props: ParentProps) {
   const command = useCommand()
   const command = useCommand()
   const theme = useTheme()
   const theme = useTheme()
   const language = useLanguage()
   const language = useLanguage()
+  const newDesign = createMemo(() => settings.general.newLayoutDesigns())
   const initialDirectory = decode64(params.dir)
   const initialDirectory = decode64(params.dir)
   const location = useLocation()
   const location = useLocation()
   const route = createMemo(() => {
   const route = createMemo(() => {
@@ -154,7 +153,7 @@ export default function Layout(props: ParentProps) {
   const currentDir = createMemo(() => route().dir)
   const currentDir = createMemo(() => route().dir)
 
 
   const [state, setState] = createStore({
   const [state, setState] = createStore({
-    autoselect: !initialDirectory && !USE_NEW_DESIGN,
+    autoselect: !initialDirectory && !newDesign(),
     busyWorkspaces: {} as Record<string, boolean>,
     busyWorkspaces: {} as Record<string, boolean>,
     hoverProject: undefined as string | undefined,
     hoverProject: undefined as string | undefined,
     scrollSessionKey: undefined as string | undefined,
     scrollSessionKey: undefined as string | undefined,
@@ -1142,7 +1141,7 @@ export default function Layout(props: ParentProps) {
       },
       },
     ]
     ]
 
 
-    if (!USE_NEW_DESIGN)
+    if (!newDesign())
       Array.from({ length: 9 }, (_, i) => {
       Array.from({ length: 9 }, (_, i) => {
         const index = i
         const index = i
         const number = index + 1
         const number = index + 1
@@ -1821,7 +1820,7 @@ export default function Layout(props: ParentProps) {
   createEffect(() => {
   createEffect(() => {
     document.documentElement.style.setProperty(
     document.documentElement.style.setProperty(
       "--dialog-left-margin",
       "--dialog-left-margin",
-      USE_NEW_DESIGN ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
+      newDesign() ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
     )
     )
   })
   })
 
 
@@ -2363,179 +2362,180 @@ export default function Layout(props: ParentProps) {
     />
     />
   )
   )
 
 
-  if (USE_NEW_DESIGN) {
-    return (
-      <div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
+  return (
+    <Show
+      when={!newDesign()}
+      fallback={
+        <div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
+          {autoselecting() ?? ""}
+          <Titlebar update={titlebarUpdate} />
+          <main
+            class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict bg-v2-background-bg-base"
+            classList={{
+              "m-2 mt-0 rounded-[10px] shadow-[var(--v2-elevation-raised)] overflow-hidden": !!params.id || !params.dir,
+            }}
+          >
+            <Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
+              {props.children}
+            </Show>
+          </main>
+          {import.meta.env.DEV && <DebugBar />}
+          <Toast.Region />
+        </div>
+      }
+    >
+      <div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
         {autoselecting() ?? ""}
         {autoselecting() ?? ""}
         <Titlebar update={titlebarUpdate} />
         <Titlebar update={titlebarUpdate} />
-        <main
-          class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict bg-v2-background-bg-base"
-          classList={{
-            "m-2 mt-0 rounded-[10px] shadow-[var(--v2-elevation-raised)] overflow-hidden": !!params.id || !params.dir,
-          }}
-        >
-          <Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
-            {props.children}
-          </Show>
-        </main>
-        {import.meta.env.DEV && <DebugBar />}
-        <Toast.Region />
-      </div>
-    )
-  }
+        <Show when={updateVersion() !== undefined}>
+          <UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
+        </Show>
+        <div class="flex-1 min-h-0 min-w-0 flex">
+          <div class="flex-1 min-h-0 relative">
+            <div class="size-full relative overflow-x-hidden">
+              <nav
+                aria-label={language.t("sidebar.nav.projectsAndSessions")}
+                data-component="sidebar-nav-desktop"
+                classList={{
+                  "hidden xl:block": true,
+                  "absolute inset-y-0 left-0": true,
+                  "z-10": true,
+                }}
+                style={{ width: `${side()}px` }}
+                ref={(el) => {
+                  setState("nav", el)
+                }}
+                onMouseEnter={() => {
+                  disarm()
+                }}
+                onMouseLeave={() => {
+                  aim.reset()
+                  if (!sidebarHovering()) return
 
 
-  return (
-    <div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
-      {autoselecting() ?? ""}
-      <Titlebar update={titlebarUpdate} />
-      <Show when={updateVersion() !== undefined}>
-        <UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
-      </Show>
-      <div class="flex-1 min-h-0 min-w-0 flex">
-        <div class="flex-1 min-h-0 relative">
-          <div class="size-full relative overflow-x-hidden">
-            <nav
-              aria-label={language.t("sidebar.nav.projectsAndSessions")}
-              data-component="sidebar-nav-desktop"
-              classList={{
-                "hidden xl:block": true,
-                "absolute inset-y-0 left-0": true,
-                "z-10": true,
-              }}
-              style={{ width: `${side()}px` }}
-              ref={(el) => {
-                setState("nav", el)
-              }}
-              onMouseEnter={() => {
-                disarm()
-              }}
-              onMouseLeave={() => {
-                aim.reset()
-                if (!sidebarHovering()) return
-
-                arm()
-              }}
-            >
-              <div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
-            </nav>
-
-            <Show when={layout.sidebar.opened()}>
-              <div
-                class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
-                style={{ left: `${side()}px` }}
-                onPointerDown={() => setState("sizing", true)}
+                  arm()
+                }}
               >
               >
-                <ResizeHandle
-                  direction="horizontal"
-                  size={layout.sidebar.width()}
-                  min={244}
-                  max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
-                  onResize={(w) => {
-                    setState("sizing", true)
-                    if (sizet !== undefined) clearTimeout(sizet)
-                    sizet = window.setTimeout(() => setState("sizing", false), 120)
-                    layout.sidebar.resize(w)
+                <div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
+              </nav>
+
+              <Show when={layout.sidebar.opened()}>
+                <div
+                  class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
+                  style={{ left: `${side()}px` }}
+                  onPointerDown={() => setState("sizing", true)}
+                >
+                  <ResizeHandle
+                    direction="horizontal"
+                    size={layout.sidebar.width()}
+                    min={244}
+                    max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
+                    onResize={(w) => {
+                      setState("sizing", true)
+                      if (sizet !== undefined) clearTimeout(sizet)
+                      sizet = window.setTimeout(() => setState("sizing", false), 120)
+                      layout.sidebar.resize(w)
+                    }}
+                  />
+                </div>
+              </Show>
+
+              <div
+                class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
+                style={{ left: "calc(4rem + 12px)" }}
+              />
+
+              <div class="xl:hidden">
+                <div
+                  classList={{
+                    "fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
+                    "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
+                    "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
+                  }}
+                  onClick={(e) => {
+                    if (e.target === e.currentTarget) layout.mobileSidebar.hide()
                   }}
                   }}
                 />
                 />
+                <nav
+                  aria-label={language.t("sidebar.nav.projectsAndSessions")}
+                  data-component="sidebar-nav-mobile"
+                  classList={{
+                    "@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
+                    "translate-x-0": layout.mobileSidebar.opened(),
+                    "-translate-x-full": !layout.mobileSidebar.opened(),
+                  }}
+                  onClick={(e) => e.stopPropagation()}
+                >
+                  {sidebarContent(true)}
+                </nav>
               </div>
               </div>
-            </Show>
-
-            <div
-              class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
-              style={{ left: "calc(4rem + 12px)" }}
-            />
 
 
-            <div class="xl:hidden">
               <div
               <div
                 classList={{
                 classList={{
-                  "fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
-                  "opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
-                  "opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
+                  "absolute inset-0": true,
+                  "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
+                  "z-20": true,
+                  "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
+                    !state.sizing,
                 }}
                 }}
-                onClick={(e) => {
-                  if (e.target === e.currentTarget) layout.mobileSidebar.hide()
+                style={{
+                  "--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
                 }}
                 }}
-              />
-              <nav
-                aria-label={language.t("sidebar.nav.projectsAndSessions")}
-                data-component="sidebar-nav-mobile"
-                classList={{
-                  "@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
-                  "translate-x-0": layout.mobileSidebar.opened(),
-                  "-translate-x-full": !layout.mobileSidebar.opened(),
-                }}
-                onClick={(e) => e.stopPropagation()}
               >
               >
-                {sidebarContent(true)}
-              </nav>
-            </div>
+                <main
+                  classList={{
+                    "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
+                  }}
+                >
+                  <Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
+                    {props.children}
+                  </Show>
+                </main>
+              </div>
 
 
-            <div
-              classList={{
-                "absolute inset-0": true,
-                "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
-                "z-20": true,
-                "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
-                  !state.sizing,
-              }}
-              style={{
-                "--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
-              }}
-            >
-              <main
+              <div
                 classList={{
                 classList={{
-                  "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
+                  "hidden xl:flex absolute inset-y-0 left-16 z-30": true,
+                  "opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
+                  "opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
+                  "transition-[opacity,transform] motion-reduce:transition-none": true,
+                  "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
+                  "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
+                }}
+                onMouseMove={disarm}
+                onMouseEnter={() => {
+                  disarm()
+                  aim.reset()
+                }}
+                onPointerDown={disarm}
+                onMouseLeave={() => {
+                  arm()
                 }}
                 }}
               >
               >
-                <Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
-                  {props.children}
+                <Show when={peekProject()}>
+                  <SidebarPanel project={peekProject} merged={false} />
                 </Show>
                 </Show>
-              </main>
-            </div>
-
-            <div
-              classList={{
-                "hidden xl:flex absolute inset-y-0 left-16 z-30": true,
-                "opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
-                "opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
-                "transition-[opacity,transform] motion-reduce:transition-none": true,
-                "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
-                "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
-              }}
-              onMouseMove={disarm}
-              onMouseEnter={() => {
-                disarm()
-                aim.reset()
-              }}
-              onPointerDown={disarm}
-              onMouseLeave={() => {
-                arm()
-              }}
-            >
-              <Show when={peekProject()}>
-                <SidebarPanel project={peekProject} merged={false} />
-              </Show>
-            </div>
+              </div>
 
 
-            <div
-              classList={{
-                "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
-                "opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
-                "opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
-                "transition-[opacity,transform] motion-reduce:transition-none": true,
-                "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
-                "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
-              }}
-              style={{ left: `calc(4rem + ${panel()}px)` }}
-            >
-              <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
+              <div
+                classList={{
+                  "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
+                  "opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
+                  "opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
+                  "transition-[opacity,transform] motion-reduce:transition-none": true,
+                  "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
+                  "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
+                }}
+                style={{ left: `calc(4rem + ${panel()}px)` }}
+              >
+                <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
+              </div>
             </div>
             </div>
           </div>
           </div>
+          {import.meta.env.DEV && <DebugBar />}
         </div>
         </div>
-        {import.meta.env.DEV && <DebugBar />}
+        <Toast.Region />
       </div>
       </div>
-      <Toast.Region />
-    </div>
+    </Show>
   )
   )
 }
 }
 
 

+ 4 - 4
packages/app/src/pages/session.tsx

@@ -75,7 +75,6 @@ const emptyFollowups: FollowupItem[] = []
 
 
 type ChangeMode = "git" | "branch" | "turn"
 type ChangeMode = "git" | "branch" | "turn"
 type VcsMode = "git" | "branch"
 type VcsMode = "git" | "branch"
-const USE_NEW_SESSION_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
 
 
 type SessionHistoryWindowInput = {
 type SessionHistoryWindowInput = {
   sessionID: () => string | undefined
   sessionID: () => string | undefined
@@ -198,6 +197,7 @@ export default function Page() {
   const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
   const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
   const location = useLocation()
   const location = useLocation()
   const { params, sessionKey, tabs, view } = useSessionLayout()
   const { params, sessionKey, tabs, view } = useSessionLayout()
+  const newSessionDesign = createMemo(() => settings.general.newLayoutDesigns())
 
 
   createEffect(() => {
   createEffect(() => {
     if (!prompt.ready()) return
     if (!prompt.ready()) return
@@ -265,7 +265,7 @@ export default function Page() {
   const isDesktop = createMediaQuery("(min-width: 768px)")
   const isDesktop = createMediaQuery("(min-width: 768px)")
   const size = createSizing()
   const size = createSizing()
   const isV2NewSessionPage = () =>
   const isV2NewSessionPage = () =>
-    shouldUseV2NewSessionPage({ channel: import.meta.env.VITE_OPENCODE_CHANNEL, sessionID: params.id })
+    shouldUseV2NewSessionPage({ newLayoutDesigns: newSessionDesign(), sessionID: params.id })
   const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
   const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
   const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
   const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
   const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
   const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
@@ -1798,14 +1798,14 @@ export default function Page() {
                 </Show>
                 </Show>
               </Match>
               </Match>
               <Match when={true}>
               <Match when={true}>
-                <Show when={USE_NEW_SESSION_DESIGN} fallback={<NewSessionView worktree={newSessionWorktree()} />}>
+                <Show when={newSessionDesign()} fallback={<NewSessionView worktree={newSessionWorktree()} />}>
                   <NewSessionDesignView>{composerRegion("inline")}</NewSessionDesignView>
                   <NewSessionDesignView>{composerRegion("inline")}</NewSessionDesignView>
                 </Show>
                 </Show>
               </Match>
               </Match>
             </Switch>
             </Switch>
           </div>
           </div>
 
 
-          <Show when={params.id || !USE_NEW_SESSION_DESIGN}>{composerRegion("dock")}</Show>
+          <Show when={params.id || !newSessionDesign()}>{composerRegion("dock")}</Show>
 
 
           <Show when={desktopReviewOpen()}>
           <Show when={desktopReviewOpen()}>
             <div onPointerDown={() => size.start()}>
             <div onPointerDown={() => size.start()}>

+ 6 - 6
packages/app/src/pages/session/new-session-layout.test.ts

@@ -2,13 +2,13 @@ import { describe, expect, test } from "bun:test"
 import { shouldUseV2NewSessionPage } from "./new-session-layout"
 import { shouldUseV2NewSessionPage } from "./new-session-layout"
 
 
 describe("shouldUseV2NewSessionPage", () => {
 describe("shouldUseV2NewSessionPage", () => {
-  test("keeps prod session pages on the legacy layout", () => {
-    expect(shouldUseV2NewSessionPage({ channel: "prod", sessionID: "ses_123" })).toBe(false)
-    expect(shouldUseV2NewSessionPage({ channel: "prod" })).toBe(false)
+  test("keeps disabled pages on the legacy layout", () => {
+    expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false, sessionID: "ses_123" })).toBe(false)
+    expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false })).toBe(false)
   })
   })
 
 
-  test("uses the v2 layout only for non-prod new-session pages", () => {
-    expect(shouldUseV2NewSessionPage({ channel: "dev" })).toBe(true)
-    expect(shouldUseV2NewSessionPage({ channel: "dev", sessionID: "ses_123" })).toBe(false)
+  test("uses the v2 layout only for enabled new-session pages", () => {
+    expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true })).toBe(true)
+    expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true, sessionID: "ses_123" })).toBe(false)
   })
   })
 })
 })

+ 2 - 2
packages/app/src/pages/session/new-session-layout.ts

@@ -1,3 +1,3 @@
-export function shouldUseV2NewSessionPage(input: { channel?: "dev" | "beta" | "prod"; sessionID?: string }) {
-  return input.channel !== "prod" && !input.sessionID
+export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) {
+  return input.newLayoutDesigns && !input.sessionID
 }
 }

+ 2 - 4
packages/app/src/pages/session/session-side-panel.tsx

@@ -28,8 +28,6 @@ import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type S
 import { setSessionHandoff } from "@/pages/session/handoff"
 import { setSessionHandoff } from "@/pages/session/handoff"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { useSessionLayout } from "@/pages/session/session-layout"
 
 
-const USE_DESKTOP_V2 = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
-
 type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
 type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
 
 
 function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
 function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
@@ -60,7 +58,7 @@ export function SessionSidePanel(props: {
   const { sessionKey, tabs, view, params } = useSessionLayout()
   const { sessionKey, tabs, view, params } = useSessionLayout()
 
 
   const isDesktop = createMediaQuery("(min-width: 768px)")
   const isDesktop = createMediaQuery("(min-width: 768px)")
-  const desktopV2 = () => platform.platform === "desktop" && USE_DESKTOP_V2
+  const desktopV2 = () => platform.platform === "desktop" && settings.general.newLayoutDesigns()
   const shown = createMemo(() => (desktopV2() ? settings.general.showFileTree() : true))
   const shown = createMemo(() => (desktopV2() ? settings.general.showFileTree() : true))
 
 
   const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
   const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
@@ -205,7 +203,7 @@ export function SessionSidePanel(props: {
   })
   })
 
 
   return (
   return (
-    <Show when={isDesktop() && !(import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" && !params.id)}>
+    <Show when={isDesktop() && !(settings.general.newLayoutDesigns() && !params.id)}>
       <aside
       <aside
         id="review-panel"
         id="review-panel"
         aria-label={language.t("session.panel.reviewAndFiles")}
         aria-label={language.t("session.panel.reviewAndFiles")}

+ 1 - 3
packages/app/src/pages/session/use-session-commands.tsx

@@ -20,8 +20,6 @@ import { extractPromptFromParts } from "@/utils/prompt"
 import { UserMessage } from "@opencode-ai/sdk/v2"
 import { UserMessage } from "@opencode-ai/sdk/v2"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { useSessionLayout } from "@/pages/session/session-layout"
 
 
-const USE_DESKTOP_V2 = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
-
 export type SessionCommandContext = {
 export type SessionCommandContext = {
   navigateMessageByOffset: (offset: number) => void
   navigateMessageByOffset: (offset: number) => void
   setActiveMessage: (message: UserMessage | undefined) => void
   setActiveMessage: (message: UserMessage | undefined) => void
@@ -72,7 +70,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
   })
   })
   const activeFileTab = tabState.activeFileTab
   const activeFileTab = tabState.activeFileTab
   const closableTab = tabState.closableTab
   const closableTab = tabState.closableTab
-  const desktopV2 = () => platform.platform === "desktop" && USE_DESKTOP_V2
+  const desktopV2 = () => platform.platform === "desktop" && settings.general.newLayoutDesigns()
   const shown = () => (desktopV2() ? settings.general.showFileTree() : true)
   const shown = () => (desktopV2() ? settings.general.showFileTree() : true)
 
 
   const messages = () => {
   const messages = () => {