fix(app): scope redesigned settings by server

This commit is contained in:
LukeParkerDev 2026-08-07 17:04:21 +10:00
parent 3b645690ea
commit d3aee774d9
29 changed files with 326 additions and 588 deletions

View file

@ -389,7 +389,7 @@ function ProviderConnection(props: {
const settings = useSettings()
const newLayout = settings.general.newLayoutDesigns
const providers = useProviders(() => props.directory?.())
const directory = () => props.directory?.() ?? decode64(params.dir)
const directory = () => (props.directory ? props.directory() : decode64(params.dir))
const location = () => {
const value = directory()
return value ? { directory: value } : undefined

View file

@ -80,10 +80,11 @@ export const ServerRowMenuView: Component<{
>
{props.labels.edit}
</MenuV2.Item>
<Show when={props.canDefault}>
<MenuV2.Item disabled={props.isDefault} onSelect={props.onSetDefault}>
{props.labels.default}
</MenuV2.Item>
<Show when={props.canDefault && !props.isDefault}>
<MenuV2.Item onSelect={props.onSetDefault}>{props.labels.default}</MenuV2.Item>
</Show>
<Show when={props.canDefault && props.isDefault}>
<MenuV2.Item onSelect={props.onRemoveDefault}>{props.labels.defaultRemove}</MenuV2.Item>
</Show>
<Show when={props.canRemove}>
<MenuV2.Separator />

View file

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

View file

@ -1,5 +1,4 @@
import { Component, Show, createMemo, createResource } from "solid-js"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
@ -8,10 +7,7 @@ import { usePlatform } from "@/context/platform"
import { ExternalLink } from "../external-link"
import { SettingsListV2 } from "./parts/list"
import { SettingsRowV2 } from "./parts/row"
import {
createAppearanceSettingsController,
type AppearanceSettingsController,
} from "./general-controllers"
import { createAppearanceSettingsController, type AppearanceSettingsController } from "./general-controllers"
import "./settings-v2.css"
const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"]
@ -86,21 +82,11 @@ export const SettingsAppearanceV2: Component = () => {
void update.catch(() => setPinchZoom(!checked))
}
const restoreDefaults = () => {
appearance.scheme.select("system")
appearance.fonts.setUI("")
appearance.fonts.setCode("")
appearance.fonts.setTerminal("")
}
return (
<>
<div class="settings-v2-tab-header">
<div class="settings-v2-tab-header-row">
<h2 class="settings-v2-tab-title">{language.t("settings.general.section.appearance")}</h2>
<ButtonV2 size="small" variant="ghost-muted" onClick={restoreDefaults}>
{language.t("common.reset")}
</ButtonV2>
</div>
</div>

View file

@ -13,10 +13,12 @@ import { SettingsModelsV2 } from "./models"
import { SettingsServersV2 } from "./servers"
import { SettingsProjectsV2 } from "./projects"
import { SettingsExtensionsV2 } from "./extensions"
import { SettingsServerScope } from "../settings-server-picker"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLayout } from "@/context/layout"
import { useTabs } from "@/context/tabs"
import { useServerSync } from "@/context/server-sync"
import { useGlobal } from "@/context/global"
import "./settings-v2.css"
export const DialogSettings: Component<{
@ -29,8 +31,11 @@ export const DialogSettings: Component<{
const layout = useLayout()
const tabs = useTabs()
const serverSync = useServerSync()
const global = useGlobal()
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
const directory = createMemo(() => {
const server = global.settings.server.selected()
if (!server || serverSync() !== global.ensureServerCtx(server).sync) return
const route = layout.route()
if (route.type === "dir-new-sesssion") return route.dir
if (route.type === "draft") {
@ -131,15 +136,17 @@ export const DialogSettings: Component<{
<TabsV2.Content value="projects" class="settings-v2-panel">
<SettingsProjectsV2 />
</TabsV2.Content>
<TabsV2.Content value="providers" class="settings-v2-panel">
<SettingsProvidersV2 directory={directory} onBack={showProviders} />
</TabsV2.Content>
<TabsV2.Content value="models" class="settings-v2-panel">
<SettingsModelsV2 />
</TabsV2.Content>
<TabsV2.Content value="extensions" class="settings-v2-panel">
<SettingsExtensionsV2 />
</TabsV2.Content>
<SettingsServerScope>
<TabsV2.Content value="providers" class="settings-v2-panel">
<SettingsProvidersV2 directory={directory} onBack={showProviders} />
</TabsV2.Content>
<TabsV2.Content value="models" class="settings-v2-panel">
<SettingsModelsV2 />
</TabsV2.Content>
<TabsV2.Content value="extensions" class="settings-v2-panel">
<SettingsExtensionsV2 />
</TabsV2.Content>
</SettingsServerScope>
</TabsV2>
</Dialog>
)

View file

@ -1,71 +1,44 @@
import { Component, For, Show, createMemo, createSignal } from "solid-js"
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 { ServerConnection, useServer } from "@/context/server"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { ExternalLink } from "../external-link"
import { InlineServerSelect } from "./parts/server-select"
import "./settings-v2.css"
type ExtensionSubTab = "mcps" | "plugins" | "skills"
interface McpRowItem {
name: string
status?: string
enabled: boolean
}
interface PluginRowItem {
name: string
}
interface SkillRowItem {
name: string
}
export const SettingsExtensionsV2: Component = () => {
const language = useLanguage()
const server = useServer()
const serverSdk = useServerSDK()
const serverSync = useServerSync()
const [selectedServer, setSelectedServer] = createSignal<ServerConnection.Key | "all">(server.key)
const [activeSubTab, setActiveSubTab] = createSignal<ExtensionSubTab>("mcps")
const [mcpOverrides, setMcpOverrides] = createSignal<Record<string, boolean>>({})
const mcps = createMemo<McpRowItem[]>(() => {
const configMcp = (serverSync().data.config as { mcp?: Record<string, { disabled?: boolean }> })?.mcp ?? {}
const configEntries = Object.entries(configMcp).map(([name, conf]) => ({
const configMcp = serverSync().data.config.mcp ?? {}
return Object.entries(configMcp).map(([name, config]) => ({
name,
status: conf.disabled ? "disabled" : "connected",
enabled: config.enabled !== false,
}))
const firstProject = server.projects.list()[0]?.worktree
const childMcp = firstProject ? serverSync().child(firstProject, { mcp: true })[0].mcp : {}
const childEntries = Object.entries(childMcp ?? {}).map(([name, stat]) => ({
name,
status: stat?.status,
}))
const map = new Map<string, McpRowItem>()
for (const item of configEntries) map.set(item.name, item)
for (const item of childEntries) {
const existing = map.get(item.name)
map.set(item.name, { ...existing, ...item })
}
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name))
})
const isMcpEnabled = (item: McpRowItem) => {
if (mcpOverrides()[item.name] !== undefined) return mcpOverrides()[item.name]
return item.status === "connected"
}
const handleMcpToggle = (item: McpRowItem, checked: boolean) => {
setMcpOverrides((prev) => ({ ...prev, [item.name]: checked }))
const firstProject = server.projects.list()[0]?.worktree
if (firstProject) {
void serverSync().mcp.toggle(firstProject, item.name)
}
const before = serverSync().data.config.mcp ?? {}
const config = before[item.name]
if (!config) 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[]>(() => {
@ -76,10 +49,14 @@ export const SettingsExtensionsV2: Component = () => {
})
})
const skills = createMemo<SkillRowItem[]>(() => {
const configSkills = (serverSync().data.config as { skills?: string[] })?.skills ?? []
return configSkills.map((name) => ({ name }))
})
const [skills] = createResource(
() => serverSdk(),
async (sdk) => {
const result = await sdk.client.v2.skill.list(undefined, { throwOnError: true })
return result.data.data
},
{ initialValue: [] },
)
return (
<>
@ -89,125 +66,95 @@ export const SettingsExtensionsV2: Component = () => {
<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.mcps.description")}</span>
</div>
<InlineServerSelect value={selectedServer()} onChange={setSelectedServer} includeAll />
<InlineServerSelect />
</div>
</div>
<div class="settings-v2-tab-body">
{/* Sub-Tabs Pill Strip */}
<div class="settings-v2-extensions-tabs" role="tablist">
<button
type="button"
role="tab"
aria-selected={activeSubTab() === "mcps"}
data-selected={activeSubTab() === "mcps" ? "" : undefined}
onClick={() => setActiveSubTab("mcps")}
>
{language.t("status.popover.tab.mcp")}
</button>
<button
type="button"
role="tab"
aria-selected={activeSubTab() === "plugins"}
data-selected={activeSubTab() === "plugins" ? "" : undefined}
onClick={() => setActiveSubTab("plugins")}
>
{language.t("status.popover.tab.plugins")}
</button>
<button
type="button"
role="tab"
aria-selected={activeSubTab() === "skills"}
data-selected={activeSubTab() === "skills" ? "" : undefined}
onClick={() => setActiveSubTab("skills")}
>
{language.t("settings.permissions.tool.skill.title")}
</button>
</div>
<TabsV2 variant="pill" defaultValue="mcps" class="settings-v2-extensions-tabs">
<TabsV2.List>
<TabsV2.Trigger value="mcps">{language.t("status.popover.tab.mcp")}</TabsV2.Trigger>
<TabsV2.Trigger value="plugins">{language.t("status.popover.tab.plugins")}</TabsV2.Trigger>
<TabsV2.Trigger value="skills">{language.t("settings.permissions.tool.skill.title")}</TabsV2.Trigger>
</TabsV2.List>
{/* MCPs Sub-Tab */}
<Show when={activeSubTab() === "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>
<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>
<Switch
checked={isMcpEnabled(item)}
onChange={(checked) => handleMcpToggle(item, checked)}
hideLabel
>
{item.name}
</Switch>
</div>
)}
</For>
)}
</For>
</div>
</div>
</div>
</Show>
</TabsV2.Content>
{/* Plugins Sub-Tab */}
<Show when={activeSubTab() === "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="cube" class="text-v2-icon-icon-muted shrink-0" />
<span class="text-13-medium text-v2-text-text-base truncate font-mono">{plugin.name}</span>
<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="cube" 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>
</div>
)}
</For>
)}
</For>
</div>
</div>
</div>
</Show>
</TabsV2.Content>
{/* Skills Sub-Tab */}
<Show when={activeSubTab() === "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>
<ExternalLink
class="text-13-regular text-v2-text-accent hover:underline"
href="https://opencode.ai/docs/skills/"
>
{language.t("settings.extensions.addSkills")}
</ExternalLink>
</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="post-skill" class="text-v2-icon-icon-muted shrink-0" />
<span class="text-13-medium text-v2-text-text-base truncate">{skill.name}</span>
<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>
<ExternalLink
class="text-13-regular text-v2-text-accent hover:underline"
href="https://opencode.ai/docs/skills/"
>
{language.t("settings.extensions.addSkills")}
</ExternalLink>
</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="post-skill" class="text-v2-icon-icon-muted shrink-0" />
<span class="text-13-medium text-v2-text-text-base truncate">{skill.name}</span>
</div>
</div>
</div>
)}
</For>
)}
</For>
</div>
</div>
</div>
</Show>
</TabsV2.Content>
</TabsV2>
</div>
</>
)

View file

@ -1,9 +1 @@
export { DialogSettings } from "./dialog-settings-v2"
export { SettingsGeneralV2 } from "./general"
export { SettingsAppearanceV2 } from "./appearance"
export { SettingsNotificationsV2 } from "./notifications"
export { SettingsProvidersV2 } from "./providers"
export { SettingsModelsV2 } from "./models"
export { SettingsServersV2 } from "./servers"
export { SettingsProjectsV2 } from "./projects"
export { SettingsExtensionsV2 } from "./extensions"

View file

@ -4,12 +4,11 @@ import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { type Component, For, Show, createSignal } from "solid-js"
import { type Component, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language"
import { useModels } from "@/context/models"
import { useServerSDK } from "@/context/server-sdk"
import { ServerConnection, useServer } from "@/context/server"
import { popularProviders } from "@/hooks/use-providers"
import { Persist, persisted } from "@/utils/persist"
import { InlineServerSelect } from "./parts/server-select"
@ -24,9 +23,7 @@ const PROVIDER_ICON_SIZE = 16
export const SettingsModelsV2: Component = () => {
const language = useLanguage()
const models = useModels()
const server = useServer()
const serverSdk = useServerSDK()
const [selectedServer, setSelectedServer] = createSignal<ServerConnection.Key | "all">(server.key)
const [store, setStore] = persisted(
Persist.serverGlobal(serverSdk().scope, "settings-v2.models.providers"),
createStore({ collapsed: {} as Record<string, boolean> }),
@ -59,11 +56,7 @@ export const SettingsModelsV2: Component = () => {
<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.models.title")}</h2>
<InlineServerSelect
value={selectedServer()}
onChange={setSelectedServer}
includeAll
/>
<InlineServerSelect />
</div>
<div class="settings-v2-tab-search">
<TextInputV2

View file

@ -1,5 +1,4 @@
import { Component } from "solid-js"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
import { useLanguage } from "@/context/language"
@ -56,23 +55,11 @@ export const SettingsNotificationsV2: Component = () => {
const settings = useSettings()
const sounds = createSoundSettingsController()
const restoreDefaults = () => {
settings.notifications.setAgent(true)
settings.notifications.setPermissions(true)
settings.notifications.setErrors(true)
settings.sounds.setAgent("bip-bop-01")
settings.sounds.setPermissions("alert-01")
settings.sounds.setErrors("nope-01")
}
return (
<>
<div class="settings-v2-tab-header">
<div class="settings-v2-tab-header-row">
<h2 class="settings-v2-tab-title">{language.t("settings.tab.notifications")}</h2>
<ButtonV2 size="small" variant="ghost-muted" onClick={restoreDefaults}>
{language.t("common.reset")}
</ButtonV2>
</div>
</div>

View file

@ -1,70 +1,25 @@
import { Show, createMemo, type Component } from "solid-js"
import { Show, type Component } from "solid-js"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { useGlobal } from "@/context/global"
import { useLanguage } from "@/context/language"
import { ServerConnection, serverName, useServer } from "@/context/server"
import { ServerConnection, serverName } from "@/context/server"
export interface ServerSelectOption {
key: ServerConnection.Key | "all"
label: string
isDefault?: boolean
isAll?: boolean
}
export const InlineServerSelect: Component<{
value: ServerConnection.Key | "all"
onChange: (key: ServerConnection.Key | "all") => void
includeAll?: boolean
disabled?: boolean
}> = (props) => {
const language = useLanguage()
export const InlineServerSelect: Component = () => {
const global = useGlobal()
const server = useServer()
const hasMultipleServers = createMemo(() => global.servers.list().length > 1)
const options = createMemo<ServerSelectOption[]>(() => {
const list: ServerSelectOption[] = []
if (props.includeAll) {
list.push({
key: "all",
label: language.t("settings.server.all"),
isAll: true,
})
}
const servers = global.servers.list()
for (const item of servers) {
const key = ServerConnection.key(item)
const isDefault = key === server.key
list.push({
key,
label: serverName(item) || key,
isDefault,
})
}
return list
})
const currentOption = createMemo(() => {
return options().find((opt) => opt.key === props.value) ?? options()[0]
})
return (
<Show when={hasMultipleServers()}>
<Show when={global.servers.list().length > 1}>
<SelectV2
appearance="inline"
data-action="settings-server-select"
options={options()}
current={currentOption()}
value={(opt) => opt.key}
label={(opt) => opt.label}
disabled={props.disabled}
options={global.servers.list()}
current={global.settings.server.selected()}
value={ServerConnection.key}
label={(server) => serverName(server) || ServerConnection.key(server)}
optionDisabled={(server) => global.servers.health[ServerConnection.key(server)]?.healthy === false}
placement="bottom-end"
gutter={6}
onSelect={(opt) => {
if (opt) props.onChange(opt.key)
onSelect={(server) => {
if (server) global.settings.server.set(ServerConnection.key(server))
}}
/>
</Show>

View file

@ -1,12 +1,12 @@
import { Component, For, Show, createMemo, createSignal } from "solid-js"
import { Component, For, Show, createMemo } 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 { ServerConnection, useServer } from "@/context/server"
import { useGlobal } from "@/context/global"
import { useLayout, getProjectAvatarVariant } from "@/context/layout"
import { getProjectAvatarVariant } from "@/context/layout"
import { displayName } from "@/pages/layout/helpers"
import { InlineServerSelect } from "./parts/server-select"
import { DialogEditProjectV2 } from "../dialog-edit-project-v2"
import "./settings-v2.css"
@ -14,19 +14,18 @@ import "./settings-v2.css"
export const SettingsProjectsV2: Component = () => {
const dialog = useDialog()
const language = useLanguage()
const server = useServer()
const global = useGlobal()
const layout = useLayout()
const [selectedServer, setSelectedServer] = createSignal<ServerConnection.Key | "all">(server.key)
const selected = global.settings.server.selected
const projects = createMemo(() => {
return layout.projects.list()
const server = selected()
if (!server) return []
return global.ensureServerCtx(server).projects.list()
})
const openProjectSettings = (project: ReturnType<typeof layout.projects.list>[number]) => {
const currentServer = server.current ?? global.servers.list()[0]
if (!currentServer) return
dialog.push(() => <DialogEditProjectV2 project={project} server={currentServer} />)
const openProjectSettings = (project: ReturnType<typeof projects>[number]) => {
const server = selected()
if (!server) return
dialog.push(() => <DialogEditProjectV2 project={project} server={server} />)
}
return (
@ -37,11 +36,7 @@ export const SettingsProjectsV2: Component = () => {
<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
value={selectedServer()}
onChange={setSelectedServer}
includeAll
/>
<InlineServerSelect />
</div>
</div>
@ -57,7 +52,7 @@ export const SettingsProjectsV2: Component = () => {
>
<For each={projects()}>
{(project) => {
const name = () => project.name || project.worktree.split(/[/\\]/).pop() || project.worktree
const name = () => displayName(project)
const color = () => getProjectAvatarVariant(project.icon?.color)
return (
@ -66,11 +61,7 @@ export const SettingsProjectsV2: Component = () => {
onClick={() => openProjectSettings(project)}
>
<div class="flex items-center gap-2.5 min-w-0 flex-1">
<ProjectAvatar
fallback={name()}
variant={color()}
class="shrink-0"
/>
<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">

View file

@ -4,13 +4,13 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { showToast } from "@/utils/toast"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { createMemo, createSignal, type Accessor, type Component, For, Show } from "solid-js"
import { createMemo, type Accessor, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { ServerConnection, useServer } from "@/context/server"
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"
@ -42,12 +42,13 @@ export const SettingsProvidersV2: Component<{
const providers = useProviders(props.directory)
const providerConnect = useProviderConnectController({ onBack: props.onBack })
const server = useServer()
const [selectedServer, setSelectedServer] = createSignal<ServerConnection.Key | "all">(server.key)
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(() => {
@ -101,9 +102,9 @@ export const SettingsProvidersV2: Component<{
return
const before = serverSync().data.config.disabled_providers ?? []
const next = before.includes(providerID) ? before : [...before, providerID]
serverSync().set("config", "disabled_providers", next)
sync.set("config", "disabled_providers", next)
await serverSync()
await sync
.updateConfig({ disabled_providers: next })
.then(() => {
showToast({
@ -114,7 +115,7 @@ export const SettingsProvidersV2: Component<{
})
})
.catch((err: unknown) => {
serverSync().set("config", "disabled_providers", before)
sync.set("config", "disabled_providers", before)
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
@ -150,11 +151,7 @@ export const SettingsProvidersV2: Component<{
<div class="settings-v2-tab-header">
<div class="settings-v2-tab-header-row">
<h2 class="settings-v2-tab-title">{language.t("settings.providers.title")}</h2>
<InlineServerSelect
value={selectedServer()}
onChange={setSelectedServer}
includeAll
/>
<InlineServerSelect />
</div>
</div>
@ -258,7 +255,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

@ -182,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;
@ -736,39 +736,19 @@
color: var(--v2-state-fg-danger);
}
.settings-v2-extensions-tabs {
display: flex;
align-items: flex-start;
gap: 6px;
.settings-v2-extensions-tabs [data-slot="tabs-v2-list"] {
width: 280px;
height: 32px;
overflow: visible;
padding-inline: 0;
}
.settings-v2-extensions-tabs > button {
display: flex;
align-items: center;
justify-content: center;
flex: none;
height: 24px;
padding: 0 8px;
border: 0.5px solid transparent;
.settings-v2-extensions-tabs [data-slot="tabs-v2-list"]::before {
display: none;
}
.settings-v2-extensions-tabs [data-slot="tabs-v2-trigger-wrapper"] {
border-radius: 6px;
background: transparent;
color: var(--v2-text-text-muted);
font-size: 13px;
font-weight: 440;
line-height: 1;
letter-spacing: -0.04px;
}
.settings-v2-extensions-tabs > button[data-selected] {
border-color: var(--v2-border-border-muted);
background: var(--v2-background-bg-layer-03);
color: var(--v2-text-text-base);
}
.settings-v2-extensions-tabs > button:focus-visible {
outline: 2px solid var(--v2-border-border-focus);
outline-offset: 2px;
.settings-v2-extensions-tabs [data-slot="tabs-v2-trigger"] {
padding-inline: 8px;
}

View file

@ -1005,24 +1005,4 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة",
"error.childStore.storeCreateFailed": "فشل إنشاء المخزن",
"terminal.connectionLost.abnormalClose": "تم إغلاق WebSocket بشكل غير طبيعي: {{code}}",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
}

View file

@ -1021,24 +1021,4 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente",
"error.childStore.storeCreateFailed": "Falha ao criar armazenamento",
"terminal.connectionLost.abnormalClose": "WebSocket fechado anormalmente: {{code}}",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
}

View file

@ -1097,24 +1097,4 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta",
"error.childStore.storeCreateFailed": "Nije uspjelo kreiranje skladišta",
"terminal.connectionLost.abnormalClose": "WebSocket zatvoren nenormalno: {{code}}",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
}

View file

@ -849,7 +849,6 @@ export const dict = {
"settings.tab.notifications": "Notifications",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",

View file

@ -1105,24 +1105,4 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente",
"error.childStore.storeCreateFailed": "Error al crear almacén",
"terminal.connectionLost.abnormalClose": "WebSocket cerrado anormalmente: {{code}}",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
}

View file

@ -1108,24 +1108,4 @@ export const dict = {
"settings.general.row.pinchZoom.description": "Tillat knipebevegelser på styreflaten og Ctrl-rulling for å zoome",
"settings.updates.action.downloading": "Laster ned...",
"settings.updates.action.installing": "Installerer...",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
} satisfies Partial<Record<Keys, string>>

View file

@ -20,6 +20,27 @@ const appLocales = [
"zht",
] as const
const desktopLocales = appLocales.filter((locale) => locale !== "th" && locale !== "tr")
const appFallbackKeys = new Set([
"command.session.export",
"command.session.export.description",
"context.export.session",
"toast.session.export.success.title",
"toast.session.export.success.description",
"toast.session.export.failed.title",
"toast.session.export.failed.description",
"common.export",
"settings.tab.preferences",
"settings.tab.notifications",
"settings.tab.projects",
"settings.tab.extensions",
"settings.projects.title",
"settings.projects.description",
"settings.projects.empty",
"settings.mcps.description",
"settings.extensions.availableAll",
"settings.extensions.manageConfig",
"settings.extensions.addSkills",
])
const domains = [
{
@ -48,7 +69,9 @@ describe.skipIf(!!process.env.CI)("i18n parity", () => {
const source = await dictionary(domain.source)
for (const locale of domain.locales) {
const target = await dictionary(domain.target(locale))
const missing = Object.keys(source).filter((key) => !Object.hasOwn(target, key))
const missing = Object.keys(source).filter(
(key) => !Object.hasOwn(target, key) && (domain.name !== "app" || !appFallbackKeys.has(key)),
)
const extra = Object.keys(target).filter((key) => !Object.hasOwn(source, key))
expect({ domain: domain.name, locale, missing, extra }).toEqual({
domain: domain.name,

View file

@ -1021,24 +1021,4 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu",
"error.childStore.storeCreateFailed": "Nie udało się utworzyć magazynu",
"terminal.connectionLost.abnormalClose": "WebSocket zamknięty nieprawidłowo: {{code}}",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
}

View file

@ -1100,24 +1100,4 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта",
"error.childStore.storeCreateFailed": "Не удалось создать хранилище",
"terminal.connectionLost.abnormalClose": "WebSocket закрыт аварийно: {{code}}",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
}

View file

@ -1084,24 +1084,4 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร",
"error.childStore.storeCreateFailed": "ไม่สามารถสร้างที่เก็บ",
"terminal.connectionLost.abnormalClose": "WebSocket ปิดอย่างผิดปกติ: {{code}}",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
}

View file

@ -1103,24 +1103,4 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı",
"error.childStore.storeCreateFailed": "Depo oluşturulamadı",
"terminal.connectionLost.abnormalClose": "WebSocket anormal şekilde kapandı: {{code}}",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
} satisfies Partial<Record<Keys, string>>

View file

@ -1102,24 +1102,4 @@ export const dict = {
"workspace.reset.archived.one": "1 сесію буде заархівовано.",
"workspace.reset.archived.many": "{{count}} сесій буде заархівовано.",
"workspace.reset.note": "Це скине робочу область, щоб вона відповідала гілці за замовчуванням.",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
}

View file

@ -1075,24 +1075,4 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败",
"error.childStore.storeCreateFailed": "创建存储失败",
"terminal.connectionLost.abnormalClose": "WebSocket 异常关闭:{{code}}",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
} satisfies Partial<Record<Keys, string>>

View file

@ -1071,24 +1071,4 @@ export const dict = {
"error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗",
"error.childStore.storeCreateFailed": "建立儲存區失敗",
"terminal.connectionLost.abnormalClose": "WebSocket 異常關閉:{{code}}",
"command.session.export": "Export session",
"command.session.export.description": "Export the full session transcript as JSON",
"context.export.session": "Export session",
"toast.session.export.success.title": "Session exported",
"toast.session.export.success.description": "Saved session to {{filename}}",
"toast.session.export.failed.title": "Failed to export session",
"toast.session.export.failed.description": "An error occurred while exporting the session",
"common.export": "Export",
"settings.tab.preferences": "Preferences",
"settings.tab.projects": "Projects",
"settings.tab.extensions": "Extensions",
"settings.server.all": "All servers",
"settings.projects.title": "Projects",
"settings.projects.description": "View and configure projects on this server",
"settings.projects.empty": "No projects found",
"settings.mcps.description": "Manage Model Context Protocol (MCP) servers and tools",
"settings.extensions.availableAll": "Available to all projects",
"settings.extensions.manageConfig": "Manage in opencode.json",
"settings.extensions.addSkills": "How to add skills",
"settings.tab.notifications": "Notifications",
} satisfies Partial<Record<Keys, string>>

View file

@ -145,14 +145,16 @@ export function WslServerSettings(props: {
{language.t("wsl.server.retryStart")}
</MenuV2.Item>
</Show>
<Show when={props.controller.canDefault()}>
<MenuV2.Item
disabled={props.controller.defaultKey() === key}
onSelect={() => props.controller.setDefault(key)}
>
<Show when={props.controller.canDefault() && props.controller.defaultKey() !== key}>
<MenuV2.Item onSelect={() => props.controller.setDefault(key)}>
{language.t("dialog.server.menu.default")}
</MenuV2.Item>
</Show>
<Show when={props.controller.canDefault() && props.controller.defaultKey() === key}>
<MenuV2.Item onSelect={() => props.controller.setDefault(null)}>
{language.t("dialog.server.menu.defaultRemove")}
</MenuV2.Item>
</Show>
<MenuV2.Separator />
<MenuV2.Item onSelect={() => remove(key)}>
{language.t("dialog.server.menu.delete")}

View file

@ -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;
@ -186,7 +186,9 @@
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-left: 4px;
color: var(--v2-text-text-muted);
@ -195,7 +197,9 @@
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;
@ -204,7 +208,9 @@
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;
@ -213,12 +219,14 @@
}
[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);