Apply PR #40845: feat(app): redesign non-modal settings

This commit is contained in:
opencode-agent[bot] 2026-08-08 23:40:43 +00:00
commit 2b7b7728c5
15 changed files with 1773 additions and 1037 deletions

1334
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,238 @@
.project-settings-v2-dialog [data-slot="dialog-container"] {
background: var(--v2-background-bg-base);
}
.project-settings-v2-dialog [data-slot="dialog-body"] {
padding: 0;
overflow: hidden;
}
.project-settings-v2 {
height: 100%;
}
.project-settings-v2-nav {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
}
.project-settings-v2-panel {
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
overflow: hidden !important;
user-select: none;
}
.project-settings-v2-panel :is(input, textarea, [contenteditable="true"]) {
user-select: text;
}
.project-settings-v2-form {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.project-settings-v2-scroll {
display: flex;
flex: 1;
flex-direction: column;
gap: 24px;
min-height: 0;
padding: 40px;
overflow-y: auto;
}
.project-settings-page-header {
display: flex;
flex-direction: column;
gap: 6px;
}
.project-settings-page-header h2 {
color: var(--v2-text-text-base);
font-size: 15px;
font-weight: 640;
line-height: 1;
}
.project-settings-page-header > span {
color: var(--v2-text-text-muted);
font-size: 11px;
font-weight: 440;
line-height: 1;
}
.project-settings-extensions {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 40px;
}
.project-settings-extension-tabs {
flex: 1;
height: auto;
min-height: 0;
margin-top: 24px;
overflow: visible;
}
.project-settings-extension-tabs[data-component="tabs-v2"][data-variant="pill"] > [data-slot="tabs-v2-list"] {
width: auto;
padding-inline: 0 !important;
}
.project-settings-extension-tabs[data-component="tabs-v2"][data-variant="pill"] > [data-slot="tabs-v2-list"]::before {
display: none;
}
.project-settings-extension-tabs[data-component="tabs-v2"][data-variant="pill"]
> [data-slot="tabs-v2-list"]
[data-slot="tabs-v2-trigger-wrapper"] {
border-radius: 6px;
}
.project-settings-extension-tabs[data-component="tabs-v2"][data-variant="pill"]
> [data-slot="tabs-v2-list"]
[data-slot="tabs-v2-trigger"] {
padding-inline: 8px;
}
.project-settings-extension-tabs [data-slot="tabs-v2-content"] {
overflow-y: auto;
}
.project-settings-extension-section {
display: flex;
flex-direction: column;
gap: 16px;
padding-top: 20px;
}
.project-settings-extension-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
color: var(--v2-text-text-base);
font-size: 13px;
font-weight: 530;
line-height: 1;
}
.project-settings-extension-section-header > :last-child {
color: var(--v2-text-text-faint);
font-size: 11px;
font-weight: 440;
}
.project-settings-extension-link {
color: var(--v2-text-text-accent) !important;
text-decoration: none;
}
.project-settings-extension-link:hover {
text-decoration: underline;
}
.project-settings-extension-card {
padding-inline: 12px;
overflow: hidden;
border: 0.5px solid var(--v2-border-border-base);
border-radius: 8px;
background: var(--v2-background-bg-base);
}
.project-settings-extension-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 48px;
border-bottom: 0.5px solid var(--v2-border-border-base);
}
.project-settings-extension-row:last-child {
border-bottom: 0;
}
.project-settings-extension-row-main {
display: flex;
flex: 1;
align-items: center;
gap: 10px;
min-width: 0;
}
.project-settings-extension-row-icon {
flex-shrink: 0;
color: var(--v2-icon-icon-muted);
}
.project-settings-extension-row-name {
min-width: 0;
overflow: hidden;
color: var(--v2-text-text-base);
font-size: 13px;
font-weight: 530;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-settings-extension-row-status {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
color: var(--v2-text-text-muted);
font-size: 11px;
}
.project-settings-extension-row-status-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--v2-state-fg-warning);
}
.project-settings-shared {
display: flex;
flex-direction: column;
gap: 12px;
}
.project-settings-shared-trigger {
display: flex;
align-items: center;
align-self: flex-start;
gap: 6px;
color: var(--v2-text-text-base);
font-size: 11px;
font-weight: 530;
}
.project-settings-shared-chevron {
width: 12px;
height: 12px;
color: var(--v2-icon-icon-muted);
transition: transform 120ms ease;
}
.project-settings-shared-chevron.open {
transform: rotate(90deg);
}
.project-settings-shared-count {
padding: 1px 4px;
border-radius: 3px;
background: var(--v2-background-bg-layer-02);
color: var(--v2-text-text-muted);
font-size: 9px;
}

View file

