From 8bcbfd63960120efa3cb770f8e07de1bb57e93b0 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:21:00 -0600 Subject: [PATCH 01/95] wip(app): settings --- packages/app/src/app.tsx | 43 +++--- .../app/src/components/dialog-settings.tsx | 87 ++++++++++++ .../app/src/components/settings-agents.tsx | 12 ++ .../app/src/components/settings-commands.tsx | 12 ++ .../app/src/components/settings-general.tsx | 134 ++++++++++++++++++ .../app/src/components/settings-keybinds.tsx | 12 ++ packages/app/src/components/settings-mcp.tsx | 12 ++ .../app/src/components/settings-models.tsx | 12 ++ .../src/components/settings-permissions.tsx | 12 ++ .../app/src/components/settings-providers.tsx | 12 ++ packages/app/src/context/settings.tsx | 103 ++++++++++++++ packages/app/src/pages/layout.tsx | 5 + packages/ui/src/components/dialog.css | 16 ++- packages/ui/src/components/dialog.tsx | 4 +- packages/ui/src/components/tabs.css | 122 +++++++++++++--- packages/ui/src/components/tabs.tsx | 9 +- 16 files changed, 563 insertions(+), 44 deletions(-) create mode 100644 packages/app/src/components/dialog-settings.tsx create mode 100644 packages/app/src/components/settings-agents.tsx create mode 100644 packages/app/src/components/settings-commands.tsx create mode 100644 packages/app/src/components/settings-general.tsx create mode 100644 packages/app/src/components/settings-keybinds.tsx create mode 100644 packages/app/src/components/settings-mcp.tsx create mode 100644 packages/app/src/components/settings-models.tsx create mode 100644 packages/app/src/components/settings-permissions.tsx create mode 100644 packages/app/src/components/settings-providers.tsx create mode 100644 packages/app/src/context/settings.tsx diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index d03d10d0ea7..33a5556effa 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 }) { ( - - - - - {props.children} - - - - + + + + + + {props.children} + + + + + )} > } /> ( - - - - }> - - - - - + component={(p) => ( + + + + + }> + + + + + + )} /> diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx new file mode 100644 index 00000000000..872cc4c801e --- /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 ( + + + + + Desktop + + + General + + + + Shortcuts + + Server + + + Permissions + + + + Providers + + + + Models + + + + Agents + + + + Commands + + + + MCP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/app/src/components/settings-agents.tsx b/packages/app/src/components/settings-agents.tsx new file mode 100644 index 00000000000..892be152b32 --- /dev/null +++ b/packages/app/src/components/settings-agents.tsx @@ -0,0 +1,12 @@ +import { Component } from "solid-js" + +export const SettingsAgents: Component = () => { + return ( +
+
+

Agents

+

Agent settings will be configurable here.

+
+
+ ) +} diff --git a/packages/app/src/components/settings-commands.tsx b/packages/app/src/components/settings-commands.tsx new file mode 100644 index 00000000000..e98c0eeb032 --- /dev/null +++ b/packages/app/src/components/settings-commands.tsx @@ -0,0 +1,12 @@ +import { Component } from "solid-js" + +export const SettingsCommands: Component = () => { + return ( +
+
+

Commands

+

Command settings will be configurable here.

+
+
+ ) +} diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx new file mode 100644 index 00000000000..e9965b0fa0d --- /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 ( +
+
+ {/* Header */} +

General

+ + {/* Appearance Section */} +
+

Appearance

+ + + o.id === theme.themeId())} + value={(o) => o.id} + label={(o) => o.name} + onSelect={(option) => option && theme.setTheme(option.id)} + variant="secondary" + size="small" + /> + + + + o.id === settings.sounds.agent())} + value={(o) => o.id} + label={(o) => o.label} + onSelect={(option) => { + if (!option) return + settings.sounds.setAgent(option.id) + playSound(option.src) + }} + variant="secondary" + size="small" + /> + + + + o.id === settings.sounds.errors())} + value={(o) => o.id} + label={(o) => o.label} + onSelect={(option) => { + if (!option) return + settings.sounds.setErrors(option.id) + playSound(option.src) + }} + variant="secondary" + size="small" + /> + +
) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 8001e2caadc..f19366b8ab9 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -1,6 +1,7 @@ import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web" import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js" import { useSDK } from "@/context/sdk" +import { monoFontFamily, useSettings } from "@/context/settings" import { SerializeAddon } from "@/addons/serialize" import { LocalPTY } from "@/context/terminal" import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme" @@ -36,6 +37,7 @@ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = { export const Terminal = (props: TerminalProps) => { const sdk = useSDK() + const settings = useSettings() const theme = useTheme() let container!: HTMLDivElement const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"]) @@ -82,6 +84,14 @@ export const Terminal = (props: TerminalProps) => { setOption("theme", colors) }) + createEffect(() => { + const font = monoFontFamily(settings.appearance.font()) + if (!term) return + const setOption = (term as unknown as { setOption?: (key: string, value: string) => void }).setOption + if (!setOption) return + setOption("fontFamily", font) + }) + const focusTerminal = () => { const t = term if (!t) return @@ -112,7 +122,7 @@ export const Terminal = (props: TerminalProps) => { cursorBlink: true, cursorStyle: "bar", fontSize: 14, - fontFamily: "IBM Plex Mono, monospace", + fontFamily: monoFontFamily(settings.appearance.font()), allowTransparency: true, theme: terminalColors(), scrollback: 10_000, diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index 16b3d306c2d..8b108851949 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -4,13 +4,12 @@ import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSDK } from "./global-sdk" import { useGlobalSync } from "./global-sync" import { usePlatform } from "@/context/platform" +import { useSettings } from "@/context/settings" import { Binary } from "@opencode-ai/util/binary" import { base64Encode } from "@opencode-ai/util/encode" import { EventSessionError } from "@opencode-ai/sdk/v2" -import { makeAudioPlayer } from "@solid-primitives/audio" -import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac" -import errorSound from "@opencode-ai/ui/audio/nope-03.aac" import { Persist, persisted } from "@/utils/persist" +import { playSound, soundSrc } from "@/utils/sound" type NotificationBase = { directory?: string @@ -44,19 +43,10 @@ function pruneNotifications(list: Notification[]) { export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({ name: "Notification", init: () => { - let idlePlayer: ReturnType | undefined - let errorPlayer: ReturnType | undefined - - try { - idlePlayer = makeAudioPlayer(idleSound) - errorPlayer = makeAudioPlayer(errorSound) - } catch (err) { - console.log("Failed to load audio", err) - } - const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() const platform = usePlatform() + const settings = useSettings() const [store, setStore, _, ready] = persisted( Persist.global("notification", ["notification.v1"]), @@ -93,16 +83,20 @@ export const { use: useNotification, provider: NotificationProvider } = createSi const match = Binary.search(syncStore.session, sessionID, (s) => s.id) const session = match.found ? syncStore.session[match.index] : undefined if (session?.parentID) break - try { - idlePlayer?.play() - } catch {} + + playSound(soundSrc(settings.sounds.agent())) + append({ ...base, type: "turn-complete", session: sessionID, }) + const href = `/${base64Encode(directory)}/session/${sessionID}` - void platform.notify("Response ready", session?.title ?? sessionID, href) + if (settings.notifications.agent()) { + void platform.notify("Response ready", session?.title ?? sessionID, href) + } + break } case "session.error": { @@ -111,9 +105,9 @@ export const { use: useNotification, provider: NotificationProvider } = createSi const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined const session = sessionID && match?.found ? syncStore.session[match.index] : undefined if (session?.parentID) break - try { - errorPlayer?.play() - } catch {} + + playSound(soundSrc(settings.sounds.errors())) + const error = "error" in event.properties ? event.properties.error : undefined append({ ...base, @@ -121,9 +115,13 @@ export const { use: useNotification, provider: NotificationProvider } = createSi session: sessionID ?? "global", error, }) + const description = session?.title ?? (typeof error === "string" ? error : "An error occurred") const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}` - void platform.notify("Session error", description, href) + if (settings.notifications.errors()) { + void platform.notify("Session error", description, href) + } + break } } diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index 6aca57ae2c9..4160d1b70a1 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -1,5 +1,5 @@ import { createStore } from "solid-js/store" -import { createMemo } from "solid-js" +import { createEffect, createMemo } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { persisted } from "@/utils/persist" @@ -9,6 +9,12 @@ export interface NotificationSettings { errors: boolean } +export interface SoundSettings { + agent: string + permissions: string + errors: string +} + export interface Settings { general: { autoSave: boolean @@ -22,6 +28,7 @@ export interface Settings { autoApprove: boolean } notifications: NotificationSettings + sounds: SoundSettings } const defaultSettings: Settings = { @@ -37,16 +44,47 @@ const defaultSettings: Settings = { autoApprove: false, }, notifications: { - agent: false, - permissions: false, + agent: true, + permissions: true, errors: false, }, + sounds: { + agent: "staplebops-01", + permissions: "staplebops-02", + errors: "nope-03", + }, +} + +const monoFallback = + 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' + +const monoFonts: Record = { + "ibm-plex-mono": `"IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "cascadia-code": `"Cascadia Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "fira-code": `"Fira Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + hack: `"Hack Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + inconsolata: `"Inconsolata Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "intel-one-mono": `"Intel One Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "jetbrains-mono": `"JetBrains Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "meslo-lgs": `"Meslo LGS Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "roboto-mono": `"Roboto Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "source-code-pro": `"Source Code Pro Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, + "ubuntu-mono": `"Ubuntu Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`, +} + +export function monoFontFamily(font: string | undefined) { + return monoFonts[font ?? defaultSettings.appearance.font] ?? monoFonts[defaultSettings.appearance.font] } export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({ name: "Settings", init: () => { - const [store, setStore, _, ready] = persisted("settings.v1", createStore(defaultSettings)) + const [store, setStore, _, ready] = persisted("settings.v3", createStore(defaultSettings)) + + createEffect(() => { + if (typeof document === "undefined") return + document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font)) + }) return { ready, @@ -98,6 +136,20 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setStore("notifications", "errors", value) }, }, + sounds: { + agent: createMemo(() => store.sounds?.agent ?? defaultSettings.sounds.agent), + setAgent(value: string) { + setStore("sounds", "agent", value) + }, + permissions: createMemo(() => store.sounds?.permissions ?? defaultSettings.sounds.permissions), + setPermissions(value: string) { + setStore("sounds", "permissions", value) + }, + errors: createMemo(() => store.sounds?.errors ?? defaultSettings.sounds.errors), + setErrors(value: string) { + setStore("sounds", "errors", value) + }, + }, } }, }) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 8c04f10dbac..f4e202b6ea7 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -37,6 +37,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { getFilename } from "@opencode-ai/util/path" import { Session, type Message, type TextPart } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" +import { useSettings } from "@/context/settings" import { createStore, produce, reconcile } from "solid-js/store" import { DragDropProvider, @@ -54,6 +55,7 @@ import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" import { Binary } from "@opencode-ai/util/binary" import { retry } from "@opencode-ai/util/retry" +import { playSound, soundSrc } from "@/utils/sound" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" @@ -98,6 +100,7 @@ export default function Layout(props: ParentProps) { const layout = useLayout() const layoutReady = createMemo(() => layout.ready()) const platform = usePlatform() + const settings = useSettings() const server = useServer() const notification = useNotification() const permission = usePermission() @@ -329,7 +332,18 @@ export default function Layout(props: ParentProps) { if (now - lastAlerted < cooldownMs) return alertedAtBySession.set(sessionKey, now) - void platform.notify(config.title, description, href) + if (e.details.type === "permission.asked") { + playSound(soundSrc(settings.sounds.permissions())) + if (settings.notifications.permissions()) { + void platform.notify(config.title, description, href) + } + } + + if (e.details.type === "question.asked") { + if (settings.notifications.agent()) { + void platform.notify(config.title, description, href) + } + } const currentDir = params.dir ? base64Decode(params.dir) : undefined const currentSession = params.id diff --git a/packages/app/src/utils/sound.ts b/packages/app/src/utils/sound.ts new file mode 100644 index 00000000000..e8db0bf7b9a --- /dev/null +++ b/packages/app/src/utils/sound.ts @@ -0,0 +1,44 @@ +import nope01 from "@opencode-ai/ui/audio/nope-01.aac" +import nope02 from "@opencode-ai/ui/audio/nope-02.aac" +import nope03 from "@opencode-ai/ui/audio/nope-03.aac" +import nope04 from "@opencode-ai/ui/audio/nope-04.aac" +import nope05 from "@opencode-ai/ui/audio/nope-05.aac" +import staplebops01 from "@opencode-ai/ui/audio/staplebops-01.aac" +import staplebops02 from "@opencode-ai/ui/audio/staplebops-02.aac" +import staplebops03 from "@opencode-ai/ui/audio/staplebops-03.aac" +import staplebops04 from "@opencode-ai/ui/audio/staplebops-04.aac" +import staplebops05 from "@opencode-ai/ui/audio/staplebops-05.aac" +import staplebops06 from "@opencode-ai/ui/audio/staplebops-06.aac" +import staplebops07 from "@opencode-ai/ui/audio/staplebops-07.aac" + +export const SOUND_OPTIONS = [ + { id: "staplebops-01", label: "Boopy", src: staplebops01 }, + { id: "staplebops-02", label: "Beepy", src: staplebops02 }, + { id: "staplebops-03", label: "Staplebops 03", src: staplebops03 }, + { id: "staplebops-04", label: "Staplebops 04", src: staplebops04 }, + { id: "staplebops-05", label: "Staplebops 05", src: staplebops05 }, + { id: "staplebops-06", label: "Staplebops 06", src: staplebops06 }, + { id: "staplebops-07", label: "Staplebops 07", src: staplebops07 }, + { id: "nope-01", label: "Nope 01", src: nope01 }, + { id: "nope-02", label: "Nope 02", src: nope02 }, + { id: "nope-03", label: "Oopsie", src: nope03 }, + { id: "nope-04", label: "Nope 04", src: nope04 }, + { id: "nope-05", label: "Nope 05", src: nope05 }, +] as const + +export type SoundOption = (typeof SOUND_OPTIONS)[number] +export type SoundID = SoundOption["id"] + +const soundById = Object.fromEntries(SOUND_OPTIONS.map((s) => [s.id, s.src])) as Record + +export function soundSrc(id: string | undefined) { + if (!id) return + if (!(id in soundById)) return + return soundById[id as SoundID] +} + +export function playSound(src: string | undefined) { + if (typeof Audio === "undefined") return + if (!src) return + void new Audio(src).play().catch(() => undefined) +} From df094a10ff1f1a95f66abc6bdccfa69080480afa Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 7 Jan 2026 06:54:48 -0600 Subject: [PATCH 03/95] wip(app): settings --- .../app/src/components/settings-general.tsx | 10 +- .../app/src/components/settings-keybinds.tsx | 309 +++++++++++++++++- packages/app/src/context/command.tsx | 55 +++- packages/app/src/context/settings.tsx | 5 +- 4 files changed, 361 insertions(+), 18 deletions(-) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 52672d01f47..15dc98bfbed 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -37,10 +37,14 @@ export const SettingsGeneral: Component = () => { return (
-
- {/* Header */} -

General

+
+
+

General

+

Appearance, notifications, and sound preferences.

+
+
+
{/* Appearance Section */}

Appearance

diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index 3688559bcba..811b34f9b2a 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -1,11 +1,310 @@ -import { Component } from "solid-js" +import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js" +import { Button } from "@opencode-ai/ui/button" +import { showToast } from "@opencode-ai/ui/toast" +import { formatKeybind, parseKeybind, useCommand } from "@/context/command" +import { useSettings } from "@/context/settings" + +const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) +const PALETTE_ID = "command.palette" +const DEFAULT_PALETTE_KEYBIND = "mod+shift+p" + +type KeybindGroup = "General" | "Session" | "Navigation" | "Model and agent" | "Terminal" | "Prompt" + +type KeybindMeta = { + title: string + group: KeybindGroup +} + +const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"] + +function groupFor(id: string): KeybindGroup { + if (id === PALETTE_ID) return "General" + if (id.startsWith("terminal.")) return "Terminal" + if (id.startsWith("model.") || id.startsWith("agent.") || id.startsWith("mcp.")) return "Model and agent" + if (id.startsWith("file.")) return "Navigation" + if (id.startsWith("prompt.")) return "Prompt" + if ( + id.startsWith("session.") || + id.startsWith("message.") || + id.startsWith("permissions.") || + id.startsWith("steps.") || + id.startsWith("review.") + ) + return "Session" + + return "General" +} + +function isModifier(key: string) { + return key === "Shift" || key === "Control" || key === "Alt" || key === "Meta" +} + +function normalizeKey(key: string) { + if (key === ",") return "comma" + if (key === "+") return "plus" + if (key === " ") return "space" + return key.toLowerCase() +} + +function recordKeybind(event: KeyboardEvent) { + if (isModifier(event.key)) return + + const parts: string[] = [] + + const mod = IS_MAC ? event.metaKey : event.ctrlKey + if (mod) parts.push("mod") + + if (IS_MAC && event.ctrlKey) parts.push("ctrl") + if (!IS_MAC && event.metaKey) parts.push("meta") + if (event.altKey) parts.push("alt") + if (event.shiftKey) parts.push("shift") + + const key = normalizeKey(event.key) + if (!key) return + parts.push(key) + + return parts.join("+") +} + +function signatures(config: string | undefined) { + if (!config) return [] + const sigs: string[] = [] + + for (const kb of parseKeybind(config)) { + const parts: string[] = [] + if (kb.ctrl) parts.push("ctrl") + if (kb.alt) parts.push("alt") + if (kb.shift) parts.push("shift") + if (kb.meta) parts.push("meta") + if (kb.key) parts.push(kb.key) + if (parts.length === 0) continue + sigs.push(parts.join("+")) + } + + return sigs +} export const SettingsKeybinds: Component = () => { + const command = useCommand() + const settings = useSettings() + + const [active, setActive] = createSignal(null) + + const stop = () => { + if (!active()) return + setActive(null) + command.keybinds(true) + } + + const start = (id: string) => { + if (active() === id) { + stop() + return + } + + if (active()) stop() + + setActive(id) + command.keybinds(false) + } + + const hasOverrides = createMemo(() => { + const keybinds = settings.current.keybinds as Record | undefined + if (!keybinds) return false + return Object.values(keybinds).some((x) => typeof x === "string") + }) + + const resetAll = () => { + stop() + settings.keybinds.resetAll() + showToast({ title: "Shortcuts reset", description: "Keyboard shortcuts have been reset to defaults." }) + } + + const list = createMemo(() => { + const out = new Map() + out.set(PALETTE_ID, { title: "Command palette", group: "General" }) + + for (const opt of command.options) { + if (opt.id.startsWith("suggested.")) continue + + out.set(opt.id, { + title: opt.title, + group: groupFor(opt.id), + }) + } + + return out + }) + + const title = (id: string) => list().get(id)?.title ?? "" + + const grouped = createMemo(() => { + const map = list() + const out = new Map() + + for (const group of GROUPS) out.set(group, []) + + for (const [id, item] of map) { + const ids = out.get(item.group) + if (!ids) continue + ids.push(id) + } + + for (const group of GROUPS) { + const ids = out.get(group) + if (!ids) continue + + ids.sort((a, b) => { + const at = map.get(a)?.title ?? "" + const bt = map.get(b)?.title ?? "" + return at.localeCompare(bt) + }) + } + + return out + }) + + const used = createMemo(() => { + const map = new Map() + + const add = (key: string, value: { id: string; title: string }) => { + const list = map.get(key) + if (!list) { + map.set(key, [value]) + return + } + list.push(value) + } + + const palette = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND + for (const sig of signatures(palette)) { + add(sig, { id: PALETTE_ID, title: "Command palette" }) + } + + for (const opt of command.options) { + if (opt.id.startsWith("suggested.")) continue + if (!opt.keybind) continue + for (const sig of signatures(opt.keybind)) { + add(sig, { id: opt.id, title: opt.title }) + } + } + + return map + }) + + const setKeybind = (id: string, keybind: string) => { + settings.keybinds.set(id, keybind) + } + + onMount(() => { + const handle = (event: KeyboardEvent) => { + const id = active() + if (!id) return + + event.preventDefault() + event.stopPropagation() + event.stopImmediatePropagation() + + if (event.key === "Escape") { + stop() + return + } + + const clear = + (event.key === "Backspace" || event.key === "Delete") && + !event.ctrlKey && + !event.metaKey && + !event.altKey && + !event.shiftKey + if (clear) { + setKeybind(id, "none") + stop() + return + } + + const next = recordKeybind(event) + if (!next) return + + const map = used() + const conflicts = new Map() + + for (const sig of signatures(next)) { + const list = map.get(sig) ?? [] + for (const item of list) { + if (item.id === id) continue + conflicts.set(item.id, item.title) + } + } + + if (conflicts.size > 0) { + showToast({ + title: "Shortcut already in use", + description: `${formatKeybind(next)} is already assigned to ${[...conflicts.values()].join(", ")}.`, + }) + return + } + + setKeybind(id, next) + stop() + } + + document.addEventListener("keydown", handle, true) + onCleanup(() => { + document.removeEventListener("keydown", handle, true) + }) + }) + + onCleanup(() => { + if (active()) command.keybinds(true) + }) + return ( -
-
-

Shortcuts

-

Keyboard shortcuts will be configurable here.

+
+
+
+
+

Keyboard shortcuts

+

Click a shortcut to edit. Press Esc to cancel.

+
+ +
+
+ +
+ + {(group) => ( + 0}> +
+

{group}

+
+ + {(id) => ( +
+ {title(id)} + +
+ )} +
+
+
+
+ )} +
) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index d8dc13e2344..7986e7509a6 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -1,9 +1,26 @@ import { createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useSettings } from "@/context/settings" const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) +const PALETTE_ID = "command.palette" +const DEFAULT_PALETTE_KEYBIND = "mod+shift+p" +const SUGGESTED_PREFIX = "suggested." + +function actionId(id: string) { + if (!id.startsWith(SUGGESTED_PREFIX)) return id + return id.slice(SUGGESTED_PREFIX.length) +} + +function normalizeKey(key: string) { + if (key === ",") return "comma" + if (key === "+") return "plus" + if (key === " ") return "space" + return key.toLowerCase() +} + export type KeybindConfig = string export interface Keybind { @@ -73,7 +90,7 @@ export function parseKeybind(config: string): Keybind[] { } export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean { - const eventKey = event.key.toLowerCase() + const eventKey = normalizeKey(event.key) for (const kb of keybinds) { const keyMatch = kb.key === eventKey @@ -105,15 +122,18 @@ export function formatKeybind(config: string): string { if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta") if (kb.key) { - const arrows: Record = { + const keys: Record = { arrowup: "↑", arrowdown: "↓", arrowleft: "←", arrowright: "→", + comma: ",", + plus: "+", + space: "Space", } + const key = kb.key.toLowerCase() const displayKey = - arrows[kb.key.toLowerCase()] ?? - (kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1)) + keys[key] ?? (key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1)) parts.push(displayKey) } @@ -124,9 +144,17 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex name: "Command", init: () => { const dialog = useDialog() + const settings = useSettings() const [registrations, setRegistrations] = createSignal[]>([]) const [suspendCount, setSuspendCount] = createSignal(0) + const bind = (id: string, def: KeybindConfig | undefined) => { + const custom = settings.keybinds.get(actionId(id)) + const config = custom ?? def + if (!config || config === "none") return + return config + } + const options = createMemo(() => { const seen = new Set() const all: CommandOption[] = [] @@ -139,15 +167,20 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex } } - const suggested = all.filter((x) => x.suggested && !x.disabled) + const resolved = all.map((opt) => ({ + ...opt, + keybind: bind(opt.id, opt.keybind), + })) + + const suggested = resolved.filter((x) => x.suggested && !x.disabled) return [ ...suggested.map((x) => ({ ...x, - id: "suggested." + x.id, + id: SUGGESTED_PREFIX + x.id, category: "Suggested", })), - ...all, + ...resolved, ] }) @@ -169,7 +202,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const handleKeyDown = (event: KeyboardEvent) => { if (suspended() || dialog.active) return - const paletteKeybinds = parseKeybind("mod+shift+p") + const paletteKeybinds = parseKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND) if (matchKeybind(paletteKeybinds, event)) { event.preventDefault() showPalette() @@ -209,7 +242,11 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex run(id, source) }, keybind(id: string) { - const option = options().find((x) => x.id === id || x.id === "suggested." + id) + if (id === PALETTE_ID) { + return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND) + } + + const option = options().find((x) => x.id === id || x.id === SUGGESTED_PREFIX + id) if (!option?.keybind) return "" return formatKeybind(option.keybind) }, diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index 4160d1b70a1..b44b4e14372 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -1,4 +1,4 @@ -import { createStore } from "solid-js/store" +import { createStore, reconcile } from "solid-js/store" import { createEffect, createMemo } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { persisted } from "@/utils/persist" @@ -115,6 +115,9 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont reset(action: string) { setStore("keybinds", action, undefined!) }, + resetAll() { + setStore("keybinds", reconcile({})) + }, }, permissions: { autoApprove: createMemo(() => store.permissions?.autoApprove ?? defaultSettings.permissions.autoApprove), From 924fc9ed803d4dfa89faed65579a5a85cd7666c0 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:31:03 -0600 Subject: [PATCH 04/95] wip(app): settings --- packages/app/src/components/prompt-input.tsx | 1 - .../src/components/settings-permissions.tsx | 151 +++++++++++- packages/app/src/context/global-sync.tsx | 219 +++++++++++------- packages/opencode/src/config/config.ts | 108 ++++++++- packages/opencode/src/file/watcher.ts | 23 +- packages/opencode/src/server/event.ts | 7 + packages/opencode/src/server/server.ts | 5 - packages/sdk/js/src/v2/gen/sdk.gen.ts | 39 ++++ packages/sdk/js/src/v2/gen/types.gen.ts | 41 ++++ 9 files changed, 490 insertions(+), 104 deletions(-) create mode 100644 packages/opencode/src/server/event.ts diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 56bbdc8cb55..072ef0bdd05 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -255,7 +255,6 @@ export const PromptInput: Component = (props) => { createEffect(() => { params.id - editorRef.focus() if (params.id) return const interval = setInterval(() => { setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length) diff --git a/packages/app/src/components/settings-permissions.tsx b/packages/app/src/components/settings-permissions.tsx index 67c3bfb6224..f5ee7665017 100644 --- a/packages/app/src/components/settings-permissions.tsx +++ b/packages/app/src/components/settings-permissions.tsx @@ -1,12 +1,153 @@ -import { Component } from "solid-js" +import { Select } from "@opencode-ai/ui/select" +import { showToast } from "@opencode-ai/ui/toast" +import { Component, For, createMemo, type JSX } from "solid-js" +import { useGlobalSync } from "@/context/global-sync" + +type PermissionAction = "allow" | "ask" | "deny" + +type PermissionObject = Record +type PermissionValue = PermissionAction | PermissionObject | string[] | undefined +type PermissionMap = Record + +type PermissionItem = { + id: string + title: string + description: string +} + +const ACTIONS: Array<{ value: PermissionAction; label: string }> = [ + { value: "allow", label: "Allow" }, + { value: "ask", label: "Ask" }, + { value: "deny", label: "Deny" }, +] + +const ITEMS: PermissionItem[] = [ + { id: "read", title: "Read", description: "Reading a file (matches the file path)" }, + { id: "edit", title: "Edit", description: "Modify files, including edits, writes, patches, and multi-edits" }, + { id: "glob", title: "Glob", description: "Match files using glob patterns" }, + { id: "grep", title: "Grep", description: "Search file contents using regular expressions" }, + { id: "list", title: "List", description: "List files within a directory" }, + { id: "bash", title: "Bash", description: "Run shell commands" }, + { id: "task", title: "Task", description: "Launch sub-agents" }, + { id: "skill", title: "Skill", description: "Load a skill by name" }, + { id: "lsp", title: "LSP", description: "Run language server queries" }, + { id: "todoread", title: "Todo Read", description: "Read the todo list" }, + { id: "todowrite", title: "Todo Write", description: "Update the todo list" }, + { id: "webfetch", title: "Web Fetch", description: "Fetch content from a URL" }, + { id: "websearch", title: "Web Search", description: "Search the web" }, + { id: "codesearch", title: "Code Search", description: "Search code on the web" }, + { id: "external_directory", title: "External Directory", description: "Access files outside the project directory" }, + { id: "doom_loop", title: "Doom Loop", description: "Detect repeated tool calls with identical input" }, +] + +const VALID_ACTIONS = new Set(["allow", "ask", "deny"]) + +function toMap(value: unknown): PermissionMap { + if (value && typeof value === "object" && !Array.isArray(value)) return value as PermissionMap + + const action = getAction(value) + if (action) return { "*": action } + + return {} +} + +function getAction(value: unknown): PermissionAction | undefined { + if (typeof value === "string" && VALID_ACTIONS.has(value as PermissionAction)) return value as PermissionAction + return +} + +function getRuleDefault(value: unknown): PermissionAction | undefined { + const action = getAction(value) + if (action) return action + + if (!value || typeof value !== "object" || Array.isArray(value)) return + + return getAction((value as Record)["*"]) +} export const SettingsPermissions: Component = () => { + const globalSync = useGlobalSync() + + const permission = createMemo(() => { + return toMap(globalSync.data.config.permission) + }) + + const actionFor = (id: string): PermissionAction => { + const value = permission()[id] + const direct = getRuleDefault(value) + if (direct) return direct + + const wildcard = getRuleDefault(permission()["*"]) + if (wildcard) return wildcard + + return "allow" + } + + const setPermission = async (id: string, action: PermissionAction) => { + const before = globalSync.data.config.permission + const map = toMap(before) + const existing = map[id] + + const nextValue = + existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing, "*": action } : action + + globalSync.set("config", "permission", { ...map, [id]: nextValue }) + globalSync.updateConfig({ permission: { [id]: nextValue } }).catch((err: unknown) => { + globalSync.set("config", "permission", before) + const message = err instanceof Error ? err.message : String(err) + showToast({ title: "Failed to update permissions", description: message }) + }) + } + + return ( +
+
+
+

Permissions

+

Control what tools the server can use by default.

+
+
+ +
+
+

Appearance

+
+ + {(item) => ( + + o.value === theme.colorScheme())} - value={(o) => o.value} - label={(o) => o.label} - onSelect={(option) => option && theme.setColorScheme(option.value)} - variant="secondary" - size="small" - /> - - - - Customise how OpenCode is themed.{" "} - - Learn more - - - } - > - 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" - /> - +
+ + o.id === theme.themeId())} + value={(o) => o.id} + label={(o) => o.name} + onSelect={(option) => { + if (!option) return + theme.setTheme(option.id) + }} + onHighlight={(option) => { + if (!option) return + theme.previewTheme(option.id) + return () => theme.cancelPreview() + }} + variant="secondary" + size="small" + /> + + + + o.id === settings.sounds.agent())} - value={(o) => o.id} - label={(o) => o.label} - onHighlight={(option) => { - if (!option) return - playSound(option.src) - }} - onSelect={(option) => { - if (!option) return - settings.sounds.setAgent(option.id) - playSound(option.src) - }} - variant="secondary" - size="small" - /> - - - - o.id === settings.sounds.errors())} - value={(o) => o.id} - label={(o) => o.label} - onHighlight={(option) => { - if (!option) return - playSound(option.src) - }} - onSelect={(option) => { - if (!option) return - settings.sounds.setErrors(option.id) - playSound(option.src) - }} - variant="secondary" - size="small" - /> - +
+ + o.id === settings.sounds.permissions())} + value={(o) => o.id} + label={(o) => o.label} + onHighlight={(option) => { + if (!option) return + playSound(option.src) + }} + onSelect={(option) => { + if (!option) return + settings.sounds.setPermissions(option.id) + playSound(option.src) + }} + variant="secondary" + size="small" + /> + + + +
From 0ffc2c2b3968c18c0eaf1a97af21680304ccb612 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:12:36 +0000 Subject: [PATCH 52/95] increase select dropdown padding to 4px --- packages/ui/src/components/select.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index fa05063006b..d1f07fae137 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -51,7 +51,7 @@ overflow: hidden; border-radius: var(--radius-md); background-color: var(--surface-raised-stronger-non-alpha); - padding: 2px; + padding: 4px; box-shadow: var(--shadow-xs-border); z-index: 60; From af8d91117cba906e71d33e1b4e719da76046de90 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:14:11 +0000 Subject: [PATCH 53/95] update select item styling: 4px radius, default cursor, 8px 2px padding --- packages/ui/src/components/select.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index d1f07fae137..866a64385c6 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -80,9 +80,10 @@ position: relative; display: flex; align-items: center; - padding: 0 6px 0 6px; + padding: 2px 8px; gap: 12px; - border-radius: var(--radius-sm); + border-radius: 4px; + cursor: default; /* text-12-medium */ font-family: var(--font-family-sans); From 09a6107649c967bb05cbf8f4a0a76830406ce261 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:14:38 +0000 Subject: [PATCH 54/95] set select dropdown min-width to 180px --- packages/ui/src/components/select.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index 866a64385c6..0d270118fee 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -46,7 +46,7 @@ } [data-component="select-content"] { - min-width: 4rem; + min-width: 180px; max-width: 23rem; overflow: hidden; border-radius: var(--radius-md); From f3b0f312bf362fbd7c44366722410868e3b134b9 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:16:43 +0000 Subject: [PATCH 55/95] adjust select dropdown positioning and padding structure --- packages/ui/src/components/select.css | 3 ++- packages/ui/src/components/select.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index 0d270118fee..6e298f3c59f 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -51,7 +51,7 @@ overflow: hidden; border-radius: var(--radius-md); background-color: var(--surface-raised-stronger-non-alpha); - padding: 4px; + padding: 0; box-shadow: var(--shadow-xs-border); z-index: 60; @@ -66,6 +66,7 @@ overflow-x: hidden; display: flex; flex-direction: column; + padding: 4px; &:focus { outline: none; diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index 245a36d38f3..1a1de0bef20 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -79,7 +79,7 @@ export function Select(props: SelectProps & ButtonProps) { {...others} data-component="select" - placement="bottom-start" + placement="bottom-end" value={local.current} options={grouped()} optionValue={(x) => (local.value ? local.value(x) : (x as string))} From 9ffb7141e5de14d38ce4d9e2e427caac7437416b Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:17:17 +0000 Subject: [PATCH 56/95] set select dropdown border-radius to 8px --- packages/ui/src/components/select.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index 6e298f3c59f..a8fb7e9d166 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -49,7 +49,7 @@ min-width: 180px; max-width: 23rem; overflow: hidden; - border-radius: var(--radius-md); + border-radius: 8px; background-color: var(--surface-raised-stronger-non-alpha); padding: 0; box-shadow: var(--shadow-xs-border); From c57491ba48863195ed0df0124687f6d5d3463882 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:20:20 +0000 Subject: [PATCH 57/95] add triggerStyle prop to Select and use it for font selector --- packages/app/src/components/settings-general.tsx | 1 + packages/ui/src/components/select.tsx | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 2a8e1d72063..c4efdb696dd 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -100,6 +100,7 @@ export const SettingsGeneral: Component = () => { onSelect={(option) => option && settings.appearance.setFont(option.value)} variant="secondary" size="small" + triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()) }} > {(option) => {option?.label}} diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index 1a1de0bef20..9d6c45609e2 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -16,6 +16,7 @@ export type SelectProps = Omit>, "value" | " class?: ComponentProps<"div">["class"] classList?: ComponentProps<"div">["classList"] children?: (item: T | undefined) => JSX.Element + triggerStyle?: JSX.CSSProperties } export function Select(props: SelectProps & ButtonProps) { @@ -32,6 +33,7 @@ export function Select(props: SelectProps & ButtonProps) { "onHighlight", "onOpenChange", "children", + "triggerStyle", ]) const state = { @@ -127,6 +129,7 @@ export function Select(props: SelectProps & ButtonProps) { as={Button} size={props.size} variant={props.variant} + style={local.triggerStyle} classList={{ ...(local.classList ?? {}), [local.class ?? ""]: !!local.class, From bcb8d970f190d83aece1f03aaf011ba84e546827 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:22:24 +0000 Subject: [PATCH 58/95] add selector icon and use it for select dropdown trigger --- packages/ui/src/components/icon.tsx | 1 + packages/ui/src/components/select.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index ccb9f06dce0..817f7fd6a8c 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -68,6 +68,7 @@ const icons = { trash: ``, sliders: ``, keyboard: ``, + selector: ``, } export interface IconProps extends ComponentProps<"svg"> { diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index 9d6c45609e2..cdf7adfdb2d 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -144,7 +144,7 @@ export function Select(props: SelectProps & ButtonProps) { }} - + From a8113ee0df2976755560b1bc32c243dae8332fb3 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:29:32 +0000 Subject: [PATCH 59/95] update select trigger and dropdown styling --- packages/ui/src/components/select.css | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index a8fb7e9d166..ab0f62665c7 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -1,12 +1,17 @@ [data-component="select"] { [data-slot="select-select-trigger"] { - padding: 0 4px 0 8px; + padding: 6px 12px; box-shadow: none; + border-radius: 6px; + min-width: 160px; + height: 32px; + justify-content: flex-end; [data-slot="select-select-trigger-value"] { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + font-size: var(--font-size-base); } [data-slot="select-select-trigger-icon"] { width: 16px; @@ -19,16 +24,14 @@ transition: transform 0.1s ease-in-out; } + &:hover { + background-color: var(--input-base); + box-shadow: var(--shadow-xs-border-base); + } + &[data-expanded] { - &[data-variant="secondary"] { - background-color: var(--button-secondary-hover); - } - &[data-variant="ghost"] { - background-color: var(--surface-raised-base-active); - } - &[data-variant="primary"] { - background-color: var(--icon-strong-active); - } + background-color: var(--input-base); + box-shadow: var(--shadow-xs-border-base); } &:not([data-expanded]):focus { @@ -46,7 +49,7 @@ } [data-component="select-content"] { - min-width: 180px; + min-width: 160px; max-width: 23rem; overflow: hidden; border-radius: 8px; @@ -86,12 +89,12 @@ border-radius: 4px; cursor: default; - /* text-12-medium */ + /* text-14-regular */ font-family: var(--font-family-sans); - font-size: var(--font-size-small); + font-size: var(--font-size-base); font-style: normal; - font-weight: var(--font-weight-medium); - line-height: var(--line-height-large); /* 166.667% */ + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); letter-spacing: var(--letter-spacing-normal); color: var(--text-strong); From 36bbe809fae8ef7345b0dfffced31920d36a4b6d Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:31:02 +0000 Subject: [PATCH 60/95] add 4px gutter between select trigger and dropdown --- packages/ui/src/components/select.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index cdf7adfdb2d..9fb3a901642 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -82,6 +82,7 @@ export function Select(props: SelectProps & ButtonProps) { {...others} data-component="select" placement="bottom-end" + gutter={4} value={local.current} options={grouped()} optionValue={(x) => (local.value ? local.value(x) : (x as string))} From 2b95956132a9577082059d179f3a998caab10af7 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:34:19 +0000 Subject: [PATCH 61/95] add x-large dialog size and use it for settings modal --- packages/app/src/components/dialog-settings.tsx | 2 +- packages/ui/src/components/dialog.css | 5 +++++ packages/ui/src/components/dialog.tsx | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index 8963d09cc2e..5ef89b8bfc7 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -13,7 +13,7 @@ import { SettingsMcp } from "./settings-mcp" export const DialogSettings: Component = () => { return ( - +
["class"] classList?: ComponentProps<"div">["classList"] fit?: boolean From 0d9ce6ad7bc3093e78f945124fcdf2e8c18c3481 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:35:16 +0000 Subject: [PATCH 62/95] set settings sidebar width to 200px --- packages/ui/src/components/tabs.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index 1fea6308f05..7043ecd12cb 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -289,8 +289,8 @@ &[data-variant="settings"] { [data-slot="tabs-list"] { - width: 180px; - min-width: 180px; + width: 200px; + min-width: 200px; padding: 12px; gap: 0; background-color: var(--background-base); From 19ac6f19483cb7969ba5f62a63c7aeb1d5b13978 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:36:28 +0000 Subject: [PATCH 63/95] set hover background of active sidebar item to surface-raised-base --- packages/ui/src/components/tabs.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index 7043ecd12cb..19004ef3e7e 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -344,6 +344,10 @@ [data-component="icon"] { color: var(--icon-strong-base); } + + &:hover:not(:disabled) { + background-color: var(--surface-raised-base); + } } } From c2c2bb1fa975ca92d8edec58287ad5f7492c9037 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:37:21 +0000 Subject: [PATCH 64/95] add 4px left padding to sidebar section title --- packages/ui/src/components/tabs.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css index 19004ef3e7e..10f586b51b4 100644 --- a/packages/ui/src/components/tabs.css +++ b/packages/ui/src/components/tabs.css @@ -303,7 +303,7 @@ [data-slot="tabs-section-title"] { width: 100%; - padding: 0; + padding: 0 0 0 4px; font-family: var(--font-family-sans); font-size: var(--font-size-small); font-weight: var(--font-weight-medium); From f250a229c903bf350eb14de1677cb3524208309d Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:43:17 +0000 Subject: [PATCH 65/95] update select trigger icon styling and spacing --- packages/ui/src/components/select.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index ab0f62665c7..5ee8e80229b 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -1,11 +1,12 @@ [data-component="select"] { [data-slot="select-select-trigger"] { - padding: 6px 12px; + padding: 6px 6px 6px 12px; box-shadow: none; border-radius: 6px; min-width: 160px; height: 32px; justify-content: flex-end; + gap: 8px; [data-slot="select-select-trigger-value"] { overflow: hidden; @@ -15,12 +16,14 @@ } [data-slot="select-select-trigger-icon"] { width: 16px; - height: 16px; + height: 20px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--text-weak); + background-color: var(--surface-raised-base); + border-radius: 4px; transition: transform 0.1s ease-in-out; } From 26f66b5f5d28612daf033cc6b8ce7aaf111c5acd Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:46:03 +0000 Subject: [PATCH 66/95] update: color token --- packages/ui/src/theme/themes/oc-1.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/theme/themes/oc-1.json b/packages/ui/src/theme/themes/oc-1.json index 2c767385015..fe04b190113 100644 --- a/packages/ui/src/theme/themes/oc-1.json +++ b/packages/ui/src/theme/themes/oc-1.json @@ -30,11 +30,11 @@ "surface-inset-base-hover": "var(--smoke-light-alpha-3)", "surface-inset-strong": "#1f000017", "surface-inset-strong-hover": "#1f000017", - "surface-raised-base": "var(--smoke-light-alpha-1)", + "surface-raised-base": "var(--smoke-light-alpha-2)", "surface-float-base": "var(--smoke-dark-1)", "surface-float-base-hover": "var(--smoke-dark-2)", - "surface-raised-base-hover": "var(--smoke-light-alpha-2)", - "surface-raised-base-active": "var(--smoke-light-alpha-3)", + "surface-raised-base-hover": "var(--smoke-light-alpha-3)", + "surface-raised-base-active": "var(--smoke-light-alpha-4)", "surface-raised-strong": "var(--smoke-light-1)", "surface-raised-strong-hover": "var(--white)", "surface-raised-stronger": "var(--white)", From 715860f997b19cedda32cde73246be01d3f5b22b Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:47:30 +0000 Subject: [PATCH 67/95] set default select trigger background to transparent --- packages/ui/src/components/select.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index 5ee8e80229b..eea1b743dc6 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -7,6 +7,7 @@ height: 32px; justify-content: flex-end; gap: 8px; + background-color: transparent; [data-slot="select-select-trigger-value"] { overflow: hidden; From 0c270b474368a65fb6d869ff31d7b47930148a6e Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:50:19 +0000 Subject: [PATCH 68/95] reset select trigger to default state after selection --- packages/app/src/components/settings-general.tsx | 2 +- packages/ui/src/components/select.css | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index c4efdb696dd..f8ffb421d98 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -100,7 +100,7 @@ export const SettingsGeneral: Component = () => { onSelect={(option) => option && settings.appearance.setFont(option.value)} variant="secondary" size="small" - triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()) }} + triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }} > {(option) => {option?.label}} diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index eea1b743dc6..fc5cffc1355 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -39,15 +39,8 @@ } &:not([data-expanded]):focus { - &[data-variant="secondary"] { - background-color: var(--button-secondary-base); - } - &[data-variant="ghost"] { - background-color: transparent; - } - &[data-variant="primary"] { - background-color: var(--icon-strong-base); - } + background-color: transparent; + box-shadow: none; } } } From 1092cf40349746b943576e6dd3caac34a53bf50d Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:50:49 +0000 Subject: [PATCH 69/95] increase gap between label and icon in select trigger to 12px --- packages/ui/src/components/select.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index fc5cffc1355..0c8872d87f3 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -6,7 +6,7 @@ min-width: 160px; height: 32px; justify-content: flex-end; - gap: 8px; + gap: 12px; background-color: transparent; [data-slot="select-select-trigger-value"] { From 3eea1d424eff415a56401053a7c590232be1a595 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:51:39 +0000 Subject: [PATCH 70/95] set select trigger value font weight to regular --- packages/ui/src/components/select.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index 0c8872d87f3..d00f04ab759 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -14,6 +14,7 @@ text-overflow: ellipsis; white-space: nowrap; font-size: var(--font-size-base); + font-weight: var(--font-weight-regular); } [data-slot="select-select-trigger-icon"] { width: 16px; From 73b1bc42f485d7097cd5cbd8ec6cca933319b435 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 17:55:45 +0000 Subject: [PATCH 71/95] increase specificity of select trigger hover/expanded states --- packages/ui/src/components/select.css | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index d00f04ab759..c5ad67e33a2 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -29,12 +29,9 @@ transition: transform 0.1s ease-in-out; } - &:hover { - background-color: var(--input-base); - box-shadow: var(--shadow-xs-border-base); - } - - &[data-expanded] { + &[data-slot="select-select-trigger"]:hover:not(:disabled), + &[data-slot="select-select-trigger"][data-expanded], + &[data-slot="select-select-trigger"][data-expanded]:hover:not(:disabled) { background-color: var(--input-base); box-shadow: var(--shadow-xs-border-base); } From 39afc055bf777d8eeeee2fa747e1d29b90cfe567 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 18:02:13 +0000 Subject: [PATCH 72/95] add fade gradient to settings panel headers --- packages/app/src/components/settings-general.tsx | 8 +++++++- packages/app/src/components/settings-keybinds.tsx | 8 +++++++- packages/app/src/components/settings-permissions.tsx | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index f8ffb421d98..dc0652e60cd 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -37,7 +37,13 @@ export const SettingsGeneral: Component = () => { return (
-
+

General

diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index 208bd909e66..ca5df993d51 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -280,7 +280,13 @@ export const SettingsKeybinds: Component = () => { return (
-
+

Keyboard shortcuts

diff --git a/packages/app/src/components/settings-permissions.tsx b/packages/app/src/components/settings-permissions.tsx index b311da0ef10..aa83de01e05 100644 --- a/packages/app/src/components/settings-permissions.tsx +++ b/packages/app/src/components/settings-permissions.tsx @@ -101,7 +101,13 @@ export const SettingsPermissions: Component = () => { return (
-
+

Permissions

Control what tools the server can use by default.

From 7f4277695d4a8b2de750e68c7c1bc5e4b4e4d05c Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 18:02:49 +0000 Subject: [PATCH 73/95] set 32px spacing between main title and group title --- packages/app/src/components/settings-general.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index dc0652e60cd..5b7c59d8c72 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -49,7 +49,7 @@ export const SettingsGeneral: Component = () => {
-
+
{/* Appearance Section */}

Appearance

From 602b6be4d4376722b7c215326f05b82bc04bc1a8 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 18:05:21 +0000 Subject: [PATCH 74/95] update settings panel padding and make content full width --- packages/app/src/components/settings-general.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 5b7c59d8c72..29e8ae77aee 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -36,7 +36,7 @@ export const SettingsGeneral: Component = () => { const soundOptions = [...SOUND_OPTIONS] return ( -
+
{ "linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)", }} > -
+

General

-
+
{/* Appearance Section */}

Appearance

From dbc15d4816bd5f2300ff1c03bf7d3afdb119af56 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 18:06:58 +0000 Subject: [PATCH 75/95] add color scheme preview on hover in appearance dropdown --- packages/app/src/components/settings-general.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 29e8ae77aee..4e0fa765ff9 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -62,6 +62,11 @@ export const SettingsGeneral: Component = () => { value={(o) => o.value} label={(o) => o.label} onSelect={(option) => option && theme.setColorScheme(option.value)} + onHighlight={(option) => { + if (!option) return + theme.previewColorScheme(option.value) + return () => theme.cancelPreview() + }} variant="secondary" size="small" /> From 261b1eca2ed90ac7aaa7b9db19cb41e2be360313 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 18:14:25 +0000 Subject: [PATCH 76/95] update keyboard shortcuts panel to match general settings styling --- .../app/src/components/settings-keybinds.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index ca5df993d51..bac727cd7ad 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -279,7 +279,7 @@ export const SettingsKeybinds: Component = () => { }) return ( -
+
{ "linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)", }} > -
-
-

Keyboard shortcuts

-

Click a shortcut to edit. Press Esc to cancel.

-
+
+

Keyboard shortcuts

-
+
{(group) => ( 0}> -
-

{group}

-
+
+

{group}

+
{(id) => ( -
+
{title(id)}
}> Date: Tue, 20 Jan 2026 21:14:37 +0000 Subject: [PATCH 90/95] fix: remove close delay on hover cards to stop overlapping --- packages/app/src/pages/session.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 7d6f31dc366..e794b8b8f4d 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1275,7 +1275,7 @@ export default function Page() {
-
No changes in this session yet
+
No changes in this session yet
@@ -1547,7 +1547,7 @@ export default function Page() {
-
No changes in this session yet
+
No changes in this session yet
From 4350b8fd6b7c6c0499976fb42ae52e7685e8a2ae Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 21:22:32 +0000 Subject: [PATCH 91/95] fix: show View all sessions button for active project and close hovercard on click --- packages/app/src/pages/layout.tsx | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index ceaddeeb5ef..3fe3c75ecc8 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1841,6 +1841,7 @@ export default function Layout(props: ParentProps) { // @ts-ignore
- -
- -
-
+
+ +
From 575cc59b376bbd9420f6a9d94fa6d121914c535a Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 21:49:49 +0000 Subject: [PATCH 92/95] fix: increase sidebar icon size by removing 16px constraint --- packages/ui/src/components/icon-button.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/ui/src/components/icon-button.css b/packages/ui/src/components/icon-button.css index 374cd37923a..7d5d3280fce 100644 --- a/packages/ui/src/components/icon-button.css +++ b/packages/ui/src/components/icon-button.css @@ -125,11 +125,6 @@ /* padding: 0 8px 0 6px; */ gap: 8px; - [data-slot="icon-svg"] { - height: 16px; - width: 16px; - } - /* text-12-medium */ font-family: var(--font-family-sans); font-size: var(--font-size-small); From 7be6671e6ea797a38aae1a6673faf74768199d07 Mon Sep 17 00:00:00 2001 From: David Hill Date: Tue, 20 Jan 2026 21:52:50 +0000 Subject: [PATCH 93/95] refactor Select component to use settings variant for settings modal styling --- packages/app/src/components/prompt-input.tsx | 1 - .../app/src/components/settings-general.tsx | 6 + .../src/components/settings-permissions.tsx | 1 + packages/ui/src/components/select.css | 124 ++++++++++++------ packages/ui/src/components/select.tsx | 7 +- 5 files changed, 92 insertions(+), 47 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index e05b6646bd5..072ef0bdd05 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1583,7 +1583,6 @@ export const PromptInput: Component = (props) => { onSelect={local.agent.set} class="capitalize" variant="ghost" - triggerVariant="button" /> { }} variant="secondary" size="small" + triggerVariant="settings" /> @@ -99,6 +100,7 @@ export const SettingsGeneral: Component = () => { }} variant="secondary" size="small" + triggerVariant="settings" /> @@ -111,6 +113,7 @@ export const SettingsGeneral: Component = () => { onSelect={(option) => option && settings.appearance.setFont(option.value)} variant="secondary" size="small" + triggerVariant="settings" triggerStyle={{ "font-family": monoFontFamily(settings.appearance.font()), "min-width": "180px" }} > {(option) => {option?.label}} @@ -172,6 +175,7 @@ export const SettingsGeneral: Component = () => { }} variant="secondary" size="small" + triggerVariant="settings" /> @@ -192,6 +196,7 @@ export const SettingsGeneral: Component = () => { }} variant="secondary" size="small" + triggerVariant="settings" /> @@ -212,6 +217,7 @@ export const SettingsGeneral: Component = () => { }} variant="secondary" size="small" + triggerVariant="settings" />
diff --git a/packages/app/src/components/settings-permissions.tsx b/packages/app/src/components/settings-permissions.tsx index aa83de01e05..d0551d24752 100644 --- a/packages/app/src/components/settings-permissions.tsx +++ b/packages/app/src/components/settings-permissions.tsx @@ -129,6 +129,7 @@ export const SettingsPermissions: Component = () => { onSelect={(option) => option && setPermission(item.id, option.value)} variant="secondary" size="small" + triggerVariant="settings" /> )} diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css index 73e2b2a6d03..d9791b51e8f 100644 --- a/packages/ui/src/components/select.css +++ b/packages/ui/src/components/select.css @@ -1,70 +1,89 @@ [data-component="select"] { [data-slot="select-select-trigger"] { - padding: 6px 6px 6px 12px; + padding: 0 4px 0 8px; box-shadow: none; - border-radius: 6px; - min-width: 160px; - height: 32px; - justify-content: flex-end; - gap: 12px; - background-color: transparent; [data-slot="select-select-trigger-value"] { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: var(--font-size-base); - font-weight: var(--font-weight-regular); } [data-slot="select-select-trigger-icon"] { width: 16px; - height: 20px; + height: 16px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--text-weak); - background-color: var(--surface-raised-base); - border-radius: 4px; transition: transform 0.1s ease-in-out; } - &[data-slot="select-select-trigger"]:hover:not(:disabled), - &[data-slot="select-select-trigger"][data-expanded], - &[data-slot="select-select-trigger"][data-expanded]:hover:not(:disabled) { - background-color: var(--input-base); - box-shadow: var(--shadow-xs-border-base); + &[data-expanded] { + &[data-variant="secondary"] { + background-color: var(--button-secondary-hover); + } + &[data-variant="ghost"] { + background-color: var(--surface-raised-base-active); + } + &[data-variant="primary"] { + background-color: var(--icon-strong-active); + } } &:not([data-expanded]):focus { - background-color: transparent; - box-shadow: none; + &[data-variant="secondary"] { + background-color: var(--button-secondary-base); + } + &[data-variant="ghost"] { + background-color: transparent; + } + &[data-variant="primary"] { + background-color: var(--icon-strong-base); + } } } - &[data-trigger-style="button"] { + &[data-trigger-style="settings"] { [data-slot="select-select-trigger"] { - padding: 0 6px; - min-width: auto; - height: 24px; - justify-content: center; - gap: 6px; + padding: 6px 6px 6px 12px; + box-shadow: none; + border-radius: 6px; + min-width: 160px; + height: 32px; + justify-content: flex-end; + gap: 12px; + background-color: transparent; - [data-slot="select-select-trigger-icon"] { - width: auto; - height: auto; - background-color: transparent; - border-radius: 0; + [data-slot="select-select-trigger-value"] { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: var(--font-size-base); + font-weight: var(--font-weight-regular); } - - &[data-slot="select-select-trigger"]:hover:not(:disabled) { - background-color: var(--surface-raised-base-hover); - box-shadow: none; + [data-slot="select-select-trigger-icon"] { + width: 16px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--text-weak); + background-color: var(--surface-raised-base); + border-radius: 4px; + transition: transform 0.1s ease-in-out; } + &[data-slot="select-select-trigger"]:hover:not(:disabled), &[data-slot="select-select-trigger"][data-expanded], &[data-slot="select-select-trigger"][data-expanded]:hover:not(:disabled) { - background-color: var(--surface-raised-base-active); + background-color: var(--input-base); + box-shadow: var(--shadow-xs-border-base); + } + + &:not([data-expanded]):focus { + background-color: transparent; box-shadow: none; } } @@ -72,12 +91,12 @@ } [data-component="select-content"] { - min-width: 160px; + min-width: 104px; max-width: 23rem; overflow: hidden; - border-radius: 8px; + border-radius: var(--radius-md); background-color: var(--surface-raised-stronger-non-alpha); - padding: 0; + padding: 4px; box-shadow: var(--shadow-xs-border); z-index: 60; @@ -92,7 +111,6 @@ overflow-x: hidden; display: flex; flex-direction: column; - padding: 4px; &:focus { outline: none; @@ -112,12 +130,12 @@ border-radius: 4px; cursor: default; - /* text-14-regular */ + /* text-12-medium */ font-family: var(--font-family-sans); - font-size: var(--font-size-base); + font-size: var(--font-size-small); font-style: normal; - font-weight: var(--font-weight-regular); - line-height: var(--line-height-large); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); /* 166.667% */ letter-spacing: var(--letter-spacing-normal); color: var(--text-strong); @@ -152,6 +170,26 @@ } } +[data-component="select-content"][data-trigger-style="settings"] { + min-width: 160px; + border-radius: 8px; + padding: 0; + + [data-slot="select-select-content-list"] { + padding: 4px; + } + + [data-slot="select-select-item"] { + /* text-14-regular */ + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-style: normal; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); + letter-spacing: var(--letter-spacing-normal); + } +} + @keyframes select-open { from { opacity: 0; diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index e334ede422b..b83f5f4630a 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -17,7 +17,7 @@ export type SelectProps = Omit>, "value" | " classList?: ComponentProps<"div">["classList"] children?: (item: T | undefined) => JSX.Element triggerStyle?: JSX.CSSProperties - triggerVariant?: "default" | "button" + triggerVariant?: "settings" } export function Select(props: SelectProps & Omit) { @@ -84,7 +84,7 @@ export function Select(props: SelectProps & Omit) {...others} data-component="select" data-trigger-style={local.triggerVariant} - placement="bottom-end" + placement={local.triggerVariant === "settings" ? "bottom-end" : "bottom-start"} gutter={4} value={local.current} options={grouped()} @@ -148,7 +148,7 @@ export function Select(props: SelectProps & Omit) }} - + @@ -158,6 +158,7 @@ export function Select(props: SelectProps & Omit) [local.class ?? ""]: !!local.class, }} data-component="select-content" + data-trigger-style={local.triggerVariant} > From 8137e4dd9ca85ccca18a68414d1fdd1c7e014714 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:32:26 -0600 Subject: [PATCH 94/95] chore: agents.md --- packages/app/AGENTS.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/app/AGENTS.md b/packages/app/AGENTS.md index 98b681bca8a..85e6a846641 100644 --- a/packages/app/AGENTS.md +++ b/packages/app/AGENTS.md @@ -1,7 +1,5 @@ ## Debugging -- To test the opencode app, use the playwright MCP server, the app is already - running at http://localhost:3000 - NEVER try to restart the app, or the server process, EVER. ## SolidJS @@ -11,3 +9,14 @@ ## Tool Calling - ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. + +## Browser Automation + +Use `agent-browser` for web automation. Run `agent-browser --help` for all commands. + +Core workflow: + +1. `agent-browser open ` - Navigate to page +2. `agent-browser snapshot -i` - Get interactive elements with refs (@e1, @e2) +3. `agent-browser click @e1` / `fill @e2 "text"` - Interact using refs +4. Re-snapshot after page changes From d2fcdef571464c64668062718f3dceec0e79fbeb Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 20 Jan 2026 22:04:12 +0000 Subject: [PATCH 95/95] release: v1.1.28 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 4 ++-- packages/sdk/js/package.json | 4 ++-- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/util/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bun.lock b/bun.lock index e7b9dc09cea..76eb6ef34f2 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -72,7 +72,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -106,7 +106,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -133,7 +133,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -157,7 +157,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -181,7 +181,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -210,7 +210,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -239,7 +239,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -255,7 +255,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.1.27", + "version": "1.1.28", "bin": { "opencode": "./bin/opencode", }, @@ -359,7 +359,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -380,7 +380,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.1.27", + "version": "1.1.28", "devDependencies": { "@hey-api/openapi-ts": "0.90.4", "@tsconfig/node22": "catalog:", @@ -391,7 +391,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -404,7 +404,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -445,7 +445,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "zod": "catalog:", }, @@ -456,7 +456,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 54098906009..dd4f8f3c0e5 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.1.27", + "version": "1.1.28", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 7638b54d2db..211eb8e1e74 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.1.27", + "version": "1.1.28", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index f5620a54f38..d2f48fc58f7 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.1.27", + "version": "1.1.28", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 0a79603fdc4..4e3e4e34916 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.1.27", + "version": "1.1.28", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index fd1f5702d5e..1f39b81387c 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.1.27", + "version": "1.1.28", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 70e2aeac9f2..b49175658e4 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.1.27", + "version": "1.1.28", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index c96acbadda5..f7f890119e7 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.1.27", + "version": "1.1.28", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index d7762cf1375..a3ec52e4600 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.1.27" +version = "1.1.28" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.28/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.28/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.28/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.28/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.27/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.28/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 7f581dcb5f2..58fe9593441 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.1.27", + "version": "1.1.28", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index d4b9731eb10..5b993a4d7c1 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.27", + "version": "1.1.28", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 3e8f84f0130..370e124e7c3 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.1.27", + "version": "1.1.28", "type": "module", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "typescript": "catalog:", "@typescript/native-preview": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 472c9c78a3d..6bfe22655df 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.1.27", + "version": "1.1.28", "type": "module", "license": "MIT", "scripts": { @@ -30,4 +30,4 @@ "publishConfig": { "directory": "dist" } -} +} \ No newline at end of file diff --git a/packages/slack/package.json b/packages/slack/package.json index c3ae3ea91cc..de50bea5bbb 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.1.27", + "version": "1.1.28", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index e940631468f..be9f8b923e2 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.1.27", + "version": "1.1.28", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 299244dd6aa..d4b11aa2bb4 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.1.27", + "version": "1.1.28", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 15f3bd42eb2..fc10bcaabd8 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.1.27", + "version": "1.1.28", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 7345aae1bbd..c9b2d9cdbaf 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.1.27", + "version": "1.1.28", "publisher": "sst-dev", "repository": { "type": "git",