-
Notifications
You must be signed in to change notification settings - Fork 0
feat: integrate oh-my-opencode as native built-in plugin #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 }, | ||
| }) | ||
| } | ||
|
|
||
| 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> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
|
||||||||||
| "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)", |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
|
||||||||||||||||||||||||||||||||||||||||||||||
| 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
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||
| config: result, | ||||||||||||||||||||||||||||||||||||||||||||||
| directories, | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
|
||||||||||||||||||||||||||||||||||||||||||||||
| 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
AI
Feb 10, 2026
There was a problem hiding this comment.
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.
| 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"), |
There was a problem hiding this comment.
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/currentvalues are derived fromglobalSync.data.config, butupdate()doesn’t update the local store (unlike other settings screens that callglobalSync.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.