Просмотр исходного кода

feat(app): /new-session route for new design (#31457)

Brendan Allan 2 месяцев назад
Родитель
Сommit
0fc33e2a06

+ 84 - 8
packages/app/src/app.tsx

@@ -9,7 +9,7 @@ import { Font } from "@opencode-ai/ui/font"
 import { Splash } from "@opencode-ai/ui/logo"
 import { ThemeProvider } from "@opencode-ai/ui/theme/context"
 import { MetaProvider } from "@solidjs/meta"
-import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
+import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
 import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
 import { Effect } from "effect"
 import {
@@ -43,25 +43,88 @@ import { PromptProvider } from "@/context/prompt"
 import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
 import { SettingsProvider, useSettings } from "@/context/settings"
 import { TerminalProvider } from "@/context/terminal"
-import { TabsProvider } from "@/context/tabs"
+import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs"
+import { SDKProvider, useSDK } from "@/context/sdk"
 import { WslServersProvider } from "@/wsl/context"
-import DirectoryLayout from "@/pages/directory-layout"
+import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout"
 import Layout from "@/pages/layout"
 import { ErrorPage } from "./pages/error"
 import { useCheckServerHealth } from "./utils/server-health"
 
 const HomeRoute = lazy(() => import("@/pages/home"))
 const Session = lazy(() => import("@/pages/session"))
+const NewSession = lazy(() => import("@/pages/new-session"))
 
 const SessionRoute = Object.assign(
-  () => (
-    <SessionProviders>
-      <Session />
-    </SessionProviders>
-  ),
+  () => {
+    const settings = useSettings()
+    const params = useParams()
+    const [search] = useSearchParams<{ draftId?: string; prompt?: string }>()
+    const sdk = useSDK()
+    const server = useServer()
+    const tabs = useTabs()
+
+    // When the new layout is enabled, the legacy new-session route (/:dir/session with no id)
+    // is replaced by a draft at /new-session?draftId=…
+    createEffect(() => {
+      if (!settings.general.newLayoutDesigns()) return
+      if (params.id || search.draftId) return
+      if (!tabs.ready() || !sdk.directory) return
+      tabs.newDraft({ server: server.key, directory: sdk.directory }, search.prompt)
+    })
+
+    return (
+      <SessionProviders>
+        <Session />
+      </SessionProviders>
+    )
+  },
   { preload: Session.preload },
 )
 
+function DraftRoute() {
+  const [search] = useSearchParams<{ draftId?: string }>()
+  const tabs = useTabs()
+  return (
+    <Show when={tabs.ready()}>
+      <Show when={search.draftId} keyed fallback={<Navigate href="/" />}>
+        {(draftID) => <ResolvedDraftRoute draftID={draftID} />}
+      </Show>
+    </Show>
+  )
+}
+
+function ResolvedDraftRoute(props: { draftID: string }) {
+  const server = useServer()
+  const tabs = useTabs()
+  const draft = createMemo(() =>
+    tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === props.draftID),
+  )
+
+  createEffect(() => {
+    const current = draft()
+    if (current && current.server !== server.key) server.setActive(current.server)
+  })
+
+  // Key on the directory so retargeting the draft's project re-instantiates the
+  // SDK/data providers for the new directory while keeping the same draft id.
+  const directory = () => draft()?.directory
+
+  return (
+    <Show when={directory()} keyed>
+      {(dir) => (
+        <SDKProvider directory={dir}>
+          <DirectoryDataProvider directory={dir} draftID={props.draftID}>
+            <DraftProviders>
+              <NewSession />
+            </DraftProviders>
+          </DirectoryDataProvider>
+        </SDKProvider>
+      )}
+    </Show>
+  )
+}
+
 function UiI18nBridge(props: ParentProps) {
   const language = useLanguage()
   return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
@@ -141,6 +204,18 @@ function SessionProviders(props: ParentProps) {
   )
 }
 
+// The draft page only renders the prompt composer, so it drops TerminalProvider.
+// FileProvider and CommentsProvider stay because PromptInput uses file search and comment context.
+function DraftProviders(props: ParentProps) {
+  return (
+    <FileProvider>
+      <PromptProvider>
+        <CommentsProvider>{props.children}</CommentsProvider>
+      </PromptProvider>
+    </FileProvider>
+  )
+}
+
 function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
   return (
     <AppShellProviders>
@@ -335,6 +410,7 @@ export function AppInterface(props: {
             )}
           >
             <Route path="/" component={HomeRoute} />
+            <Route path="/new-session" component={DraftRoute} />
             <Route path="/:dir" component={DirectoryLayout}>
               <Route path="/" component={() => <Navigate href="session" />} />
               <Route path="/session/:id?" component={SessionRoute} />

+ 2 - 0
packages/app/src/components/file-tree.test.ts

@@ -8,6 +8,8 @@ beforeAll(async () => {
   mock.module("@solidjs/router", () => ({
     useNavigate: () => () => undefined,
     useParams: () => ({}),
+    useLocation: () => ({}),
+    useSearchParams: () => [{}, () => undefined],
   }))
   mock.module("@/context/file", () => ({
     useFile: () => ({

+ 14 - 1
packages/app/src/components/prompt-input.tsx

@@ -31,9 +31,10 @@ import {
   FileAttachmentPart,
 } from "@/context/prompt"
 import { useLayout } from "@/context/layout"
-import { useNavigate } from "@solidjs/router"
+import { useNavigate, useSearchParams } from "@solidjs/router"
 import { useSDK } from "@/context/sdk"
 import { useServer } from "@/context/server"
+import { useTabs } from "@/context/tabs"
 import { useSync } from "@/context/sync"
 import { useComments } from "@/context/comments"
 import { Button } from "@opencode-ai/ui/button"
@@ -144,6 +145,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const platform = usePlatform()
   const pickDirectory = useDirectoryPicker()
   const settings = useSettings()
+  const tabsStore = useTabs()
+  const [search] = useSearchParams<{ draftId?: string }>()
   const { params, tabs, view } = useSessionLayout()
   let editorRef!: HTMLDivElement
   let fileInputRef: HTMLInputElement | undefined
@@ -1398,6 +1401,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     }
     layout.projects.open(worktree)
     server.projects.touch(worktree)
+
+    // On the draft route, retarget the existing draft in place so we keep the same
+    // draft id (and its tab/prompt) instead of spawning a new draft for the new directory.
+    const draftID = search.draftId
+    if (draftID) {
+      tabsStore.updateDraft(draftID, { server: server.key, directory: worktree })
+      restoreFocus()
+      return
+    }
+
     navigate(`/${base64Encode(worktree)}/session`)
   }
   const addProject = () => {

+ 12 - 0
packages/app/src/components/prompt-input/submit.test.ts

@@ -61,6 +61,8 @@ beforeAll(async () => {
   mock.module("@solidjs/router", () => ({
     useNavigate: () => () => undefined,
     useParams: () => params,
+    useLocation: () => ({}),
+    useSearchParams: () => [{}, () => undefined],
   }))
 
   mock.module("@opencode-ai/sdk/v2/client", () => ({
@@ -103,6 +105,16 @@ beforeAll(async () => {
     }),
   }))
 
+  mock.module("@/context/server", () => ({
+    useServer: () => ({ key: "server-key" }),
+  }))
+
+  mock.module("@/context/tabs", () => ({
+    useTabs: () => ({
+      promoteDraft: () => undefined,
+    }),
+  }))
+
   mock.module("@/context/prompt", () => ({
     usePrompt: () => ({
       current: () => promptValue,

+ 14 - 2
packages/app/src/components/prompt-input/submit.ts

@@ -2,9 +2,11 @@ import type { Message, Session } from "@opencode-ai/sdk/v2/client"
 import { showToast } from "@/utils/toast"
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { Binary } from "@opencode-ai/core/util/binary"
-import { useNavigate, useParams } from "@solidjs/router"
+import { useNavigate, useParams, useSearchParams } from "@solidjs/router"
 import { batch, type Accessor } from "solid-js"
 import type { FileSelection } from "@/context/file"
+import { useServer } from "@/context/server"
+import { useTabs } from "@/context/tabs"
 import { useServerSync } from "@/context/server-sync"
 import { useLanguage } from "@/context/language"
 import { useLayout } from "@/context/layout"
@@ -213,6 +215,9 @@ export function createPromptSubmit(input: PromptSubmitInput) {
   const layout = useLayout()
   const language = useLanguage()
   const params = useParams()
+  const [search] = useSearchParams<{ draftId?: string }>()
+  const server = useServer()
+  const tabs = useTabs()
   const pendingKey = (sessionID: string) => ScopedKey.from(sdk.scope, sessionID)
 
   const errorMessage = (err: unknown) => {
@@ -381,7 +386,14 @@ export function createPromptSubmit(input: PromptSubmitInput) {
         if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
         local.session.promote(sessionDirectory, session.id)
         layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
-        navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
+        const draftID = search.draftId
+        if (draftID)
+          tabs.promoteDraft(draftID, {
+            server: server.key,
+            dirBase64: base64Encode(sessionDirectory),
+            sessionId: session.id,
+          })
+        else navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
       }
     }
     if (!session) {

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

@@ -280,7 +280,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
 
             const matchRoute = (route: LayoutRoute) => {
               if (route.type === "home") return
-              if (route.type === "dir-new-sesssion") {
+              if (route.type === "draft") {
+                return tabsStore.find((item) => item.type === "draft" && item.draftID === route.draftID)
               }
               if (route.type === "session") {
                 const main = tabsStore.find(
@@ -447,13 +448,33 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
                           refreshTabsAreOverflowing()
                         })
 
-                        if (tab.type !== "session") return null
+                        const divider = () =>
+                          i() !== 0 && (
+                            <div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
+                          )
+
+                        if (tab.type === "draft") {
+                          return (
+                            <>
+                              {divider()}
+                              <DraftTabItem
+                                ref={ref}
+                                href={tabHref(tab)}
+                                title={language.t("command.session.new")}
+                                active={currentTab() === tab}
+                                onNavigate={() => {
+                                  navigateTab(tab)
+                                  ref.scrollIntoView({ behavior: "instant" })
+                                }}
+                                onClose={() => tabsStoreActions.removeTab(i())}
+                              />
+                            </>
+                          )
+                        }
 
                         return (
                           <>
-                            {i() !== 0 && (
-                              <div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
-                            )}
+                            {divider()}
                             <TabNavItem
                               ref={ref}
                               href={tabHref(tab)}
@@ -784,7 +805,6 @@ function TabNavItem(props: {
     >
       <Show when={session.latest}>
         {(session) => {
-          console.log({ session: session() })
           const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? []))
 
           return (
@@ -853,6 +873,59 @@ function ProjectTabAvatar(props: {
   )
 }
 
+function DraftTabItem(props: {
+  ref?: HTMLDivElement
+  href: string
+  title: string
+  active?: boolean
+  onNavigate: () => void
+  onClose: () => void
+}) {
+  const closeTab = (event: MouseEvent) => {
+    event.preventDefault()
+    event.stopPropagation()
+    props.onClose()
+  }
+  return (
+    <div
+      ref={props.ref}
+      data-active={props.active}
+      class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] pl-1.5 pr-8 whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)] focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
+      onMouseDown={(event) => {
+        if (event.button !== 1) return
+        closeTab(event)
+      }}
+    >
+      <a
+        href={props.href}
+        onClick={(event) => {
+          event.preventDefault()
+          props.onNavigate()
+        }}
+        class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-5 text-v2-text-text-faint group-data-[active='true']:text-[var(--v2-text-text-base)]"
+      >
+        <span class="flex size-4 shrink-0 rotate-90 items-center justify-center">
+          <IconV2 name="edit" />
+        </span>
+        <span class="truncate leading-5">{props.title}</span>
+      </a>
+      <div class="absolute right-0 inset-y-0 flex w-7 items-center justify-center">
+        <IconButtonV2
+          size="small"
+          variant="ghost-muted"
+          onMouseDown={(event) => {
+            event.preventDefault()
+            event.stopPropagation()
+          }}
+          onClick={closeTab}
+          icon={<IconV2 name="xmark-small" />}
+          aria-label="Close tab"
+        />
+      </div>
+    </div>
+  )
+}
+
 function NewSessionTabItem(props: { ref?: HTMLDivElement; href: string; title: string; onClose: () => void }) {
   const closeTab = (event: MouseEvent) => {
     event.preventDefault()

+ 2 - 0
packages/app/src/context/comments.test.ts

@@ -8,6 +8,8 @@ beforeAll(async () => {
   mock.module("@solidjs/router", () => ({
     useNavigate: () => () => undefined,
     useParams: () => ({}),
+    useLocation: () => ({}),
+    useSearchParams: () => [{}, () => undefined],
   }))
   mock.module("@opencode-ai/ui/context", () => ({
     createSimpleContext: () => ({

+ 9 - 2
packages/app/src/context/layout.tsx

@@ -77,6 +77,7 @@ export type ReviewDiffStyle = "unified" | "split"
 
 export type LayoutRoute =
   | { type: "home" }
+  | { type: "draft"; draftID: string; server?: ServerConnection.Key }
   | { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key }
   | { type: "session"; dir: string; dirBase64: string; sessionId: string; server?: ServerConnection.Key }
 
@@ -120,10 +121,16 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
   }
 }
 
-const currentRoute = (pathname: string): LayoutRoute => {
+const currentRoute = (pathname: string, search: string): LayoutRoute => {
   const parts = pathname.split("/").filter(Boolean)
   if (parts.length === 0) return { type: "home" }
 
+  if (parts[0] === "new-session") {
+    const draftID = new URLSearchParams(search).get("draftId")
+    if (!draftID) return { type: "home" }
+    return { type: "draft", draftID }
+  }
+
   const dirBase64 = parts[0]
   const dir = decode64(dirBase64)
   if (!dir) return { type: "home" }
@@ -145,7 +152,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
     const platform = usePlatform()
     const location = useLocation()
     const route = createMemo(() => {
-      const value = currentRoute(location.pathname)
+      const value = currentRoute(location.pathname, location.search)
       if (value.type === "home") return value
       return { ...value, server: server.key }
     })

+ 16 - 9
packages/app/src/context/tabs.tsx

@@ -5,7 +5,7 @@ import { createStore, produce } from "solid-js/store"
 import { Persist, persisted, removePersisted, draftPersistedKeys } from "@/utils/persist"
 import { ServerConnection, useServer } from "./server"
 import { createEffect, startTransition } from "solid-js"
-import { useNavigate, useParams } from "@solidjs/router"
+import { useLocation, useNavigate, useParams } from "@solidjs/router"
 import { usePlatform } from "./platform"
 import { uuid } from "@/utils/uuid"
 import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events"
@@ -65,6 +65,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
 
     const params = useParams()
     const navigate = useNavigate()
+    const location = useLocation()
 
     const closing = new Set<string>()
 
@@ -123,14 +124,20 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
         )
       },
       promoteDraft(draftID: string, session: Omit<SessionTab, "type">) {
-        const active = `${location.pathname}${location.search}` === draftHref(draftID)
-        setStore(
-          produce((tabs) => {
-            const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID)
-            if (index !== -1) tabs[index] = { type: "session", ...session }
-          }),
-        )
-        if (active) navigateTab({ type: "session", ...session })
+        // We're viewing this draft when /new-session?draftId=… points at it. Promoting
+        // replaces the draft tab with a session tab, so the draft route would stop resolving
+        // and fall back home. Navigate to the new session first so we leave /new-session
+        // before the draft is removed from the store.
+        const active = location.pathname === "/new-session" && location.query.draftId === draftID
+        startTransition(() => {
+          setStore(
+            produce((tabs) => {
+              const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID)
+              if (index !== -1) tabs[index] = { type: "session", ...session }
+            }),
+          )
+          if (active) navigateTab({ type: "session", ...session })
+        })
         removeDraftPersisted(draftID)
       },
       removeTab: (index: number) => {

+ 2 - 0
packages/app/src/context/terminal.test.ts

@@ -9,6 +9,8 @@ beforeAll(async () => {
   mock.module("@solidjs/router", () => ({
     useNavigate: () => () => undefined,
     useParams: () => ({}),
+    useLocation: () => ({}),
+    useSearchParams: () => [{}, () => undefined],
   }))
   mock.module("@opencode-ai/ui/context", () => ({
     createSimpleContext: () => ({

+ 3 - 1
packages/app/src/pages/directory-layout.tsx

@@ -10,7 +10,7 @@ import { useSync } from "@/context/sync"
 import { decode64 } from "@/utils/base64"
 import { Schema } from "effect"
 
-function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
+export function DirectoryDataProvider(props: ParentProps<{ directory: string; draftID?: string }>) {
   const location = useLocation()
   const navigate = useNavigate()
   const params = useParams()
@@ -18,6 +18,8 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
   const slug = createMemo(() => base64Encode(props.directory))
 
   createEffect(() => {
+    // A draft lives at /new-session?draftId=… and has no directory segment to normalize.
+    if (props.draftID) return
     const next = sync.data.path.directory
     if (!next || next === props.directory) return
     const path = location.pathname.slice(slug().length + 1)

+ 78 - 0
packages/app/src/pages/new-session.tsx

@@ -0,0 +1,78 @@
+import { createEffect, createMemo, onMount, untrack } from "solid-js"
+import { createStore } from "solid-js/store"
+import { useSearchParams } from "@solidjs/router"
+import { NewSessionDesignView } from "@/components/session"
+import { useComments } from "@/context/comments"
+import { usePrompt } from "@/context/prompt"
+import { useSDK } from "@/context/sdk"
+import { useSync } from "@/context/sync"
+import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
+
+/**
+ * The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt
+ * composer for a brand-new session — no terminal, review pane, file tree, or message
+ * timeline. Submitting promotes the draft into a real session (see prompt-input/submit).
+ */
+export default function NewSessionPage() {
+  const prompt = usePrompt()
+  const sdk = useSDK()
+  const sync = useSync()
+  const comments = useComments()
+  const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
+
+  let inputRef: HTMLDivElement | undefined
+
+  const composer = createSessionComposerState()
+
+  const [store, setStore] = createStore({
+    worktree: "main",
+  })
+
+  const newSessionWorktree = createMemo(() => {
+    if (store.worktree === "create") return "create"
+    const project = sync.project
+    if (project && sdk.directory !== project.worktree) return sdk.directory
+    return "main"
+  })
+
+  createEffect(() => {
+    if (!prompt.ready()) return
+    untrack(() => {
+      const text = searchParams.prompt
+      if (!text) return
+      prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
+      setSearchParams({ ...searchParams, prompt: undefined })
+    })
+  })
+
+  onMount(() => {
+    requestAnimationFrame(() => inputRef?.focus())
+  })
+
+  return (
+    <div class="relative size-full overflow-hidden flex flex-col">
+      <div class="flex-1 min-h-0 flex flex-col gap-2 p-2">
+        <div class="@container relative flex flex-col min-h-0 h-full bg-background-stronger flex-1">
+          <div class="flex-1 min-h-0 overflow-hidden rounded-[10px]">
+            <NewSessionDesignView>
+              <SessionComposerRegion
+                state={composer}
+                ready
+                centered={false}
+                placement="inline"
+                inputRef={(el) => {
+                  inputRef = el
+                }}
+                newSessionWorktree={newSessionWorktree()}
+                onNewSessionWorktreeReset={() => setStore("worktree", "main")}
+                onSubmit={() => comments.clear()}
+                onResponseSubmit={() => {}}
+                setPromptDockRef={() => {}}
+              />
+            </NewSessionDesignView>
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}

+ 5 - 14
packages/app/src/pages/session.tsx

@@ -31,7 +31,7 @@ import { Button } from "@opencode-ai/ui/button"
 import { showToast } from "@/utils/toast"
 import { checksum } from "@opencode-ai/core/util/encode"
 import { useLocation, useSearchParams } from "@solidjs/router"
-import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session"
+import { NewSessionView, SessionHeader } from "@/components/session"
 import { useComments } from "@/context/comments"
 import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
 import { useServerSync } from "@/context/server-sync"
@@ -63,7 +63,6 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
 import { TerminalPanel } from "@/pages/session/terminal-panel"
 import { useSessionCommands } from "@/pages/session/use-session-commands"
 import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
-import { shouldUseV2NewSessionPage } from "@/pages/session/new-session-layout"
 import { Identifier } from "@/utils/id"
 import { diffs as list } from "@/utils/diffs"
 import { Persist, persisted } from "@/utils/persist"
@@ -271,13 +270,10 @@ export default function Page() {
 
   const isDesktop = createMediaQuery("(min-width: 768px)")
   const size = createSizing()
-  const isV2NewSessionPage = () =>
-    shouldUseV2NewSessionPage({ newLayoutDesigns: newSessionDesign(), sessionID: params.id })
-  const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
+  const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
   const desktopFileTreeOpen = createMemo(
     () =>
       isDesktop() &&
-      !isV2NewSessionPage() &&
       shouldShowFileTree({
         desktopV2: platform.platform === "desktop" && settings.general.newLayoutDesigns(),
         showFileTree: settings.general.showFileTree(),
@@ -1757,10 +1753,9 @@ export default function Page() {
 
         <div
           classList={{
-            "@container relative shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none": true,
+            "@container relative shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none transition-[width]": true,
             "duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
               !size.active() && !ui.reviewSnap,
-            "transition-[width]": !isV2NewSessionPage(),
           }}
           style={{
             width: sessionPanelWidth(),
@@ -1768,9 +1763,7 @@ export default function Page() {
         >
           <div
             classList={{
-              "flex-1 min-h-0 flex flex-col": true,
-              "bg-v2-background-bg-deep": isV2NewSessionPage(),
-              "bg-background-stronger": !isV2NewSessionPage(),
+              "flex-1 min-h-0 flex flex-col bg-background-stronger": true,
               "rounded-[10px] overflow-hidden": settings.general.newLayoutDesigns(),
               "shadow-[var(--v2-elevation-raised)]": settings.general.newLayoutDesigns() && !!params.id,
             }}
@@ -1826,9 +1819,7 @@ export default function Page() {
                   </Show>
                 </Match>
                 <Match when={true}>
-                  <Show when={newSessionDesign()} fallback={<NewSessionView worktree={newSessionWorktree()} />}>
-                    <NewSessionDesignView>{composerRegion("inline")}</NewSessionDesignView>
-                  </Show>
+                  <NewSessionView worktree={newSessionWorktree()} />
                 </Match>
               </Switch>
             </div>

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

@@ -1,14 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { shouldUseV2NewSessionPage } from "./new-session-layout"
-
-describe("shouldUseV2NewSessionPage", () => {
-  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 enabled new-session pages", () => {
-    expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true })).toBe(true)
-    expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true, sessionID: "ses_123" })).toBe(false)
-  })
-})

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

@@ -1,6 +1,2 @@
 /** Inline new-session content width — keep in sync with session composer `placement === "inline"`. */
 export const NEW_SESSION_CONTENT_WIDTH = "w-full max-w-[720px] px-0"
-
-export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) {
-  return input.newLayoutDesigns && !input.sessionID
-}