Skip to content

Sync Omarchy themes to OpenCode without interrupting it - #8684

Closed
ekollof wants to merge 11 commits into
omacom:quattrofrom
ekollof:opencode-theme-hot-reload
Closed

ekollof wants to merge 11 commits into
omacom:quattrofrom
ekollof:opencode-theme-hot-reload

Conversation

@ekollof

@ekollof ekollof commented Aug 27, 2026 •

Copy link
Copy Markdown

Sync Omarchy's generated palette into OpenCode without restarting its server or interrupting agents. The template and sync command produce themes/omarchy.json; startup activation selects it, and choosing another theme opts out of its visual changes.

OpenCode 1.x uses the existing in-process TUI theme adapter. OpenCode 2.0.28 no longer exposes that theme-management API, so the plugin now also exports a setup adapter that watches atomic palette replacements and emits SIGUSR2 inside its own TUI process. OpenCode 2 handles this as palette/custom-theme refresh, not server-instance reload. No OS signal is sent to the shared server.

For OpenCode 2, the sync command registers a directory-based TUI entrypoint in cli.json and removes the migrated legacy file registration. Ordinary syncs repair registration while preserving the user's theme selection and other plugins, including Herdr. Explicit --activate selects Omarchy while preserving the configured light/dark mode. The legacy tui.json activation remains for 1.x.

The original session-disposal bug motivating this PR appears fixed upstream in OpenCode 2.0.28; the remaining value is explicit Omarchy palette synchronization and compatibility with both plugin generations. The manual restart command remains available but theme changes use palette sync instead.

Validation: plugin watcher and migration tests pass. New OpenCode 2 coverage checks actual adapter import, atomic replacements triggering one debounced local refresh, cleanup, activation, custom-theme opt-out and Herdr preservation. A separate 2.0.28 TUI was opened without starting agent work; changing and restoring a temporary palette redrew it. The selected-PR integration rebuild passes. No live in-flight-agent interruption test is claimed.

Copilot AI balanced review requested due to automatic review settings August 27, 2026 23:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread migrations/1787871792.sh Outdated
Comment thread config/opencode/tui-plugins/omarchy-theme.ts Outdated
Comment thread migrations/1787871792.sh
Comment thread config/opencode/tui-plugins/omarchy-theme.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_HOME values 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 relative opencode/themes directory, 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")

Comment thread config/opencode/tui-plugins/omarchy-theme.ts Outdated
Comment thread config/opencode/tui-plugins/omarchy-theme.ts
Comment thread migrations/1787871792.sh Outdated
@ekollof

ekollof commented Aug 28, 2026

Copy link
Copy Markdown
Author

Addressed the remaining Copilot finding from the second review round: the TUI plugin's themesDir() now uses truthy fallbacks for OPENCODE_CONFIG_DIR/XDG_CONFIG_HOME (e674e80), matching the ${VAR:-} semantics in omarchy-theme-set-opencode. An empty-but-set env var previously made the watcher and the sync command resolve different trees, so live retinting never fired. ./test/cli, the migration test, and the staging test all pass on the new commit.

@marvreichmann

Copy link
Copy Markdown

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 — default/themed/opencode.json.tpl → ~/.config/omarchy/themed/, bin/omarchy-theme-set-opencode → ~/.local/bin/, and a theme-set.d hook standing in for the post_theme_commands entry.

What verified clean

  • Template renders with 0 unresolved {{ }} placeholders and is valid JSON; emitted values match colors.toml exactly (background #222222, accent #686868).
  • The atomic write does what it claims — inotifywait on the themes dir shows MODIFY on the omarchy.json.XXXXXX temp followed by MOVED_TO omarchy.json, never a partial omarchy.json.
  • --activate sets .theme and appends the plugin without disturbing $schema or existing keys.
  • The plugin's import resolves: @opencode-ai/plugin 1.18.22 exports a ./tui subpath.
  • Applies cleanly on 4.0.3 despite the branch age; nothing in the two files depends on anything that moved.

Evidence for the plugin approach specifically

The design argument for the TUI plugin over a plain file sync holds up under measurement. inotifywait on ~/.config/opencode/tui.json records zero reads across a SIGUSR2 reload, while themes/omarchy.json is opened once per running instance in the same window. So opencode re-reads theme files on the signal but re-reads the selection only at startup.

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 "theme": "omarchy" even though the file underneath is correct. That's a real failure I hit before finding this PR, and it's the gap the watcher closes. Worth weighing against the simpler competing PRs.

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 theme-staging-test.sh registration.

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, omarchy-theme-set-foot never reaches the panes — it walks pgrep -P <foot_pid> only. Pinning opencode to a generated theme file, which is what this PR does, routes around that entirely. I've filed that separately.

@ekollof

ekollof commented Sep 12, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough independent verification — especially the inotifywait traces. They pin down exactly why the plugin exists: opencode re-reads theme files on SIGUSR2 but the selection only at startup, so a file-sync-only integration can never retint an already-running session. That matches the failure you hit before finding this PR.

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 themes/omarchy.json each time): capturing the pane's SGR attributes before/after shows the repaint landing every time, with no restart and — the point of the whole PR — no signals sent. Sessions and in-flight agents stay untouched throughout.