@ -0,0 +1,240 @@
import { Icon } from "@opencode-ai/ui/icon"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
import { type Component, For, Show, createMemo, createResource, createSignal } from "solid-js"
import { useLanguage } from "@/context/language"
import { useMcpToggle } from "@/context/mcp"
import { useSDK } from "@/context/sdk"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { useSync } from "@/context/sync"
type SkillItem = {
name: string
location: string
}
const pluginName = (item: string | [string, Record<string, unknown>]) => (typeof item === "string" ? item : item[0])
const skillKey = (item: SkillItem) => `${item.name}\n${item.location}`
const ExtensionCard: Component<{ children: unknown }> = (props) => (
<div class="project-settings-extension-card">{props.children as any}</div>
)
const ExtensionRow: Component<{
icon: "mcp" | "models" | "brain" | "code"
name: string
status?: string
children?: unknown
}> = (props) => (
<div class="project-settings-extension-row">
<div class="project-settings-extension-row-main">
<Icon name={props.icon} class="project-settings-extension-row-icon" />
<span class="project-settings-extension-row-name">{props.name}</span>
</div>
<Show when={props.status}>
{(status) => (
<span class="project-settings-extension-row-status">
<span class="project-settings-extension-row-status-dot" />
{status()}
</span>
)}
</Show>
{props.children as any}
</div>
)
const SharedSection: Component<{
count: number
children: unknown
}> = (props) => {
const language = useLanguage()
const [open, setOpen] = createSignal(false)
return (
<Show when={props.count > 0}>
<div class="project-settings-shared">
<button
type="button"
class="project-settings-shared-trigger"
aria-expanded={open()}
onClick={() => setOpen((value) => !value)}
>
<Icon name="chevron-right" classList={{ "project-settings-shared-chevron": true, open: open() }} />
<span>{language.t("project.settings.extensions.shared")}</span>
<span class="project-settings-shared-count">{props.count}</span>
</button>
<Show when={open()}>
<ExtensionCard>{props.children}</ExtensionCard>
</Show>
</div>
</Show>
)
}
export const ProjectSettingsExtensions: Component = () => {
const language = useLanguage()
const serverSDK = useServerSDK()
const directorySDK = useSDK()
const serverSync = useServerSync()
const sync = useSync()
const toggleMcp = useMcpToggle()
const [serverMcp] = createResource(
serverSDK,
(sdk) =>
sdk.api.mcp
.list()
.then((result) => Object.fromEntries(result.data.map((server) => [server.name, server.status])))
.catch(() => ({})),
{ initialValue: {} },
)
const globalMcpNames = createMemo(() =>
[...new Set([...Object.keys(serverSync().data.config.mcp ?? {}), ...Object.keys(serverMcp.latest)])].sort(),
)
const projectMcpNames = createMemo(() => {
const shared = new Set(globalMcpNames())
const configured = Object.keys(sync().data.config.mcp ?? {}).filter((name) => !shared.has(name))
if (configured.length > 0) return configured.sort()
return Object.keys(sync().data.mcp ?? {})
.filter((name) => !shared.has(name))
.sort()
})
const mcpEnabled = (name: string) => sync().data.mcp?.[name]?.status === "connected"
const globalPlugins = createMemo(() => (serverSync().data.config.plugin ?? []).map(pluginName))
const projectPlugins = createMemo(() => {
const shared = new Set(globalPlugins())
return (sync().data.config.plugin ?? []).map(pluginName).filter((name) => !shared.has(name))
})
const [serverSkills] = createResource(
serverSDK,
(sdk): Promise<SkillItem[]> =>
sdk.api.skill.list().then((result) => result.data.map((item) => ({ name: item.name, location: item.location }))),
{ initialValue: [] },
)
const [directorySkills] = createResource(
directorySDK,
(sdk): Promise<SkillItem[]> =>
sdk.api.skill
.list({ location: { directory: sdk.directory } })
.then((result) => result.data.map((item) => ({ name: item.name, location: item.location }))),
{ initialValue: [] },
)
const projectSkills = createMemo(() => {
const shared = new Set(serverSkills.latest.map(skillKey))
return directorySkills.latest.filter((item) => !shared.has(skillKey(item)))
})
const mcpRows = (items: string[]) => (
<For each={items}>
{(name) => (
<ExtensionRow icon="mcp" name={name}>
<Switch
checked={mcpEnabled(name)}
disabled={toggleMcp.isPending && toggleMcp.variables === name}
hideLabel
onChange={() => {
if (toggleMcp.isPending) return
toggleMcp.mutate(name)
}}
>
{name}
</Switch>
</ExtensionRow>
)}
</For>
)
const pluginRows = (items: string[]) => <For each={items}>{(name) => <ExtensionRow icon="models" name={name} />}</For>
const skillRows = (items: SkillItem[]) => <For each={items}>{(item) => <ExtensionRow icon="brain" name={item.name} />}</For>
return (
<div class="project-settings-extensions">
<div class="project-settings-page-header">
<h2>{language.t("settings.tab.extensions")}</h2>
<span>{language.t("project.settings.extensions.description")}</span>
</div>
<TabsV2 variant="pill" defaultValue="mcps" class="project-settings-extension-tabs">
<TabsV2.List>
<TabsV2.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</TabsV2.Trigger>
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
<TabsV2.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</TabsV2.Trigger>
<TabsV2.Trigger value="lsps">{language.t("project.settings.extensions.tab.lsps")}</TabsV2.Trigger>
</TabsV2.List>
<TabsV2.Content value="mcps">
<div class="project-settings-extension-section">
<div class="project-settings-extension-section-header">
<span>{language.t("project.settings.extensions.added")}</span>
<span>{language.t("settings.extensions.manageConfig")}</span>
</div>
<Show when={projectMcpNames().length > 0}>
<ExtensionCard>{mcpRows(projectMcpNames())}</ExtensionCard>
</Show>
<SharedSection count={globalMcpNames().length}>{mcpRows(globalMcpNames())}</SharedSection>
</div>
</TabsV2.Content>
<TabsV2.Content value="plugins">
<div class="project-settings-extension-section">
<div class="project-settings-extension-section-header">
<span>{language.t("project.settings.extensions.added")}</span>
<span>{language.t("settings.extensions.manageConfig")}</span>
</div>
<Show when={projectPlugins().length > 0}>
<ExtensionCard>{pluginRows(projectPlugins())}</ExtensionCard>
</Show>
<SharedSection count={globalPlugins().length}>{pluginRows(globalPlugins())}</SharedSection>
</div>
</TabsV2.Content>
<TabsV2.Content value="skills">
<div class="project-settings-extension-section">
<div class="project-settings-extension-section-header">
<span>{language.t("project.settings.extensions.added")}</span>
<a
class="project-settings-extension-link"
href="https://opencode.ai/docs/skills/"
target="_blank"
rel="noreferrer"
>
{language.t("settings.extensions.addSkills")}
</a>
</div>
<Show when={projectSkills().length > 0}>
<ExtensionCard>{skillRows(projectSkills())}</ExtensionCard>
</Show>
<SharedSection count={serverSkills.latest.length}>{skillRows(serverSkills.latest)}</SharedSection>
</div>
</TabsV2.Content>
<TabsV2.Content value="lsps">
<div class="project-settings-extension-section">
<div class="project-settings-extension-section-header">
<span>{language.t("project.settings.extensions.lsp.detected")}</span>
<span>{language.t("project.settings.extensions.lsp.description")}</span>
</div>
<Show when={sync().data.lsp.length > 0}>
<ExtensionCard>
<For each={sync().data.lsp}>
{(item) => (
<ExtensionRow
icon="code"
name={item.name || item.id}
status={
item.status === "error" ? language.t("project.settings.extensions.setupRequired") : undefined
}
/>
)}
</For>
</ExtensionCard>
</Show>
</div>
</TabsV2.Content>
</TabsV2>
</div>
)
}

