Sfoglia il codice sorgente

feat(app): add servers tab to settings dialog (#29675)

Brendan Allan 3 mesi fa
parent
commit
c6f684366a
30 ha cambiato i file con 920 aggiunte e 412 eliminazioni
  1. 3 3
      packages/app/src/app.tsx
  2. 2 0
      packages/app/src/components/dialog-connect-provider.tsx
  3. 1 1
      packages/app/src/components/dialog-fork.tsx
  4. 1 0
      packages/app/src/components/dialog-manage-models.tsx
  5. 9 12
      packages/app/src/components/dialog-select-directory.tsx
  6. 1 0
      packages/app/src/components/dialog-select-file.tsx
  7. 1 0
      packages/app/src/components/dialog-select-mcp.tsx
  8. 2 2
      packages/app/src/components/dialog-select-model-unpaid.tsx
  9. 1 1
      packages/app/src/components/dialog-select-model.tsx
  10. 1 0
      packages/app/src/components/dialog-select-provider.tsx
  11. 207 167
      packages/app/src/components/dialog-select-server.tsx
  12. 9 1
      packages/app/src/components/dialog-settings.tsx
  13. 3 1
      packages/app/src/components/prompt-input.tsx
  14. 1 1
      packages/app/src/components/session/session-new-design-view.tsx
  15. 13 1
      packages/app/src/components/settings-models.tsx
  16. 11 1
      packages/app/src/components/settings-providers.tsx
  17. 106 0
      packages/app/src/components/settings-server-picker.tsx
  18. 33 0
      packages/app/src/components/settings-servers.tsx
  19. 8 0
      packages/app/src/components/settings-v2/dialog-settings-v2.tsx
  20. 80 74
      packages/app/src/components/status-popover-body.tsx
  21. 9 9
      packages/app/src/components/status-popover.tsx
  22. 248 0
      packages/app/src/context/global.tsx
  23. 14 9
      packages/app/src/context/server-sdk.tsx
  24. 30 12
      packages/app/src/context/server-sync.tsx
  25. 0 20
      packages/app/src/context/servers.tsx
  26. 4 0
      packages/app/src/context/settings.tsx
  27. 117 94
      packages/app/src/pages/home.tsx
  28. 3 1
      packages/app/src/pages/layout.tsx
  29. 1 1
      packages/app/src/pages/session.tsx
  30. 1 1
      packages/ui/src/components/list.css

+ 3 - 3
packages/app/src/app.tsx

@@ -33,6 +33,7 @@ import { CommentsProvider } from "@/context/comments"
 import { FileProvider } from "@/context/file"
 import { ServerSDKProvider } from "@/context/server-sdk"
 import { ServerSyncProvider } from "@/context/server-sync"
+import { GlobalProvider } from "@/context/global"
 import { HighlightsProvider } from "@/context/highlights"
 import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
 import { LayoutProvider } from "@/context/layout"
@@ -47,7 +48,6 @@ import DirectoryLayout from "@/pages/directory-layout"
 import Layout from "@/pages/layout"
 import { ErrorPage } from "./pages/error"
 import { useCheckServerHealth } from "./utils/server-health"
-import { ServersProvider } from "./context/servers"
 
 const HomeRoute = lazy(() => import("@/pages/home"))
 const Session = lazy(() => import("@/pages/session"))
@@ -316,7 +316,7 @@ export function AppInterface(props: {
 }) {
   return (
     <ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
-      <ServersProvider>
+      <GlobalProvider defaultServer={props.defaultServer} servers={props.servers}>
         <ConnectionGate disableHealthCheck={props.disableHealthCheck}>
           <ServerKey>
             <QueryProvider>
@@ -337,7 +337,7 @@ export function AppInterface(props: {
             </QueryProvider>
           </ServerKey>
         </ConnectionGate>
-      </ServersProvider>
+      </GlobalProvider>
     </ServerProvider>
   )
 }

+ 2 - 0
packages/app/src/components/dialog-connect-provider.tsx

@@ -277,6 +277,7 @@ export function DialogConnectProvider(props: { provider: string }) {
               <div class="text-14-regular text-text-base">{select()?.message}</div>
               <div>
                 <List
+                  class="px-3"
                   items={select()?.options ?? []}
                   key={(x) => x.value}
                   current={select()?.options.find((x) => x.value === formStore.value[select()!.key])}
@@ -364,6 +365,7 @@ export function DialogConnectProvider(props: { provider: string }) {
         </div>
         <div>
           <List
+            class="px-3"
             ref={(ref) => {
               listRef = ref
             }}

+ 1 - 1
packages/app/src/components/dialog-fork.tsx

@@ -88,7 +88,7 @@ export const DialogFork: Component = () => {
   return (
     <Dialog title={language.t("command.session.fork")}>
       <List
-        class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
+        class="flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
         search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
         emptyMessage={language.t("dialog.fork.empty")}
         key={(x) => x.id}

+ 1 - 0
packages/app/src/components/dialog-manage-models.tsx

@@ -39,6 +39,7 @@ export const DialogManageModels: Component = () => {
       }
     >
       <List
+        class="px-3"
         search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
         emptyMessage={language.t("dialog.model.empty")}
         key={(x) => `${x?.provider?.id}:${x?.id}`}

+ 9 - 12
packages/app/src/components/dialog-select-directory.tsx

@@ -6,15 +6,16 @@ import type { ListRef } from "@opencode-ai/ui/list"
 import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
 import fuzzysort from "fuzzysort"
 import { createMemo, createResource, createSignal } from "solid-js"
-import { useServerSDK } from "@/context/server-sdk"
-import { useServerSync } from "@/context/server-sync"
-import { useLayout } from "@/context/layout"
+import { ServerSDK } from "@/context/server-sdk"
 import { useLanguage } from "@/context/language"
+import { ServerConnection } from "@/context/server"
+import { useGlobal } from "@/context/global"
 
 interface DialogSelectDirectoryProps {
   title?: string
   multiple?: boolean
   onSelect: (result: string | string[] | null) => void
+  server: ServerConnection.Any
 }
 
 type Row = {
@@ -127,11 +128,7 @@ function uniqueRows(rows: Row[]) {
   })
 }
 
-function useDirectorySearch(args: {
-  sdk: ReturnType<typeof useServerSDK>
-  start: () => string | undefined
-  home: () => string
-}) {
+function useDirectorySearch(args: { sdk: ServerSDK; start: () => string | undefined; home: () => string }) {
   const cache = new Map<string, Promise<Array<{ name: string; absolute: string }>>>()
   let current = 0
 
@@ -246,9 +243,8 @@ function useDirectorySearch(args: {
 }
 
 export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
-  const sync = useServerSync()
-  const sdk = useServerSDK()
-  const layout = useLayout()
+  const global = useGlobal()
+  const { sync, sdk, ...serverCtx } = global.createServerCtx(props.server)
   const dialog = useDialog()
   const language = useLanguage()
 
@@ -279,7 +275,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
   })
 
   const recentProjects = createMemo(() => {
-    const projects = layout.projects.list()
+    const projects = serverCtx.projects.list()
     const byProject = new Map<string, number>()
 
     for (const project of projects) {
@@ -324,6 +320,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
   return (
     <Dialog title={props.title ?? language.t("command.project.open")}>
       <List
+        class="px-3"
         search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
         emptyMessage={language.t("dialog.directory.empty")}
         loadingMessage={language.t("common.loading")}

+ 1 - 0
packages/app/src/components/dialog-select-file.tsx

@@ -386,6 +386,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
   return (
     <Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
       <List
+        class="px-3"
         search={{
           placeholder: filesOnly()
             ? language.t("session.header.searchFiles")

+ 1 - 0
packages/app/src/components/dialog-select-mcp.tsx

@@ -55,6 +55,7 @@ export const DialogSelectMcp: Component = () => {
       description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
     >
       <List
+        class="px-3"
         search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
         emptyMessage={language.t("dialog.mcp.empty")}
         key={(x) => x?.name ?? ""}

+ 2 - 2
packages/app/src/components/dialog-select-model-unpaid.tsx

@@ -45,7 +45,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
       <div class="flex flex-col gap-3 px-2.5" onKeyDown={handleKeyDown}>
         <div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
         <List
-          class="[&_[data-slot=list-scroll]]:overflow-visible"
+          class="px-3 [&_[data-slot=list-scroll]]:overflow-visible"
           ref={(ref) => (listRef = ref)}
           items={model.list}
           current={model.current()}
@@ -90,7 +90,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
             <div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
             <div class="w-full">
               <List
-                class="w-full px-0"
+                class="w-full px-3"
                 key={(p) => p.id}
                 items={providers.popular}
                 activeIcon="plus-small"

+ 1 - 1
packages/app/src/components/dialog-select-model.tsx

@@ -37,7 +37,7 @@ const ModelList: Component<{
 
   return (
     <List
-      class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
+      class={`flex-1 px-3 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
       search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true, action: props.action }}
       emptyMessage={language.t("dialog.model.empty")}
       key={(x) => `${x.provider.id}:${x.id}`}

+ 1 - 0
packages/app/src/components/dialog-select-provider.tsx

@@ -29,6 +29,7 @@ export const DialogSelectProvider: Component = () => {
   return (
     <Dialog title={language.t("command.provider.connect")} transition>
       <List
+        class="px-3"
         search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
         emptyMessage={language.t("dialog.provider.empty")}
         activeIcon="plus-small"

+ 207 - 167
packages/app/src/components/dialog-select-server.tsx

@@ -9,13 +9,15 @@ import { TextField } from "@opencode-ai/ui/text-field"
 import { useMutation } from "@tanstack/solid-query"
 import { showToast } from "@/utils/toast"
 import { useNavigate } from "@solidjs/router"
-import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
-import { createStore, reconcile } from "solid-js/store"
+import { createEffect, createMemo, createResource, Show } from "solid-js"
+import { createStore } from "solid-js/store"
 import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
+import { useGlobal } from "@/context/global"
 import { useLanguage } from "@/context/language"
 import { usePlatform } from "@/context/platform"
 import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
 import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health"
+import { useSettings } from "@/context/settings"
 
 const DEFAULT_USERNAME = "opencode"
 
@@ -71,7 +73,7 @@ function useDefaultServer() {
     }
   }
 
-  return { defaultKey, canDefault, setDefault }
+  return { defaultKey: () => defaultKey.latest, canDefault, setDefault }
 }
 
 function useServerPreview() {
@@ -121,7 +123,7 @@ function ServerForm(props: ServerFormProps) {
   }
 
   return (
-    <div class="px-5">
+    <div>
       <div class="bg-surface-base rounded-md p-5 flex flex-col gap-3">
         <div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
           <TextField
@@ -172,16 +174,30 @@ function ServerForm(props: ServerFormProps) {
 }
 
 export function DialogSelectServer() {
-  const navigate = useNavigate()
   const dialog = useDialog()
+  const controller = useServerManagementController({ onSelect: dialog.close })
+
+  return (
+    <Dialog title={controller.formTitle()}>
+      <div class="flex flex-1 min-h-0 flex-col px-5">
+        <Show when={controller.isFormMode()} fallback={<ServerConnectionList controller={controller} />}>
+          <ServerConnectionForm controller={controller} />
+        </Show>
+      </div>
+    </Dialog>
+  )
+}
+
+export function useServerManagementController(options: { onSelect?: () => void } = {}) {
+  const navigate = useNavigate()
   const server = useServer()
+  const global = useGlobal()
   const platform = usePlatform()
   const language = useLanguage()
   const { defaultKey, canDefault, setDefault } = useDefaultServer()
   const { previewStatus } = useServerPreview()
   const checkServerHealth = useCheckServerHealth()
   const [store, setStore] = createStore({
-    status: {} as Record<ServerConnection.Key, ServerHealth | undefined>,
     addServer: {
       url: "",
       name: "",
@@ -311,7 +327,12 @@ export function DialogSelectServer() {
     return [current, ...list.filter((x) => x !== current)]
   })
 
-  const current = createMemo(() => items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0])
+  const settings = useSettings()
+  const current = createMemo<ServerConnection.Any | undefined>(() =>
+    settings.general.newLayoutDesigns()
+      ? undefined
+      : (items().find((x) => ServerConnection.key(x) === server.key) ?? items()[0]),
+  )
 
   const sortedItems = createMemo(() => {
     const list = items()
@@ -326,32 +347,16 @@ export function DialogSelectServer() {
     return list.slice().sort((a, b) => {
       if (a === active) return -1
       if (b === active) return 1
-      const diff = rank(store.status[ServerConnection.key(a)]) - rank(store.status[ServerConnection.key(b)])
+      const diff =
+        rank(global.servers.health[ServerConnection.key(a)]) - rank(global.servers.health[ServerConnection.key(b)])
       if (diff !== 0) return diff
       return (order.get(a) ?? 0) - (order.get(b) ?? 0)
     })
   })
 
-  async function refreshHealth() {
-    const results: Record<ServerConnection.Key, ServerHealth> = {}
-    await Promise.all(
-      items().map(async (conn) => {
-        results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
-      }),
-    )
-    setStore("status", reconcile(results))
-  }
-
-  createEffect(() => {
-    items()
-    void refreshHealth()
-    const interval = setInterval(refreshHealth, 10_000)
-    onCleanup(() => clearInterval(interval))
-  })
-
   async function select(conn: ServerConnection.Any, persist?: boolean) {
-    if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return
-    dialog.close()
+    if (!persist && global.servers.health[ServerConnection.key(conn)]?.healthy === false) return
+    options.onSelect?.()
     if (persist && conn.type === "http") {
       server.add(conn)
       navigate("/")
@@ -457,7 +462,7 @@ export function DialogSelectServer() {
       username: conn.http.username ?? "",
       password: conn.http.password ?? "",
       error: "",
-      status: store.status[ServerConnection.key(conn)]?.healthy,
+      status: global.servers.health[ServerConnection.key(conn)]?.healthy,
     })
   }
 
@@ -502,148 +507,183 @@ export function DialogSelectServer() {
     }
   }
 
+  return {
+    defaultKey,
+    canDefault,
+    current,
+    sortedItems,
+    status: () => global.servers.health,
+    isFormMode,
+    isAddMode,
+    formTitle,
+    formBusy,
+    formValue: () => (isAddMode() ? store.addServer.url : store.editServer.value),
+    formName: () => (isAddMode() ? store.addServer.name : store.editServer.name),
+    formUsername: () => (isAddMode() ? store.addServer.username : store.editServer.username),
+    formPassword: () => (isAddMode() ? store.addServer.password : store.editServer.password),
+    formError: () => (isAddMode() ? store.addServer.error : store.editServer.error),
+    formStatus: () => (isAddMode() ? store.addServer.status : store.editServer.status),
+    select,
+    setDefault,
+    startAdd,
+    startEdit,
+    resetForm,
+    submitForm,
+    handleRemove,
+    handleFormChange: () => (isAddMode() ? handleAddChange : handleEditChange),
+    handleFormNameChange: () => (isAddMode() ? handleAddNameChange : handleEditNameChange),
+    handleFormUsernameChange: () => (isAddMode() ? handleAddUsernameChange : handleEditUsernameChange),
+    handleFormPasswordChange: () => (isAddMode() ? handleAddPasswordChange : handleEditPasswordChange),
+  }
+}
+
+export function ServerConnectionList(props: { controller: ReturnType<typeof useServerManagementController> }) {
+  const language = useLanguage()
+  const settings = useSettings()
+
   return (
-    <Dialog title={formTitle()}>
-      <div class="flex flex-1 min-h-0 flex-col gap-2">
-        <Show
-          when={!isFormMode()}
-          fallback={
-            <ServerForm
-              value={isAddMode() ? store.addServer.url : store.editServer.value}
-              name={isAddMode() ? store.addServer.name : store.editServer.name}
-              username={isAddMode() ? store.addServer.username : store.editServer.username}
-              password={isAddMode() ? store.addServer.password : store.editServer.password}
-              placeholder={language.t("dialog.server.add.placeholder")}
-              busy={formBusy()}
-              error={isAddMode() ? store.addServer.error : store.editServer.error}
-              status={isAddMode() ? store.addServer.status : store.editServer.status}
-              onChange={isAddMode() ? handleAddChange : handleEditChange}
-              onNameChange={isAddMode() ? handleAddNameChange : handleEditNameChange}
-              onUsernameChange={isAddMode() ? handleAddUsernameChange : handleEditUsernameChange}
-              onPasswordChange={isAddMode() ? handleAddPasswordChange : handleEditPasswordChange}
-              onSubmit={submitForm}
-              onBack={resetForm}
-            />
-          }
+    <div class="flex flex-1 min-h-0 flex-col gap-4">
+      <List
+        class="flex-1 min-h-0 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
+        search={{
+          placeholder: language.t("dialog.server.search.placeholder"),
+          autofocus: false,
+        }}
+        noInitialSelection
+        emptyMessage={language.t("dialog.server.empty")}
+        items={props.controller.sortedItems}
+        key={(x) => x.http.url}
+        onSelect={(x) => {
+          if (x && !settings.general.newLayoutDesigns()) void props.controller.select(x)
+        }}
+        divider={true}
+      >
+        {(i) => {
+          const key = ServerConnection.key(i)
+          return (
+            <div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
+              <div class="flex flex-col h-full items-center w-5">
+                <ServerHealthIndicator health={props.controller.status()[key]} />
+              </div>
+              <ServerRow
+                conn={i}
+                dimmed={props.controller.status()[key]?.healthy === false}
+                status={props.controller.status()[key]}
+                class="flex items-center gap-3 min-w-0 flex-1"
+                badge={
+                  <Show when={props.controller.defaultKey() === ServerConnection.key(i)}>
+                    <span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
+                      {language.t("dialog.server.status.default")}
+                    </span>
+                  </Show>
+                }
+                showCredentials
+              />
+              <div class="flex items-center justify-center gap-4 pl-4">
+                <Show when={props.controller.current() && ServerConnection.key(props.controller.current()!) === key}>
+                  <Icon name="check" class="h-6" />
+                </Show>
+
+                <Show when={i.type === "http"}>
+                  <DropdownMenu>
+                    <DropdownMenu.Trigger
+                      as={IconButton}
+                      icon="dot-grid"
+                      variant="ghost"
+                      class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
+                      onClick={(e: MouseEvent) => e.stopPropagation()}
+                      onPointerDown={(e: PointerEvent) => e.stopPropagation()}
+                    />
+                    <DropdownMenu.Portal>
+                      <DropdownMenu.Content class="mt-1">
+                        <DropdownMenu.Item
+                          onSelect={() => {
+                            if (i.type !== "http") return
+                            props.controller.startEdit(i)
+                          }}
+                        >
+                          <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
+                        </DropdownMenu.Item>
+                        <Show when={props.controller.canDefault() && props.controller.defaultKey() !== key}>
+                          <DropdownMenu.Item onSelect={() => props.controller.setDefault(key)}>
+                            <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.default")}</DropdownMenu.ItemLabel>
+                          </DropdownMenu.Item>
+                        </Show>
+                        <Show when={props.controller.canDefault() && props.controller.defaultKey() === key}>
+                          <DropdownMenu.Item onSelect={() => props.controller.setDefault(null)}>
+                            <DropdownMenu.ItemLabel>
+                              {language.t("dialog.server.menu.defaultRemove")}
+                            </DropdownMenu.ItemLabel>
+                          </DropdownMenu.Item>
+                        </Show>
+                        <DropdownMenu.Separator />
+                        <DropdownMenu.Item
+                          onSelect={() => props.controller.handleRemove(ServerConnection.key(i))}
+                          class="text-text-on-critical-base hover:bg-surface-critical-weak"
+                        >
+                          <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
+                        </DropdownMenu.Item>
+                      </DropdownMenu.Content>
+                    </DropdownMenu.Portal>
+                  </DropdownMenu>
+                </Show>
+              </div>
+            </div>
+          )
+        }}
+      </List>
+
+      <div class="shrink-0 pb-5">
+        <Button
+          variant="secondary"
+          icon="plus-small"
+          size="large"
+          onClick={props.controller.startAdd}
+          class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
         >
-          <List
-            search={{
-              placeholder: language.t("dialog.server.search.placeholder"),
-              autofocus: false,
-            }}
-            noInitialSelection
-            emptyMessage={language.t("dialog.server.empty")}
-            items={sortedItems}
-            key={(x) => x.http.url}
-            onSelect={(x) => {
-              if (x) void select(x)
-            }}
-            divider={true}
-            class="flex-1 min-h-0 px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
-          >
-            {(i) => {
-              const key = ServerConnection.key(i)
-              return (
-                <div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
-                  <div class="flex flex-col h-full items-start w-5">
-                    <ServerHealthIndicator health={store.status[key]} />
-                  </div>
-                  <ServerRow
-                    conn={i}
-                    dimmed={store.status[key]?.healthy === false}
-                    status={store.status[key]}
-                    class="flex items-center gap-3 min-w-0 flex-1"
-                    badge={
-                      <Show when={defaultKey() === ServerConnection.key(i)}>
-                        <span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
-                          {language.t("dialog.server.status.default")}
-                        </span>
-                      </Show>
-                    }
-                    showCredentials
-                  />
-                  <div class="flex items-center justify-center gap-4 pl-4">
-                    <Show when={ServerConnection.key(current()) === key}>
-                      <Icon name="check" class="h-6" />
-                    </Show>
-
-                    <Show when={i.type === "http"}>
-                      <DropdownMenu>
-                        <DropdownMenu.Trigger
-                          as={IconButton}
-                          icon="dot-grid"
-                          variant="ghost"
-                          class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
-                          onClick={(e: MouseEvent) => e.stopPropagation()}
-                          onPointerDown={(e: PointerEvent) => e.stopPropagation()}
-                        />
-                        <DropdownMenu.Portal>
-                          <DropdownMenu.Content class="mt-1">
-                            <DropdownMenu.Item
-                              onSelect={() => {
-                                if (i.type !== "http") return
-                                startEdit(i)
-                              }}
-                            >
-                              <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
-                            </DropdownMenu.Item>
-                            <Show when={canDefault() && defaultKey() !== key}>
-                              <DropdownMenu.Item onSelect={() => setDefault(key)}>
-                                <DropdownMenu.ItemLabel>
-                                  {language.t("dialog.server.menu.default")}
-                                </DropdownMenu.ItemLabel>
-                              </DropdownMenu.Item>
-                            </Show>
-                            <Show when={canDefault() && defaultKey() === key}>
-                              <DropdownMenu.Item onSelect={() => setDefault(null)}>
-                                <DropdownMenu.ItemLabel>
-                                  {language.t("dialog.server.menu.defaultRemove")}
-                                </DropdownMenu.ItemLabel>
-                              </DropdownMenu.Item>
-                            </Show>
-                            <DropdownMenu.Separator />
-                            <DropdownMenu.Item
-                              onSelect={() => handleRemove(ServerConnection.key(i))}
-                              class="text-text-on-critical-base hover:bg-surface-critical-weak"
-                            >
-                              <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
-                            </DropdownMenu.Item>
-                          </DropdownMenu.Content>
-                        </DropdownMenu.Portal>
-                      </DropdownMenu>
-                    </Show>
-                  </div>
-                </div>
-              )
-            }}
-          </List>
-        </Show>
+          {language.t("dialog.server.add.button")}
+        </Button>
+      </div>
+    </div>
+  )
+}
 
-        <div class="shrink-0 px-5 pb-5">
-          <Show
-            when={isFormMode()}
-            fallback={
-              <Button
-                variant="secondary"
-                icon="plus-small"
-                size="large"
-                onClick={startAdd}
-                class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
-              >
-                {language.t("dialog.server.add.button")}
-              </Button>
-            }
-          >
-            <Button variant="primary" size="large" onClick={submitForm} disabled={formBusy()} class="px-3 py-1.5">
-              {formBusy()
-                ? language.t("dialog.server.add.checking")
-                : isAddMode()
-                  ? language.t("dialog.server.add.button")
-                  : language.t("common.save")}
-            </Button>
-          </Show>
-        </div>
+export function ServerConnectionForm(props: { controller: ReturnType<typeof useServerManagementController> }) {
+  const language = useLanguage()
+
+  return (
+    <div class="flex flex-1 min-h-0 flex-col gap-4">
+      <ServerForm
+        value={props.controller.formValue()}
+        name={props.controller.formName()}
+        username={props.controller.formUsername()}
+        password={props.controller.formPassword()}
+        placeholder={language.t("dialog.server.add.placeholder")}
+        busy={props.controller.formBusy()}
+        error={props.controller.formError()}
+        status={props.controller.formStatus()}
+        onChange={props.controller.handleFormChange()}
+        onNameChange={props.controller.handleFormNameChange()}
+        onUsernameChange={props.controller.handleFormUsernameChange()}
+        onPasswordChange={props.controller.handleFormPasswordChange()}
+        onSubmit={props.controller.submitForm}
+        onBack={props.controller.resetForm}
+      />
+      <div class="shrink-0 pb-5">
+        <Button
+          variant="primary"
+          size="large"
+          onClick={props.controller.submitForm}
+          disabled={props.controller.formBusy()}
+          class="px-3 py-1.5"
+        >
+          {props.controller.formBusy()
+            ? language.t("dialog.server.add.checking")
+            : props.controller.isAddMode()
+              ? language.t("dialog.server.add.button")
+              : language.t("common.save")}
+        </Button>
       </div>
-    </Dialog>
+    </div>
   )
 }

+ 9 - 1
packages/app/src/components/dialog-settings.tsx

@@ -8,6 +8,7 @@ import { SettingsGeneral } from "./settings-general"
 import { SettingsKeybinds } from "./settings-keybinds"
 import { SettingsProviders } from "./settings-providers"
 import { SettingsModels } from "./settings-models"
+import { SettingsServers } from "./settings-servers"
 
 export const DialogSettings: Component = () => {
   const language = useLanguage()
@@ -17,7 +18,7 @@ export const DialogSettings: Component = () => {
     <Dialog size="x-large" transition>
       <Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
         <Tabs.List>
-          <div class="flex flex-col justify-between h-full w-full">
+          <div class="flex flex-col justify-between h-full w-full gap-4">
             <div class="flex flex-col gap-3 w-full pt-3">
               <div class="flex flex-col gap-3">
                 <div class="flex flex-col gap-1.5">
@@ -31,6 +32,10 @@ export const DialogSettings: Component = () => {
                       <Icon name="keyboard" />
                       {language.t("settings.tab.shortcuts")}
                     </Tabs.Trigger>
+                    <Tabs.Trigger value="servers">
+                      <Icon name="server" />
+                      {language.t("status.popover.tab.servers")}
+                    </Tabs.Trigger>
                   </div>
                 </div>
 
@@ -61,6 +66,9 @@ export const DialogSettings: Component = () => {
         <Tabs.Content value="shortcuts" class="no-scrollbar">
           <SettingsKeybinds />
         </Tabs.Content>
+        <Tabs.Content value="servers" class="no-scrollbar">
+          <SettingsServers />
+        </Tabs.Content>
         <Tabs.Content value="providers" class="no-scrollbar">
           <SettingsProviders />
         </Tabs.Content>

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

@@ -1363,6 +1363,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     navigate(`/${base64Encode(worktree)}/session`)
   }
   const addProject = async () => {
+    const conn = server.current
+    if (!conn) return
     const select = (result: string | string[] | null) => {
       const directory = Array.isArray(result) ? result[0] : result
       if (!directory) return
@@ -1374,7 +1376,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     }
     void import("@/components/dialog-select-directory").then((x) => {
       dialog.show(
-        () => <x.DialogSelectDirectory onSelect={select} />,
+        () => <x.DialogSelectDirectory onSelect={select} server={conn} />,
         () => select(null),
       )
     })

+ 1 - 1
packages/app/src/components/session/session-new-design-view.tsx

@@ -4,7 +4,7 @@ import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
 
 export function NewSessionDesignView(props: { children: JSX.Element }) {
   return (
-    <div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep">
+    <div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep ">
       <div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
         <div class={NEW_SESSION_CONTENT_WIDTH}>
           <WordmarkV2 class="h-auto w-full text-v2-icon-icon-base" />

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

@@ -9,6 +9,7 @@ import { useLanguage } from "@/context/language"
 import { useModels } from "@/context/models"
 import { popularProviders } from "@/hooks/use-providers"
 import { SettingsList } from "./settings-list"
+import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker"
 
 type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
 
@@ -32,6 +33,14 @@ const ListEmptyState: Component<{ message: string; filter: string }> = (props) =
 }
 
 export const SettingsModels: Component = () => {
+  return (
+    <SettingsServerScope>
+      <SettingsModelsContent />
+    </SettingsServerScope>
+  )
+}
+
+const SettingsModelsContent: Component = () => {
   const language = useLanguage()
   const models = useModels()
 
@@ -61,7 +70,10 @@ export const SettingsModels: Component = () => {
     <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
       <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
         <div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
-          <h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
+          <div class="flex items-center justify-between gap-4">
+            <h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
+            <SettingsServerPicker />
+          </div>
           <div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
             <Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
             <TextField

+ 11 - 1
packages/app/src/components/settings-providers.tsx

@@ -12,6 +12,7 @@ import { DialogConnectProvider } from "./dialog-connect-provider"
 import { DialogSelectProvider } from "./dialog-select-provider"
 import { DialogCustomProvider } from "./dialog-custom-provider"
 import { SettingsList } from "./settings-list"
+import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker"
 
 type ProviderSource = "env" | "api" | "config" | "custom"
 type ProviderItem = ReturnType<ReturnType<typeof useProviders>["connected"]>[number]
@@ -28,6 +29,14 @@ const PROVIDER_NOTES = [
 ] as const
 
 export const SettingsProviders: Component = () => {
+  return (
+    <SettingsServerScope>
+      <SettingsProvidersContent />
+    </SettingsServerScope>
+  )
+}
+
+const SettingsProvidersContent: Component = () => {
   const dialog = useDialog()
   const language = useLanguage()
   const serverSDK = useServerSDK()
@@ -129,8 +138,9 @@ export const SettingsProviders: Component = () => {
   return (
     <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
       <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
-        <div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
+        <div class="flex items-center justify-between gap-4 pt-6 pb-8 max-w-[720px]">
           <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
+          <SettingsServerPicker />
         </div>
       </div>
 

+ 106 - 0
packages/app/src/components/settings-server-picker.tsx

@@ -0,0 +1,106 @@
+import { Button } from "@opencode-ai/ui/button"
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
+import { Icon } from "@opencode-ai/ui/icon"
+import { QueryClientProvider } from "@tanstack/solid-query"
+import { createMemo, For, type ParentProps, Show } from "solid-js"
+import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
+import { ModelsProvider } from "@/context/models"
+import { ServerConnection } from "@/context/server"
+import { ServerSDKProvider } from "@/context/server-sdk"
+import { ServerSyncProvider } from "@/context/server-sync"
+import { useGlobal } from "@/context/global"
+import { useSettings } from "@/context/settings"
+
+export function SettingsServerScope(props: ParentProps) {
+  const global = useGlobal()
+  const settings = useSettings()
+
+  return (
+    <Show when={settings.general.newLayoutDesigns()} fallback={props.children}>
+      <Show when={global.settings.server.selected()}>
+        {(server) => <SettingsServerDataProviders server={server()}>{props.children}</SettingsServerDataProviders>}
+      </Show>
+    </Show>
+  )
+}
+
+function SettingsServerDataProviders(props: ParentProps<{ server: ServerConnection.Any }>) {
+  const global = useGlobal()
+  const serverCtx = () => global.createServerCtx(props.server)
+
+  return (
+    <QueryClientProvider client={serverCtx().queryClient}>
+      <ServerSDKProvider server={props.server}>
+        <ServerSyncProvider>
+          <ModelsProvider>{props.children}</ModelsProvider>
+        </ServerSyncProvider>
+      </ServerSDKProvider>
+    </QueryClientProvider>
+  )
+}
+
+export function SettingsServerPicker() {
+  const global = useGlobal()
+  const settings = useSettings()
+  const selected = createMemo(() =>
+    settings.general.newLayoutDesigns() ? global.settings.server.selected() : undefined,
+  )
+
+  return (
+    <Show when={selected()}>
+      {(conn) => (
+        <DropdownMenu gutter={4} placement="bottom-end">
+          <DropdownMenu.Trigger
+            as={Button}
+            variant="secondary"
+            size="large"
+            class="h-8 max-w-[260px] gap-2 px-2 py-1.5 data-[expanded]:bg-surface-base-active"
+          >
+            <ServerHealthIndicator health={global.servers.health[ServerConnection.key(conn())]} />
+            <ServerRow
+              conn={conn()}
+              status={global.servers.health[ServerConnection.key(conn())]}
+              class="flex items-center gap-2 min-w-0 flex-1"
+              nameClass="text-14-regular text-text-base truncate"
+              versionClass="hidden"
+            />
+            <Icon name="chevron-down" size="small" class="text-icon-weak shrink-0" />
+          </DropdownMenu.Trigger>
+          <DropdownMenu.Portal>
+            <DropdownMenu.Content class="w-[320px] mt-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-2 [&_[data-slot=dropdown-menu-radio-item]]:pr-2">
+              <DropdownMenu.RadioGroup
+                value={global.settings.server.key}
+                onChange={(key) => {
+                  if (typeof key === "string") global.settings.server.set(ServerConnection.Key.make(key))
+                }}
+              >
+                <For each={global.servers.list()}>
+                  {(item) => {
+                    const key = ServerConnection.key(item)
+                    const blocked = () => global.servers.health[key]?.healthy === false
+                    return (
+                      <DropdownMenu.RadioItem value={key} disabled={blocked()}>
+                        <ServerHealthIndicator health={global.servers.health[key]} />
+                        <ServerRow
+                          conn={item}
+                          dimmed={blocked()}
+                          status={global.servers.health[key]}
+                          class="flex items-center gap-2 min-w-0 flex-1"
+                          nameClass="text-14-regular text-text-base truncate"
+                          versionClass="text-12-regular text-text-weak truncate"
+                        />
+                        <DropdownMenu.ItemIndicator>
+                          <Icon name="check-small" size="small" class="text-icon-weak" />
+                        </DropdownMenu.ItemIndicator>
+                      </DropdownMenu.RadioItem>
+                    )
+                  }}
+                </For>
+              </DropdownMenu.RadioGroup>
+            </DropdownMenu.Content>
+          </DropdownMenu.Portal>
+        </DropdownMenu>
+      )}
+    </Show>
+  )
+}

+ 33 - 0
packages/app/src/components/settings-servers.tsx

@@ -0,0 +1,33 @@
+import { Show, type Component } from "solid-js"
+import { useLanguage } from "@/context/language"
+import { ServerConnectionForm, ServerConnectionList, useServerManagementController } from "./dialog-select-server"
+
+export const SettingsServers: Component = () => {
+  const language = useLanguage()
+  const controller = useServerManagementController()
+
+  return (
+    <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
+      <div class="flex flex-col flex-1 min-h-0 max-w-[720px]">
+        <Show
+          when={controller.isFormMode()}
+          fallback={
+            <>
+              <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
+                <div class="flex flex-col gap-1 pt-6 pb-8">
+                  <h2 class="text-16-medium text-text-strong">{language.t("status.popover.tab.servers")}</h2>
+                </div>
+              </div>
+              <ServerConnectionList controller={controller} />
+            </>
+          }
+        >
+          <div class="flex flex-1 min-h-0 flex-col gap-4 pt-6">
+            <div class="text-16-medium text-text-strong">{controller.formTitle()}</div>
+            <ServerConnectionForm controller={controller} />
+          </div>
+        </Show>
+      </div>
+    </div>
+  )
+}

+ 8 - 0
packages/app/src/components/settings-v2/dialog-settings-v2.tsx

@@ -9,6 +9,7 @@ import { SettingsKeybinds } from "../settings-keybinds"
 import { SettingsProvidersV2 } from "./providers"
 import { SettingsModelsV2 } from "./models"
 import "./settings-v2.css"
+import { SettingsServers } from "../settings-servers"
 
 export const DialogSettings: Component = () => {
   const language = useLanguage()
@@ -38,6 +39,10 @@ export const DialogSettings: Component = () => {
                       <Icon name="keyboard" />
                       {language.t("settings.tab.shortcuts")}
                     </TabsV2.Trigger>
+                    <TabsV2.Trigger value="servers">
+                      <Icon name="server" />
+                      {language.t("status.popover.tab.servers")}
+                    </TabsV2.Trigger>
                   </div>
                 </div>
 
@@ -68,6 +73,9 @@ export const DialogSettings: Component = () => {
         <TabsV2.Content value="shortcuts" class="settings-v2-panel">
           <SettingsKeybinds v2 />
         </TabsV2.Content>
+        <TabsV2.Content value="servers" class="settings-v2-panel">
+          <SettingsServers />
+        </TabsV2.Content>
         <TabsV2.Content value="providers" class="settings-v2-panel">
           <SettingsProvidersV2 />
         </TabsV2.Content>

+ 80 - 74
packages/app/src/components/status-popover-body.tsx

@@ -17,7 +17,8 @@ import { useSync } from "@/context/sync"
 import { type ServerHealth } from "@/utils/server-health"
 import { useQueryOptions } from "@/context/server-sync"
 import { pathKey } from "@/utils/path-key"
-import { useServers } from "@/context/servers"
+import { useGlobal } from "@/context/global"
+import { useSettings } from "@/context/settings"
 
 const pollMs = 10_000
 
@@ -153,7 +154,7 @@ type ServerStatusItem = {
 }
 
 export function StatusPopoverServerBody() {
-  const servers = useServers()
+  const global = useGlobal()
   const server = useServer()
   const platform = usePlatform()
   const dialog = useDialog()
@@ -167,7 +168,7 @@ export function StatusPopoverServerBody() {
     dialogRun += 1
   })
 
-  const sortedServers = createMemo(() => listServersByHealth(servers.list(), server.key, servers.health))
+  const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health))
   const defaultServer = useDefaultServerKey(platform.getDefaultServer)
   const serverItems = createMemo(() =>
     sortedServers().map((conn) => {
@@ -175,8 +176,8 @@ export function StatusPopoverServerBody() {
       return {
         key,
         conn,
-        health: servers.health[key],
-        blocked: servers.health[key]?.healthy === false,
+        health: global.servers.health[key],
+        blocked: global.servers.health[key]?.healthy === false,
         active: !!server.current && key === ServerConnection.key(server.current),
         onSelect: () => {
           navigate("/")
@@ -288,12 +289,13 @@ function ServerStatusList(props: { state: ServerStatusState }) {
 
 export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
   const sync = useSync()
-  const servers = useServers()
+  const global = useGlobal()
   const server = useServer()
   const platform = usePlatform()
   const dialog = useDialog()
   const language = useLanguage()
   const navigate = useNavigate()
+  const settings = useSettings()
 
   const fail = (err: unknown) => {
     showToast({
@@ -313,7 +315,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
     dialogDead = true
     dialogRun += 1
   })
-  const sortedServers = createMemo(() => listServersByHealth(servers.list(), server.key, servers.health))
+  const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health))
   const toggleMcp = useMcpToggleMutation()
   const defaultServer = useDefaultServerKey(platform.getDefaultServer)
   const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
@@ -333,15 +335,17 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
         aria-label={language.t("status.popover.ariaLabel")}
         class="tabs bg-background-strong rounded-xl overflow-hidden"
         data-component="tabs"
-        data-active="servers"
-        defaultValue="servers"
+        data-active={settings.general.newLayoutDesigns() ? "mcp" : "servers"}
+        defaultValue={settings.general.newLayoutDesigns() ? "mcp" : "servers"}
         variant="alt"
       >
         <Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
-          <Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
-            {servers.list().length > 0 ? `${servers.list().length} ` : ""}
-            {language.t("status.popover.tab.servers")}
-          </Tabs.Trigger>
+          {!settings.general.newLayoutDesigns() && (
+            <Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
+              {global.servers.list().length > 0 ? `${global.servers.list().length} ` : ""}
+              {language.t("status.popover.tab.servers")}
+            </Tabs.Trigger>
+          )}
           <Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
             {mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
             {language.t("status.popover.tab.mcp")}
@@ -356,70 +360,72 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
           </Tabs.Trigger>
         </Tabs.List>
 
-        <Tabs.Content value="servers">
-          <div class="flex flex-col px-2 pb-2">
-            <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
-              <For each={sortedServers()}>
-                {(s) => {
-                  const key = ServerConnection.key(s)
-                  const blocked = () => servers.health[key]?.healthy === false
-                  return (
-                    <button
-                      type="button"
-                      class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
-                      classList={{
-                        "hover:bg-surface-raised-base-hover": !blocked(),
-                        "cursor-not-allowed": blocked(),
-                      }}
-                      aria-disabled={blocked()}
-                      onClick={() => {
-                        if (blocked()) return
-                        navigate("/")
-                        queueMicrotask(() => server.setActive(key))
-                      }}
-                    >
-                      <ServerHealthIndicator health={servers.health[key]} />
-                      <ServerRow
-                        conn={s}
-                        dimmed={blocked()}
-                        status={servers.health[key]}
-                        class="flex items-center gap-2 w-full min-w-0"
-                        nameClass="text-14-regular text-text-base truncate"
-                        versionClass="text-12-regular text-text-weak truncate"
-                        badge={
-                          <Show when={key === defaultServer.key()}>
-                            <span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
-                              {language.t("common.default")}
-                            </span>
-                          </Show>
-                        }
+        {!settings.general.newLayoutDesigns() && (
+          <Tabs.Content value="servers">
+            <div class="flex flex-col px-2 pb-2">
+              <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
+                <For each={sortedServers()}>
+                  {(s) => {
+                    const key = ServerConnection.key(s)
+                    const blocked = () => global.servers.health[key]?.healthy === false
+                    return (
+                      <button
+                        type="button"
+                        class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
+                        classList={{
+                          "hover:bg-surface-raised-base-hover": !blocked(),
+                          "cursor-not-allowed": blocked(),
+                        }}
+                        aria-disabled={blocked()}
+                        onClick={() => {
+                          if (blocked()) return
+                          navigate("/")
+                          queueMicrotask(() => server.setActive(key))
+                        }}
                       >
-                        <div class="flex-1" />
-                        <Show when={server.current && key === ServerConnection.key(server.current)}>
-                          <Icon name="check" size="small" class="text-icon-weak shrink-0" />
-                        </Show>
-                      </ServerRow>
-                    </button>
-                  )
-                }}
-              </For>
+                        <ServerHealthIndicator health={global.servers.health[key]} />
+                        <ServerRow
+                          conn={s}
+                          dimmed={blocked()}
+                          status={global.servers.health[key]}
+                          class="flex items-center gap-2 w-full min-w-0"
+                          nameClass="text-14-regular text-text-base truncate"
+                          versionClass="text-12-regular text-text-weak truncate"
+                          badge={
+                            <Show when={key === defaultServer.key()}>
+                              <span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
+                                {language.t("common.default")}
+                              </span>
+                            </Show>
+                          }
+                        >
+                          <div class="flex-1" />
+                          <Show when={server.current && key === ServerConnection.key(server.current)}>
+                            <Icon name="check" size="small" class="text-icon-weak shrink-0" />
+                          </Show>
+                        </ServerRow>
+                      </button>
+                    )
+                  }}
+                </For>
 
-              <Button
-                variant="secondary"
-                class="mt-3 self-start h-8 px-3 py-1.5"
-                onClick={() => {
-                  const run = ++dialogRun
-                  void import("./dialog-select-server").then((x) => {
-                    if (dialogDead || dialogRun !== run) return
-                    dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
-                  })
-                }}
-              >
-                {language.t("status.popover.action.manageServers")}
-              </Button>
+                <Button
+                  variant="secondary"
+                  class="mt-3 self-start h-8 px-3 py-1.5"
+                  onClick={() => {
+                    const run = ++dialogRun
+                    void import("./dialog-select-server").then((x) => {
+                      if (dialogDead || dialogRun !== run) return
+                      dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
+                    })
+                  }}
+                >
+                  {language.t("status.popover.action.manageServers")}
+                </Button>
+              </div>
             </div>
-          </div>
-        </Tabs.Content>
+          </Tabs.Content>
+        )}
 
         <Tabs.Content value="mcp">
           <div class="flex flex-col px-2 pb-2">

+ 9 - 9
packages/app/src/components/status-popover.tsx

@@ -7,7 +7,7 @@ import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-
 import { useLanguage } from "@/context/language"
 import { useServer } from "@/context/server"
 import { useSync } from "@/context/sync"
-import { useServers } from "@/context/servers"
+import { useGlobal } from "@/context/global"
 
 const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
 const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody })))
@@ -15,10 +15,10 @@ const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ def
 export function StatusPopover() {
   const language = useLanguage()
   const server = useServer()
-  const servers = useServers()
+  const global = useGlobal()
   const sync = useSync()
   const [shown, setShown] = createSignal(false)
-  const ready = createMemo(() => servers.health[server.key]?.healthy === false || sync.data.mcp_ready)
+  const ready = createMemo(() => global.servers.health[server.key]?.healthy === false || sync.data.mcp_ready)
   const mcpIssue = createMemo(() => {
     const mcp = Object.values(sync.data.mcp ?? {})
     const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
@@ -26,8 +26,8 @@ export function StatusPopover() {
     if (failed) return "critical" as const
     if (warn) return "warning" as const
   })
-  const serverHealthy = () => servers.health[server.key]?.healthy === true
-  const healthy = createMemo(() => servers.health[server.key]?.healthy === true && !mcpIssue())
+  const serverHealthy = () => global.servers.health[server.key]?.healthy === true
+  const healthy = createMemo(() => global.servers.health[server.key]?.healthy === true && !mcpIssue())
 
   return (
     <Popover
@@ -82,10 +82,10 @@ export function StatusPopoverV2(props: { scope?: "server" }) {
 function DirectoryStatusPopover() {
   const language = useLanguage()
   const server = useServer()
-  const servers = useServers()
+  const global = useGlobal()
   const sync = useSync()
   const [shown, setShown] = createSignal(false)
-  const serverHealth = () => servers.health[server.key]?.healthy
+  const serverHealth = () => global.servers.health[server.key]?.healthy
   const ready = createMemo(() => serverHealth() === false || sync.data.mcp_ready)
   const mcpIssue = createMemo(() => {
     const mcp = Object.values(sync.data.mcp ?? {})
@@ -116,9 +116,9 @@ function DirectoryStatusPopover() {
 function ServerStatusPopover() {
   const language = useLanguage()
   const server = useServer()
-  const servers = useServers()
+  const global = useGlobal()
   const [shown, setShown] = createSignal(false)
-  const serverHealth = () => servers.health[server.key]?.healthy
+  const serverHealth = () => global.servers.health[server.key]?.healthy
   const state = createMemo<StatusPopoverState>(() => ({
     shown: shown(),
     ready: serverHealth() !== undefined,

+ 248 - 0
packages/app/src/context/global.tsx

@@ -0,0 +1,248 @@
+import { createSimpleContext } from "@opencode-ai/ui/context"
+import { createEffect, createMemo, createRoot } from "solid-js"
+import { createStore } from "solid-js/store"
+import { ServerConnection, useServer } from "./server"
+import { useServerHealth } from "@/utils/server-health"
+import { QueryClient } from "@tanstack/solid-query"
+import { createServerSdkContext } from "./server-sdk"
+import { createServerSyncContext } from "./server-sync"
+import { getOwner } from "solid-js/web"
+import { Persist, persisted } from "@/utils/persist"
+
+export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext({
+  name: "Global",
+  init: (props: { defaultServer: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
+    const server = useServer()
+    const serverHealth = useServerHealth(
+      () => server.list,
+      () => true,
+    )
+    const [store, setStore] = createStore({
+      settings: {
+        serverKey: undefined as ServerConnection.Key | undefined,
+      },
+    })
+
+    const serversAndProjects = createServersAndProjectStore()
+
+    const settingsServer = createMemo(() => {
+      const list = server.list
+      return list.find((conn) => ServerConnection.key(conn) === store.settings.serverKey) ?? list[0]
+    })
+
+    createEffect(() => {
+      const conn = settingsServer()
+      const key = conn ? ServerConnection.key(conn) : undefined
+      if (store.settings.serverKey !== key) setStore("settings", "serverKey", key)
+    })
+
+    const serverCtxs = new Map<
+      ServerConnection.Key,
+      { dispose: () => void; serverCtx: ReturnType<typeof createServerCtx> }
+    >()
+
+    const owner = getOwner()
+
+    createMemo(() => {
+      for (const conn of server.list) {
+        const key = ServerConnection.key(conn)
+        if (!serverCtxs.has(key)) {
+          const root = createRoot((dispose) => {
+            const serverCtx = createServerCtx(conn, serversAndProjects)
+            return { dispose, serverCtx }
+          }, owner as any)
+          serverCtxs.set(key, root)
+        }
+      }
+
+      for (const [key] of serverCtxs) {
+        if (!server.list.find((conn) => ServerConnection.key(conn) === key)) {
+          const { dispose } = serverCtxs.get(key)!
+          dispose()
+          serverCtxs.delete(key)
+        }
+      }
+    })
+
+    const allServers = createMemo(
+      (): Array<ServerConnection.Any> =>
+        resolveServerList({ stored: serversAndProjects.store.list, props: props.servers }),
+    )
+
+    return {
+      servers: {
+        list: allServers,
+        health: serverHealth,
+      },
+      settings: {
+        server: {
+          get key() {
+            return store.settings.serverKey
+          },
+          selected: settingsServer,
+          set(key: ServerConnection.Key) {
+            if (store.settings.serverKey !== key) setStore("settings", "serverKey", key)
+          },
+        },
+      },
+      createServerCtx(conn: ServerConnection.Any) {
+        const key = ServerConnection.key(conn)
+        const ctx = serverCtxs.get(key)
+        if (!ctx) return createServerCtx(conn, serversAndProjects)
+        return ctx.serverCtx
+      },
+    }
+  },
+})
+
+type StoredProject = { worktree: string; expanded: boolean }
+type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
+
+const createServersAndProjectStore = () => {
+  const [store, setStore, _, ready] = persisted(
+    Persist.global("server", ["server.v3"]),
+    createStore({
+      list: [] as StoredServer[],
+      projects: {} as Record<string, StoredProject[]>,
+      lastProject: {} as Record<string, string>,
+    }),
+  )
+  return { store, setStore, ready }
+}
+
+function createServerCtx(
+  conn: ServerConnection.Any,
+  { store, setStore }: ReturnType<typeof createServersAndProjectStore>,
+) {
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        refetchOnReconnect: false,
+        refetchOnMount: false,
+        refetchOnWindowFocus: false,
+      },
+    },
+  })
+
+  const sdk = createServerSdkContext(conn)
+  const sync = createServerSyncContext(sdk)
+
+  const key = ServerConnection.key(conn)
+  const storeKey = projectsKey(key)
+
+  function enrich(project: { worktree: string; expanded: boolean }) {
+    const [childStore] = sync.child(project.worktree, { bootstrap: false })
+    const projectID = childStore.project
+    const metadata = projectID
+      ? sync.data.project.find((x) => x.id === projectID)
+      : sync.data.project.find((x) => x.worktree === project.worktree)
+
+    // Preserve local icon override from per-workspace localStorage cache (childStore.icon).
+    // Without this, different subdirectories of the same git repo would share the same
+    // icon from the database instead of using their individual overrides.
+    const base = { ...metadata, ...project }
+    if (childStore.icon) {
+      return { ...base, icon: { ...base.icon, override: childStore.icon } }
+    }
+    return base
+  }
+
+  const projectsList = createMemo(() => (store.projects[storeKey] ?? []).map(enrich))
+
+  const isLocal =
+    (conn?.type === "sidecar" && conn.variant === "base") || (conn?.type === "http" && isLocalHost(conn.http.url))
+
+  return {
+    queryClient,
+    sdk,
+    sync,
+    isLocal,
+    projects: {
+      list: projectsList,
+      open(directory: string) {
+        const current = store.projects[storeKey] ?? []
+        if (current.find((x) => x.worktree === directory)) return
+        setStore("projects", storeKey, [{ worktree: directory, expanded: true }, ...current])
+      },
+      close(directory: string) {
+        const current = store.projects[storeKey] ?? []
+        setStore(
+          "projects",
+          storeKey,
+          current.filter((x) => x.worktree !== directory),
+        )
+      },
+      expand(directory: string) {
+        const current = store.projects[storeKey] ?? []
+        const index = current.findIndex((x) => x.worktree === directory)
+        if (index !== -1) setStore("projects", storeKey, index, "expanded", true)
+      },
+      collapse(directory: string) {
+        const current = store.projects[storeKey] ?? []
+        const index = current.findIndex((x) => x.worktree === directory)
+        if (index !== -1) setStore("projects", storeKey, index, "expanded", false)
+      },
+      move(directory: string, toIndex: number) {
+        const current = store.projects[storeKey] ?? []
+        const fromIndex = current.findIndex((x) => x.worktree === directory)
+        if (fromIndex === -1 || fromIndex === toIndex) return
+        const result = [...current]
+        const [item] = result.splice(fromIndex, 1)
+        result.splice(toIndex, 0, item)
+        setStore("projects", storeKey, result)
+      },
+      last() {
+        return store.lastProject[storeKey]
+      },
+      touch(directory: string) {
+        setStore("lastProject", storeKey, directory)
+      },
+    },
+  }
+}
+
+export type ServerCtx = ReturnType<typeof createServerCtx>
+
+function isLocalHost(url: string) {
+  const host = url.replace(/^https?:\/\//, "").split(":")[0]
+  if (host === "localhost" || host === "127.0.0.1") return "local"
+}
+
+function projectsKey(key: ServerConnection.Key) {
+  if (key === "sidecar") return "local"
+  if (isLocalHost(key)) return "local"
+  return key
+}
+
+export function resolveServerList(input: {
+  props?: Array<ServerConnection.Any>
+  stored: StoredServer[]
+}): Array<ServerConnection.Any> {
+  const deduped = new Map<ServerConnection.Key, ServerConnection.Any>(
+    input.props?.map((v) => [ServerConnection.key(v), v]) ?? [],
+  )
+
+  for (const value of input.stored) {
+    const conn: ServerConnection.Http =
+      typeof value === "string"
+        ? {
+            type: "http" as const,
+            http: { url: value },
+          }
+        : "http" in value
+          ? value
+          : { type: "http", http: value }
+    const key = ServerConnection.key(conn)
+
+    const existing = deduped.get(key)
+    if (existing)
+      deduped.set(key, {
+        ...existing,
+        ...conn,
+        http: { ...existing.http, ...conn.http },
+      })
+    else deduped.set(key, conn)
+  }
+
+  return [...deduped.values()]
+}

+ 14 - 9
packages/app/src/context/server-sdk.tsx

@@ -8,11 +8,12 @@ import { useLanguage } from "./language"
 import { usePlatform } from "./platform"
 import { ServerConnection, useServer } from "./server"
 import { createRefCountMap } from "@/utils/refcount"
+import { useGlobal } from "./global"
 
 const isAbortError = (error: unknown) =>
   error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
 
-function createServerSdkContext(server: ServerConnection.Any) {
+export function createServerSdkContext(server: ServerConnection.Any) {
   const platform = usePlatform()
   const abort = new AbortController()
 
@@ -244,18 +245,22 @@ function createServerSdkContext(server: ServerConnection.Any) {
   }
 }
 
+export type ServerSDK = ReturnType<typeof createServerSdkContext>
+
 export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleContext({
   name: "ServerSDK",
-  init: () => {
+  init: (props: { server?: ServerConnection.Any }) => {
+    const global = useGlobal()
     const language = useLanguage()
     const server = useServer()
 
-    if (!server.current) throw new Error(language.t("error.serverSDK.noServerAvailable"))
-    const sdk = createServerSdkContext(server.current)
-    return {
-      ...sdk,
-      createDirSdkContext: createRefCountMap((dir) => createDirSdkContext(dir, sdk)),
-    }
+    const conn = props.server ?? server.current
+    if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable"))
+
+    const ctx = global.createServerCtx(conn)
+    return Object.assign(ctx.sdk, {
+      createDirSdkContext: createRefCountMap((dir) => createDirSdkContext(dir, ctx.sdk)),
+    })
   },
 })
 
@@ -263,7 +268,7 @@ type SDKEventMap = {
   [key in Event["type"]]: Extract<Event, { type: key }>
 }
 
-function createDirSdkContext(directory: string, serverSDK: ReturnType<typeof createServerSdkContext>) {
+function createDirSdkContext(directory: string, serverSDK: ServerSDK) {
   const client = serverSDK.createClient({
     directory,
     throwOnError: true,

+ 30 - 12
packages/app/src/context/server-sync.tsx

@@ -1,11 +1,21 @@
 import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
 import { showToast } from "@/utils/toast"
 import { getFilename } from "@opencode-ai/core/util/path"
-import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
+import {
+  batch,
+  createContext,
+  createEffect,
+  getOwner,
+  onCleanup,
+  onMount,
+  type ParentProps,
+  untrack,
+  useContext,
+} from "solid-js"
 import { createStore, produce, reconcile } from "solid-js/store"
 import { useLanguage } from "@/context/language"
 import type { InitError } from "../pages/error"
-import { useServerSDK } from "./server-sdk"
+import { ServerSDK, useServerSDK } from "./server-sdk"
 import {
   bootstrapDirectory,
   bootstrapGlobal,
@@ -31,6 +41,8 @@ import { PathKey } from "@/utils/path-key"
 import { createDirSyncContext } from "./directory-sync"
 import { createSimpleContext, NormalizedProviderListResponse } from "@opencode-ai/ui/context"
 import { createRefCountMap } from "@/utils/refcount"
+import { useGlobal } from "./global"
+import { ServerConnection, useServer } from "./server"
 import { retry } from "@opencode-ai/core/util/retry"
 
 type GlobalStore = {
@@ -74,8 +86,8 @@ function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: Path
 }
 export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
 
-export function createServerSyncContext() {
-  const serverSDK = useServerSDK()
+export function createServerSyncContext(_serverSDK?: ServerSDK) {
+  const serverSDK: ServerSDK = _serverSDK ?? useServerSDK()
   const language = useLanguage()
   const owner = getOwner()
   if (!owner) throw new Error("ServerSync must be created within owner")
@@ -105,7 +117,7 @@ export function createServerSyncContext() {
 
   const [globalStore, setGlobalStore] = createStore<GlobalStore>({
     get ready() {
-      return bootstrap.isPending
+      return !bootstrap.isPending
     },
     project: [],
     session_todo: {},
@@ -128,6 +140,7 @@ export function createServerSyncContext() {
       return updateConfigMutation.isPending ? "pending" : undefined
     },
   })
+
   const queryClient = useQueryClient()
 
   let bootedAt = 0
@@ -465,17 +478,22 @@ export function createServerSyncContext() {
 
 export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({
   name: "ServerSync",
-  init: () => {
-    const sync = createServerSyncContext()
+  init: (props: { server?: ServerConnection.Any }) => {
+    const global = useGlobal()
+    const language = useLanguage()
+    const server = useServer()
+
+    const conn = props.server ?? server.current
+    if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable"))
+    const ctx = global.createServerCtx(conn)
 
-    return {
-      ...sync,
+    return Object.assign(ctx.sync, {
       createDirSyncContext: createRefCountMap(
-        (dir) => createDirSyncContext(dir, sync),
-        (dir) => sync.disableMcp(dir),
+        (dir) => createDirSyncContext(dir, ctx.sync),
+        (dir) => ctx.sync.disableMcp(dir),
         directoryKey,
       ),
-    }
+    })
   },
 })
 

+ 0 - 20
packages/app/src/context/servers.tsx

@@ -1,20 +0,0 @@
-import { createSimpleContext } from "@opencode-ai/ui/context"
-import { useServer } from "./server"
-import { useServerHealth } from "@/utils/server-health"
-
-export const { use: useServers, provider: ServersProvider } = createSimpleContext({
-  name: "Servers",
-  init: () => {
-    const server = useServer()
-
-    const health = useServerHealth(
-      () => server.list,
-      () => true,
-    )
-
-    return {
-      list: () => server.list,
-      health,
-    }
-  },
-})

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

@@ -159,6 +159,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
   init: () => {
     const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
 
+    createEffect(() => {
+      console.log("settings", { ready: ready() })
+    })
+
     createEffect(() => {
       if (typeof document === "undefined") return
       const root = document.documentElement

+ 117 - 94
packages/app/src/pages/home.tsx

@@ -23,7 +23,6 @@ import { DialogSelectDirectory } from "@/components/dialog-select-directory"
 import { DialogSelectServer } from "@/components/dialog-select-server"
 import { ServerConnection, useServer } from "@/context/server"
 import { useServerSync } from "@/context/server-sync"
-import { useServers } from "@/context/servers"
 import { useLanguage } from "@/context/language"
 import { useNotification } from "@/context/notification"
 import { usePermission } from "@/context/permission"
@@ -32,6 +31,7 @@ import { sessionTitle } from "@/utils/session-title"
 import { pathKey } from "@/utils/path-key"
 import { messageAgentColor } from "@/utils/agent"
 import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
+import { useGlobal } from "@/context/global"
 import { useCommand } from "@/context/command"
 import { useSettings } from "@/context/settings"
 import { ServerHealthIndicator } from "@/components/server/server-row"
@@ -116,6 +116,7 @@ function HomeDesign() {
   const navigate = useNavigate()
   const server = useServer()
   const language = useLanguage()
+  const global = useGlobal()
   const command = useCommand()
   const notification = useNotification()
   let focusSessionSearch: (() => void) | undefined
@@ -187,8 +188,9 @@ function HomeDesign() {
     setState("project", state.project === directory ? undefined : directory)
   }
 
-  function addProject(directory: string) {
-    layout.projects.open(directory)
+  function addProject(conn: ServerConnection.Any, directory: string) {
+    const server = global.createServerCtx(conn)
+    server.projects.open(directory)
     server.projects.touch(directory)
     setState("project", directory)
   }
@@ -196,7 +198,8 @@ function HomeDesign() {
   function openNewSession() {
     const project = selectedProject()
     if (!project) {
-      void chooseProject()
+      const conn = server.current
+      if (conn) void chooseProject(conn)
       return
     }
     layout.projects.open(project.worktree)
@@ -233,17 +236,19 @@ function HomeDesign() {
     navigate(`/${base64Encode(session.directory)}/session/${session.id}`)
   }
 
-  async function chooseProject() {
+  async function chooseProject(conn: ServerConnection.Any) {
     function resolve(result: string | string[] | null) {
       if (Array.isArray(result)) {
-        result.forEach(addProject)
+        result.forEach((r) => addProject(conn, r))
         if (result[0]) setState("project", result[0])
         return
       }
-      if (result) addProject(result)
+      if (result) addProject(conn, result)
     }
 
-    if (platform.openDirectoryPickerDialog && server.isLocal()) {
+    const server = global.createServerCtx(conn)
+
+    if (platform.openDirectoryPickerDialog && server.isLocal) {
       const result = await platform.openDirectoryPickerDialog?.({
         title: language.t("command.project.open"),
         multiple: true,
@@ -253,7 +258,7 @@ function HomeDesign() {
     }
 
     dialog.show(
-      () => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
+      () => <DialogSelectDirectory multiple={true} onSelect={resolve} server={conn} />,
       () => resolve(null),
     )
   }
@@ -265,72 +270,77 @@ function HomeDesign() {
   }
 
   return (
-    <div class="mx-auto grid w-full h-full max-w-[1080px] gap-8 px-6 pb-16 lg:grid-cols-[280px_minmax(0,720px)]">
-      <HomeProjectColumn
-        projects={projects()}
-        selected={selectedProject()?.worktree}
-        selectProject={selectProject}
-        openNewSession={openProjectNewSession}
-        chooseProject={() => void chooseProject()}
-        editProject={editProject}
-        closeProject={(directory) => {
-          layout.projects.close(directory)
-          if (state.project === directory) setState("project", undefined)
-        }}
-        clearNotifications={clearNotifications}
-        unseenCount={unseenCount}
-        openSettings={openSettings}
-        openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
-        language={language}
-      />
-
-      <section class="min-w-0 flex-1 flex flex-col pt-12" aria-label={language.t("sidebar.project.recentSessions")}>
-        <HomeSessionSearch
-          value={state.search}
-          placeholder={language.t("home.sessions.search.placeholder")}
-          open={searchOpen()}
-          loading={sessionLoad.isLoading}
-          results={searchResults()}
-          noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })}
-          bindFocus={(focus) => {
-            focusSessionSearch = focus
+    <div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 bg-background-base self-stretch flex-1">
+      <div class="mx-auto grid w-full h-full max-w-[1080px] gap-8 px-6 pb-16 lg:grid-cols-[280px_minmax(0,720px)]">
+        <HomeProjectColumn
+          projects={projects()}
+          selected={selectedProject()?.worktree}
+          selectProject={selectProject}
+          openNewSession={openProjectNewSession}
+          chooseProject={(conn) => void chooseProject(conn)}
+          editProject={editProject}
+          closeProject={(directory) => {
+            layout.projects.close(directory)
+            if (state.project === directory) setState("project", undefined)
           }}
-          onInput={(value) => setState("search", value)}
-          onFocus={() => setState("searchFocused", true)}
-          onClose={closeSearch}
-          onSelect={selectSearchSession}
+          clearNotifications={clearNotifications}
+          unseenCount={unseenCount}
+          openSettings={openSettings}
+          openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
+          language={language}
         />
-        <div class="mt-3 min-h-0 flex-1 overflow-y-auto">
-          <div class="pt-3 flex flex-col gap-6">
-            <Show when={!sessionLoad.isLoading} fallback={<HomeSessionSkeleton label={language.t("common.loading")} />}>
+
+        <section class="min-w-0 flex-1 flex flex-col pt-12" aria-label={language.t("sidebar.project.recentSessions")}>
+          <HomeSessionSearch
+            value={state.search}
+            placeholder={language.t("home.sessions.search.placeholder")}
+            open={searchOpen()}
+            loading={sessionLoad.isLoading}
+            results={searchResults()}
+            noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })}
+            bindFocus={(focus) => {
+              focusSessionSearch = focus
+            }}
+            onInput={(value) => setState("search", value)}
+            onFocus={() => setState("searchFocused", true)}
+            onClose={closeSearch}
+            onSelect={selectSearchSession}
+          />
+          <div class="mt-3 min-h-0 flex-1 overflow-y-auto">
+            <div class="pt-3 flex flex-col gap-6">
               <Show
-                when={groups().length > 0}
-                fallback={
-                  <div class="flex min-w-0 flex-col gap-4">
-                    <HomeSessionGroupHeader title={language.t("home.sessions.empty")} onNewSession={openNewSession} />
-                  </div>
-                }
+                when={!sessionLoad.isLoading}
+                fallback={<HomeSessionSkeleton label={language.t("common.loading")} />}
               >
-                <For each={groups()}>
-                  {(group, index) => (
+                <Show
+                  when={groups().length > 0}
+                  fallback={
                     <div class="flex min-w-0 flex-col gap-4">
-                      <HomeSessionGroupHeader
-                        title={group.title}
-                        onNewSession={index() === 0 ? openNewSession : undefined}
-                      />
-                      <div class="flex min-w-0 flex-col gap-px">
-                        <For each={group.sessions}>
-                          {(record) => <HomeSessionRow record={record} openSession={openSession} />}
-                        </For>
-                      </div>
+                      <HomeSessionGroupHeader title={language.t("home.sessions.empty")} onNewSession={openNewSession} />
                     </div>
-                  )}
-                </For>
+                  }
+                >
+                  <For each={groups()}>
+                    {(group, index) => (
+                      <div class="flex min-w-0 flex-col gap-4">
+                        <HomeSessionGroupHeader
+                          title={group.title}
+                          onNewSession={index() === 0 ? openNewSession : undefined}
+                        />
+                        <div class="flex min-w-0 flex-col gap-px">
+                          <For each={group.sessions}>
+                            {(record) => <HomeSessionRow record={record} openSession={openSession} />}
+                          </For>
+                        </div>
+                      </div>
+                    )}
+                  </For>
+                </Show>
               </Show>
-            </Show>
+            </div>
           </div>
-        </div>
-      </section>
+        </section>
+      </div>
     </div>
   )
 }
@@ -340,7 +350,7 @@ function HomeProjectColumn(props: {
   selected?: string
   selectProject: (directory: string) => void
   openNewSession: (directory: string) => void
-  chooseProject: () => void
+  chooseProject: (server: ServerConnection.Any) => void
   editProject: (project: LocalProject) => void
   closeProject: (directory: string) => void
   clearNotifications: (project: LocalProject) => void
@@ -349,27 +359,33 @@ function HomeProjectColumn(props: {
   openHelp: () => void
   language: ReturnType<typeof useLanguage>
 }) {
-  const servers = useServers()
+  const global = useGlobal()
   return (
     <aside class="flex min-w-0 flex-col lg:pt-[52px] mt-14 gap-4" aria-label={props.language.t("home.projects")}>
       <div class="flex h-7 min-w-0 items-center justify-between pl-1.5">
         <div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
-        <IconButtonV2
-          data-action="home-add-project"
-          variant="ghost-muted"
-          size="large"
-          class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
-          icon={<IconV2 name="folder-add-left" />}
-          onClick={props.chooseProject}
-          aria-label={props.language.t("home.project.add")}
-        />
+        <Show when={global.servers.list().length === 1}>
+          <IconButtonV2
+            data-action="home-add-project"
+            variant="ghost-muted"
+            size="large"
+            class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
+            icon={<IconV2 name="folder-add-left" />}
+            onClick={() => props.chooseProject(global.servers.list()[0]!)}
+            aria-label={props.language.t("home.project.add")}
+          />
+        </Show>
       </div>
-      <Show when={servers.list().length > 1} fallback={<HomeProjectList {...props} />}>
-        <For each={servers.list()}>
+      <Show
+        when={global.servers.list().length > 1}
+        fallback={<HomeProjectList {...props} chooseProject={() => props.chooseProject(global.servers.list()[0]!)} />}
+      >
+        <For each={global.servers.list()}>
           {(item) => {
             const key = ServerConnection.key(item)
-            const healthy = () => !!servers.health[key]?.healthy
+            const healthy = () => !!global.servers.health[key]?.healthy
             const [state, setState] = createStore({ open: true })
+            const serverCtx = global.createServerCtx(item)
             return (
               <div class="flex max-h-[min(572px,calc(100vh_-_300px))] min-w-0 flex-col gap-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
                 <button
@@ -379,7 +395,7 @@ function HomeProjectColumn(props: {
                   onClick={() => setState("open", !state.open)}
                 >
                   <div class="flex size-4 shrink-0 items-center justify-center">
-                    <ServerHealthIndicator health={servers.health[key]} />
+                    <ServerHealthIndicator health={global.servers.health[key]} />
                   </div>
                   <span class={HOME_PROJECT_NAV_LABEL}>{item.displayName ?? new URL(item.http.url).host}</span>
                   <Show when={healthy()}>
@@ -392,7 +408,11 @@ function HomeProjectColumn(props: {
                 </button>
                 <Show when={healthy() && state.open}>
                   <div class="mx-3 h-px bg-v2-border-border-base" />
-                  <HomeProjectList {...props} />
+                  <HomeProjectList
+                    {...props}
+                    projects={serverCtx.projects.list()}
+                    chooseProject={() => props.chooseProject(item)}
+                  />
                 </Show>
               </div>
             )
@@ -974,12 +994,11 @@ function groupSessions(records: HomeSessionRecord[], language: ReturnType<typeof
 
 function LegacyHome() {
   const sync = useServerSync()
-  const layout = useLayout()
   const platform = usePlatform()
   const dialog = useDialog()
   const navigate = useNavigate()
+  const global = useGlobal()
   const server = useServer()
-  const servers = useServers()
   const language = useLanguage()
   const homedir = createMemo(() => sync.data.path.home)
   const recent = createMemo(() => {
@@ -990,26 +1009,30 @@ function LegacyHome() {
   })
 
   const serverDotClass = createMemo(() => {
-    const healthy = servers.health[server.key]?.healthy
+    const healthy = global.servers.health[server.key]?.healthy
     if (healthy === true) return "bg-icon-success-base"
     if (healthy === false) return "bg-icon-critical-base"
     return "bg-border-weak-base"
   })
 
-  function openProject(directory: string) {
-    layout.projects.open(directory)
-    server.projects.touch(directory)
+  function openProject(server: ServerConnection.Any, directory: string) {
+    const serverCtx = global.createServerCtx(server)
+    serverCtx.projects.open(directory)
+    serverCtx.projects.touch(directory)
     navigate(`/${base64Encode(directory)}`)
   }
 
   async function chooseProject() {
-    function resolve(result: string | string[] | null) {
+    const s = server.current
+    if (!s) return
+
+    const resolve = (result: string | string[] | null) => {
       if (Array.isArray(result)) {
         for (const directory of result) {
-          openProject(directory)
+          openProject(s, directory)
         }
       } else if (result) {
-        openProject(result)
+        openProject(s, result)
       }
     }
 
@@ -1021,7 +1044,7 @@ function LegacyHome() {
       resolve(result)
     } else {
       dialog.show(
-        () => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
+        () => <DialogSelectDirectory multiple={true} onSelect={resolve} server={s} />,
         () => resolve(null),
       )
     }
@@ -1060,7 +1083,7 @@ function LegacyHome() {
                     size="large"
                     variant="ghost"
                     class="text-14-mono text-left justify-between px-3"
-                    onClick={() => openProject(project.worktree)}
+                    onClick={() => openProject(server.current!, project.worktree)}
                   >
                     {project.worktree.replace(homedir(), "~")}
                     <div class="text-14-regular text-text-weak">

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

@@ -1468,6 +1468,8 @@ export default function Layout(props: ParentProps) {
   }
 
   async function chooseProject() {
+    const conn = server.current
+    if (!conn) return
     function resolve(result: string | string[] | null) {
       if (Array.isArray(result)) {
         for (const directory of result) {
@@ -1490,7 +1492,7 @@ export default function Layout(props: ParentProps) {
       void import("@/components/dialog-select-directory").then((x) => {
         if (dialogDead || dialogRun !== run) return
         dialog.show(
-          () => <x.DialogSelectDirectory multiple={true} onSelect={resolve} />,
+          () => <x.DialogSelectDirectory multiple={true} onSelect={resolve} server={conn} />,
           () => resolve(null),
         )
       })

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

@@ -1747,7 +1747,7 @@ export default function Page() {
             "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(),
-            "rounded-[10px] shadow-[var(--v2-elevation-raised)]": settings.general.newLayoutDesigns(),
+            "rounded-[10px] shadow-[var(--v2-elevation-raised)]": settings.general.newLayoutDesigns() && !!params.id,
           }}
           style={{
             width: sessionPanelWidth(),

+ 1 - 1
packages/ui/src/components/list.css

@@ -21,7 +21,7 @@
   flex-direction: column;
   gap: 12px;
   overflow: hidden;
-  padding: 0 12px;
+  /*padding: 0 12px;*/
 
   [data-slot="list-search-wrapper"] {
     display: flex;