Compare commits

...

13 Commits

Author SHA1 Message Date
Brendan Allan 1140d8422e chore(app): schedule layout sunset outside prod 2026-07-14 14:36:34 +00:00
Brendan Allan 9ecfb847fa fix(desktop): derive layout eligibility from install state 2026-07-14 14:06:26 +00:00
usrnk1 588288ce87 test(app): add layout transition stories 2026-07-13 21:36:41 +02:00
usrnk1 0156d3028f feat(app): simplify layout transition messaging 2026-07-13 21:36:08 +02:00
usrnk1 5e5e305bb7 fix(app): avoid layout flash during cohort detection 2026-07-13 14:40:32 +02:00
usrnk1 d87f987800 feat(app): preserve previous layout for active users 2026-07-13 14:37:01 +02:00
usrnk1 545b86587d feat(app): detect prior server activity for layout transition 2026-07-13 14:36:34 +02:00
usrnk1 7fb3fbdf27 feat(app): clarify layout transition timing 2026-07-13 13:35:26 +02:00
usrnk1 31f946eed7 feat(app): enforce layout sunset for existing users 2026-07-13 13:35:08 +02:00
usrnk1 45daa7fece feat(app): localize interface retirement notice 2026-07-13 13:08:13 +02:00
usrnk1 a5bbe636e7 feat(app): clarify interface transition and retirement 2026-07-13 13:06:17 +02:00
usrnk1 2e1ce87bf3 feat(app): add interface transition setting 2026-07-13 13:05:46 +02:00
usrnk1 56e8a4f7d7 feat(ui): add accent badge variant 2026-07-13 13:05:46 +02:00
38 changed files with 550 additions and 100 deletions
@@ -5,6 +5,7 @@ import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch" import { Switch } from "@opencode-ai/ui/switch"
import { TextField } from "@opencode-ai/ui/text-field" import { TextField } from "@opencode-ai/ui/text-field"
import { Tooltip } from "@opencode-ai/ui/tooltip" 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 { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
@@ -248,6 +249,50 @@ export const SettingsGeneral: Component = () => {
triggerVariant: "settings" as const, 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 = () => ( const GeneralSection = () => (
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<SettingsList> <SettingsList>
@@ -335,23 +380,6 @@ export const SettingsGeneral: Component = () => {
</div> </div>
</SettingsRow> </SettingsRow>
<SettingsRow
title={language.t("settings.general.row.newLayoutDesigns.title")}
description={language.t("settings.general.row.newLayoutDesigns.description")}
>
<div data-action="settings-new-layout-designs">
<Switch
checked={settings.general.newLayoutDesigns()}
onChange={(checked) => {
settings.general.setNewLayoutDesigns(checked)
if (!checked) return
void import("@/components/settings-v2").then((module) => {
dialog.show(() => <module.DialogSettings />)
})
}}
/>
</div>
</SettingsRow>
</SettingsList> </SettingsList>
</div> </div>
) )
@@ -718,6 +746,14 @@ export const SettingsGeneral: Component = () => {
</div> </div>
<div class="flex flex-col gap-8 w-full"> <div class="flex flex-col gap-8 w-full">
<Show when={settings.general.layoutTransitionAvailable()}>
<InterfaceSection />
</Show>
<Show when={settings.general.newInterfaceNoticeVisible()}>
<InterfaceNoticeSection />
</Show>
<GeneralSection /> <GeneralSection />
<AppearanceSection /> <AppearanceSection />
@@ -28,6 +28,7 @@ import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "../link" import { Link } from "../link"
import { SettingsListV2 } from "./parts/list" import { SettingsListV2 } from "./parts/list"
import { SettingsRowV2 } from "./parts/row" import { SettingsRowV2 } from "./parts/row"
import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition"
import "./settings-v2.css" import "./settings-v2.css"
let demoSoundState = { 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 = () => ( const GeneralSection = () => (
<div class="settings-v2-section"> <div class="settings-v2-section">
<SettingsListV2> <SettingsListV2>
@@ -312,24 +338,6 @@ export const SettingsGeneralV2: Component<{
</div> </div>
</SettingsRowV2> </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"}> <Show when={mobile() && import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"}>
<SettingsRowV2 <SettingsRowV2
title={language.t("settings.general.row.mobileTitlebarBottom.title")} title={language.t("settings.general.row.mobileTitlebarBottom.title")}
@@ -683,6 +691,14 @@ export const SettingsGeneralV2: Component<{
</div> </div>
<div class="settings-v2-tab-body"> <div class="settings-v2-tab-body">
<Show when={settings.general.layoutTransitionAvailable()}>
<InterfaceSection />
</Show>
<Show when={settings.general.newInterfaceNoticeVisible()}>
<InterfaceNoticeSection />
</Show>
<GeneralSection /> <GeneralSection />
<AppearanceSection /> <AppearanceSection />
@@ -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>
),
}
@@ -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>
)
}
@@ -90,6 +90,11 @@
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-muted); 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"] { [data-component="settings-v2-row"] {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -128,7 +133,7 @@
} }
[data-slot="settings-v2-row-description"] { [data-slot="settings-v2-row-description"] {
font-size: 11px; font-size: 13px;
font-weight: 440; font-weight: 440;
line-height: 1; line-height: 1;
color: var(--v2-text-text-muted); color: var(--v2-text-text-muted);
+40
View File
@@ -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
View File
@@ -1,5 +1,5 @@
import { createStore, reconcile } from "solid-js/store" 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 { createSimpleContext } from "@opencode-ai/ui/context"
import { persisted } from "@/utils/persist" import { persisted } from "@/utils/persist"
@@ -34,6 +34,8 @@ export interface Settings {
showCustomAgents: boolean showCustomAgents: boolean
mobileTitlebarPosition: "top" | "bottom" mobileTitlebarPosition: "top" | "bottom"
newLayoutDesigns?: boolean newLayoutDesigns?: boolean
layoutTransitionEligible?: boolean
newInterfaceNoticeDismissed?: boolean
} }
appearance: { appearance: {
fontSize: number fontSize: number
@@ -52,7 +54,28 @@ export interface Settings {
export const monoDefault = "System Mono" export const monoDefault = "System Mono"
export const sansDefault = "System Sans" export const sansDefault = "System Sans"
export const terminalDefault = "JetBrainsMono Nerd Font Mono" export const terminalDefault = "JetBrainsMono Nerd Font Mono"
export const newLayoutDesignsDefault = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" const 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 = const monoFallback =
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
@@ -160,9 +183,50 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
() => store.general?.showCustomAgents, () => store.general?.showCustomAgents,
defaultSettings.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()) 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(() => { createEffect(() => {
if (typeof document === "undefined") return if (typeof document === "undefined") return
const root = document.documentElement const root = document.documentElement
@@ -250,7 +314,18 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
}, },
newLayoutDesigns, newLayoutDesigns,
setNewLayoutDesigns(value: boolean) { 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: { visibility: {
+7 -3
View File
@@ -735,9 +735,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit", "settings.general.row.editToolPartsExpanded.title": "توسيع أجزاء أداة edit",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني", "إظهار أجزاء أدوات edit و write و patch موسعة بشكل افتراضي في الشريط الزمني",
"settings.general.row.newLayoutDesigns.title": "التخطيط والتصاميم الجديدة", "settings.general.row.newInterface.title": "التخطيط الجديد",
"settings.general.row.newLayoutDesigns.description": "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.title": "التكبير بإيماءة القرص",
"settings.general.row.pinchZoom.description": "السماح بإيماءة القرص على لوحة اللمس وإيماءة Ctrl-scroll للتكبير", "settings.general.row.pinchZoom.description": "السماح بإيماءة القرص على لوحة اللمس وإيماءة Ctrl-scroll للتكبير",
"settings.general.row.wayland.title": "استخدام Wayland الأصلي", "settings.general.row.wayland.title": "استخدام Wayland الأصلي",
+7 -3
View File
@@ -746,9 +746,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição", "settings.general.row.editToolPartsExpanded.title": "Expandir partes da ferramenta de edição",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"Mostrar partes das ferramentas de edição, escrita e patch expandidas por padrão na linha do tempo", "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.newInterface.title": "Novo layout",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "Novo",
"Ativar o layout, a página inicial, a área de composição e a interface de sessão reformulados", "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.title": "Zoom com gesto de pinça",
"settings.general.row.pinchZoom.description": "settings.general.row.pinchZoom.description":
"Permitir gestos de pinça no trackpad e de Ctrl+rolagem para aplicar zoom", "Permitir gestos de pinça no trackpad e de Ctrl+rolagem para aplicar zoom",
+7 -3
View File
@@ -811,9 +811,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje", "settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci", "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.newInterface.title": "Novi raspored",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "Novo",
"Omogući redizajnirani raspored, početnu stranicu, uređivač poruke i interfejs sesije", "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.title": "Zumiranje štipanjem",
"settings.general.row.pinchZoom.description": "settings.general.row.pinchZoom.description":
"Dozvoli zumiranje gestom štipanja na dodirnoj ploči i pomoću Ctrl-pomjeranja", "Dozvoli zumiranje gestom štipanja na dodirnoj ploči i pomoću Ctrl-pomjeranja",
+7 -3
View File
@@ -804,9 +804,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele", "settings.general.row.editToolPartsExpanded.title": "Udvid edit-værktøjsdele",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"Vis edit-, write- og patch-værktøjsdele udvidet som standard i tidslinjen", "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.newInterface.title": "Nyt layout",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "Ny",
"Aktivér det nydesignede layout, startsiden, promptfeltet og sessionsbrugerfladen", "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.title": "Knib for at zoome",
"settings.general.row.pinchZoom.description": "Tillad knibebevægelser på pegefeltet og Ctrl-rulning 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", "settings.general.row.wayland.title": "Brug native Wayland",
+7 -3
View File
@@ -759,9 +759,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen", "settings.general.row.editToolPartsExpanded.title": "Edit-Tool-Abschnitte ausklappen",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"Edit-, Write- und Patch-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen", "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.newInterface.title": "Neues Layout",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "Neu",
"Neu gestaltetes Layout, Startseite, Eingabebereich und Sitzungsoberfläche aktivieren", "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.title": "Zoom per Fingergeste",
"settings.general.row.pinchZoom.description": "Zoomen per Zwei-Finger- und Ctrl-Scroll-Geste erlauben", "settings.general.row.pinchZoom.description": "Zoomen per Zwei-Finger- und Ctrl-Scroll-Geste erlauben",
"settings.general.row.wayland.title": "Natives Wayland verwenden", "settings.general.row.wayland.title": "Natives Wayland verwenden",
+12 -7
View File
@@ -855,9 +855,8 @@ export const dict = {
"settings.general.row.language.title": "Language", "settings.general.row.language.title": "Language",
"settings.general.row.language.description": "Change the display language for OpenCode", "settings.general.row.language.description": "Change the display language for OpenCode",
"settings.general.row.shell.title": "Terminal Shell", "settings.general.row.shell.title": "Terminal shell",
"settings.general.row.shell.description": "settings.general.row.shell.description": "Shell used by the terminal and agent tools",
"Choose the shell used for your terminal. Compatible shells are also used for agent tool calls.",
"settings.general.row.shell.autoDefault": "Auto (Default)", "settings.general.row.shell.autoDefault": "Auto (Default)",
"settings.general.row.shell.terminalOnly": "terminal only", "settings.general.row.shell.terminalOnly": "terminal only",
"settings.general.row.appearance.title": "Appearance", "settings.general.row.appearance.title": "Appearance",
@@ -889,8 +888,9 @@ export const dict = {
"settings.general.row.mobileTitlebarBottom.title": "Bottom navigation", "settings.general.row.mobileTitlebarBottom.title": "Bottom navigation",
"settings.general.row.mobileTitlebarBottom.description": "settings.general.row.mobileTitlebarBottom.description":
"Place the title bar and session tabs at the bottom of the screen on mobile", "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.title": "Show agent",
"settings.general.row.showCustomAgents.description": "Show the agent picker in the composer", "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.title": "Show reasoning summaries",
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline", "settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts", "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.title": "Expand edit tool parts",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"Show edit, write, and patch tool parts expanded by default in the timeline", "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.newInterface.title": "New layout",
"settings.general.row.newLayoutDesigns.description": "Enable the redesigned layout, home, composer, and session UI", "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.title": "Pinch to zoom",
"settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom", "settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom",
+7 -3
View File
@@ -816,9 +816,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición", "settings.general.row.editToolPartsExpanded.title": "Expandir partes de la herramienta de edición",
"settings.general.row.editToolPartsExpanded.description": "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", "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.newInterface.title": "Nuevo diseño",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "Nuevo",
"Activar el nuevo diseño de la interfaz, la página de inicio, el editor y las sesiones", "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.title": "Pellizcar para ampliar",
"settings.general.row.pinchZoom.description": "settings.general.row.pinchZoom.description":
"Permitir ampliar con el gesto de pellizco del panel táctil y con Ctrl-scroll", "Permitir ampliar con el gesto de pellizco del panel táctil y con Ctrl-scroll",
+7 -3
View File
@@ -756,9 +756,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit", "settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie", "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.newInterface.title": "Nouvelle mise en page",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "Nouveau",
"Activer la nouvelle mise en page et les nouvelles interfaces d'accueil, de saisie et de session", "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.title": "Pincer pour zoomer",
"settings.general.row.pinchZoom.description": "settings.general.row.pinchZoom.description":
"Autoriser les gestes de pincement du pavé tactile et Ctrl-défilement pour zoomer", "Autoriser les gestes de pincement du pavé tactile et Ctrl-défilement pour zoomer",
+7 -3
View File
@@ -741,9 +741,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開", "settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します", "タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します",
"settings.general.row.newLayoutDesigns.title": "新しいレイアウトとデザイン", "settings.general.row.newInterface.title": "新しいレイアウト",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "新機能",
"刷新されたレイアウト、ホーム、コンポーザー、セッションUIを有効にします", "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.title": "ピンチでズーム",
"settings.general.row.pinchZoom.description": "settings.general.row.pinchZoom.description":
"トラックパッドのピンチ操作とCtrl+スクロール操作によるズームを許可します", "トラックパッドのピンチ操作とCtrl+スクロール操作によるズームを許可します",
+7 -2
View File
@@ -999,8 +999,13 @@ export const dict = {
"settings.general.row.mobileTitlebarBottom.description": "모바일에서 제목 표시줄과 세션 탭을 화면 하단에 배치", "settings.general.row.mobileTitlebarBottom.description": "모바일에서 제목 표시줄과 세션 탭을 화면 하단에 배치",
"settings.general.row.showCustomAgents.title": "사용자 지정 에이전트", "settings.general.row.showCustomAgents.title": "사용자 지정 에이전트",
"settings.general.row.showCustomAgents.description": "입력창에 에이전트 선택기 표시", "settings.general.row.showCustomAgents.description": "입력창에 에이전트 선택기 표시",
"settings.general.row.newLayoutDesigns.title": "새 레이아웃 및 디자인", "settings.general.row.newInterface.title": "새 레이아웃",
"settings.general.row.newLayoutDesigns.description": "새롭게 디자인된 레이아웃, 홈, 입력창 및 세션 UI 활성화", "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.title": "핀치 줌",
"settings.general.row.pinchZoom.description": "트랙패드 핀치 및 Ctrl-스크롤 제스처로 확대/축소 허용", "settings.general.row.pinchZoom.description": "트랙패드 핀치 및 Ctrl-스크롤 제스처로 확대/축소 허용",
"settings.updates.action.downloading": "다운로드 중...", "settings.updates.action.downloading": "다운로드 중...",
+7 -3
View File
@@ -1095,9 +1095,13 @@ export const dict = {
"Plasser tittellinjen og sesjonsfanene nederst på mobilskjermen", "Plasser tittellinjen og sesjonsfanene nederst på mobilskjermen",
"settings.general.row.showCustomAgents.title": "Egendefinerte agenter", "settings.general.row.showCustomAgents.title": "Egendefinerte agenter",
"settings.general.row.showCustomAgents.description": "Vis agentvelgeren i skrivefeltet", "settings.general.row.showCustomAgents.description": "Vis agentvelgeren i skrivefeltet",
"settings.general.row.newLayoutDesigns.title": "Ny layout og utforming", "settings.general.row.newInterface.title": "Nytt oppsett",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "Ny",
"Aktiver ny layout og utforming for startsiden, skrivefeltet og sesjonsgrensesnittet", "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.title": "Knip for å zoome",
"settings.general.row.pinchZoom.description": "Tillat knipebevegelser på styreflaten og Ctrl-rulling for å zoome", "settings.general.row.pinchZoom.description": "Tillat knipebevegelser på styreflaten og Ctrl-rulling for å zoome",
"settings.updates.action.downloading": "Laster ned...", "settings.updates.action.downloading": "Laster ned...",
+7 -3
View File
@@ -746,9 +746,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit", "settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu", "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.newInterface.title": "Nowy układ",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "Nowość",
"Włącz przeprojektowany układ, stronę główną, edytor wiadomości i interfejs sesji", "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.title": "Powiększanie gestem szczypania",
"settings.general.row.pinchZoom.description": "settings.general.row.pinchZoom.description":
"Zezwalaj na powiększanie gestem szczypania na gładziku i przewijaniem z klawiszem Ctrl", "Zezwalaj na powiększanie gestem szczypania na gładziku i przewijaniem z klawiszem Ctrl",
+7 -3
View File
@@ -812,9 +812,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit", "settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию", "Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию",
"settings.general.row.newLayoutDesigns.title": "Новая компоновка и оформление", "settings.general.row.newInterface.title": "Новая компоновка",
"settings.general.row.newLayoutDesigns.description": "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.title": "Масштабирование щипком",
"settings.general.row.pinchZoom.description": "settings.general.row.pinchZoom.description":
"Разрешить масштабирование жестом щипка на трекпаде и прокруткой с Ctrl", "Разрешить масштабирование жестом щипка на трекпаде и прокруткой с Ctrl",
+7 -3
View File
@@ -801,9 +801,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit", "settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์", "แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
"settings.general.row.newLayoutDesigns.title": "เลย์เอาต์และดีไซน์ใหม่", "settings.general.row.newInterface.title": "เลย์เอาต์ใหม่",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "ใหม่",
"เปิดใช้เลย์เอาต์ หน้าแรก ช่องเขียนข้อความ และ UI เซสชันที่ออกแบบใหม่", "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.title": "บีบนิ้วเพื่อซูม",
"settings.general.row.pinchZoom.description": "อนุญาตให้ใช้ท่าบีบนิ้วบนแทร็คแพดและ Ctrl-scroll เพื่อซูม", "settings.general.row.pinchZoom.description": "อนุญาตให้ใช้ท่าบีบนิ้วบนแทร็คแพดและ Ctrl-scroll เพื่อซูม",
"settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ", "settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ",
+7 -3
View File
@@ -816,9 +816,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Düzenleme araç bileşenlerini genişlet", "settings.general.row.editToolPartsExpanded.title": "Düzenleme araç bileşenlerini genişlet",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster", "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.newInterface.title": "Yeni düzen",
"settings.general.row.newLayoutDesigns.description": "settings.general.row.newInterface.badge": "Yeni",
"Yeniden tasarlanan düzeni, ana sayfayı, yazma alanını ve oturum arayüzünü etkinleştir", "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.title": "Sıkıştırarak yakınlaştır",
"settings.general.row.pinchZoom.description": "settings.general.row.pinchZoom.description":
"İzleme dörtgeninde sıkıştırma ve Ctrl-kaydırma hareketleriyle yakınlaştırmaya izin ver", "İzleme dörtgeninde sıkıştırma ve Ctrl-kaydırma hareketleriyle yakınlaştırmaya izin ver",
+7 -3
View File
@@ -902,9 +902,13 @@ export const dict = {
"settings.general.row.editToolPartsExpanded.title": "Розгортати частини інструменту редагування", "settings.general.row.editToolPartsExpanded.title": "Розгортати частини інструменту редагування",
"settings.general.row.editToolPartsExpanded.description": "settings.general.row.editToolPartsExpanded.description":
"Показувати частини інструментів редагування, запису та патчів розгорнутими за замовчуванням на часовій шкалі", "Показувати частини інструментів редагування, запису та патчів розгорнутими за замовчуванням на часовій шкалі",
"settings.general.row.newLayoutDesigns.title": "Новий макет і дизайн", "settings.general.row.newInterface.title": "Новий макет",
"settings.general.row.newLayoutDesigns.description": "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.title": "Масштабування щипком",
"settings.general.row.pinchZoom.description": "settings.general.row.pinchZoom.description":
"Дозволити масштабування жестом щипка на трекпаді та прокручуванням із Ctrl", "Дозволити масштабування жестом щипка на трекпаді та прокручуванням із Ctrl",
+6 -2
View File
@@ -796,8 +796,12 @@ export const dict = {
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分", "settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分", "settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
"settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分", "settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具部分",
"settings.general.row.newLayoutDesigns.title": "新布局和设计", "settings.general.row.newInterface.title": "新布局",
"settings.general.row.newLayoutDesigns.description": "启用重新设计的布局、主页、输入框和会话界面", "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.title": "双指缩放",
"settings.general.row.pinchZoom.description": "允许使用触控板双指捏合和 Ctrl-scroll 手势缩放", "settings.general.row.pinchZoom.description": "允许使用触控板双指捏合和 Ctrl-scroll 手势缩放",
"settings.general.row.wayland.title": "使用原生 Wayland", "settings.general.row.wayland.title": "使用原生 Wayland",
+6 -2
View File
@@ -792,8 +792,12 @@ export const dict = {
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊", "settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
"settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊", "settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊",
"settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊", "settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊",
"settings.general.row.newLayoutDesigns.title": "新版面與設計", "settings.general.row.newInterface.title": "新版面",
"settings.general.row.newLayoutDesigns.description": "啟用重新設計的版面、首頁、輸入區和工作階段介面", "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.title": "雙指縮放",
"settings.general.row.pinchZoom.description": "允許使用觸控板雙指開合和 Ctrl-捲動手勢縮放", "settings.general.row.pinchZoom.description": "允許使用觸控板雙指開合和 Ctrl-捲動手勢縮放",
"settings.general.row.wayland.title": "使用原生 Wayland", "settings.general.row.wayland.title": "使用原生 Wayland",
+1
View File
@@ -3,6 +3,7 @@ export { useLayout } from "./context/layout"
export { useServerSDK } from "./context/server-sdk" export { useServerSDK } from "./context/server-sdk"
export { useServerSync } from "./context/server-sync" export { useServerSync } from "./context/server-sync"
export { useServer } from "./context/server" export { useServer } from "./context/server"
export { useSettings } from "./context/settings"
export { useTabs } from "./context/tabs" export { useTabs } from "./context/tabs"
export { useProviders } from "./hooks/use-providers" export { useProviders } from "./hooks/use-providers"
export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker" export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker"
+8 -1
View File
@@ -19,7 +19,12 @@ import { forwardInitializationFailure } from "./initialization"
import { exportDebugLogs, initCrashReporter, initLogging, startNetLog, write as writeLog } from "./logging" import { exportDebugLogs, initCrashReporter, initLogging, startNetLog, write as writeLog } from "./logging"
import { parseMarkdown } from "./markdown" import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu" import { createMenu } from "./menu"
import { finishFirstLaunchOnboarding, isFirstLaunchOnboardingPending } from "./onboarding" import {
finishFirstLaunchOnboarding,
initializeOldLayoutEligibility,
isFirstLaunchOnboardingPending,
isOldLayoutEligible,
} from "./onboarding"
import { import {
getDefaultServerUrl, getDefaultServerUrl,
preferAppEnv, preferAppEnv,
@@ -137,6 +142,7 @@ const main = Effect.gen(function* () {
onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId), onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId),
) )
if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session")) if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session"))
initializeOldLayoutEligibility(app.getPath("userData"))
logger = initLogging() logger = initLogging()
initCrashReporter() initCrashReporter()
@@ -280,6 +286,7 @@ const main = Effect.gen(function* () {
setDefaultServerUrl: (url) => setDefaultServerUrl(url), setDefaultServerUrl: (url) => setDefaultServerUrl(url),
isFirstLaunchOnboardingPending, isFirstLaunchOnboardingPending,
finishFirstLaunchOnboarding, finishFirstLaunchOnboarding,
isOldLayoutEligible,
getDisplayBackend: async () => null, getDisplayBackend: async () => null,
setDisplayBackend: async () => undefined, setDisplayBackend: async () => undefined,
parseMarkdown: async (markdown) => parseMarkdown(markdown), parseMarkdown: async (markdown) => parseMarkdown(markdown),
@@ -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)
})
})
@@ -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
View File
@@ -29,6 +29,7 @@ type Deps = {
setDefaultServerUrl: (url: string | null) => Promise<void> | void setDefaultServerUrl: (url: string | null) => Promise<void> | void
isFirstLaunchOnboardingPending: () => Promise<boolean> | boolean isFirstLaunchOnboardingPending: () => Promise<boolean> | boolean
finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise<string | null> | string | null finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise<string | null> | string | null
isOldLayoutEligible: () => Promise<boolean> | boolean
getDisplayBackend: () => Promise<string | null> getDisplayBackend: () => Promise<string | null>
setDisplayBackend: (backend: string | null) => Promise<void> | void setDisplayBackend: (backend: string | null) => Promise<void> | void
parseMarkdown: (markdown: string) => Promise<string> | string 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) => ipcMain.handle("finish-first-launch-onboarding", (_event: IpcMainInvokeEvent, createDefaultProject: boolean) =>
deps.finishFirstLaunchOnboarding(createDefaultProject), deps.finishFirstLaunchOnboarding(createDefaultProject),
) )
ipcMain.handle("is-old-layout-eligible", () => deps.isOldLayoutEligible())
ipcMain.handle("get-display-backend", () => deps.getDisplayBackend()) ipcMain.handle("get-display-backend", () => deps.getDisplayBackend())
ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) => ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) =>
deps.setDisplayBackend(backend), deps.setDisplayBackend(backend),
+18 -1
View File
@@ -1,12 +1,29 @@
import { existsSync, readdirSync } from "node:fs"
import { mkdir } from "node:fs/promises" import { mkdir } from "node:fs/promises"
import { join } from "node:path" import { join } from "node:path"
import { app } from "electron" import { app } from "electron"
import { getStore } from "./store" 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 { write as writeLog } from "./logging"
import { hasExistingAppState } from "./install-state"
const DEFAULT_PROJECT_DIR = "New OpenCode Project" 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() { export function isFirstLaunchOnboardingPending() {
const pending = getStore().get(FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY) !== true const pending = getStore().get(FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY) !== true
writeLog("onboarding", "first launch onboarding pending checked", { pending }) writeLog("onboarding", "first launch onboarding pending checked", { pending })
+1
View File
@@ -1,6 +1,7 @@
export const SETTINGS_STORE = "opencode.settings" export const SETTINGS_STORE = "opencode.settings"
export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl" export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
export const FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY = "firstLaunchOnboardingComplete" export const FIRST_LAUNCH_ONBOARDING_COMPLETE_KEY = "firstLaunchOnboardingComplete"
export const OLD_LAYOUT_ELIGIBLE_KEY = "oldLayoutEligible"
export const WSL_SERVERS_KEY = "wslServers" export const WSL_SERVERS_KEY = "wslServers"
export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled" export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled"
export const WINDOW_IDS_KEY = "windowIds" export const WINDOW_IDS_KEY = "windowIds"
+1
View File
@@ -62,6 +62,7 @@ const api: ElectronAPI = {
isFirstLaunchOnboardingPending: () => ipcRenderer.invoke("is-first-launch-onboarding-pending"), isFirstLaunchOnboardingPending: () => ipcRenderer.invoke("is-first-launch-onboarding-pending"),
finishFirstLaunchOnboarding: (createDefaultProject) => finishFirstLaunchOnboarding: (createDefaultProject) =>
ipcRenderer.invoke("finish-first-launch-onboarding", createDefaultProject), ipcRenderer.invoke("finish-first-launch-onboarding", createDefaultProject),
isOldLayoutEligible: () => ipcRenderer.invoke("is-old-layout-eligible"),
getDisplayBackend: () => ipcRenderer.invoke("get-display-backend"), getDisplayBackend: () => ipcRenderer.invoke("get-display-backend"),
setDisplayBackend: (backend) => ipcRenderer.invoke("set-display-backend", backend), setDisplayBackend: (backend) => ipcRenderer.invoke("set-display-backend", backend),
parseMarkdownCommand: (markdown) => ipcRenderer.invoke("parse-markdown", markdown), parseMarkdownCommand: (markdown) => ipcRenderer.invoke("parse-markdown", markdown),
+1
View File
@@ -52,6 +52,7 @@ export type ElectronAPI = {
setDefaultServerUrl: (url: string | null) => Promise<void> setDefaultServerUrl: (url: string | null) => Promise<void>
isFirstLaunchOnboardingPending: () => Promise<boolean> isFirstLaunchOnboardingPending: () => Promise<boolean>
finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise<string | null> finishFirstLaunchOnboarding: (createDefaultProject: boolean) => Promise<string | null>
isOldLayoutEligible: () => Promise<boolean>
getDisplayBackend: () => Promise<LinuxDisplayBackend | null> getDisplayBackend: () => Promise<LinuxDisplayBackend | null>
setDisplayBackend: (backend: LinuxDisplayBackend | null) => Promise<void> setDisplayBackend: (backend: LinuxDisplayBackend | null) => Promise<void>
parseMarkdownCommand: (markdown: string) => Promise<string> parseMarkdownCommand: (markdown: string) => Promise<string>
@@ -5,6 +5,7 @@ import {
useServer, useServer,
useServerSDK, useServerSDK,
useServerSync, useServerSync,
useSettings,
useTabs, useTabs,
} from "@opencode-ai/app" } from "@opencode-ai/app"
import { onMount, startTransition } from "solid-js" import { onMount, startTransition } from "solid-js"
@@ -13,6 +14,7 @@ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoad
const server = useServer() const server = useServer()
const serverSDK = useServerSDK() const serverSDK = useServerSDK()
const serverSync = useServerSync() const serverSync = useServerSync()
const settings = useSettings()
const layout = useLayout() const layout = useLayout()
const providers = useProviders() const providers = useProviders()
const tabs = useTabs() const tabs = useTabs()
@@ -28,6 +30,7 @@ export function DesktopFirstLaunchOnboarding(props: { initialUrl: string; onLoad
(p) => p ?? Promise.resolve(), (p) => p ?? Promise.resolve(),
), ),
) )
settings.general.setOldLayoutEligible(await window.api.isOldLayoutEligible())
if (!server.isLocal()) return if (!server.isLocal()) return
const pending = await window.api.isFirstLaunchOnboardingPending() const pending = await window.api.isFirstLaunchOnboardingPending()
@@ -25,3 +25,9 @@
[data-component="tag"][data-high-contrast] { [data-component="tag"][data-high-contrast] {
border-color: var(--v2-border-border-strong); 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);
}
@@ -8,10 +8,11 @@ Use alongside headings or lists for quick metadata.
### API ### API
- Accepts standard span props. - Accepts standard span props.
- Optional: \`variant\` is \`neutral\` (default) or \`accent\`.
- Optional: \`data-high-contrast\` attribute for stronger border contrast. - Optional: \`data-high-contrast\` attribute for stronger border contrast.
### Variants and states ### Variants and states
- Single size style. - Neutral and accent variants.
- Optional high-contrast border style. - Optional high-contrast border style.
### Behavior ### Behavior
@@ -52,3 +53,7 @@ export const HighContrast = {
</div> </div>
), ),
} }
export const Accent = {
render: () => <Tag variant="accent">New</Tag>,
}
+5 -2
View File
@@ -1,14 +1,17 @@
import { type ComponentProps, splitProps } from "solid-js" import { type ComponentProps, splitProps } from "solid-js"
import "./badge-v2.css" import "./badge-v2.css"
export interface TagProps extends ComponentProps<"span"> {} export interface TagProps extends ComponentProps<"span"> {
variant?: "neutral" | "accent"
}
export function Tag(props: TagProps) { export function Tag(props: TagProps) {
const [split, rest] = splitProps(props, ["class", "classList", "children"]) const [split, rest] = splitProps(props, ["class", "classList", "children", "variant"])
return ( return (
<span <span
{...rest} {...rest}
data-component="tag" data-component="tag"
data-variant={split.variant ?? "neutral"}
classList={{ classList={{
...split.classList, ...split.classList,
[split.class ?? ""]: !!split.class, [split.class ?? ""]: !!split.class,