Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/app/src/components/dialog-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SettingsGeneral } from "./settings-general"
import { SettingsKeybinds } from "./settings-keybinds"
import { SettingsProviders } from "./settings-providers"
import { SettingsModels } from "./settings-models"
import { SettingsOhMyOpenCode } from "./settings-ohmyopencode"

export const DialogSettings: Component = () => {
const language = useLanguage()
Expand Down Expand Up @@ -45,6 +46,10 @@ export const DialogSettings: Component = () => {
<Icon name="models" />
{language.t("settings.models.title")}
</Tabs.Trigger>
<Tabs.Trigger value="ohmyopencode">
<Icon name="sliders" />
{language.t("settings.ohmyopencode.title")}
</Tabs.Trigger>
</div>
</div>
</div>
Expand All @@ -67,6 +72,9 @@ export const DialogSettings: Component = () => {
<Tabs.Content value="models" class="no-scrollbar">
<SettingsModels />
</Tabs.Content>
<Tabs.Content value="ohmyopencode" class="no-scrollbar">
<SettingsOhMyOpenCode />
</Tabs.Content>
{/* <Tabs.Content value="agents" class="no-scrollbar"> */}
{/* <SettingsAgents /> */}
{/* </Tabs.Content> */}
Expand Down
169 changes: 169 additions & 0 deletions packages/app/src/components/settings-ohmyopencode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { Component, createMemo, type JSX } from "solid-js"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
import { useLanguage } from "@/context/language"
import { useGlobalSync } from "@/context/global-sync"

export const SettingsOhMyOpenCode: Component = () => {
const language = useLanguage()
const globalSync = useGlobalSync()

const config = createMemo(() => globalSync.data.config.oh_my_opencode ?? {})

const update = (patch: Record<string, unknown>) => {
void globalSync.updateConfig({
oh_my_opencode: { ...config(), ...patch },
})
}
Comment on lines +11 to +17
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI controls here are effectively read-only because their checked/current values are derived from globalSync.data.config, but update() doesn’t update the local store (unlike other settings screens that call globalSync.set(...) optimistically). With a controlled <Switch>/<Select>, the value will snap back until the global reload finishes. Update the local store first (and rollback on request failure) so toggles/selections reflect immediately.

Copilot uses AI. Check for mistakes.

const tmuxLayoutOptions = [
{ value: "main-vertical", label: "Main Vertical" },
{ value: "main-horizontal", label: "Main Horizontal" },
{ value: "tiled", label: "Tiled" },
{ value: "even-horizontal", label: "Even Horizontal" },
{ value: "even-vertical", label: "Even Vertical" },
] as const
const tmuxLayoutList = [...tmuxLayoutOptions]

const browserOptions = [
{ value: "playwright", label: "Playwright" },
{ value: "agent-browser", label: "Agent Browser" },
] as const
const browserList = [...browserOptions]

return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 pt-6 pb-8">
<h2 class="text-16-medium text-text-strong">{language.t("settings.ohmyopencode.title")}</h2>
<p class="text-12-regular text-text-weak">{language.t("settings.ohmyopencode.description")}</p>
</div>
</div>

<div class="flex flex-col gap-8 w-full">
{/* Features Section */}
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.ohmyopencode.section.features")}</h3>

<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsRow
title={language.t("settings.ohmyopencode.row.enabled.title")}
description={language.t("settings.ohmyopencode.row.enabled.description")}
>
<Switch checked={true} disabled={true} onChange={() => {}} />
</SettingsRow>

<SettingsRow
title={language.t("settings.ohmyopencode.row.ralphLoop.title")}
description={language.t("settings.ohmyopencode.row.ralphLoop.description")}
>
<Switch
checked={config().ralph_loop?.enabled ?? false}
onChange={(checked) =>
update({
ralph_loop: { ...config().ralph_loop, enabled: checked },
})
}
/>
</SettingsRow>

<SettingsRow
title={language.t("settings.ohmyopencode.row.autoUpdate.title")}
description={language.t("settings.ohmyopencode.row.autoUpdate.description")}
>
<Switch
checked={config().auto_update ?? true}
onChange={(checked) => update({ auto_update: checked })}
/>
</SettingsRow>

<SettingsRow
title={language.t("settings.ohmyopencode.row.browserEngine.title")}
description={language.t("settings.ohmyopencode.row.browserEngine.description")}
>
<Select
options={browserList}
current={browserList.find(
(o) => o.value === (config().browser_automation_engine?.provider ?? "playwright"),
)}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(option) =>
option &&
update({
browser_automation_engine: { provider: option.value },
})
}
variant="secondary"
size="small"
triggerVariant="settings"
/>
</SettingsRow>
</div>
</div>

{/* Tmux Section */}
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.ohmyopencode.section.tmux")}</h3>

<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsRow
title={language.t("settings.ohmyopencode.row.tmux.title")}
description={language.t("settings.ohmyopencode.row.tmux.description")}
>
<Switch
checked={config().tmux?.enabled ?? false}
onChange={(checked) =>
update({
tmux: { ...config().tmux, enabled: checked },
})
}
/>
</SettingsRow>

<SettingsRow
title={language.t("settings.ohmyopencode.row.tmuxLayout.title")}
description={language.t("settings.ohmyopencode.row.tmuxLayout.description")}
>
<Select
options={tmuxLayoutList}
current={tmuxLayoutList.find(
(o) => o.value === (config().tmux?.layout ?? "main-vertical"),
)}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(option) =>
option &&
update({
tmux: { ...config().tmux, layout: option.value },
})
}
variant="secondary"
size="small"
triggerVariant="settings"
/>
</SettingsRow>
</div>
</div>
</div>
</div>
)
}

