Hot-reload every installed plugin, not just local clones

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-01 13:13:39 -05:00
co-authored by Claude Fable 5
parent 19144983e5
commit 2050f28d55
5 changed files with 22 additions and 17 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ editor, confirmation, and output stay visible.
Cloning `omarchy.clock`, for example, creates and switches to
`~/.config/omarchy/plugins/local.clock/`, names it `My Clock`, and preserves
the built-in IPC identity so existing shortcuts keep working. Saving files in
a `local.*` clone reloads its code automatically, and removing an active clone
any installed plugin reloads its code automatically, and removing an active clone
switches back to its built-in source.
For a bar widget, on and off means its place in the bar. Everything else is
+1 -1
View File
@@ -155,7 +155,7 @@ the interactive picker, then opens the new `local.*` directory in `$EDITOR`.
Existing shortcuts and shell IPC calls made to the built-in id are routed to
the enabled clone, so cloning does not require changing its callers. Removing
an active clone switches back to its built-in source.
Saving a file anywhere inside a `local.*` plugin reloads plugin code
Saving a file anywhere under `~/.config/omarchy/plugins/` reloads plugin code
automatically; `omarchy-shell shell rescanPlugins` remains available to force a reload.
First-party plugins under `shell/plugins/` are discovered the same way and load
+3 -1
View File
@@ -668,9 +668,11 @@ QtObject {
function localPluginIdForPath(filePath) {
var base = pluginsDir.replace(/\/$/, "") + "/"
var path = String(filePath || "").trim()
if (path.indexOf(base + "local.") !== 0) return ""
if (path.indexOf(base) !== 0) return ""
var relative = path.slice(base.length)
// Hidden entries are not plugins: clone staging dirs, remove backups.
if (relative.indexOf(".") === 0) return ""
if (relative.indexOf("/.git/") !== -1 || relative.endsWith("/.git")) return ""
var slash = relative.indexOf("/")
@@ -337,10 +337,11 @@ ShellRoot {
root.assertTrue(root.config.disabledPlugins === undefined, "disabling a multi-kind widget records nothing else")
root.assertTrue(registry.isEnabled("omarchy.hybrid"), "a multi-kind built-in remains loadable without its widget")
var localBase = registry.pluginsDir + "/local.clock"
root.assertEqual(registry.localPluginIdForPath(localBase + "/BarWidget.qml"), "local.clock", "local clone changes are watched")
root.assertEqual(registry.localPluginIdForPath(registry.pluginsDir + "/acme.clock/BarWidget.qml"), "", "installed plugins are not treated as local clones")
root.assertEqual(registry.localPluginIdForPath(localBase + "/.git/index"), "", "clone git metadata is ignored")
var cloneBase = registry.pluginsDir + "/dhh.clock"
root.assertEqual(registry.localPluginIdForPath(cloneBase + "/BarWidget.qml"), "dhh.clock", "personal clone changes are watched")
root.assertEqual(registry.localPluginIdForPath(registry.pluginsDir + "/acme.clock/BarWidget.qml"), "acme.clock", "installed plugin changes are watched")
root.assertEqual(registry.localPluginIdForPath(cloneBase + "/.git/index"), "", "plugin git metadata is ignored")
root.assertEqual(registry.localPluginIdForPath(registry.pluginsDir + "/.clone.abc123/manifest.json"), "", "hidden staging and backup dirs are ignored")
root.assertTrue(changeCount > 0, "registry emits change notifications")
writeResult()
+12 -10
View File
@@ -53,12 +53,14 @@ cp -a "$ROOT/shell" "$test_root/shell"
ln -s "$ROOT/config" "$test_root/config"
ln -s "$ROOT/bin" "$test_root/bin"
hot_reload_dir="$test_home/.config/omarchy/plugins/local.hot-reload"
# Every plugin under ~/.config/omarchy/plugins hot-reloads, whoever wrote it.
hot_reload_id="acme.hot-reload"
hot_reload_dir="$test_home/.config/omarchy/plugins/$hot_reload_id"
mkdir -p "$hot_reload_dir"
cat >"$hot_reload_dir/manifest.json" <<'JSON'
cat >"$hot_reload_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "local.hot-reload",
"id": "$hot_reload_id",
"name": "Before Hot Reload",
"version": "1.0.0",
"kinds": ["overlay"],
@@ -144,23 +146,23 @@ mv "$hot_reload_dir/manifest.json.tmp" "$hot_reload_dir/manifest.json"
hot_reload_name=""
for _ in {1..80}; do
hot_reload_name=$(shell_ipc shell listPlugins 2>/dev/null |
jq -r '.[] | select(.id == "local.hot-reload") | .name' 2>/dev/null || true)
jq -r --arg id "$hot_reload_id" '.[] | select(.id == $id) | .name' 2>/dev/null || true)
[[ $hot_reload_name == "After Hot Reload" ]] && break
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited while reloading a changed local clone"
fail_with_log "test shell exited while reloading a changed installed plugin"
fi
sleep 0.1
done
[[ $hot_reload_name == "After Hot Reload" ]] ||
fail_with_log "local clone changes reload without an explicit rescan"
pass "local clone changes reload without an explicit rescan"
fail_with_log "installed plugin changes reload without an explicit rescan"
pass "installed plugin changes reload without an explicit rescan"
[[ $(shell_ipc shell setPluginEnabled local.hot-reload true) == "ok" ]] ||
fail_with_log "local clone could not be enabled"
[[ $(shell_ipc shell setPluginEnabled "$hot_reload_id" true) == "ok" ]] ||
fail_with_log "installed plugin could not be enabled"
[[ $(shell_ipc shell summon omarchy.emojis "{}") == "ok" ]] ||
fail_with_log "calls to a cloned source id do not reach its enabled clone"
shell_ipc_quiet shell hide omarchy.emojis >/dev/null
shell_ipc_quiet shell setPluginEnabled local.hot-reload false >/dev/null
shell_ipc_quiet shell setPluginEnabled "$hot_reload_id" false >/dev/null
pass "shell IPC routes built-in ids to enabled clones"
shell_config=$(shell_ipc shell listShellConfig)