wip(app): settings
This commit is contained in:
@@ -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>
|
||||
|
||||
87
packages/app/src/components/dialog-settings.tsx
Normal file
87
packages/app/src/components/dialog-settings.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
12
packages/app/src/components/settings-agents.tsx
Normal file
12
packages/app/src/components/settings-agents.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
12
packages/app/src/components/settings-commands.tsx
Normal file
12
packages/app/src/components/settings-commands.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
134
packages/app/src/components/settings-general.tsx
Normal file
134
packages/app/src/components/settings-general.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
12
packages/app/src/components/settings-keybinds.tsx
Normal file
12
packages/app/src/components/settings-keybinds.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
12
packages/app/src/components/settings-mcp.tsx
Normal file
12
packages/app/src/components/settings-mcp.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
12
packages/app/src/components/settings-models.tsx
Normal file
12
packages/app/src/components/settings-models.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
12
packages/app/src/components/settings-permissions.tsx
Normal file
12
packages/app/src/components/settings-permissions.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
12
packages/app/src/components/settings-providers.tsx
Normal file
12
packages/app/src/components/settings-providers.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
103
packages/app/src/context/settings.tsx
Normal file
103
packages/app/src/context/settings.tsx
Normal file
@@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user