Two notes on the current head, since the branch moved on from the revision you tested:

  • The migration now selects "theme": "omarchy" by default (opt-out), and the plugin stays completely off the theme registry unless Omarchy is selected — users who never opt in see no omarchy-* entries at all.
  • system explicitly opts out: it is terminal-adaptive (ANSI/none) and never reads omarchy.json, so live retint only ever follows the generated file.

Agreed on the foot/multiplexer point too — pane-walking with pgrep -P can't reach panes owned by a detached server, while a generated theme file sidesteps per-pane delivery entirely. Good that it's filed separately.

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.

@ekollof

ekollof commented Sep 12, 2026

Copy link
Copy Markdown
Author

Follow-up pushed: the migration now handles staged themes that predate this PR.

Every existing install has a staged theme from before opencode.json.tpl shipped, so the migration's sync would find no source file — while tui.json already points at the theme. The migration now runs omarchy-theme-refresh once first when the staged opencode.json is missing and the recorded theme still exists (same precedent as 1787481315 and the T3 installer). A removed theme is left alone, so a dangling theme.name can never fail the update. Three migration-test cases cover it: stale stages refresh then sync, current stages skip the refresh, removed themes are untouched.

Also merged upstream/quattro in cleanly — nothing upstream touched these files — and re-ran the suites (120 cli, 14 migration, staging tests green).

@ekollof
ekollof force-pushed the opencode-theme-hot-reload branch from f9686f8 to 10fe602 Compare September 16, 2026 00:10
@ekollof

ekollof commented Sep 16, 2026

Copy link
Copy Markdown
Author

Rebased onto current quattro (2fbac0c). One conflict in the append-only colour_only list in theme-staging-test.sh, resolved as sorted union (keeps hermes.yaml / t3code.json, adds opencode.json). opencode-theme-migration-test.sh and theme-staging-test.sh pass.

@ekollof

ekollof commented Sep 16, 2026

Copy link
Copy Markdown
Author

Follow-up fix: react to every themes-dir event in the OpenCode watcher (c3a41a2).

Wallpaper/theme changes synced omarchy.json correctly, but running sessions never retinted. Root cause: the watcher only reacted to events named omarchy.json, and opencode runs on Bun, whose fs.watch does not reliably report the atomic rename that replaces omarchy.json -- only the temp-file write/rename precursors arrive (verified with an instrumented plugin in a test TUI: startup apply works, watch events for the replace never fire; Node reports them fine).

The fix schedules the debounced apply on any directory event instead; the apply re-reads omarchy.json itself, so unrelated files changing here cost at most one cheap re-read.

Verified live: a fresh TUI installs and selects the current palette on startup, and theme switches in both directions retint the running session within seconds (hash install, kv.json update, stale prune). Also added test/shell.d/opencode-theme-plugin-test.sh as a regression guard. test/cli passes 120/120.

Side note: this also means wallpaperctl's watcher-nudge rewrite cannot work as intended on this Bun version -- tmp+replace is equally invisible there -- but the plugin-side fix covers it, since the nudge's temp file itself now triggers an apply.

@ekollof
ekollof force-pushed the opencode-theme-hot-reload branch 2 times, most recently from 20e49ad to b8a5e54 Compare September 20, 2026 19:07
@ekollof
ekollof force-pushed the opencode-theme-hot-reload branch from b8a5e54 to bf5e17b Compare September 25, 2026 22:07
@omarchybot omarchybot added the enhancement New feature or request label Sep 27, 2026
Emiel Kollof added 7 commits September 28, 2026 20:32
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.
Emiel Kollof added 3 commits September 28, 2026 20:32
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.
opencode runs on Bun, whose fs.watch does not reliably report the atomic rename that replaces omarchy.json (only the temp-file precursors arrive), so running sessions never retinted on wallpaper/theme changes. Schedule the debounced apply on any directory event instead; it re-reads omarchy.json itself, so unrelated files cost one cheap re-read.
@ekollof
ekollof force-pushed the opencode-theme-hot-reload branch from bf5e17b to 5455a56 Compare September 28, 2026 18:33
@ekollof

ekollof commented Oct 13, 2026

Copy link
Copy Markdown
Author

Superseded by #15350, which targets OpenCode 2 explicitly. The original SIGUSR2 session-disposal bug appears fixed upstream in 2.0.28; the replacement retains explicit Omarchy palette synchronization using the new TUI API and cli.json, without the legacy 1.x adapter. The replacement is open and mergeable, and has been verified in an integration build with all other selected PRs while excluding this PR; focused tests pass on that combined tree. Closing this only after the replacement was prepared and verified keeps the theme integration covered.

@ekollof ekollof closed this Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants