diff options
| author | Adam <[email protected]> | 2026-01-06 15:21:00 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-20 07:33:44 -0600 |
| commit | 8bcbfd63960120efa3cb770f8e07de1bb57e93b0 (patch) | |
| tree | 75429fd33bf5bc2d3a81e27db72b5671a2eb7629 /packages/app/src | |
| parent | e521fee0023a604bb6d5ef39b4b892cbf1a0f9d4 (diff) | |
| download | opencode-8bcbfd63960120efa3cb770f8e07de1bb57e93b0.tar.gz opencode-8bcbfd63960120efa3cb770f8e07de1bb57e93b0.zip | |
wip(app): settings
Diffstat (limited to 'packages/app/src')
| -rw-r--r-- | packages/app/src/app.tsx | 43 | ||||
| -rw-r--r-- | packages/app/src/components/dialog-settings.tsx | 87 | ||||
| -rw-r--r-- | packages/app/src/components/settings-agents.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-commands.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-general.tsx | 134 | ||||
| -rw-r--r-- | packages/app/src/components/settings-keybinds.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-mcp.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-models.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-permissions.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/components/settings-providers.tsx | 12 | ||||
| -rw-r--r-- | packages/app/src/context/settings.tsx | 103 | ||||
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 5 |
12 files changed, 437 insertions, 19 deletions
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index d03d10d0e..33a5556ef 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -14,6 +14,7 @@ import { PermissionProvider } from "@/context/permission" import { LayoutProvider } from "@/context/layout" import { GlobalSDKProvider } from "@/context/global-sdk" import { ServerProvider, useServer } from "@/context/server" +import { SettingsProvider } from "@/context/settings" import { TerminalProvider } from "@/context/terminal" import { PromptProvider } from "@/context/prompt" import { FileProvider } from "@/context/file" @@ -82,15 +83,17 @@ export function AppInterface(props: { defaultUrl?: string }) { <GlobalSyncProvider> <Router root={(props) => ( - <PermissionProvider> - <LayoutProvider> - <NotificationProvider> - <CommandProvider> - <Layout>{props.children}</Layout> - </CommandProvider> - </NotificationProvider> - </LayoutProvider> - </PermissionProvider> + <SettingsProvider> + <PermissionProvider> + <LayoutProvider> + <NotificationProvider> + <CommandProvider> + <Layout>{props.children}</Layout> + </CommandProvider> + </NotificationProvider> + </LayoutProvider> + </PermissionProvider> + </SettingsProvider> )} > <Route @@ -105,16 +108,18 @@ export function AppInterface(props: { defaultUrl?: string }) { <Route path="/" component={() => <Navigate href="session" />} /> <Route path="/session/:id?" - component={() => ( - <TerminalProvider> - <FileProvider> - <PromptProvider> - <Suspense fallback={<Loading />}> - <Session /> - </Suspense> - </PromptProvider> - </FileProvider> - </TerminalProvider> + component={(p) => ( + <Show when={p.params.id ?? "new"} keyed> + <TerminalProvider> + <FileProvider> + <PromptProvider> + <Suspense fallback={<Loading />}> + <Session /> + </Suspense> + </PromptProvider> + </FileProvider> + </TerminalProvider> + </Show> )} /> </Route> diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx new file mode 100644 index 000000000..872cc4c80 --- /dev/null +++ b/packages/app/src/components/dialog-settings.tsx @@ -0,0 +1,87 @@ +import { Component, createSignal } from "solid-js" +import { Dialog } from "@opencode-ai/ui/dialog" +import { Tabs } from "@opencode-ai/ui/tabs" +import { Icon } from "@opencode-ai/ui/icon" +import { TextField } from "@opencode-ai/ui/text-field" +import { SettingsGeneral } from "./settings-general" +import { SettingsKeybinds } from "./settings-keybinds" +import { SettingsPermissions } from "./settings-permissions" +import { SettingsProviders } from "./settings-providers" +import { SettingsModels } from "./settings-models" +import { SettingsAgents } from "./settings-agents" +import { SettingsCommands } from "./settings-commands" +import { SettingsMcp } from "./settings-mcp" + +export const DialogSettings: Component = () => { + const [search, setSearch] = createSignal("") + + return ( + <Dialog size="large"> + <Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog"> + <Tabs.List> + <div class="settings-dialog__search px-3 pb-3"> + <TextField placeholder="Search" value={search()} onChange={setSearch} variant="normal" /> + </div> + <Tabs.SectionTitle>Desktop</Tabs.SectionTitle> + <Tabs.Trigger value="general"> + <Icon name="settings-gear" /> + General + </Tabs.Trigger> + <Tabs.Trigger value="shortcuts"> + <Icon name="console" /> + Shortcuts + </Tabs.Trigger> + <Tabs.SectionTitle>Server</Tabs.SectionTitle> + <Tabs.Trigger value="permissions"> + <Icon name="checklist" /> + Permissions + </Tabs.Trigger> + <Tabs.Trigger value="providers"> + <Icon name="server" /> + Providers + </Tabs.Trigger> + <Tabs.Trigger value="models"> + <Icon name="brain" /> + Models + </Tabs.Trigger> + <Tabs.Trigger value="agents"> + <Icon name="task" /> + Agents + </Tabs.Trigger> + <Tabs.Trigger value="commands"> + <Icon name="console" /> + Commands + </Tabs.Trigger> + <Tabs.Trigger value="mcp"> + <Icon name="mcp" /> + MCP + </Tabs.Trigger> + </Tabs.List> + <Tabs.Content value="general" class="no-scrollbar"> + <SettingsGeneral /> + </Tabs.Content> + <Tabs.Content value="shortcuts" class="no-scrollbar"> + <SettingsKeybinds /> + </Tabs.Content> + <Tabs.Content value="permissions" class="no-scrollbar"> + <SettingsPermissions /> + </Tabs.Content> + <Tabs.Content value="providers" class="no-scrollbar"> + <SettingsProviders /> + </Tabs.Content> + <Tabs.Content value="models" class="no-scrollbar"> + <SettingsModels /> + </Tabs.Content> + <Tabs.Content value="agents" class="no-scrollbar"> + <SettingsAgents /> + </Tabs.Content> + <Tabs.Content value="commands" class="no-scrollbar"> + <SettingsCommands /> + </Tabs.Content> + <Tabs.Content value="mcp" class="no-scrollbar"> + <SettingsMcp /> + </Tabs.Content> + </Tabs> + </Dialog> + ) +} diff --git a/packages/app/src/components/settings-agents.tsx b/packages/app/src/components/settings-agents.tsx new file mode 100644 index 000000000..892be152b --- /dev/null +++ b/packages/app/src/components/settings-agents.tsx @@ -0,0 +1,12 @@ +import { Component } from "solid-js" + +export const SettingsAgents: Component = () => { + return ( + <div class="flex flex-col h-full overflow-y-auto"> + <div class="flex flex-col gap-6 p-6 max-w-[600px]"> + <h2 class="text-16-medium text-text-strong">Agents</h2> + <p class="text-14-regular text-text-weak">Agent settings will be configurable here.</p> + </div> + </div> + ) +} diff --git a/packages/app/src/components/settings-commands.tsx b/packages/app/src/components/settings-commands.tsx new file mode 100644 index 000000000..e98c0eeb0 --- /dev/null +++ b/packages/app/src/components/settings-commands.tsx @@ -0,0 +1,12 @@ +import { Component } from "solid-js" + +export const SettingsCommands: Component = () => { + return ( + <div class="flex flex-col h-full overflow-y-auto"> + <div class="flex flex-col gap-6 p-6 max-w-[600px]"> + <h2 class="text-16-medium text-text-strong">Commands</h2> + <p class="text-14-regular text-text-weak">Command settings will be configurable here.</p> + </div> + </div> + ) +} diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx new file mode 100644 index 000000000..e9965b0fa --- /dev/null +++ b/packages/app/src/components/settings-general.tsx @@ -0,0 +1,134 @@ +import { Component, createMemo, type JSX } from "solid-js" +import { Select } from "@opencode-ai/ui/select" +import { Switch } from "@opencode-ai/ui/switch" +import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" +import { useSettings } from "@/context/settings" + +export const SettingsGeneral: Component = () => { + const theme = useTheme() + const settings = useSettings() + + const themeOptions = createMemo(() => + Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })), + ) + + const colorSchemeOptions: { value: ColorScheme; label: string }[] = [ + { value: "system", label: "System setting" }, + { value: "light", label: "Light" }, + { value: "dark", label: "Dark" }, + ] + + const fontOptions = [ + { value: "ibm-plex-mono", label: "IBM Plex Mono" }, + { value: "fira-code", label: "Fira Code" }, + { value: "jetbrains-mono", label: "JetBrains Mono" }, + { value: "source-code-pro", label: "Source Code Pro" }, + ] + + return ( + <div class="flex flex-col h-full overflow-y-auto no-scrollbar"> + <div class="flex flex-col gap-8 p-8 max-w-[720px]"> + {/* Header */} + <h2 class="text-16-medium text-text-strong">General</h2> + + {/* Appearance Section */} + <div class="flex flex-col gap-1"> + <h3 class="text-14-medium text-text-strong pb-2">Appearance</h3> + + <SettingsRow title="Appearance" description="Customise how OpenCode looks on your device"> + <Select + options={colorSchemeOptions} + current={colorSchemeOptions.find((o) => o.value === theme.colorScheme())} + value={(o) => o.value} + label={(o) => o.label} + onSelect={(option) => option && theme.setColorScheme(option.value)} + variant="secondary" + size="small" + /> + </SettingsRow> + + <SettingsRow + title="Theme" + description={ + <> + Customise how OpenCode is themed.{" "} + <a href="#" class="text-text-interactive-base"> + Learn more + </a> + </> + } + > + <Select + options={themeOptions()} + current={themeOptions().find((o) => o.id === theme.themeId())} + value={(o) => o.id} + label={(o) => o.name} + onSelect={(option) => option && theme.setTheme(option.id)} + variant="secondary" + size="small" + /> + </SettingsRow> + + <SettingsRow title="Font" description="Customise the mono font used in code blocks"> + <Select + options={fontOptions} + current={fontOptions.find((o) => o.value === settings.appearance.font())} + value={(o) => o.value} + label={(o) => o.label} + onSelect={(option) => option && settings.appearance.setFont(option.value)} + variant="secondary" + size="small" + /> + </SettingsRow> + </div> + + {/* System notifications Section */} + <div class="flex flex-col gap-1"> + <h3 class="text-14-medium text-text-strong pb-2">System notifications</h3> + + <SettingsRow + title="Agent" + description="Show system notification when the agent is complete or needs attention" + > + <Switch + checked={settings.notifications.agent()} + onChange={(checked) => settings.notifications.setAgent(checked)} + /> + </SettingsRow> + + <SettingsRow title="Permissions" description="Show system notification when a permission is required"> + <Switch + checked={settings.notifications.permissions()} + onChange={(checked) => settings.notifications.setPermissions(checked)} + /> + </SettingsRow> + + <SettingsRow title="Errors" description="Show system notification when an error occurs"> + <Switch + checked={settings.notifications.errors()} + onChange={(checked) => settings.notifications.setErrors(checked)} + /> + </SettingsRow> + </div> + </div> + </div> + ) +} + +interface SettingsRowProps { + title: string + description: string | JSX.Element + children: JSX.Element +} + +const SettingsRow: Component<SettingsRowProps> = (props) => { + return ( + <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none"> + <div class="flex flex-col gap-0.5"> + <span class="text-14-medium text-text-strong">{props.title}</span> + <span class="text-12-regular text-text-weak">{props.description}</span> + </div> + <div class="flex-shrink-0">{props.children}</div> + </div> + ) +} diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx new file mode 100644 index 000000000..3688559bc --- /dev/null +++ b/packages/app/src/components/settings-keybinds.tsx @@ -0,0 +1,12 @@ +import { Component } from "solid-js" + +export const SettingsKeybinds: Component = () => { + return ( + <div class="flex flex-col h-full overflow-y-auto"> + <div class="flex flex-col gap-6 p-6 max-w-[600px]"> + <h2 class="text-16-medium text-text-strong">Shortcuts</h2> + <p class="text-14-regular text-text-weak">Keyboard shortcuts will be configurable here.</p> + </div> + </div> + ) +} diff --git a/packages/app/src/components/settings-mcp.tsx b/packages/app/src/components/settings-mcp.tsx new file mode 100644 index 000000000..ea6bf350f --- /dev/null +++ b/packages/app/src/components/settings-mcp.tsx @@ -0,0 +1,12 @@ +import { Component } from "solid-js" + +export const SettingsMcp: Component = () => { + return ( + <div class="flex flex-col h-full overflow-y-auto"> + <div class="flex flex-col gap-6 p-6 max-w-[600px]"> + <h2 class="text-16-medium text-text-strong">MCP</h2> + <p class="text-14-regular text-text-weak">MCP settings will be configurable here.</p> + </div> + </div> + ) +} diff --git a/packages/app/src/components/settings-models.tsx b/packages/app/src/components/settings-models.tsx new file mode 100644 index 000000000..5fbeb144e --- /dev/null +++ b/packages/app/src/components/settings-models.tsx @@ -0,0 +1,12 @@ +import { Component } from "solid-js" + +export const SettingsModels: Component = () => { + return ( + <div class="flex flex-col h-full overflow-y-auto"> + <div class="flex flex-col gap-6 p-6 max-w-[600px]"> + <h2 class="text-16-medium text-text-strong">Models</h2> + <p class="text-14-regular text-text-weak">Model settings will be configurable here.</p> + </div> + </div> + ) +} diff --git a/packages/app/src/components/settings-permissions.tsx b/packages/app/src/components/settings-permissions.tsx new file mode 100644 index 000000000..67c3bfb62 --- /dev/null +++ b/packages/app/src/components/settings-permissions.tsx @@ -0,0 +1,12 @@ +import { Component } from "solid-js" + +export const SettingsPermissions: Component = () => { + return ( + <div class="flex flex-col h-full overflow-y-auto"> + <div class="flex flex-col gap-6 p-6 max-w-[600px]"> + <h2 class="text-16-medium text-text-strong">Permissions</h2> + <p class="text-14-regular text-text-weak">Permission settings will be configurable here.</p> + </div> + </div> + ) +} diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx new file mode 100644 index 000000000..cf90b6c13 --- /dev/null +++ b/packages/app/src/components/settings-providers.tsx @@ -0,0 +1,12 @@ +import { Component } from "solid-js" + +export const SettingsProviders: Component = () => { + return ( + <div class="flex flex-col h-full overflow-y-auto"> + <div class="flex flex-col gap-6 p-6 max-w-[600px]"> + <h2 class="text-16-medium text-text-strong">Providers</h2> + <p class="text-14-regular text-text-weak">Provider settings will be configurable here.</p> + </div> + </div> + ) +} diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx new file mode 100644 index 000000000..6aca57ae2 --- /dev/null +++ b/packages/app/src/context/settings.tsx @@ -0,0 +1,103 @@ +import { createStore } from "solid-js/store" +import { createMemo } from "solid-js" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { persisted } from "@/utils/persist" + +export interface NotificationSettings { + agent: boolean + permissions: boolean + errors: boolean +} + +export interface Settings { + general: { + autoSave: boolean + } + appearance: { + fontSize: number + font: string + } + keybinds: Record<string, string> + permissions: { + autoApprove: boolean + } + notifications: NotificationSettings +} + +const defaultSettings: Settings = { + general: { + autoSave: true, + }, + appearance: { + fontSize: 14, + font: "ibm-plex-mono", + }, + keybinds: {}, + permissions: { + autoApprove: false, + }, + notifications: { + agent: false, + permissions: false, + errors: false, + }, +} + +export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({ + name: "Settings", + init: () => { + const [store, setStore, _, ready] = persisted("settings.v1", createStore<Settings>(defaultSettings)) + + return { + ready, + get current() { + return store + }, + general: { + autoSave: createMemo(() => store.general?.autoSave ?? defaultSettings.general.autoSave), + setAutoSave(value: boolean) { + setStore("general", "autoSave", value) + }, + }, + appearance: { + fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize), + setFontSize(value: number) { + setStore("appearance", "fontSize", value) + }, + font: createMemo(() => store.appearance?.font ?? defaultSettings.appearance.font), + setFont(value: string) { + setStore("appearance", "font", value) + }, + }, + keybinds: { + get: (action: string) => store.keybinds?.[action], + set(action: string, keybind: string) { + setStore("keybinds", action, keybind) + }, + reset(action: string) { + setStore("keybinds", action, undefined!) + }, + }, + permissions: { + autoApprove: createMemo(() => store.permissions?.autoApprove ?? defaultSettings.permissions.autoApprove), + setAutoApprove(value: boolean) { + setStore("permissions", "autoApprove", value) + }, + }, + notifications: { + agent: createMemo(() => store.notifications?.agent ?? defaultSettings.notifications.agent), + setAgent(value: boolean) { + setStore("notifications", "agent", value) + }, + permissions: createMemo(() => store.notifications?.permissions ?? defaultSettings.notifications.permissions), + setPermissions(value: boolean) { + setStore("notifications", "permissions", value) + }, + errors: createMemo(() => store.notifications?.errors ?? defaultSettings.notifications.errors), + setErrors(value: boolean) { + setStore("notifications", "errors", value) + }, + }, + } + }, +}) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 5f5954c90..8c04f10db 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -59,6 +59,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" import { DialogSelectProvider } from "@/components/dialog-select-provider" import { DialogSelectServer } from "@/components/dialog-select-server" +import { DialogSettings } from "@/components/dialog-settings" import { useCommand, type CommandOption } from "@/context/command" import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { navStart } from "@/utils/perf" @@ -880,6 +881,10 @@ export default function Layout(props: ParentProps) { dialog.show(() => <DialogSelectServer />) } + function openSettings() { + dialog.show(() => <DialogSettings />) + } + function navigateToProject(directory: string | undefined) { if (!directory) return server.projects.touch(directory) |