View file

@ -451,7 +451,10 @@ function SettingsKeybindsV2View(props: {
<>
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
<div class="settings-v2-tab-header-row">
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
<div class="flex flex-col gap-1">
<h2 class="settings-v2-tab-title">{language.t("settings.shortcuts.title")}</h2>
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.shortcuts.description")}</span>
</div>
<ButtonV2 variant="ghost" onClick={props.onReset} disabled={!props.hasOverrides()}>
{language.t("settings.shortcuts.reset.button")}
</ButtonV2>

View file

@ -0,0 +1,138 @@
import { Component, createMemo } from "solid-js"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { useLanguage } from "@/context/language"
import { SettingsListV2 } from "./parts/list"
import { SettingsRowV2 } from "./parts/row"
import { createAppearanceSettingsController, type AppearanceSettingsController } from "./general-controllers"
import "./settings-v2.css"
const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"]
const fontSettings = {
ui: {
action: "settings-ui-font",
title: "settings.general.row.uiFont.title",
description: "settings.general.row.uiFont.description",
font: "ui",
input: "setUI",
},
code: {
action: "settings-code-font",
title: "settings.general.row.font.title",
description: "settings.general.row.font.description",
font: "code",
input: "setCode",
},
terminal: {
action: "settings-terminal-font",
title: "settings.general.row.terminalFont.title",
description: "settings.general.row.terminalFont.description",
font: "terminal",
input: "setTerminal",
},
} as const
const FontSetting: Component<{
kind: "ui" | "code" | "terminal"
fonts: AppearanceSettingsController["fonts"]
}> = (props) => {
const language = useLanguage()
const config = () => fontSettings[props.kind]
return (
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
<div class="w-full sm:w-[220px]">
<TextInputV2
data-action={config().action}
type="text"
appearance="base"
value={props.fonts[config().font]().value}
onInput={(event) => props.fonts[config().input](event.currentTarget.value)}
placeholder={props.fonts[config().font]().placeholder}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
aria-label={language.t(config().title)}
style={{ "font-family": props.fonts[config().font]().family }}
/>
</div>
</SettingsRowV2>
)
}
export const SettingsAppearanceV2: Component = () => {
const language = useLanguage()
const appearance = createAppearanceSettingsController()
return (
<>
<div class="settings-v2-tab-header">
<div class="settings-v2-tab-header-row">
<div class="flex flex-col gap-1">
<h2 class="settings-v2-tab-title">{language.t("settings.general.section.appearance")}</h2>
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.appearance.description")}</span>
</div>
</div>
</div>
<div class="settings-v2-tab-body">
<div class="settings-v2-section">
<SettingsListV2>
<SettingsRowV2
title={language.t("settings.general.row.colorScheme.title")}
description={language.t("settings.general.row.colorScheme.description")}
>
<SelectV2
appearance="inline"
data-action="settings-color-scheme"
options={schemeOptions}
current={schemeOptions.find((option) => option === appearance.scheme.current())}
placement="bottom-end"
gutter={6}
label={(option) => {
if (option === "system") return language.t("theme.scheme.system")
if (option === "light") return language.t("theme.scheme.light")
return language.t("theme.scheme.dark")
}}
onSelect={(option) => option && appearance.scheme.select(option)}
/>
</SettingsRowV2>
<SettingsRowV2
title={language.t("settings.general.row.theme.title")}
description={
<>
{language.t("settings.general.row.theme.description")}{" "}
<a
class="settings-v2-link"
href="https://opencode.ai/docs/themes/"
target="_blank"
rel="noreferrer"
>
{language.t("common.learnMore")}
</a>
</>
}
>
<SelectV2
appearance="inline"
data-action="settings-theme"
options={appearance.theme.options()}
current={appearance.theme.current()}
placement="bottom-end"
gutter={6}
value={(option) => option.id}
label={(option) => option.name}
onSelect={appearance.theme.select}
/>
</SettingsRowV2>
<FontSetting kind="ui" fonts={appearance.fonts} />
<FontSetting kind="code" fonts={appearance.fonts} />
<FontSetting kind="terminal" fonts={appearance.fonts} />
</SettingsListV2>
</div>
</div>
</>
)
}

View file

@ -0,0 +1,157 @@
import { Component, For, createMemo, createResource } from "solid-js"
import { Icon } from "@opencode-ai/ui/icon"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
import { useLanguage } from "@/context/language"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { InlineServerSelect } from "./parts/server-select"
import "./settings-v2.css"
interface McpRowItem {
name: string
enabled: boolean
}
interface PluginRowItem {
name: string
}
export const SettingsExtensionsV2: Component = () => {
const language = useLanguage()
const serverSdk = useServerSDK()
const serverSync = useServerSync()
const mcps = createMemo<McpRowItem[]>(() => {
const configMcp = serverSync().data.config.mcp ?? {}
return Object.entries(configMcp).map(([name, config]) => ({
name,
enabled: typeof config !== "object" || config === null || !("enabled" in config) || config.enabled !== false,
}))
})
const handleMcpToggle = (item: McpRowItem, checked: boolean) => {
const before = serverSync().data.config.mcp ?? {}
const config = before[item.name]
if (typeof config !== "object" || config === null) return
const next = { ...before, [item.name]: { ...config, enabled: checked } }
serverSync().set("config", "mcp", next)
void serverSync()
.updateConfig({ mcp: next })
.catch(() => serverSync().set("config", "mcp", before))
}
const plugins = createMemo<PluginRowItem[]>(() => {
const raw = serverSync().data.config.plugin ?? []
return raw.map((item) => {
const name = typeof item === "string" ? item : item[0]
return { name }
})
})
const [skills] = createResource(serverSdk, (sdk) => sdk.api.skill.list().then((result) => result.data), {
initialValue: [],
})
return (
<>
<div class="settings-v2-tab-header">
<div class="settings-v2-tab-header-row">
<div class="flex flex-col gap-1">
<h2 class="settings-v2-tab-title">{language.t("settings.tab.extensions")}</h2>
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.extensions.description")}</span>
</div>
<InlineServerSelect />
</div>
</div>
<div class="settings-v2-tab-body">
<TabsV2 variant="pill" defaultValue="mcps" class="settings-v2-extensions-tabs">
<TabsV2.List>
<TabsV2.Trigger value="mcps">{language.t("settings.extensions.tab.mcps")}</TabsV2.Trigger>
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
<TabsV2.Trigger value="skills">{language.t("settings.extensions.tab.skills")}</TabsV2.Trigger>
</TabsV2.List>
<TabsV2.Content value="mcps">
<div class="settings-v2-section">
<div class="flex items-center justify-between">
<span class="text-13-medium text-v2-text-text-base">
{language.t("settings.extensions.availableAll")}
</span>
<span class="text-13-regular text-v2-text-faint">{language.t("settings.extensions.manageConfig")}</span>
</div>
<div class="bg-[var(--v2-background-bg-base)] border-[0.5px] border-[var(--v2-border-border-base)] rounded-[8px] pl-4 pr-3 overflow-hidden">
<For each={mcps()}>
{(item) => (
<div class="py-4 flex items-center justify-between border-b-[0.5px] border-[var(--v2-border-border-base)] last:border-b-0">
<div class="flex items-center gap-2.5 min-w-0">
<Icon name="mcp" class="text-v2-icon-icon-muted shrink-0" />
<span class="text-13-medium text-v2-text-text-base truncate">{item.name}</span>
</div>
<Switch checked={item.enabled} onChange={(checked) => handleMcpToggle(item, checked)} hideLabel>
{item.name}
</Switch>
</div>
)}
</For>
</div>
</div>
</TabsV2.Content>
<TabsV2.Content value="plugins">
<div class="settings-v2-section">
<div class="flex items-center justify-between">
<span class="text-13-medium text-v2-text-text-base">
{language.t("settings.extensions.availableAll")}
</span>
<span class="text-13-regular text-v2-text-faint">{language.t("settings.extensions.manageConfig")}</span>
</div>
<div class="bg-[var(--v2-background-bg-base)] border-[0.5px] border-[var(--v2-border-border-base)] rounded-[8px] pl-4 pr-3 overflow-hidden">
<For each={plugins()}>
{(plugin) => (
<div class="py-4 flex items-center justify-between border-b-[0.5px] border-[var(--v2-border-border-base)] last:border-b-0">
<div class="flex items-center gap-2.5 min-w-0">
<Icon name="models" class="text-v2-icon-icon-muted shrink-0" />
<span class="text-13-medium text-v2-text-text-base truncate font-mono">{plugin.name}</span>
</div>
</div>
)}
</For>
</div>
</div>
</TabsV2.Content>
<TabsV2.Content value="skills">
<div class="settings-v2-section">
<div class="flex items-center justify-between">
<span class="text-13-medium text-v2-text-text-base">
{language.t("settings.extensions.availableAll")}
</span>
<a
class="text-13-regular text-v2-text-accent hover:underline"
href="https://opencode.ai/docs/skills/"
target="_blank"
rel="noreferrer"
>
{language.t("settings.extensions.addSkills")}
</a>
</div>
<div class="bg-[var(--v2-background-bg-base)] border-[0.5px] border-[var(--v2-border-border-base)] rounded-[8px] pl-4 pr-3 overflow-hidden">
<For each={skills()}>
{(skill) => (
<div class="py-4 flex items-center justify-between border-b-[0.5px] border-[var(--v2-border-border-base)] last:border-b-0">
<div class="flex items-center gap-2.5 min-w-0">
<Icon name="brain" class="text-v2-icon-icon-muted shrink-0" />
<span class="text-13-medium text-v2-text-text-base truncate">{skill.name}</span>
</div>
</div>
)}
</For>
</div>
</div>
</TabsV2.Content>
</TabsV2>
</div>
</>
)
}

View file

@ -9,9 +9,9 @@ import { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language"
import { useModels } from "@/context/models"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { popularProviders } from "@/hooks/use-providers"
import { Persist, persisted } from "@/utils/persist"
import { InlineServerSelect } from "./parts/server-select"
import { SettingsListV2 } from "./parts/list"
import { SettingsRowV2 } from "./parts/row"
import "./settings-v2.css"
@ -24,7 +24,6 @@ export const SettingsModelsV2: Component = () => {
const language = useLanguage()
const models = useModels()
const serverSdk = useServerSDK()
useServerSync()().loadProviders()
const [store, setStore] = persisted(
Persist.serverGlobal(serverSdk().scope, "settings-v2.models.providers"),
createStore({ collapsed: {} as Record<string, boolean> }),
@ -55,7 +54,13 @@ export const SettingsModelsV2: Component = () => {
return (
<>
<div class="settings-v2-tab-header settings-v2-tab-header--stacked">
<h2 class="settings-v2-tab-title">{language.t("settings.models.title")}</h2>
<div class="settings-v2-tab-header-row">
<div class="flex flex-col gap-1">
<h2 class="settings-v2-tab-title">{language.t("settings.models.title")}</h2>
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.models.description")}</span>
</div>
<InlineServerSelect />
</div>
<div class="settings-v2-tab-search">
<TextInputV2
type="search"

View file

@ -0,0 +1,124 @@
import { Component } from "solid-js"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { SettingsListV2 } from "./parts/list"
import { SettingsRowV2 } from "./parts/row"
import { createSoundSettingsController, soundOptions, type SoundSettingsController } from "./general-controllers"
import "./settings-v2.css"
const soundSettings = {
agent: {
action: "settings-sounds-agent",
title: "settings.general.sounds.agent.title",
description: "settings.general.sounds.agent.description",
},
permissions: {
action: "settings-sounds-permissions",
title: "settings.general.sounds.permissions.title",
description: "settings.general.sounds.permissions.description",
},
errors: {
action: "settings-sounds-errors",
title: "settings.general.sounds.errors.title",
description: "settings.general.sounds.errors.description",
},
} as const
const SoundSetting: Component<{
kind: "agent" | "permissions" | "errors"
channel: SoundSettingsController["agent"]
}> = (props) => {
const language = useLanguage()
const config = () => soundSettings[props.kind]
return (
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
<SelectV2
appearance="inline"
data-action={config().action}
options={soundOptions}
current={props.channel.current()}
value={(option) => option.id}
label={(option) => language.t(option.label)}
onHighlight={props.channel.highlight}
onSelect={props.channel.select}
placement="bottom-end"
gutter={6}
/>
</SettingsRowV2>
)
}
export const SettingsNotificationsV2: Component = () => {
const language = useLanguage()
const settings = useSettings()
const sounds = createSoundSettingsController()
return (
<>
<div class="settings-v2-tab-header">
<div class="settings-v2-tab-header-row">
<div class="flex flex-col gap-1">
<h2 class="settings-v2-tab-title">{language.t("settings.tab.notifications")}</h2>
<span class="text-11-regular text-v2-text-text-muted">
{language.t("settings.notifications.description")}
</span>
</div>
</div>
</div>
<div class="settings-v2-tab-body">
<div class="settings-v2-section">
<h3 class="settings-v2-section-title">{language.t("settings.general.section.notifications")}</h3>
<SettingsListV2>
<SettingsRowV2
title={language.t("settings.general.notifications.agent.title")}
description={language.t("settings.general.notifications.agent.description")}
>
<div data-action="settings-notifications-agent">
<Switch
checked={settings.notifications.agent()}
onChange={(checked) => settings.notifications.setAgent(checked)}
/>
</div>
</SettingsRowV2>
<SettingsRowV2
title={language.t("settings.general.notifications.permissions.title")}
description={language.t("settings.general.notifications.permissions.description")}
>
<div data-action="settings-notifications-permissions">
<Switch
checked={settings.notifications.permissions()}
onChange={(checked) => settings.notifications.setPermissions(checked)}
/>
</div>
</SettingsRowV2>
<SettingsRowV2
title={language.t("settings.general.notifications.errors.title")}
description={language.t("settings.general.notifications.errors.description")}
>
<div data-action="settings-notifications-errors">
<Switch
checked={settings.notifications.errors()}
onChange={(checked) => settings.notifications.setErrors(checked)}
/>
</div>
</SettingsRowV2>
</SettingsListV2>
</div>
<div class="settings-v2-section">
<h3 class="settings-v2-section-title">{language.t("settings.general.section.sounds")}</h3>
<SettingsListV2>
<SoundSetting kind="agent" channel={sounds.agent} />
<SoundSetting kind="permissions" channel={sounds.permissions} />
<SoundSetting kind="errors" channel={sounds.errors} />
</SettingsListV2>
</div>
</div>
</>
)
}

View file

@ -0,0 +1,49 @@
import { Show, createMemo, type Component } from "solid-js"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { useGlobal } from "@/context/global"
import { ServerConnection, serverName } from "@/context/server"
const allServers = { type: "all" } as const
type ServerOption = ServerConnection.Any | typeof allServers
export const InlineServerSelect: Component<{
all?: {
label: string
selected: () => boolean
onSelect: () => void
}
onServerSelect?: () => void
}> = (props) => {
const global = useGlobal()
const options = createMemo<ServerOption[]>(() => [...(props.all ? [allServers] : []), ...global.servers.list()])
const current = () => (props.all?.selected() ? allServers : global.settings.server.selected())
return (
<Show when={options().length > 1}>
<SelectV2
appearance="inline"
data-action="settings-server-select"
options={options()}
current={current()}
value={(server) => (server.type === "all" ? server.type : ServerConnection.key(server))}
label={(server) =>
server.type === "all" ? (props.all?.label ?? "") : serverName(server) || ServerConnection.key(server)
}
optionDisabled={(server) =>
server.type === "all" ? false : global.servers.health[ServerConnection.key(server)]?.healthy === false
}
placement="bottom-end"
gutter={6}
onSelect={(server) => {
if (!server) return
if (server.type === "all") {
props.all?.onSelect()
return
}
global.settings.server.set(ServerConnection.key(server))
props.onServerSelect?.()
}}
/>
</Show>
)
}

View file

@ -0,0 +1,139 @@
import { Component, For, Show, createMemo, createSignal } from "solid-js"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language"
import { useGlobal } from "@/context/global"
import { getProjectAvatarVariant } from "@/context/layout"
import { ServerConnection, serverName } from "@/context/server"
import { displayName } from "@/pages/layout/helpers"
import { InlineServerSelect } from "./parts/server-select"
import { DialogEditProjectV2 } from "../dialog-edit-project-v2"
import "./settings-v2.css"
export const SettingsProjectsV2: Component = () => {
const dialog = useDialog()
const language = useLanguage()
const global = useGlobal()
const [allServers, setAllServers] = createSignal(true)
const selected = global.settings.server.selected
const projects = createMemo(() => {
const server = selected()
if (!server) return []
return global.ensureServerCtx(server).projects.list()
})
type ProjectItem = ReturnType<typeof projects>[number]
const groups = createMemo(() =>
global.servers
.list()
.map((server) => ({ server, projects: global.ensureServerCtx(server).projects.list() }))
.filter((group) => group.projects.length > 0),
)
const openProjectSettings = (project: ProjectItem, server = selected()) => {
if (!server) return
dialog.push(() => <DialogEditProjectV2 project={project} server={server} />)
}
const ProjectRow: Component<{ project: ProjectItem; server: ServerConnection.Any }> = (props) => {
const name = () => displayName(props.project)
const color = () => getProjectAvatarVariant(props.project.icon?.color)
return (
<div
class="group flex items-center justify-between gap-5 px-4 py-2.5 rounded-lg bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)] cursor-pointer transition-all hover:bg-v2-background-bg-layer-01"
onClick={() => openProjectSettings(props.project, props.server)}
>
<div class="flex items-center gap-2.5 min-w-0 flex-1">
<ProjectAvatar fallback={name()} variant={color()} class="shrink-0" />
<span class="text-13-medium text-v2-text-text-base truncate">{name()}</span>
</div>
<div class="flex items-center gap-2 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<IconButtonV2
type="button"
variant="ghost-muted"
size="small"
icon={<IconV2 name="settings-gear" size="small" class="text-v2-icon-icon-muted" />}
onClick={(event: MouseEvent) => {
event.stopPropagation()
openProjectSettings(props.project, props.server)
}}
/>
</div>
</div>
)
}
return (
<>
<div class="settings-v2-tab-header">
<div class="settings-v2-tab-header-row">
<div class="flex flex-col gap-1">
<h2 class="settings-v2-tab-title">{language.t("settings.projects.title")}</h2>
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.projects.description")}</span>
</div>
<InlineServerSelect
all={{
label: language.t("settings.projects.server.all"),
selected: allServers,
onSelect: () => setAllServers(true),
}}
onServerSelect={() => setAllServers(false)}
/>
</div>
</div>
<div class="settings-v2-tab-body">
<Show
when={allServers()}
fallback={
<div class="flex flex-col gap-2 w-full">
<Show
when={projects().length > 0}
fallback={
<div class="py-12 text-center text-v2-text-text-muted text-13-regular">
{language.t("settings.projects.empty")}
</div>
}
>
<Show when={selected()} keyed>
{(server) => (
<For each={projects()}>{(project) => <ProjectRow project={project} server={server} />}</For>
)}
</Show>
</Show>
</div>
}
>
<div class="flex flex-col gap-8 w-full">
<Show
when={groups().length > 0}
fallback={
<div class="py-12 text-center text-v2-text-text-muted text-13-regular">
{language.t("settings.projects.empty")}
</div>
}
>
<For each={groups()}>
{(group) => (
<div class="settings-v2-section">
<h3 class="settings-v2-section-title">
{serverName(group.server) || ServerConnection.key(group.server)}
</h3>
<div class="flex flex-col gap-2 w-full">
<For each={group.projects}>
{(project) => <ProjectRow project={project} server={group.server} />}
</For>
</div>
</div>
)}
</For>
</Show>
</div>
</Show>
</div>
</>
)
}

View file

@ -6,10 +6,12 @@ import { showToast } from "@/utils/toast"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { createMemo, type Accessor, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { DialogConnectProvider, useProviderConnectController } from "../dialog-connect-provider"
import { DialogCustomProvider } from "../dialog-custom-provider"
import { SettingsServerScope } from "../settings-server-picker"
import { InlineServerSelect } from "./parts/server-select"
import { SettingsListV2 } from "./parts/list"
import "./settings-v2.css"
@ -36,20 +38,29 @@ export const SettingsProvidersV2: Component<{
const dialog = useDialog()
const language = useLanguage()
const serverSdk = useServerSDK()
const protocol = useServerProtocol()
const serverSync = useServerSync()
const providers = useProviders(props.directory)
const providerConnect = useProviderConnectController({ onBack: props.onBack })
const connect = (provider?: string) => {
providerConnect.select(provider)
void dialog.show(() => <DialogConnectProvider directory={props.directory} controller={providerConnect} />)
void dialog.show(() => (
<SettingsServerScope>
<DialogConnectProvider directory={props.directory} controller={providerConnect} />
</SettingsServerScope>
))
}
const connected = createMemo(() => {
return providers
.connected()
.filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input))
return providers.connected().filter(
(provider) =>
provider.id !== "opencode" ||
Object.values(provider.models).some((model) => {
if (typeof model !== "object" || model === null || !("cost" in model)) return false
const cost = model.cost
return typeof cost === "object" && cost !== null && "input" in cost
}),
)
})
const popular = createMemo(() => {
@ -81,8 +92,7 @@ export const SettingsProvidersV2: Component<{
return language.t("settings.providers.tag.other")
}
const canDisconnect = (item: ProviderItem) =>
source(item) !== "env" && (protocol() === "v1" || !isConfigCustom(item.id))
const canDisconnect = (item: ProviderItem) => source(item) !== "env" && !isConfigCustom(item.id)
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
@ -94,41 +104,16 @@ export const SettingsProvidersV2: Component<{
return true
}
const disableProvider = async (providerID: string, name: string) => {
if (protocol() !== "v1") return
const before = serverSync().data.config.disabled_providers ?? []
const next = before.includes(providerID) ? before : [...before, providerID]
serverSync().set("config", "disabled_providers", next)
await serverSync()
.updateConfig({ disabled_providers: next })
.then(() => {
showToast({
variant: "success",
icon: "circle-check",
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
})
})
.catch((err: unknown) => {
serverSync().set("config", "disabled_providers", before)
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
const disconnect = async (providerID: string, name: string) => {
if (isConfigCustom(providerID)) {
await serverSdk()
.client.auth.remove({ providerID })
.catch(() => undefined)
await disableProvider(providerID, name)
return
}
const location = props.directory() ? { directory: props.directory() } : undefined
await serverSdk()
.client.auth.remove({ providerID })
.then(async () => {
await serverSdk().client.global.dispose()
.api.integration.get({ integrationID: providerID, location })
.then(async (integration) => {
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
await Promise.all(
credentials.map((credential) => serverSdk().api.credential.remove({ credentialID: credential.id, location })),
)
showToast({
variant: "success",
icon: "circle-check",
@ -145,7 +130,13 @@ export const SettingsProvidersV2: Component<{
return (
<>
<div class="settings-v2-tab-header">
<h2 class="settings-v2-tab-title">{language.t("settings.providers.title")}</h2>
<div class="settings-v2-tab-header-row">
<div class="flex flex-col gap-1">
<h2 class="settings-v2-tab-title">{language.t("settings.providers.title")}</h2>
<span class="text-11-regular text-v2-text-text-muted">{language.t("settings.providers.description")}</span>
</div>
<InlineServerSelect />
</div>
</div>
<div class="settings-v2-tab-body settings-v2-providers">
@ -224,7 +215,7 @@ export const SettingsProvidersV2: Component<{
)}
</For>
<Show when={protocol() === "v1"}>
<Show when={false}>
<div class="settings-v2-provider-row" data-component="custom-provider-section">
<div class="settings-v2-provider-lead">
<ProviderIcon
@ -248,7 +239,11 @@ export const SettingsProvidersV2: Component<{
variant="neutral"
icon="plus"
onClick={() => {
dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
dialog.show(() => (
<SettingsServerScope>
<DialogCustomProvider onBack={dialog.close} />
</SettingsServerScope>
))
}}
>
{language.t("common.connect")}

View file

@ -97,7 +97,7 @@ export const SettingsServersV2: Component = () => {
}
>
<SettingsListV2>
<WslServerSettings controller={controller} servers={wslServers} />
<WslServerSettings controller={controller} servers={wslServers} onEdit={() => {}} />
<For each={filtered()}>
{(item) => {
const key = ServerConnection.key(item)

View file

@ -39,6 +39,14 @@
background: linear-gradient(to bottom, var(--v2-background-bg-base) calc(100% - 24px), transparent);
}
.settings-v2-tab-header-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
width: 100%;
}
.settings-v2-tab-title {
font-size: 15px;
font-weight: 640;
@ -174,13 +182,13 @@
max-width: 100%;
}
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-list"] {
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] > [data-slot="tabs-v2-list"] {
background-color: var(--v2-background-bg-layer-01);
}
@media (max-width: 639px) {
.settings-v2[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
[data-slot="tabs-v2-list"] {
> [data-slot="tabs-v2-list"] {
width: 144px;
min-width: 144px;
padding-inline: 8px;
@ -287,7 +295,7 @@
}
.settings-v2-provider-env-hint {
padding-inline-end: 12px;
padding-right: 12px;
font-size: 13px;
font-weight: 440;
line-height: 1;
@ -311,7 +319,7 @@
line-height: 1;
color: var(--v2-text-text-accent);
cursor: pointer;
text-align: start;
text-align: left;
}
.settings-v2-providers-view-all:hover {
@ -361,13 +369,13 @@
}
.settings-v2-tab-search [data-slot="text-input-v2-input"] {
padding-inline-end: 28px;
padding-right: 28px;
}
.settings-v2-tab-search-clear {
position: absolute;
top: 50%;
inset-inline-end: 6px;
right: 6px;
z-index: 1;
transform: translateY(-50%);
}
@ -684,223 +692,6 @@
color: var(--v2-text-text-base);
}
.settings-v2-tab-header.settings-v2-workspaces-header {
padding-bottom: 24px;
}
.settings-v2-workspaces-header .settings-v2-tab-title {
font-weight: 610;
}
.settings-v2-tab-body.settings-v2-workspaces {
gap: 16px;
}
.settings-v2-workspaces-toolbar {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.settings-v2-workspaces-count {
font-size: 15px;
font-weight: 530;
line-height: 1;
color: var(--v2-text-text-base);
}
.settings-v2-workspaces-toolbar-actions {
display: flex;
align-items: center;
gap: 4px;
}
.settings-v2-workspaces-delete-all {
color: var(--v2-state-fg-danger);
}
.settings-v2-workspaces-inventory [data-component="settings-v2-list"] {
display: flex;
flex-direction: column;
gap: 0;
padding: 20px;
border-radius: 6px;
background-color: var(--v2-background-bg-base);
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-base);
}
.settings-v2-workspaces-row {
display: flex;
min-width: 0;
flex-direction: column;
gap: 12px;
}
.settings-v2-workspaces-row:not(:last-child) {
padding-bottom: 20px;
margin-bottom: 20px;
border-bottom: 0.5px solid var(--v2-border-border-base);
}
.settings-v2-workspaces-row-header {
display: flex;
min-width: 0;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
}
.settings-v2-workspaces-copy {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 8px;
}
.settings-v2-workspaces-main {
display: flex;
min-width: 0;
}
.settings-v2-workspaces-row-actions {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.settings-v2-workspaces-main [data-component="tooltip-v2-trigger"] {
min-width: 0;
}
.settings-v2-workspaces-path {
display: block;
min-width: 0;
overflow: hidden;
color: var(--v2-text-text-base);
font-family: inherit;
font-size: 13px;
font-weight: 530;
line-height: 1;
letter-spacing: -0.04px;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0;
text-align: left;
cursor: default;
}
.settings-v2-workspaces-meta {
font-size: 13px;
font-weight: 440;
line-height: 1;
color: var(--v2-text-text-faint);
}
.settings-v2-workspaces-active,
.settings-v2-workspaces-more {
flex-shrink: 0;
font-size: 11px;
font-weight: 440;
line-height: 1;
color: var(--v2-text-text-faint);
}
.settings-v2-workspaces-sessions {
display: flex;
flex-direction: column;
border: 0.5px solid var(--v2-border-border-base);
border-radius: 4px;
background-color: var(--v2-background-bg-base);
overflow: hidden;
}
.settings-v2-workspaces-session {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
font-size: 13px;
font-weight: 440;
line-height: 16px;
color: var(--v2-text-text-base);
}
.settings-v2-workspaces-session:not(:last-child) {
border-bottom: 0.5px solid var(--v2-border-border-base);
}
.settings-v2-workspaces-session > span:first-child {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-v2-workspaces-session-time {
flex-shrink: 0;
font-size: 11px;
line-height: 1;
color: var(--v2-text-text-muted);
}
.settings-v2-workspaces-empty {
display: flex;
align-items: center;
justify-content: center;
padding-block: 48px;
font-size: 13px;
font-weight: 440;
line-height: 1;
color: var(--v2-text-text-muted);
}
@media (max-width: 639px) {
.settings-v2-workspaces-header {
padding: 24px 20px 20px;
}
.settings-v2-tab-body.settings-v2-workspaces {
padding: 0 20px 24px;
}
.settings-v2-workspaces-toolbar,
.settings-v2-workspaces-main {
align-items: flex-start;
}
.settings-v2-workspaces-toolbar {
flex-wrap: wrap;
}
.settings-v2-workspaces-toolbar-actions {
width: 100%;
flex-wrap: wrap;
justify-content: space-between;
}
.settings-v2-workspaces-inventory [data-component="settings-v2-list"] {
padding: 14px;
}
.settings-v2-workspaces-path {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
.settings-v2-workspaces-active {
display: none;
}
}
[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-container"] {
width: 480px;
max-width: calc(100vw - 32px);
@ -944,3 +735,24 @@
line-height: 1;
color: var(--v2-state-fg-danger);
}
.settings-v2-extensions-tabs[data-component="tabs-v2"][data-variant="pill"] > [data-slot="tabs-v2-list"] {
width: 280px;
padding-inline: 0 !important;
}
.settings-v2-extensions-tabs[data-component="tabs-v2"][data-variant="pill"] > [data-slot="tabs-v2-list"]::before {
display: none;
}
.settings-v2-extensions-tabs[data-component="tabs-v2"][data-variant="pill"]
> [data-slot="tabs-v2-list"]
[data-slot="tabs-v2-trigger-wrapper"] {
border-radius: 6px;
}
.settings-v2-extensions-tabs[data-component="tabs-v2"][data-variant="pill"]
> [data-slot="tabs-v2-list"]
[data-slot="tabs-v2-trigger"] {
padding-inline: 8px;
}

View file

@ -69,6 +69,7 @@ export function useFilteredWslServers(filter: Accessor<string>) {
export function WslServerSettings(props: {
controller: Controller
servers: ReturnType<typeof useFilteredWslServers>
onEdit: (key: ServerConnection.Key) => void
}) {
const platform = usePlatform()
const language = useLanguage()
@ -124,7 +125,7 @@ export function WslServerSettings(props: {
disabled={busy() || request.isPending}
onClick={() => api && request.mutate(() => api.installOpencode(item.config.distro))}
>
{busy() ? language.t("wsl.server.updating") : language.t(label())}
{busy() ? language.t("wsl.server.updating") : label()}
</ButtonV2>
)}
</Show>
@ -140,6 +141,9 @@ export function WslServerSettings(props: {
<MenuV2.Content>
<MenuV2.Group>
<MenuV2.GroupLabel>{language.t("wsl.server.menu.label")}</MenuV2.GroupLabel>
<MenuV2.Item onSelect={() => props.onEdit(key)}>
{language.t("dialog.server.menu.edit")}
</MenuV2.Item>
<Show when={wslRuntimeRetryable(item.runtime)}>
<MenuV2.Item onSelect={() => api && request.mutate(() => api.startServer(key))}>
{language.t("wsl.server.retryStart")}

View file

@ -71,7 +71,7 @@
[data-component="tabs-v2"] [data-slot="tabs-v2-close-button"] {
width: 20px;
height: 20px;
margin-inline-start: -5px;
margin-left: -5px;
flex: none;
display: flex;
align-items: center;
@ -109,7 +109,7 @@
background-color: var(--v2-border-border-base);
position: absolute;
bottom: 0px;
inset-inline-start: -8px;
left: -8px;
}
[data-component="tabs-v2"][data-variant="pill"][data-orientation="horizontal"] [data-slot="tabs-v2-list"] {
@ -175,7 +175,7 @@
border: 0.5px solid var(--v2-border-border-muted);
}
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-list"] {
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] > [data-slot="tabs-v2-list"] {
flex-direction: column;
width: 240px;
min-width: 200px;
@ -183,27 +183,34 @@
padding: 12px;
gap: 4px;
overflow-y: auto;
border-inline-end: 1px solid var(--v2-border-border-base);
border-right: 1px solid var(--v2-border-border-base);
}
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-section-title"] {
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
> [data-slot="tabs-v2-list"]
[data-slot="tabs-v2-section-title"] {
width: 100%;
padding-inline-start: 4px;
padding-left: 4px;
color: var(--v2-text-text-muted);
font-size: 12px;
font-weight: 500;
user-select: none;
}
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-trigger-wrapper"] {
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
> [data-slot="tabs-v2-list"]
[data-slot="tabs-v2-trigger-wrapper"] {
width: 100%;
height: 28px;
border-radius: 4px;
border: 0.5px solid transparent;
box-sizing: border-box;
color: var(--v2-text-text-muted);
}
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-trigger"] {
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
> [data-slot="tabs-v2-list"]
[data-slot="tabs-v2-trigger"] {
width: 100%;
height: 100%;
justify-content: flex-start;
@ -212,13 +219,16 @@
}
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
> [data-slot="tabs-v2-list"]
[data-slot="tabs-v2-trigger-wrapper"]:hover:not(:disabled):not(:has([data-selected])) {
background-color: var(--v2-background-bg-layer-03);
color: var(--v2-text-text-base);
}
[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"]
> [data-slot="tabs-v2-list"]
[data-slot="tabs-v2-trigger-wrapper"]:has([data-selected]) {
background-color: var(--v2-background-bg-layer-03);
color: var(--v2-text-text-base);
border: 0.5px solid var(--v2-border-border-muted);
}