interface SettingsRowProps {
title: string | JSX.Element
description: string | JSX.Element
children: JSX.Element
}

const SettingsRow: Component<SettingsRowProps> = (props) => {
return (
<div class="flex flex-wrap 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 min-w-0">
<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>
)
}
21 changes: 21 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,27 @@ export const dict = {
"settings.providers.tag.other": "Other",
"settings.models.title": "Models",
"settings.models.description": "Model settings will be configurable here.",
"settings.ohmyopencode.title": "Oh My OpenCode",
"settings.ohmyopencode.description": "Configure Oh My OpenCode plugin settings",
"settings.ohmyopencode.section.agents": "Agent overrides",
"settings.ohmyopencode.section.features": "Features",
"settings.ohmyopencode.section.tmux": "Tmux integration",
"settings.ohmyopencode.row.enabled.title": "Enabled",
"settings.ohmyopencode.row.enabled.description": "Oh My OpenCode is integrated as a built-in plugin",
"settings.ohmyopencode.row.tmux.title": "Enable tmux",
"settings.ohmyopencode.row.tmux.description": "Enable tmux integration for visual multi-agent execution",
"settings.ohmyopencode.row.tmuxLayout.title": "Tmux layout",
"settings.ohmyopencode.row.tmuxLayout.description": "Layout for tmux agent panes",
"settings.ohmyopencode.row.ralphLoop.title": "Ralph Loop",
"settings.ohmyopencode.row.ralphLoop.description": "Enable the Ralph Loop for continuous agent execution",
"settings.ohmyopencode.row.autoUpdate.title": "Auto update",
"settings.ohmyopencode.row.autoUpdate.description": "Enable auto-update checking for Oh My OpenCode",
"settings.ohmyopencode.row.browserEngine.title": "Browser engine",
"settings.ohmyopencode.row.browserEngine.description": "Browser automation provider to use",
"settings.ohmyopencode.row.disabledAgents.title": "Disabled agents",
"settings.ohmyopencode.row.disabledAgents.description": "Agents to disable (e.g., oracle, multimodal-looker)",
"settings.ohmyopencode.row.disabledHooks.title": "Disabled hooks",
"settings.ohmyopencode.row.disabledHooks.description": "Hooks to disable (e.g., comment-checker, auto-update-checker)",
Comment on lines +737 to +740
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new translation keys for disabled agents/hooks are not referenced anywhere in the UI (the settings panel currently doesn’t render corresponding rows). Consider either wiring them up in SettingsOhMyOpenCode or removing the unused keys to avoid dead strings drifting over time.

Suggested change
"settings.ohmyopencode.row.disabledAgents.title": "Disabled agents",
"settings.ohmyopencode.row.disabledAgents.description": "Agents to disable (e.g., oracle, multimodal-looker)",
"settings.ohmyopencode.row.disabledHooks.title": "Disabled hooks",
"settings.ohmyopencode.row.disabledHooks.description": "Hooks to disable (e.g., comment-checker, auto-update-checker)",

Copilot uses AI. Check for mistakes.
"settings.agents.title": "Agents",
"settings.agents.description": "Agent settings will be configurable here.",
"settings.commands.title": "Commands",
Expand Down
103 changes: 103 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,25 @@ export namespace Config {

result.plugin = deduplicatePlugins(result.plugin ?? [])

// Sync oh_my_opencode config to oh-my-opencode.json for the plugin to read.
// Uses deep merge so existing oh-my-opencode.json settings are preserved
// while coli.json settings take precedence.
if (result.oh_my_opencode) {
const omoConfigPath = path.join(Global.Path.config, "oh-my-opencode.json")
const existing = await Bun.file(omoConfigPath)
.json()
.catch((err: unknown) => {
if (err && typeof err === "object" && "code" in err && err.code !== "ENOENT") {
log.warn("failed to read oh-my-opencode.json", { error: err })
}
return {}
})
Comment on lines +245 to +252
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The read error handling only logs when the caught error has a .code property (and is not ENOENT). JSON parse errors (e.g., a corrupted oh-my-opencode.json) typically won’t have code, so they’ll be silently ignored and the file will be overwritten. Consider logging for any error except ENOENT (and/or keeping the invalid file intact) so misconfigurations are diagnosable.

Copilot uses AI. Check for mistakes.
const merged = mergeDeep(existing, result.oh_my_opencode)
await Bun.write(omoConfigPath, JSON.stringify(merged, null, 2)).catch((err) => {
log.warn("failed to sync oh-my-opencode config", { error: err })
})
}
Comment on lines +240 to +257
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior writes/syncs oh_my_opencode into oh-my-opencode.json, but there’s no corresponding test coverage in packages/opencode/test/config/config.test.ts for merge/precedence and error cases (missing file, existing file, invalid JSON). Adding a config test would help prevent regressions since this runs on every config load.

Copilot uses AI. Check for mistakes.

return {
config: result,
directories,
Expand Down Expand Up @@ -1001,6 +1020,89 @@ export namespace Config {
})
export type Provider = z.infer<typeof Provider>

const OhMyOpenCodeAgentOverride = z
.object({
model: z.string().optional().describe("Model to use for this agent (e.g., 'anthropic/claude-opus-4-5')"),
temperature: z.number().optional().describe("Temperature for this agent"),
disable: z.boolean().optional().describe("Disable this agent"),
prompt: z.string().optional().describe("Custom system prompt for this agent"),
prompt_append: z.string().optional().describe("Append to the default system prompt"),
stream: z.boolean().optional().describe("Enable/disable streaming for this agent"),
})
.catchall(z.any())

const OhMyOpenCode = z
.object({
disabled_agents: z
.array(z.string())
.optional()
.describe("List of oh-my-opencode agents to disable"),
disabled_hooks: z
.array(z.string())
.optional()
.describe("List of oh-my-opencode hooks to disable"),
disabled_skills: z
.array(z.string())
.optional()
.describe("List of oh-my-opencode skills to disable"),
disabled_mcps: z
.array(z.string())
.optional()
.describe("List of oh-my-opencode MCPs to disable"),
disabled_commands: z
.array(z.string())
.optional()
.describe("List of oh-my-opencode commands to disable"),
disabled_tools: z
.array(z.string())
.optional()
.describe("List of oh-my-opencode tools to disable"),
agents: z
.record(z.string(), OhMyOpenCodeAgentOverride)
.optional()
.describe("Override settings for oh-my-opencode agents (sisyphus, oracle, librarian, explore, etc.)"),
categories: z
.record(z.string(), z.object({ model: z.string().optional() }).catchall(z.any()))
.optional()
.describe("Override model assignments for task categories"),
tmux: z
.object({
enabled: z.boolean().optional().describe("Enable tmux integration for visual multi-agent execution"),
layout: z
.enum(["main-vertical", "main-horizontal", "tiled", "even-horizontal", "even-vertical"])
.optional()
.describe("Tmux layout for agent panes"),
main_pane_size: z.number().optional().describe("Main pane size as percentage (20-80)"),
main_pane_min_width: z.number().optional().describe("Minimum width for main pane in columns"),
agent_pane_min_width: z.number().optional().describe("Minimum width for each agent pane in columns"),
Comment on lines +1075 to +1077
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema docs describe main_pane_size as a percentage (20–80) and the pane widths as column counts, but the Zod schema currently allows any number (including negatives/floats/out-of-range). Add .int() and appropriate .min()/.max() constraints so invalid values are rejected at config-parse time.

Suggested change
main_pane_size: z.number().optional().describe("Main pane size as percentage (20-80)"),
main_pane_min_width: z.number().optional().describe("Minimum width for main pane in columns"),
agent_pane_min_width: z.number().optional().describe("Minimum width for each agent pane in columns"),
main_pane_size: z
.number()
.int()
.min(20)
.max(80)
.optional()
.describe("Main pane size as percentage (20-80)"),
main_pane_min_width: z
.number()
.int()
.min(1)
.optional()
.describe("Minimum width for main pane in columns"),
agent_pane_min_width: z
.number()
.int()
.min(1)
.optional()
.describe("Minimum width for each agent pane in columns"),

Copilot uses AI. Check for mistakes.
})
.optional()
.describe("Tmux integration configuration for visual multi-agent execution"),
ralph_loop: z
.object({
enabled: z.boolean().optional().describe("Enable the Ralph Loop for self-referential development"),
default_max_iterations: z.number().optional().describe("Default max iterations for Ralph Loop"),
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default_max_iterations is described as an iteration count but is typed as an unconstrained number. This permits non-integers/negative values; consider validating it as a positive integer (e.g., .int().positive()) to match the intent and prevent downstream runtime checks.

Suggested change
default_max_iterations: z.number().optional().describe("Default max iterations for Ralph Loop"),
default_max_iterations: z
.number()
.int()
.positive()
.optional()
.describe("Default max iterations for Ralph Loop"),

Copilot uses AI. Check for mistakes.
})
.optional()
.describe("Ralph Loop configuration for continuous agent execution"),
auto_update: z.boolean().optional().describe("Enable auto-update checking for oh-my-opencode"),
experimental: z
.object({
aggressive_truncation: z.boolean().optional().describe("Enable aggressive context truncation"),
})
.catchall(z.any())
.optional()
.describe("Experimental oh-my-opencode features"),
browser_automation_engine: z
.object({
provider: z.enum(["playwright", "agent-browser"]).optional().describe("Browser automation provider to use"),
})
.optional()
.describe("Browser automation engine configuration"),
})
.catchall(z.any())
.describe("Oh My OpenCode plugin configuration — integrated natively for centralized settings management")

export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
Expand Down Expand Up @@ -1184,6 +1286,7 @@ export namespace Config {
.describe("Timeout in milliseconds for model context protocol (MCP) requests"),
})
.optional(),
oh_my_opencode: OhMyOpenCode.optional().describe("Oh My OpenCode plugin configuration"),
})
.strict()
.meta({
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-au
export namespace Plugin {
const log = Log.create({ service: "plugin" })

const BUILTIN = ["coli-anthropic-auth@0.0.13"]
const BUILTIN = ["coli-anthropic-auth@0.0.13", "oh-my-opencode@3.4.0"]

// Built-in plugins that are directly imported (not installed from npm)
const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin]
Expand Down
Loading
Loading