Sync Omarchy themes to OpenCode without interrupting it - #8684
Conversation
There was a problem hiding this comment.
Pull request overview
Adds live OpenCode theme synchronization without restarting sessions or interrupting agents.
Changes:
- Adds an OpenCode theme template, synchronization command, and hot-reload plugin.
- Activates the integration for new installs and migrates existing installations.
- Replaces restart-based theme updates and adds CLI/template coverage.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 6 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
bin/omarchy-theme-set |
Uses theme synchronization instead of restarting OpenCode. |
bin/omarchy-theme-set-opencode |
Synchronizes and optionally activates the OpenCode theme integration. |
config/opencode/tui-plugins/omarchy-theme.ts |
Watches and hot-swaps generated themes. |
default/themed/opencode.json.tpl |
Defines the generated OpenCode palette. |
install/user/theme.sh |
Activates integration during user setup. |
migrations/1787871792.sh |
Migrates existing OpenCode installations. |
test/cli |
Tests generation, synchronization, and activation. |
test/shell.d/theme-staging-test.sh |
Classifies the generated theme as color-only. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # itself is not switched here: point tui.json at "omarchy" -- the theme picker | ||
| # also lists it -- or run `omarchy-theme-set-opencode --activate` to opt in. | ||
|
|
||
| OPENCODE_DIR="$HOME/.config/opencode" |
There was a problem hiding this comment.
The migration and the sync script now resolve the config directory the way opencode itself does -- OPENCODE_CONFIG_DIR, then XDG_CONFIG_HOME, then ~/.config. New migration test covers the XDG tree.
| const staged = path.join(os.tmpdir(), "omarchy-theme", `${name}.json`) | ||
| fs.mkdirSync(path.dirname(staged), { recursive: true }) | ||
| fs.writeFileSync(staged, text) | ||
| await api.theme.install(staged) | ||
| fs.rmSync(staged, { force: true }) |
There was a problem hiding this comment.
Fixed: each apply now stages the palette in a mkdtemp directory removed in a finally block, so concurrent sessions no longer share one staged file. The staged basename still carries the theme name since theme.install() derives it from the filename.
| if [[ -f $tui_config ]]; then | ||
| if ! jq -e --arg plugin "$plugin_target" '.plugin | index($plugin)' "$tui_config" >/dev/null 2>&1; then | ||
| tmp=$(mktemp "$tui_config.XXXXXX") | ||
| jq --arg plugin "$plugin_target" '.plugin = ((.plugin // []) + [$plugin] | unique)' "$tui_config" >"$tmp" | ||
| mv "$tmp" "$tui_config" | ||
| fi | ||
| fi |
There was a problem hiding this comment.
Fixed: the migration now creates a plugin-only tui.json (schema + plugin, no theme key) when the install has none, so the plugin registers without changing the user's theme. Covered in the new migration test.
| const stale = (candidate: string) => | ||
| candidate.startsWith(`${THEME_NAME}-`) && candidate !== `${name}.json` && candidate.endsWith(".json") |
There was a problem hiding this comment.
Fixed: pruning now matches only the generated omarchy-<8 hex>.json shape, so user themes like omarchy-custom.json are left alone.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 9 changed files in this pull request and generated 3 comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
config/opencode/tui-plugins/omarchy-theme.ts:40
- The shell command treats empty
OPENCODE_CONFIG_DIR/XDG_CONFIG_HOMEvalues as unset via:-, but the plugin's nullish checks accept empty strings. In that environment the command writes under~/.config/opencode, while the plugin watches a relativeopencode/themesdirectory, so live retinting never occurs. Use truthy fallback here to keep both paths identical.
const config =
process.env.OPENCODE_CONFIG_DIR ??
path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "opencode")
|
|
||
| const owned = () => { | ||
| const selected = api.theme.selected | ||
| return selected === THEME_NAME || selected.startsWith(`${THEME_NAME}-`) |
There was a problem hiding this comment.
Fixed -- owned() now uses the same strict omarchy-<8 hex> pattern as the pruning predicate, so selecting a user theme like omarchy-custom is fully inert.
| try { | ||
| const staged = path.join(stagedDir, `${name}.json`) | ||
| fs.writeFileSync(staged, text) | ||
| await api.theme.install(staged) |
There was a problem hiding this comment.
Fixed -- applies now run serialized on a promise chain (each re-reads the file when it starts, so the newest event wins) and a stale invocation can no longer resume after a newer one and set an older palette or prune the newer copy.
| if [[ -f $tui_config ]]; then | ||
| if ! jq -e --arg plugin "$plugin_target" '.plugin | index($plugin)' "$tui_config" >/dev/null 2>&1; then | ||
| tmp=$(mktemp "$tui_config.XXXXXX") | ||
| jq --arg plugin "$plugin_target" '.plugin = ((.plugin // []) + [$plugin] | unique)' "$tui_config" >"$tmp" |
There was a problem hiding this comment.
Fixed -- both the migration and --activate now append without unique, so existing plugin ordering is never rewritten. The migration test gained an order-preservation assertion.
|
Addressed the remaining Copilot finding from the second review round: the TUI plugin's |
omarchy-theme-set sent SIGUSR2 to opencode after every theme change. That signal makes opencode reload its worker, which disposes all instances and interrupts any agent running in a session. OpenCode themes are plain JSON in ~/.config/opencode/themes, and a TUI plugin can hot-swap the live theme in-process. So this follows the Claude Code/Pi integration shape and drops the signal: * An opencode.json template renders from each theme's colors.toml, so every stock and community theme is covered. * omarchy-theme-set-opencode copies the result into ~/.config/opencode/themes/omarchy.json and replaces the restart in the theme's post commands. With --activate it also installs the shipped omarchy-theme TUI plugin and points tui.json at the theme. * The plugin watches the synced file and retints running sessions live; running agents are never touched. Selecting Omarchy in the /theme picker (or --activate) opts a session in; without that the sync is a no-op at runtime. * A migration wires the plugin into tui.json for existing installs, without switching their theme.
* Resolve the opencode config directory the way opencode does --
OPENCODE_CONFIG_DIR, then XDG_CONFIG_HOME, then ~/.config -- in the
sync script and the migration, so XDG installs are migrated into the
tree opencode actually reads.
* Stage the theme in a mkdtemp directory removed in finally: every
running session stages the same palette at the same deterministic
path, so one session's cleanup could pull the file out from under
another session's install.
* Create a plugin-only tui.json when the install has none, which is
most existing installs; omitting "theme" keeps their current choice.
* Restrict pruning to the generated omarchy-<8 hex>.json shape so a
user theme like omarchy-custom.json survives every apply.
* Skip theme files whose colors carry unresolved {{ }} template
tokens; resolveTheme() throws on them and would take the TUI down.
* Cover the migration with opencode-theme-migration-test.sh.
…rder * owned() now matches the same generated omarchy-<8 hex> shape as pruning, so picking a user theme named omarchy-something is inert and the next synced palette does not take over. * Applies are serialized on a promise chain: theme.install() awaits, so a newer file event could otherwise interleave with an apply still finishing and let a stale invocation set an older palette or prune the newer copy. Each apply re-reads the file, so the last event wins. * Register the plugin by appending, not unique: plugin order affects initialization, so the migration and --activate must not reorder the user's existing list. * The cli suite's skip-when-missing invocation now unsets XDG_CONFIG_HOME/OPENCODE_CONFIG_DIR; with XDG support the script would otherwise resolve the real config directory on machines where it is set.
…plugin
An empty-but-set OPENCODE_CONFIG_DIR or XDG_CONFIG_HOME previously made the
plugin watch a different tree than omarchy-theme-set-opencode writes to, so
live retinting never fired. Match the sync script's ${VAR:-} semantics and
document why the fallbacks are truthy.
Skip theme.install/prune unless this session already has Omarchy selected, so migrating the plugin in does not list hashed omarchy-* themes for users who never opted in. Treat empty OPENCODE_CONFIG_DIR as unset in the CLI test, and drop the stale "used by theme switching" note on the restart command.
Live retint is the point: the migration now selects the Omarchy theme, and the plugin treats "system" as follow-the-desktop (that path used to depend on SIGUSR2). Picking any other theme in OpenCode still opts out.
system is terminal-adaptive (ANSI / none) and never reads omarchy.json. Live retint stays on the omarchy theme; picking system is an opt-out.
ec8ee2b to
67e763e
Compare
# Conflicts: # test/shell.d/theme-staging-test.sh
|
Independent verification of this PR, from arriving at the same design separately while debugging opencode lagging a theme behind under a terminal multiplexer. Tested on omarchy 4.0.3 (this branch is based on 4.0.0.alpha), Arch, foot 1.28.0 + opencode 1.18.29. To avoid patching packaged files I applied the two portable pieces through the supported override paths — What verified clean
Evidence for the plugin approach specifically The design argument for the TUI plugin over a plain file sync holds up under measurement. The consequence is that a file-sync-only integration cannot retint a session that was already running when the integration was installed — it needs a restart to pick up Context that may help triage: there are currently three open PRs covering this ground — this one, #8264, and #11402 — and none has a review. This is the only one of the three that addresses live retint, install/migration activation, and the Not verified: live retint inside a long-running session with the plugin loaded. I could confirm the plugin file installs and its import resolves, but not observe an in-process swap, so that part of the claim is untested here rather than disputed. Separately, and not this PR's problem: the reason opencode was singled out in my case is that under a multiplexer whose PTYs are owned by a detached server, |
|
Thanks for the thorough independent verification — especially the To close the one open loop: live retint in a long-running session is covered. I verified it with an opencode TUI running under tmux across several consecutive palette swaps (rewriting Two notes on the current head, since the branch moved on from the revision you tested:
Agreed on the foot/multiplexer point too — pane-walking with On the three-PR overlap: this remains the only one of the three covering live retint plus activation, so they compose rather than conflict — happy to adjust if maintainers would rather converge on one approach. |
Every existing install has a staged theme from before opencode.json.tpl shipped, so the migration's sync would find no source file and leave no themes/omarchy.json behind -- while tui.json already points at it. Re-stage once when the recorded theme still exists (cf. 1787481315); a removed theme is left alone so the migration cannot fail the update.
|
Follow-up pushed: the migration now handles staged themes that predate this PR. Every existing install has a staged theme from before Also merged |
Problem
omarchy-theme-setrunsomarchy-restart-opencodeafter every theme change, whichkillall -SIGUSR2 opencode. In opencode, that signal reaches two handlers:reload, which disposes every instance — interrupting any agent running in a session.So switching themes kills in-flight agent work in opencode.
Fix
OpenCode themes are plain JSON files in
~/.config/opencode/themes/, and an OpenCode TUI plugin can hot-swap the live theme in-process. That makes the same shape as the Claude Code/Pi integrations work here — sync a watched theme file, no signal:default/themed/opencode.json.tpl— new template rendering OpenCode's theme JSON from each theme'scolors.toml, so every stock and community theme is covered.bin/omarchy-theme-set-opencode— copies the rendered theme into~/.config/opencode/themes/omarchy.json(atomic). Replacesomarchy-restart-opencodein the theme's post commands.--activatealso installs the shipped TUI plugin, pointstui.jsonat"theme": "omarchy", and preserves any existingtui.jsonfields.config/opencode/tui-plugins/omarchy-theme.ts— the TUI plugin. It watches the synced file and retints running sessions live; sessions and agents are never touched. It installs each new palette under a content-hashed name (OpenCode'stheme.install()only upserts content while a theme is unknown to its registry) and prunes older copies. Selecting Omarchy in the/themepicker — or--activate— opts a session in; otherwise the sync is inert at runtime, and users oftheme: "system"or their own theme keep exactly what they chose.tui.jsonfor existing installs and selects"theme": "omarchy"so desktop theme changes live-retint OpenCode (same as every other themed app). Pick a different theme in OpenCode to opt out. OpenCode's built-in"system"theme is terminal-adaptive and will not follow; stay on"omarchy"for live retint.omarchy-restart-opencodestays (it remains a manual escape hatch for config changes); it is just no longer part of theme switching.Testing
test/cligains coverage for the template output, the sync respectingOPENCODE_CONFIG_DIR, skip-when-missing, activation preserving existingtui.jsonfields, and freshtui.jsongeneration.test/shell.d/theme-staging-test.sh:opencode.jsonclassified as colour-only.SIGUSR2sent and agents unaffected.Closes
Closes #8002 — theme change interrupts/kills running opencode sessions.
Closes #8389 — theme change aborts in-flight opencode agent work.