Sfoglia il codice sorgente

feat(desktop): add layout transition switch (#36667)

Co-authored-by: Brendan Allan <git@brendonovich.dev>
usrnk1 4 settimane fa
parent
commit
265a93927d
38 ha cambiato i file con 550 aggiunte e 100 eliminazioni
  1. 53 17
      packages/app/src/components/settings-general.tsx
  2. 34 18
      packages/app/src/components/settings-v2/general.tsx
  3. 76 0
      packages/app/src/components/settings-v2/interface-transition.stories.tsx
  4. 54 0
      packages/app/src/components/settings-v2/interface-transition.tsx
  5. 6 1
      packages/app/src/components/settings-v2/settings-v2.css
  6. 40 0
      packages/app/src/context/settings.test.ts
  7. 79 4
      packages/app/src/context/settings.tsx
  8. 7 3
      packages/app/src/i18n/ar.ts
  9. 7 3
      packages/app/src/i18n/br.ts
  10. 7 3
      packages/app/src/i18n/bs.ts
  11. 7 3
      packages/app/src/i18n/da.ts
  12. 7 3
      packages/app/src/i18n/de.ts
  13. 12 7
      packages/app/src/i18n/en.ts
  14. 7 3
      packages/app/src/i18n/es.ts
  15. 7 3
      packages/app/src/i18n/fr.ts
  16. 7 3
      packages/app/src/i18n/ja.ts
  17. 7 2
      packages/app/src/i18n/ko.ts
  18. 7 3
      packages/app/src/i18n/no.ts
  19. 7 3
      packages/app/src/i18n/pl.ts
  20. 7 3
      packages/app/src/i18n/ru.ts
  21. 7 3
      packages/app/src/i18n/th.ts
  22. 7 3
      packages/app/src/i18n/tr.ts
  23. 7 3
      packages/app/src/i18n/uk.ts
  24. 6 2
      packages/app/src/i18n/zh.ts
  25. 6 2
      packages/app/src/i18n/zht.ts
  26. 1 0
      packages/app/src/index.ts
  27. 8 1
      packages/desktop/src/main/index.ts
  28. 19 0
      packages/desktop/src/main/install-state.test.ts
  29. 8 0
      packages/desktop/src/main/install-state.ts
  30. 2 0
      packages/desktop/src/main/ipc.ts
  31. 18 1
      packages/desktop/src/main/onboarding.ts
  32. 1 0
      packages/desktop/src/main/store-keys.ts
  33. 1 0
      packages/desktop/src/preload/index.ts
  34. 1 0
      packages/desktop/src/preload/types.ts
  35. 3 0
      packages/desktop/src/renderer/onboarding.tsx
  36. 6 0
      packages/ui/src/v2/components/badge-v2.css
  37. 6 1
      packages/ui/src/v2/components/badge-v2.stories.tsx
  38. 5 2
      packages/ui/src/v2/components/badge-v2.tsx

+ 53 - 17
packages/app/src/components/settings-general.tsx

@@ -5,6 +5,7 @@ import { Select } from "@opencode-ai/ui/select"
 import { Switch } from "@opencode-ai/ui/switch"
 import { TextField } from "@opencode-ai/ui/text-field"
 import { Tooltip } from "@opencode-ai/ui/tooltip"
+import { Tag } from "@opencode-ai/ui/v2/badge-v2"
 import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useParams } from "@solidjs/router"
@@ -248,6 +249,50 @@ export const SettingsGeneral: Component = () => {
     triggerVariant: "settings" as const,
   })
 
+  const InterfaceSection = () => (
+    <div class="flex flex-col gap-1">
+      <SettingsList>
+        <SettingsRow
+          title={
+            <span class="flex items-center gap-2">
+              {language.t("settings.general.row.newInterface.title")}
+              <Tag variant="accent">{language.t("settings.general.row.newInterface.badge")}</Tag>
+            </span>
+          }
+          description={language.t("settings.general.row.newInterface.description")}
+        >
+          <div data-action="settings-new-layout-designs">
+            <Switch
+              checked={settings.general.newLayoutDesigns()}
+              onChange={(checked) => {
+                settings.general.setNewLayoutDesigns(checked)
+                if (!checked) return
+                void import("@/components/settings-v2").then((module) => {
+                  void dialog.show(() => <module.DialogSettings />)
+                })
+              }}
+            />
+          </div>
+        </SettingsRow>
+      </SettingsList>
+    </div>
+  )
+
+  const InterfaceNoticeSection = () => (
+    <div class="flex flex-col gap-1">
+      <SettingsList>
+        <SettingsRow
+          title={language.t("settings.general.row.newInterfaceNotice.title")}
+          description={language.t("settings.general.row.newInterfaceNotice.description")}
+        >
+          <Button size="small" variant="ghost" onClick={settings.general.dismissNewInterfaceNotice}>
+            {language.t("settings.general.row.newInterfaceNotice.dismiss")}
+          </Button>
+        </SettingsRow>
+      </SettingsList>
+    </div>
+  )
+
   const GeneralSection = () => (
     <div class="flex flex-col gap-1">
       <SettingsList>
@@ -335,23 +380,6 @@ export const SettingsGeneral: Component = () => {
           </div>
         </SettingsRow>
 
-        <SettingsRow
-          title={language.t("settings.general.row.newLayoutDesigns.title")}
-          description={language.t("settings.general.row.newLayoutDesigns.description")}
-        >
-          <div data-action="settings-new-layout-designs">
-            <Switch
-              checked={settings.general.newLayoutDesigns()}
-              onChange={(checked) => {
-                settings.general.setNewLayoutDesigns(checked)
-                if (!checked) return
-                void import("@/components/settings-v2").then((module) => {
-                  dialog.show(() => <module.DialogSettings />)
-                })
-              }}
-            />
-          </div>
-        </SettingsRow>
       </SettingsList>
     </div>
   )
@@ -718,6 +746,14 @@ export const SettingsGeneral: Component = () => {
       </div>
 
       <div class="flex flex-col gap-8 w-full">
+        <Show when={settings.general.layoutTransitionAvailable()}>
+          <InterfaceSection />
+        </Show>
+
+        <Show when={settings.general.newInterfaceNoticeVisible()}>
+          <InterfaceNoticeSection />
+        </Show>
+
         <GeneralSection />
 
         <AppearanceSection />

+ 34 - 18
packages/app/src/components/settings-v2/general.tsx

@@ -28,6 +28,7 @@ import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
 import { Link } from "../link"
 import { SettingsListV2 } from "./parts/list"
 import { SettingsRowV2 } from "./parts/row"
+import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition"
 import "./settings-v2.css"
 
 let demoSoundState = {
@@ -226,6 +227,31 @@ export const SettingsGeneralV2: Component<{
     },
   })
 
+  const InterfaceSection = () => (
+    <LayoutTransitionToggle
+      title={language.t("settings.general.row.newInterface.title")}
+      badge={language.t("settings.general.row.newInterface.badge")}
+      description={language.t("settings.general.row.newInterface.description")}
+      checked={settings.general.newLayoutDesigns()}
+      onChange={(checked) => {
+        settings.general.setNewLayoutDesigns(checked)
+        if (checked) return
+        void import("@/components/dialog-settings").then((module) => {
+          void dialog.show(() => <module.DialogSettings />)
+        })
+      }}
+    />
+  )
+
+  const InterfaceNoticeSection = () => (
+    <LayoutRetirementNotice
+      title={language.t("settings.general.row.newInterfaceNotice.title")}
+      description={language.t("settings.general.row.newInterfaceNotice.description")}
+      dismiss={language.t("settings.general.row.newInterfaceNotice.dismiss")}
+      onDismiss={settings.general.dismissNewInterfaceNotice}
+    />
+  )
+
   const GeneralSection = () => (
     <div class="settings-v2-section">
       <SettingsListV2>
@@ -312,24 +338,6 @@ export const SettingsGeneralV2: Component<{
           </div>
         </SettingsRowV2>
 
-        <SettingsRowV2
-          title={language.t("settings.general.row.newLayoutDesigns.title")}
-          description={language.t("settings.general.row.newLayoutDesigns.description")}
-        >
-          <div data-action="settings-new-layout-designs">
-            <Switch
-              checked={settings.general.newLayoutDesigns()}
-              onChange={(checked) => {
-                settings.general.setNewLayoutDesigns(checked)
-                if (checked) return
-                void import("@/components/dialog-settings").then((module) => {
-                  dialog.show(() => <module.DialogSettings />)
-                })
-              }}
-            />
-          </div>
-        </SettingsRowV2>
-
         <Show when={mobile() && import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"}>
           <SettingsRowV2
             title={language.t("settings.general.row.mobileTitlebarBottom.title")}
@@ -683,6 +691,14 @@ export const SettingsGeneralV2: Component<{
       </div>
 
       <div class="settings-v2-tab-body">
+        <Show when={settings.general.layoutTransitionAvailable()}>
+          <InterfaceSection />
+        </Show>
+
+        <Show when={settings.general.newInterfaceNoticeVisible()}>
+          <InterfaceNoticeSection />
+        </Show>
+
         <GeneralSection />
 
         <AppearanceSection />

+ 76 - 0
packages/app/src/components/settings-v2/interface-transition.stories.tsx

@@ -0,0 +1,76 @@
+// @ts-nocheck
+import { Show } from "solid-js"
+import { createStore } from "solid-js/store"
+import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition"
+
+const copy = {
+  title: "New layout",
+  badge: "New",
+  description: "Use the new tabs and home layout. Switch between layouts for a limited time.",
+  noticeTitle: "You're now using new layout",
+  noticeDescription: "The previous layout is no longer available",
+  dismiss: "Dismiss",
+}
+
+function Frame(props) {
+  return <div class="w-[640px] max-w-full">{props.children}</div>
+}
+
+function ToggleExample(props) {
+  const [state, setState] = createStore({ checked: props.checked })
+  return (
+    <Frame>
+      <LayoutTransitionToggle
+        title={copy.title}
+        badge={copy.badge}
+        description={copy.description}
+        checked={state.checked}
+        onChange={(checked) => setState("checked", checked)}
+      />
+    </Frame>
+  )
+}
+
+function NoticeExample() {
+  const [state, setState] = createStore({ dismissed: false })
+  return (
+    <Frame>
+      <Show when={!state.dismissed} fallback={<span class="text-v2-text-text-muted">Notice dismissed</span>}>
+        <LayoutRetirementNotice
+          title={copy.noticeTitle}
+          description={copy.noticeDescription}
+          dismiss={copy.dismiss}
+          onDismiss={() => setState("dismissed", true)}
+        />
+      </Show>
+    </Frame>
+  )
+}
+
+export default {
+  title: "App/Settings/Layout transition",
+  id: "app-settings-layout-transition",
+  component: LayoutTransitionToggle,
+}
+
+export const NewLayoutEnabled = {
+  render: () => <ToggleExample checked />,
+}
+
+export const PreviousLayoutEnabled = {
+  render: () => <ToggleExample checked={false} />,
+}
+
+export const PreviousLayoutRetired = {
+  render: () => <NoticeExample />,
+}
+
+export const AllStates = {
+  render: () => (
+    <div class="flex flex-col gap-8">
+      <ToggleExample checked />
+      <ToggleExample checked={false} />
+      <NoticeExample />
+    </div>
+  ),
+}

+ 54 - 0
packages/app/src/components/settings-v2/interface-transition.tsx

@@ -0,0 +1,54 @@
+import { Tag } from "@opencode-ai/ui/v2/badge-v2"
+import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
+import { Switch } from "@opencode-ai/ui/v2/switch-v2"
+import { SettingsListV2 } from "./parts/list"
+import { SettingsRowV2 } from "./parts/row"
+
+export function LayoutTransitionToggle(props: {
+  title: string
+  badge: string
+  description: string
+  checked: boolean
+  onChange: (checked: boolean) => void
+}) {
+  return (
+    <div class="settings-v2-section">
+      <div class="settings-v2-interface-feature">
+        <SettingsListV2>
+          <SettingsRowV2
+            title={
+              <span class="flex items-center gap-2">
+                {props.title}
+                <Tag variant="accent">{props.badge}</Tag>
+              </span>
+            }
+            description={props.description}
+          >
+            <div data-action="settings-new-layout-designs">
+              <Switch checked={props.checked} onChange={props.onChange} />
+            </div>
+          </SettingsRowV2>
+        </SettingsListV2>
+      </div>
+    </div>
+  )
+}
+
+export function LayoutRetirementNotice(props: {
+  title: string
+  description: string
+  dismiss: string
+  onDismiss: () => void
+}) {
+  return (
+    <div class="settings-v2-section">
+      <SettingsListV2>
+        <SettingsRowV2 title={props.title} description={props.description}>
+          <ButtonV2 size="small" variant="ghost-muted" onClick={props.onDismiss}>
+            {props.dismiss}
+          </ButtonV2>
+        </SettingsRowV2>
+      </SettingsListV2>
+    </div>
+  )
+}

+ 6 - 1
packages/app/src/components/settings-v2/settings-v2.css

@@ -90,6 +90,11 @@
   box-shadow: inset 0 0 0 0.5px var(--v2-border-border-muted);
 }
 
+.settings-v2-interface-feature [data-component="settings-v2-list"] {
+  background-color: var(--v2-background-bg-base);
+  box-shadow: var(--v2-elevation-raised);
+}
+
 [data-component="settings-v2-row"] {
   display: flex;
   flex-wrap: wrap;
@@ -128,7 +133,7 @@
 }
 
 [data-slot="settings-v2-row-description"] {
-  font-size: 11px;
+  font-size: 13px;
   font-weight: 440;
   line-height: 1;
   color: var(--v2-text-text-muted);

+ 40 - 0
packages/app/src/context/settings.test.ts

@@ -0,0 +1,40 @@
+import { describe, expect, test } from "bun:test"
+import {
+  layoutTransitionState,
+  maximumSunsetTimeout,
+  newLayoutDesignsDefault,
+  nextSunsetCheckDelay,
+  resolveNewLayoutDesigns,
+} from "./settings"
+
+describe("layout transition", () => {
+  test("blank profiles default to the new layout", () => {
+    expect(newLayoutDesignsDefault).toBe(true)
+  })
+
+  test("hides the transition until a sunset is scheduled", () => {
+    expect(layoutTransitionState(false, true, false, false)).toEqual({ available: false, notice: false })
+  })
+
+  test("existing profiles can switch before sunset", () => {
+    expect(layoutTransitionState(true, true, false, false)).toEqual({ available: true, notice: false })
+  })
+
+  test("preserves explicit and default layout preferences", () => {
+    expect(resolveNewLayoutDesigns(false, false, true)).toBe(false)
+    expect(resolveNewLayoutDesigns(false, undefined, false)).toBe(false)
+    expect(resolveNewLayoutDesigns(false, undefined, true)).toBe(true)
+  })
+
+  test("sunset replaces the toggle with a dismissible notice", () => {
+    expect(layoutTransitionState(true, true, true, false)).toEqual({ available: false, notice: true })
+    expect(layoutTransitionState(true, true, true, true)).toEqual({ available: false, notice: false })
+    expect(resolveNewLayoutDesigns(true, false)).toBe(true)
+  })
+
+  test("caps checks for sunsets beyond the browser timeout limit", () => {
+    expect(nextSunsetCheckDelay(maximumSunsetTimeout + 1_000, 0)).toBe(maximumSunsetTimeout)
+    expect(nextSunsetCheckDelay(10_000, 9_000)).toBe(1_000)
+    expect(nextSunsetCheckDelay(9_000, 10_000)).toBe(0)
+  })
+})

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

@@ -1,5 +1,5 @@
 import { createStore, reconcile } from "solid-js/store"
-import { createEffect, createMemo } from "solid-js"
+import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { persisted } from "@/utils/persist"
 
@@ -34,6 +34,8 @@ export interface Settings {
     showCustomAgents: boolean
     mobileTitlebarPosition: "top" | "bottom"
     newLayoutDesigns?: boolean
+    layoutTransitionEligible?: boolean
+    newInterfaceNoticeDismissed?: boolean
   }
   appearance: {
     fontSize: number
@@ -52,7 +54,28 @@ export interface Settings {
 export const monoDefault = "System Mono"
 export const sansDefault = "System Sans"
 export const terminalDefault = "JetBrainsMono Nerd Font Mono"
-export const newLayoutDesignsDefault = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
+const legacyNewLayoutDesignsDefault = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
+export const newLayoutDesignsDefault = true
+// Existing users can switch layouts until local midnight on this date. Set new Date(YYYY, M-1, D) to show.
+export const oldInterfaceSunset = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" ? new Date(2026, 7, 14) : null
+
+export function layoutTransitionState(scheduled: boolean, eligible: boolean, retired: boolean, dismissed: boolean) {
+  return {
+    available: scheduled && eligible && !retired,
+    notice: scheduled && eligible && retired && !dismissed,
+  }
+}
+
+export const maximumSunsetTimeout = 2_147_483_647
+
+export function nextSunsetCheckDelay(sunset: number, now: number) {
+  return Math.min(Math.max(0, sunset - now), maximumSunsetTimeout)
+}
+
+export function resolveNewLayoutDesigns(retired: boolean, preference: boolean | undefined, fallback = true) {
+  if (retired) return true
+  return preference ?? fallback
+}
 
 const monoFallback =
   'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
@@ -160,9 +183,50 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
       () => store.general?.showCustomAgents,
       defaultSettings.general.showCustomAgents,
     )
-    const newLayoutDesigns = withFallback(() => store.general?.newLayoutDesigns, newLayoutDesignsDefault)
+    const sunset = oldInterfaceSunset
+    const [oldInterfaceRetired, setOldInterfaceRetired] = createSignal(sunset ? Date.now() >= sunset.getTime() : false)
+    const layoutTransitionClassified = createMemo(
+      () => typeof store.general?.layoutTransitionEligible === "boolean",
+    )
+    const layoutTransitionEligible = withFallback(() => store.general?.layoutTransitionEligible, false)
+    const newInterfaceNoticeDismissed = withFallback(() => store.general?.newInterfaceNoticeDismissed, false)
+    const layoutTransition = createMemo(() =>
+      layoutTransitionState(!!sunset, layoutTransitionEligible(), oldInterfaceRetired(), newInterfaceNoticeDismissed()),
+    )
+    const newLayoutDesigns = createMemo(() => {
+      if (!ready() && !oldInterfaceRetired()) return legacyNewLayoutDesignsDefault
+      if (!layoutTransitionClassified()) {
+        return resolveNewLayoutDesigns(oldInterfaceRetired(), store.general?.newLayoutDesigns, legacyNewLayoutDesignsDefault)
+      }
+      return resolveNewLayoutDesigns(
+        oldInterfaceRetired(),
+        store.general?.newLayoutDesigns,
+        layoutTransitionEligible() ? legacyNewLayoutDesignsDefault : newLayoutDesignsDefault,
+      )
+    })
     const visible = (preference: () => boolean) => createMemo(() => !newLayoutDesigns() || preference())
 
+    if (sunset && !oldInterfaceRetired()) {
+      const timeout = { current: undefined as ReturnType<typeof setTimeout> | undefined }
+      const checkSunset = () => {
+        if (Date.now() >= sunset.getTime()) {
+          setOldInterfaceRetired(true)
+          return
+        }
+        timeout.current = setTimeout(checkSunset, nextSunsetCheckDelay(sunset.getTime(), Date.now()))
+      }
+      checkSunset()
+      onCleanup(() => {
+        if (timeout.current !== undefined) clearTimeout(timeout.current)
+      })
+    }
+
+    createEffect(() => {
+      if (!ready() || !oldInterfaceRetired()) return
+      if (store.general?.newLayoutDesigns === true) return
+      setStore("general", "newLayoutDesigns", true)
+    })
+
     createEffect(() => {
       if (typeof document === "undefined") return
       const root = document.documentElement
@@ -250,7 +314,18 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
         },
         newLayoutDesigns,
         setNewLayoutDesigns(value: boolean) {
-          setStore("general", "newLayoutDesigns", value)
+          setStore("general", "newLayoutDesigns", oldInterfaceRetired() ? true : value)
+        },
+        layoutTransitionClassified,
+        setOldLayoutEligible(eligible: boolean) {
+          const current = store.general?.layoutTransitionEligible
+          if (typeof current === "boolean") return
+          setStore("general", "layoutTransitionEligible", eligible)
+        },
+        layoutTransitionAvailable: createMemo(() => ready() && layoutTransition().available),
+        newInterfaceNoticeVisible: createMemo(() => ready() && layoutTransition().notice),
+        dismissNewInterfaceNotice() {
+          setStore("general", "newInterfaceNoticeDismissed", true)
         },
       },
       visibility: {

+ 7 - 3
packages/app/src/i18n/ar.ts

@@ -735,9 +735,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
   "settings.general.row.editToolPartsExpanded.description":
     "إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
-  "settings.general.row.newLayoutDesigns.title": "التخطيط والتصاميم الجديدة",
-  "settings.general.row.newLayoutDesigns.description":
-    "تمكين التخطيط والصفحة الرئيسية ومحرر الرسائل وواجهة الجلسة المعاد تصميمها",
+  "settings.general.row.newInterface.title": "التخطيط الجديد",
+  "settings.general.row.newInterface.badge": "جديد",
+  "settings.general.row.newInterface.description":
+    "استخدم علامات التبويب الجديدة وتخطيط الصفحة الرئيسية. يمكنك التبديل بين التخطيطات لفترة محدودة.",
+  "settings.general.row.newInterfaceNotice.title": "أنت تستخدم الآن التخطيط الجديد",
+  "settings.general.row.newInterfaceNotice.description": "التخطيط السابق لم يعد متاحًا",
+  "settings.general.row.newInterfaceNotice.dismiss": "رفض",
   "settings.general.row.pinchZoom.title": "التكبير بإيماءة القرص",
   "settings.general.row.pinchZoom.description": "السماح بإيماءة القرص على لوحة اللمس وإيماءة Ctrl-scroll للتكبير",
   "settings.general.row.wayland.title": "استخدام Wayland الأصلي",

+ 7 - 3
packages/app/src/i18n/br.ts

@@ -746,9 +746,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
   "settings.general.row.editToolPartsExpanded.description":
     "Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo",
-  "settings.general.row.newLayoutDesigns.title": "Novo layout e design",
-  "settings.general.row.newLayoutDesigns.description":
-    "Ativar o layout, a página inicial, a área de composição e a interface de sessão reformulados",
+  "settings.general.row.newInterface.title": "Novo layout",
+  "settings.general.row.newInterface.badge": "Novo",
+  "settings.general.row.newInterface.description":
+    "Use as novas abas e o layout da página inicial. Alterne entre os layouts por tempo limitado.",
+  "settings.general.row.newInterfaceNotice.title": "Agora você está usando o novo layout",
+  "settings.general.row.newInterfaceNotice.description": "O layout anterior não está mais disponível",
+  "settings.general.row.newInterfaceNotice.dismiss": "Descartar",
   "settings.general.row.pinchZoom.title": "Zoom com gesto de pinça",
   "settings.general.row.pinchZoom.description":
     "Permitir gestos de pinça no trackpad e de Ctrl+rolagem para aplicar zoom",

+ 7 - 3
packages/app/src/i18n/bs.ts

@@ -811,9 +811,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
   "settings.general.row.editToolPartsExpanded.description":
     "Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci",
-  "settings.general.row.newLayoutDesigns.title": "Novi raspored i dizajn",
-  "settings.general.row.newLayoutDesigns.description":
-    "Omogući redizajnirani raspored, početnu stranicu, uređivač poruke i interfejs sesije",
+  "settings.general.row.newInterface.title": "Novi raspored",
+  "settings.general.row.newInterface.badge": "Novo",
+  "settings.general.row.newInterface.description":
+    "Koristite nove kartice i raspored početne stranice. Ograničeno vrijeme možete se prebacivati između rasporeda.",
+  "settings.general.row.newInterfaceNotice.title": "Sada koristite novi raspored",
+  "settings.general.row.newInterfaceNotice.description": "Prethodni raspored više nije dostupan",
+  "settings.general.row.newInterfaceNotice.dismiss": "Odbaci",
   "settings.general.row.pinchZoom.title": "Zumiranje štipanjem",
   "settings.general.row.pinchZoom.description":
     "Dozvoli zumiranje gestom štipanja na dodirnoj ploči i pomoću Ctrl-pomjeranja",

+ 7 - 3
packages/app/src/i18n/da.ts

@@ -804,9 +804,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
   "settings.general.row.editToolPartsExpanded.description":
     "Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen",
-  "settings.general.row.newLayoutDesigns.title": "Nyt layout og design",
-  "settings.general.row.newLayoutDesigns.description":
-    "Aktivér det nydesignede layout, startsiden, promptfeltet og sessionsbrugerfladen",
+  "settings.general.row.newInterface.title": "Nyt layout",
+  "settings.general.row.newInterface.badge": "Ny",
+  "settings.general.row.newInterface.description":
+    "Brug de nye faner og startsidens layout. Du kan skifte mellem layoutene i en begrænset periode.",
+  "settings.general.row.newInterfaceNotice.title": "Du bruger nu det nye layout",
+  "settings.general.row.newInterfaceNotice.description": "Det tidligere layout er ikke længere tilgængeligt",
+  "settings.general.row.newInterfaceNotice.dismiss": "Afvis",
   "settings.general.row.pinchZoom.title": "Knib for at zoome",
   "settings.general.row.pinchZoom.description": "Tillad knibebevægelser på pegefeltet og Ctrl-rulning for at zoome",
   "settings.general.row.wayland.title": "Brug native Wayland",

+ 7 - 3
packages/app/src/i18n/de.ts

@@ -759,9 +759,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
   "settings.general.row.editToolPartsExpanded.description":
     "Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
-  "settings.general.row.newLayoutDesigns.title": "Neues Layout und Design",
-  "settings.general.row.newLayoutDesigns.description":
-    "Neu gestaltetes Layout, Startseite, Eingabebereich und Sitzungsoberfläche aktivieren",
+  "settings.general.row.newInterface.title": "Neues Layout",
+  "settings.general.row.newInterface.badge": "Neu",
+  "settings.general.row.newInterface.description":
+    "Verwenden Sie die neuen Tabs und das Startseitenlayout. Für begrenzte Zeit können Sie zwischen den Layouts wechseln.",
+  "settings.general.row.newInterfaceNotice.title": "Sie verwenden jetzt das neue Layout",
+  "settings.general.row.newInterfaceNotice.description": "Das vorherige Layout ist nicht mehr verfügbar",
+  "settings.general.row.newInterfaceNotice.dismiss": "Verwerfen",
   "settings.general.row.pinchZoom.title": "Zoom per Fingergeste",
   "settings.general.row.pinchZoom.description": "Zoomen per Zwei-Finger- und Ctrl-Scroll-Geste erlauben",
   "settings.general.row.wayland.title": "Natives Wayland verwenden",

+ 12 - 7
packages/app/src/i18n/en.ts

@@ -855,9 +855,8 @@ export const dict = {
 
   "settings.general.row.language.title": "Language",
   "settings.general.row.language.description": "Change the display language for OpenCode",
-  "settings.general.row.shell.title": "Terminal Shell",
-  "settings.general.row.shell.description":
-    "Choose the shell used for your terminal. Compatible shells are also used for agent tool calls.",
+  "settings.general.row.shell.title": "Terminal shell",
+  "settings.general.row.shell.description": "Shell used by the terminal and agent tools",
   "settings.general.row.shell.autoDefault": "Auto (Default)",
   "settings.general.row.shell.terminalOnly": "terminal only",
   "settings.general.row.appearance.title": "Appearance",
@@ -889,8 +888,9 @@ export const dict = {
   "settings.general.row.mobileTitlebarBottom.title": "Bottom navigation",
   "settings.general.row.mobileTitlebarBottom.description":
     "Place the title bar and session tabs at the bottom of the screen on mobile",
-  "settings.general.row.showCustomAgents.title": "Custom agents",
-  "settings.general.row.showCustomAgents.description": "Show the agent picker in the composer",
+  "settings.general.row.showCustomAgents.title": "Show agent",
+  "settings.general.row.showCustomAgents.description":
+    "Switch between agents in the composer. When hidden, defaults to Build agent.",
   "settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
   "settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
   "settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
@@ -899,8 +899,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Expand edit tool parts",
   "settings.general.row.editToolPartsExpanded.description":
     "Show edit, write, and patch tool parts expanded by default in the timeline",
-  "settings.general.row.newLayoutDesigns.title": "New layout and designs",
-  "settings.general.row.newLayoutDesigns.description": "Enable the redesigned layout, home, composer, and session UI",
+  "settings.general.row.newInterface.title": "New layout",
+  "settings.general.row.newInterface.badge": "New",
+  "settings.general.row.newInterface.description":
+    "Use the new tabs and home layout. Switch between layouts for a limited time.",
+  "settings.general.row.newInterfaceNotice.title": "You're now using new layout",
+  "settings.general.row.newInterfaceNotice.description": "The previous layout is no longer available",
+  "settings.general.row.newInterfaceNotice.dismiss": "Dismiss",
   "settings.general.row.pinchZoom.title": "Pinch to zoom",
   "settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom",
 

+ 7 - 3
packages/app/src/i18n/es.ts

@@ -816,9 +816,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición",
   "settings.general.row.editToolPartsExpanded.description":
     "Mostrar las partes de las herramientas de edición, escritura y parcheado expandidas por defecto en la línea de tiempo",
-  "settings.general.row.newLayoutDesigns.title": "Nuevo diseño y estilos",
-  "settings.general.row.newLayoutDesigns.description":
-    "Activar el nuevo diseño de la interfaz, la página de inicio, el editor y las sesiones",
+  "settings.general.row.newInterface.title": "Nuevo diseño",
+  "settings.general.row.newInterface.badge": "Nuevo",
+  "settings.general.row.newInterface.description":
+    "Usa las nuevas pestañas y el diseño de la página de inicio. Durante un tiempo limitado, puedes cambiar entre los diseños.",
+  "settings.general.row.newInterfaceNotice.title": "Ahora estás usando el nuevo diseño",
+  "settings.general.row.newInterfaceNotice.description": "El diseño anterior ya no está disponible",
+  "settings.general.row.newInterfaceNotice.dismiss": "Descartar",
   "settings.general.row.pinchZoom.title": "Pellizcar para ampliar",
   "settings.general.row.pinchZoom.description":
     "Permitir ampliar con el gesto de pellizco del panel táctil y con Ctrl-scroll",

+ 7 - 3
packages/app/src/i18n/fr.ts

@@ -756,9 +756,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit",
   "settings.general.row.editToolPartsExpanded.description":
     "Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie",
-  "settings.general.row.newLayoutDesigns.title": "Nouvelle mise en page et nouveaux designs",
-  "settings.general.row.newLayoutDesigns.description":
-    "Activer la nouvelle mise en page et les nouvelles interfaces d'accueil, de saisie et de session",
+  "settings.general.row.newInterface.title": "Nouvelle mise en page",
+  "settings.general.row.newInterface.badge": "Nouveau",
+  "settings.general.row.newInterface.description":
+    "Utilisez les nouveaux onglets et la mise en page de l'accueil. Vous pouvez alterner entre les mises en page pendant une durée limitée.",
+  "settings.general.row.newInterfaceNotice.title": "Vous utilisez maintenant la nouvelle mise en page",
+  "settings.general.row.newInterfaceNotice.description": "La mise en page précédente n'est plus disponible",
+  "settings.general.row.newInterfaceNotice.dismiss": "Ignorer",
   "settings.general.row.pinchZoom.title": "Pincer pour zoomer",
   "settings.general.row.pinchZoom.description":
     "Autoriser les gestes de pincement du pavé tactile et Ctrl-défilement pour zoomer",

+ 7 - 3
packages/app/src/i18n/ja.ts

@@ -741,9 +741,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開",
   "settings.general.row.editToolPartsExpanded.description":
     "タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します",
-  "settings.general.row.newLayoutDesigns.title": "新しいレイアウトとデザイン",
-  "settings.general.row.newLayoutDesigns.description":
-    "刷新されたレイアウト、ホーム、コンポーザー、セッションUIを有効にします",
+  "settings.general.row.newInterface.title": "新しいレイアウト",
+  "settings.general.row.newInterface.badge": "新機能",
+  "settings.general.row.newInterface.description":
+    "新しいタブとホーム画面のレイアウトを使用します。期間限定でレイアウトを切り替えられます。",
+  "settings.general.row.newInterfaceNotice.title": "新しいレイアウトを使用しています",
+  "settings.general.row.newInterfaceNotice.description": "以前のレイアウトは利用できなくなりました",
+  "settings.general.row.newInterfaceNotice.dismiss": "閉じる",
   "settings.general.row.pinchZoom.title": "ピンチでズーム",
   "settings.general.row.pinchZoom.description":
     "トラックパッドのピンチ操作とCtrl+スクロール操作によるズームを許可します",

+ 7 - 2
packages/app/src/i18n/ko.ts

@@ -999,8 +999,13 @@ export const dict = {
   "settings.general.row.mobileTitlebarBottom.description": "모바일에서 제목 표시줄과 세션 탭을 화면 하단에 배치",
   "settings.general.row.showCustomAgents.title": "사용자 지정 에이전트",
   "settings.general.row.showCustomAgents.description": "입력창에 에이전트 선택기 표시",
-  "settings.general.row.newLayoutDesigns.title": "새 레이아웃 및 디자인",
-  "settings.general.row.newLayoutDesigns.description": "새롭게 디자인된 레이아웃, 홈, 입력창 및 세션 UI 활성화",
+  "settings.general.row.newInterface.title": "새 레이아웃",
+  "settings.general.row.newInterface.badge": "신규",
+  "settings.general.row.newInterface.description":
+    "새 탭과 홈 화면 레이아웃을 사용합니다. 제한된 기간 동안 레이아웃을 전환할 수 있습니다.",
+  "settings.general.row.newInterfaceNotice.title": "이제 새 레이아웃을 사용 중입니다",
+  "settings.general.row.newInterfaceNotice.description": "이전 레이아웃은 더 이상 사용할 수 없습니다",
+  "settings.general.row.newInterfaceNotice.dismiss": "닫기",
   "settings.general.row.pinchZoom.title": "핀치 줌",
   "settings.general.row.pinchZoom.description": "트랙패드 핀치 및 Ctrl-스크롤 제스처로 확대/축소 허용",
   "settings.updates.action.downloading": "다운로드 중...",

+ 7 - 3
packages/app/src/i18n/no.ts

@@ -1095,9 +1095,13 @@ export const dict = {
     "Plasser tittellinjen og sesjonsfanene nederst på mobilskjermen",
   "settings.general.row.showCustomAgents.title": "Egendefinerte agenter",
   "settings.general.row.showCustomAgents.description": "Vis agentvelgeren i skrivefeltet",
-  "settings.general.row.newLayoutDesigns.title": "Ny layout og utforming",
-  "settings.general.row.newLayoutDesigns.description":
-    "Aktiver ny layout og utforming for startsiden, skrivefeltet og sesjonsgrensesnittet",
+  "settings.general.row.newInterface.title": "Nytt oppsett",
+  "settings.general.row.newInterface.badge": "Ny",
+  "settings.general.row.newInterface.description":
+    "Bruk de nye fanene og startsidens oppsett. I en begrenset periode kan du bytte mellom oppsettene.",
+  "settings.general.row.newInterfaceNotice.title": "Du bruker nå det nye oppsettet",
+  "settings.general.row.newInterfaceNotice.description": "Det forrige oppsettet er ikke lenger tilgjengelig",
+  "settings.general.row.newInterfaceNotice.dismiss": "Avvis",
   "settings.general.row.pinchZoom.title": "Knip for å zoome",
   "settings.general.row.pinchZoom.description": "Tillat knipebevegelser på styreflaten og Ctrl-rulling for å zoome",
   "settings.updates.action.downloading": "Laster ned...",

+ 7 - 3
packages/app/src/i18n/pl.ts

@@ -746,9 +746,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit",
   "settings.general.row.editToolPartsExpanded.description":
     "Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu",
-  "settings.general.row.newLayoutDesigns.title": "Nowy układ i wygląd",
-  "settings.general.row.newLayoutDesigns.description":
-    "Włącz przeprojektowany układ, stronę główną, edytor wiadomości i interfejs sesji",
+  "settings.general.row.newInterface.title": "Nowy układ",
+  "settings.general.row.newInterface.badge": "Nowość",
+  "settings.general.row.newInterface.description":
+    "Używaj nowych kart i układu strony głównej. Przez ograniczony czas możesz przełączać się między układami.",
+  "settings.general.row.newInterfaceNotice.title": "Korzystasz teraz z nowego układu",
+  "settings.general.row.newInterfaceNotice.description": "Poprzedni układ nie jest już dostępny",
+  "settings.general.row.newInterfaceNotice.dismiss": "Odrzuć",
   "settings.general.row.pinchZoom.title": "Powiększanie gestem szczypania",
   "settings.general.row.pinchZoom.description":
     "Zezwalaj na powiększanie gestem szczypania na gładziku i przewijaniem z klawiszem Ctrl",

+ 7 - 3
packages/app/src/i18n/ru.ts

@@ -812,9 +812,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit",
   "settings.general.row.editToolPartsExpanded.description":
     "Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию",
-  "settings.general.row.newLayoutDesigns.title": "Новая компоновка и оформление",
-  "settings.general.row.newLayoutDesigns.description":
-    "Включить обновлённую компоновку, главную страницу, редактор запросов и интерфейс сессии",
+  "settings.general.row.newInterface.title": "Новая компоновка",
+  "settings.general.row.newInterface.badge": "Новое",
+  "settings.general.row.newInterface.description":
+    "Используйте новые вкладки и компоновку главной страницы. В течение ограниченного времени можно переключаться между вариантами компоновки.",
+  "settings.general.row.newInterfaceNotice.title": "Теперь вы используете новую компоновку",
+  "settings.general.row.newInterfaceNotice.description": "Прежняя компоновка больше недоступна",
+  "settings.general.row.newInterfaceNotice.dismiss": "Закрыть",
   "settings.general.row.pinchZoom.title": "Масштабирование щипком",
   "settings.general.row.pinchZoom.description":
     "Разрешить масштабирование жестом щипка на трекпаде и прокруткой с Ctrl",

+ 7 - 3
packages/app/src/i18n/th.ts

@@ -801,9 +801,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
   "settings.general.row.editToolPartsExpanded.description":
     "แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
-  "settings.general.row.newLayoutDesigns.title": "เลย์เอาต์และดีไซน์ใหม่",
-  "settings.general.row.newLayoutDesigns.description":
-    "เปิดใช้เลย์เอาต์ หน้าแรก ช่องเขียนข้อความ และ UI เซสชันที่ออกแบบใหม่",
+  "settings.general.row.newInterface.title": "เลย์เอาต์ใหม่",
+  "settings.general.row.newInterface.badge": "ใหม่",
+  "settings.general.row.newInterface.description":
+    "ใช้แท็บใหม่และเลย์เอาต์หน้าแรก คุณสามารถสลับระหว่างเลย์เอาต์ได้ในช่วงเวลาจำกัด",
+  "settings.general.row.newInterfaceNotice.title": "ขณะนี้คุณกำลังใช้เลย์เอาต์ใหม่",
+  "settings.general.row.newInterfaceNotice.description": "เลย์เอาต์ก่อนหน้าไม่พร้อมใช้งานอีกต่อไป",
+  "settings.general.row.newInterfaceNotice.dismiss": "ปิด",
   "settings.general.row.pinchZoom.title": "บีบนิ้วเพื่อซูม",
   "settings.general.row.pinchZoom.description": "อนุญาตให้ใช้ท่าบีบนิ้วบนแทร็คแพดและ Ctrl-scroll เพื่อซูม",
   "settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ",

+ 7 - 3
packages/app/src/i18n/tr.ts

@@ -816,9 +816,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Düzenleme araç bileşenlerini genişlet",
   "settings.general.row.editToolPartsExpanded.description":
     "Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster",
-  "settings.general.row.newLayoutDesigns.title": "Yeni düzen ve tasarımlar",
-  "settings.general.row.newLayoutDesigns.description":
-    "Yeniden tasarlanan düzeni, ana sayfayı, yazma alanını ve oturum arayüzünü etkinleştir",
+  "settings.general.row.newInterface.title": "Yeni düzen",
+  "settings.general.row.newInterface.badge": "Yeni",
+  "settings.general.row.newInterface.description":
+    "Yeni sekmeleri ve ana sayfa düzenini kullanın. Sınırlı bir süre boyunca düzenler arasında geçiş yapabilirsiniz.",
+  "settings.general.row.newInterfaceNotice.title": "Artık yeni düzeni kullanıyorsunuz",
+  "settings.general.row.newInterfaceNotice.description": "Önceki düzen artık kullanılamıyor",
+  "settings.general.row.newInterfaceNotice.dismiss": "Kapat",
   "settings.general.row.pinchZoom.title": "Sıkıştırarak yakınlaştır",
   "settings.general.row.pinchZoom.description":
     "İzleme dörtgeninde sıkıştırma ve Ctrl-kaydırma hareketleriyle yakınlaştırmaya izin ver",

+ 7 - 3
packages/app/src/i18n/uk.ts

@@ -902,9 +902,13 @@ export const dict = {
   "settings.general.row.editToolPartsExpanded.title": "Розгортати частини інструменту редагування",
   "settings.general.row.editToolPartsExpanded.description":
     "Показувати частини інструментів редагування, запису та патчів розгорнутими за замовчуванням на часовій шкалі",
-  "settings.general.row.newLayoutDesigns.title": "Новий макет і дизайн",
-  "settings.general.row.newLayoutDesigns.description":
-    "Увімкнути оновлений макет та інтерфейси головної сторінки, редактора запитів і сесії",
+  "settings.general.row.newInterface.title": "Новий макет",
+  "settings.general.row.newInterface.badge": "Нове",
+  "settings.general.row.newInterface.description":
+    "Використовуйте нові вкладки та макет головної сторінки. Протягом обмеженого часу можна перемикатися між макетами.",
+  "settings.general.row.newInterfaceNotice.title": "Тепер ви використовуєте новий макет",
+  "settings.general.row.newInterfaceNotice.description": "Попередній макет більше недоступний",
+  "settings.general.row.newInterfaceNotice.dismiss": "Відхилити",
   "settings.general.row.pinchZoom.title": "Масштабування щипком",
   "settings.general.row.pinchZoom.description":
     "Дозволити масштабування жестом щипка на трекпаді та прокручуванням із Ctrl",

+ 6 - 2
packages/app/src/i18n/zh.ts

@@ -796,8 +796,12 @@ export const dict = {
   "settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
   "settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
   "settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分",
-  "settings.general.row.newLayoutDesigns.title": "新版布局和设计",
-  "settings.general.row.newLayoutDesigns.description": "启用重新设计的布局、主页、输入框和会话界面",
+  "settings.general.row.newInterface.title": "新布局",
+  "settings.general.row.newInterface.badge": "新",
+  "settings.general.row.newInterface.description": "使用新的标签页和主页布局。在有限时间内可在不同布局之间切换。",
+  "settings.general.row.newInterfaceNotice.title": "你现在使用的是新布局",
+  "settings.general.row.newInterfaceNotice.description": "之前的布局不再可用",
+  "settings.general.row.newInterfaceNotice.dismiss": "忽略",
   "settings.general.row.pinchZoom.title": "双指缩放",
   "settings.general.row.pinchZoom.description": "允许使用触控板双指捏合和 Ctrl-scroll 手势缩放",
   "settings.general.row.wayland.title": "使用原生 Wayland",

+ 6 - 2
packages/app/src/i18n/zht.ts

@@ -792,8 +792,12 @@ export const dict = {
   "settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
   "settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊",
   "settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊",
-  "settings.general.row.newLayoutDesigns.title": "新版面與設計",
-  "settings.general.row.newLayoutDesigns.description": "啟用重新設計的版面、首頁、輸入區和工作階段介面",
+  "settings.general.row.newInterface.title": "新版面",
+  "settings.general.row.newInterface.badge": "新",
+  "settings.general.row.newInterface.description": "使用新的分頁和首頁版面。在限定時間內可切換版面。",
+  "settings.general.row.newInterfaceNotice.title": "你現在使用的是新版面",
+  "settings.general.row.newInterfaceNotice.description": "先前的版面已無法使用",
+  "settings.general.row.newInterfaceNotice.dismiss": "忽略",
   "settings.general.row.pinchZoom.title": "雙指縮放",
   "settings.general.row.pinchZoom.description": "允許使用觸控板雙指開合和 Ctrl-捲動手勢縮放",
   "settings.general.row.wayland.title": "使用原生 Wayland",

+ 1 - 0
packages/app/src/index.ts

@@ -3,6 +3,7 @@ export { useLayout } from "./context/layout"
 export { useServerSDK } from "./context/server-sdk"
 export { useServerSync } from "./context/server-sync"
 export { useServer } from "./context/server"
+export { useSettings } from "./context/settings"
 export { useTabs } from "./context/tabs"
 export { useProviders } from "./hooks/use-providers"
 export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker"

+ 8 - 1
packages/desktop/src/main/index.ts

@@ -19,7 +19,12 @@ import { forwardInitializationFailure } from "./initialization"
 import { exportDebugLogs, initCrashReporter, initLogging, startNetLog, write as writeLog } from "./logging"
 import { parseMarkdown } from "./markdown"
 import { createMenu } from "./menu"
-import { finishFirstLaunchOnboarding, isFirstLaunchOnboardingPending } from "./onboarding"
+import {
+  finishFirstLaunchOnboarding,
+  initializeOldLayoutEligibility,
+  isFirstLaunchOnboardingPending,
+  isOldLayoutEligible,
+} from "./onboarding"
 import {
   getDefaultServerUrl,
   preferAppEnv,
@@ -137,6 +142,7 @@ const main = Effect.gen(function* () {
     onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId),
   )
   if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session"))
+  initializeOldLayoutEligibility(app.getPath("userData"))
   logger = initLogging()
   initCrashReporter()
 
@@ -280,6 +286,7 @@ const main = Effect.gen(function* () {
     setDefaultServerUrl: (url) => setDefaultServerUrl(url),
     isFirstLaunchOnboardingPending,
     finishFirstLaunchOnboarding,
+    isOldLayoutEligible,
     getDisplayBackend: async () => null,
     setDisplayBackend: async () => undefined,
     parseMarkdown: async (markdown) => parseMarkdown(markdown),

+ 19 - 0
packages/desktop/src/main/install-state.test.ts

@@ -0,0 +1,19 @@
+import { describe, expect, test } from "bun:test"
+import { hasExistingAppState } from "./install-state"
+
+const file = (name: string) => ({ name, isDirectory: () => false })
+const directory = (name: string) => ({ name, isDirectory: () => true })
+
+describe("hasExistingAppState", () => {
+  test("ignores files Electron may create on a fresh install", () => {
+    expect(hasExistingAppState([])).toBe(false)
+    expect(hasExistingAppState([file("Local State"), directory("Crashpad")])).toBe(false)
+  })
+
+  test("recognizes state written by an earlier OpenCode launch", () => {
+    expect(hasExistingAppState([file("opencode.settings")])).toBe(true)
+    expect(hasExistingAppState([file("opencode.global.dat")])).toBe(true)
+    expect(hasExistingAppState([file("window-state-abc.json")])).toBe(true)
+    expect(hasExistingAppState([directory("opencode")])).toBe(true)
+  })
+})

+ 8 - 0
packages/desktop/src/main/install-state.ts

@@ -0,0 +1,8 @@
+export function hasExistingAppState(entries: Array<{ name: string; isDirectory: () => boolean }>) {
+  return entries.some((entry) => {
+    if (entry.name === "opencode.settings") return true
+    if (entry.name.endsWith(".dat")) return true
+    if (/^window-state-.+\.json$/.test(entry.name)) return true
+    return entry.isDirectory() && entry.name === "opencode"
+  })
+}

+ 2 - 0
packages/desktop/src/main/ipc.ts

@@ -29,6 +29,7 @@ type Deps = {
   setDefaultServerUrl: (url: string | null) => Promise<void> | void
   isFirstLaunchOnboardingPending: () => Promise<boolean> | boolean
   finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise<string | null> | string | null
+  isOldLayoutEligible: () => Promise<boolean> | boolean
   getDisplayBackend: () => Promise<string | null>
   setDisplayBackend: (backend: string | null) => Promise<void> | void
   parseMarkdown: (markdown: string) => Promise<string> | string
@@ -56,6 +57,7 @@ export function registerIpcHandlers(deps: Deps) {
   ipcMain.handle("finish-first-launch-onboarding", (_event: IpcMainInvokeEvent, createDefaultProject: boolean) =>
     deps.finishFirstLaunchOnboarding(createDefaultProject),
   )
+  ipcMain.handle("is-old-layout-eligible", () => deps.isOldLayoutEligible())
   ipcMain.handle("get-display-backend", () => deps.getDisplayBackend())
   ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) =>
     deps.setDisplayBackend(backend),

+ 18 - 1
packages/desktop/src/main/onboarding.ts

@@ -1,12 +1,29 @@
+import { existsSync, readdirSync } from "node:fs"
 import { mkdir } from "node:fs/promises"
 import { join } from "node:path"
 import { app } from "electron"
 import { getStore } from "./store"
-import { FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY } from "./store-keys"
+import { FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY, OLD_LAYOUT_ELIGIBLE_KEY } from "./store-keys"
 import { write as writeLog } from "./logging"
+import { hasExistingAppState } from "./install-state"
 
 const DEFAULT_PROJECT_DIR = "New OpenCode Project"
 
+export function initializeOldLayoutEligibility(userDataPath: string) {
+  const entries = existsSync(userDataPath) ? readdirSync(userDataPath, { withFileTypes: true }) : []
+  const store = getStore()
+  const current = store.get(OLD_LAYOUT_ELIGIBLE_KEY)
+  if (typeof current === "boolean") return current
+
+  const eligible = hasExistingAppState(entries)
+  store.set(OLD_LAYOUT_ELIGIBLE_KEY, eligible)
+  return eligible
+}
+
+export function isOldLayoutEligible() {
+  return getStore().get(OLD_LAYOUT_ELIGIBLE_KEY) === true
+}
+
 export function isFirstLaunchOnboardingPending() {
   const pending = getStore().get(FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY) !== true
   writeLog("onboarding", "first launch onboarding pending checked", { pending })

+ 1 - 0
packages/desktop/src/main/store-keys.ts

@@ -1,6 +1,7 @@
 export const SETTINGS_STORE = "opencode.settings"
 export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
 export const FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY = "firstLaunchOnboardingComplete"
+export const OLD_LAYOUT_ELIGIBLE_KEY = "oldLayoutEligible"
 export const WSL_SERVERS_KEY = "wslServers"
 export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled"
 export const WINDOW_IDS_KEY = "windowIds"

+ 1 - 0
packages/desktop/src/preload/index.ts

@@ -62,6 +62,7 @@ const api: ElectronAPI = {
   isFirstLaunchOnboardingPending: () => ipcRenderer.invoke("is-first-launch-onboarding-pending"),
   finishFirstLaunchOnboarding: (createDefaultProject) =>
     ipcRenderer.invoke("finish-first-launch-onboarding", createDefaultProject),
+  isOldLayoutEligible: () => ipcRenderer.invoke("is-old-layout-eligible"),
   getDisplayBackend: () => ipcRenderer.invoke("get-display-backend"),
   setDisplayBackend: (backend) => ipcRenderer.invoke("set-display-backend", backend),
   parseMarkdownCommand: (markdown) => ipcRenderer.invoke("parse-markdown", markdown),

+ 1 - 0
packages/desktop/src/preload/types.ts

@@ -52,6 +52,7 @@ export type ElectronAPI = {
   setDefaultServerUrl: (url: string | null) => Promise<void>
   isFirstLaunchOnboardingPending: () => Promise<boolean>
   finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise<string | null>
+  isOldLayoutEligible: () => Promise<boolean>
   getDisplayBackend: () => Promise<LinuxDisplayBackend | null>
   setDisplayBackend: (backend: LinuxDisplayBackend | null) => Promise<void>
   parseMarkdownCommand: (markdown: string) => Promise<string>

+ 3 - 0
packages/desktop/src/renderer/onboarding.tsx

@@ -5,6 +5,7 @@ import {
   useServer,
   useServerSDK,
   useServerSync,
+  useSettings,
   useTabs,
 } from "@opencode-ai/app"
 import { onMount, startTransition } from "solid-js"
@@ -13,6 +14,7 @@ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoad
   const server = useServer()
   const serverSDK = useServerSDK()
   const serverSync = useServerSync()
+  const settings = useSettings()
   const layout = useLayout()
   const providers = useProviders()
   const tabs = useTabs()
@@ -28,6 +30,7 @@ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoad
           (p) => p ?? Promise.resolve(),
         ),
       )
+      settings.general.setOldLayoutEligible(await window.api.isOldLayoutEligible())
       if (!server.isLocal()) return
 
       const pending = await window.api.isFirstLaunchOnboardingPending()

+ 6 - 0
packages/ui/src/v2/components/badge-v2.css

@@ -25,3 +25,9 @@
 [data-component="tag"][data-high-contrast] {
   border-color: var(--v2-border-border-strong);
 }
+
+[data-component="tag"][data-variant="accent"] {
+  border: 0;
+  background: var(--v2-background-bg-accent);
+  color: var(--v2-text-text-contrast);
+}

+ 6 - 1
packages/ui/src/v2/components/badge-v2.stories.tsx

@@ -8,10 +8,11 @@ Use alongside headings or lists for quick metadata.
 
 ### API
 - Accepts standard span props.
+- Optional: \`variant\` is \`neutral\` (default) or \`accent\`.
 - Optional: \`data-high-contrast\` attribute for stronger border contrast.
 
 ### Variants and states
-- Single size style.
+- Neutral and accent variants.
 - Optional high-contrast border style.
 
 ### Behavior
@@ -52,3 +53,7 @@ export const HighContrast = {
     </div>
   ),
 }
+
+export const Accent = {
+  render: () => <Tag variant="accent">New</Tag>,
+}

+ 5 - 2
packages/ui/src/v2/components/badge-v2.tsx

@@ -1,14 +1,17 @@
 import { type ComponentProps, splitProps } from "solid-js"
 import "./badge-v2.css"
 
-export interface TagProps extends ComponentProps<"span"> {}
+export interface TagProps extends ComponentProps<"span"> {
+  variant?: "neutral" | "accent"
+}
 
 export function Tag(props: TagProps) {
-  const [split, rest] = splitProps(props, ["class", "classList", "children"])
+  const [split, rest] = splitProps(props, ["class", "classList", "children", "variant"])
   return (
     <span
       {...rest}
       data-component="tag"
+      data-variant={split.variant ?? "neutral"}
       classList={{
         ...split.classList,
         [split.class ?? ""]: !!split.class,