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 Cloning `omarchy.clock`, for example, creates and switches to
`~/.config/omarchy/plugins/local.clock/`, names it `My Clock`, and preserves `~/.config/omarchy/plugins/local.clock/`, names it `My Clock`, and preserves
the built-in IPC identity so existing shortcuts keep working. Saving files in 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. switches back to its built-in source.
For a bar widget, on and off means its place in the bar. Everything else is 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 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 the enabled clone, so cloning does not require changing its callers. Removing
an active clone switches back to its built-in source. 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. automatically; `omarchy-shell shell rescanPlugins` remains available to force a reload.
First-party plugins under `shell/plugins/` are discovered the same way and load 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) { function localPluginIdForPath(filePath) {
var base = pluginsDir.replace(/\/$/, "") + "/" var base = pluginsDir.replace(/\/$/, "") + "/"
var path = String(filePath || "").trim() var path = String(filePath || "").trim()
if (path.indexOf(base + "local.") !== 0) return "" if (path.indexOf(base) !== 0) return ""
var relative = path.slice(base.length) 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 "" if (relative.indexOf("/.git/") !== -1 || relative.endsWith("/.git")) return ""
var slash = relative.indexOf("/") 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(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") root.assertTrue(registry.isEnabled("omarchy.hybrid"), "a multi-kind built-in remains loadable without its widget")
var localBase = registry.pluginsDir + "/local.clock" var cloneBase = registry.pluginsDir + "/dhh.clock"
root.assertEqual(registry.localPluginIdForPath(localBase + "/BarWidget.qml"), "local.clock", "local clone changes are watched") root.assertEqual(registry.localPluginIdForPath(cloneBase + "/BarWidget.qml"), "dhh.clock", "personal 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(registry.pluginsDir + "/acme.clock/BarWidget.qml"), "acme.clock", "installed plugin changes are watched")
root.assertEqual(registry.localPluginIdForPath(localBase + "/.git/index"), "", "clone git metadata is ignored") 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") root.assertTrue(changeCount > 0, "registry emits change notifications")
writeResult() 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/config" "$test_root/config"
ln -s "$ROOT/bin" "$test_root/bin" 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" mkdir -p "$hot_reload_dir"
cat >"$hot_reload_dir/manifest.json" <<'JSON' cat >"$hot_reload_dir/manifest.json" <<JSON
{ {
"schemaVersion": 1, "schemaVersion": 1,
"id": "local.hot-reload", "id": "$hot_reload_id",
"name": "Before Hot Reload", "name": "Before Hot Reload",
"version": "1.0.0", "version": "1.0.0",
"kinds": ["overlay"], "kinds": ["overlay"],
@@ -144,23 +146,23 @@ mv "$hot_reload_dir/manifest.json.tmp" "$hot_reload_dir/manifest.json"
hot_reload_name="" hot_reload_name=""
for _ in {1..80}; do for _ in {1..80}; do
hot_reload_name=$(shell_ipc shell listPlugins 2>/dev/null | 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 [[ $hot_reload_name == "After Hot Reload" ]] && break
if ! kill -0 "$QS_PID" 2>/dev/null; then 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 fi
sleep 0.1 sleep 0.1
done done
[[ $hot_reload_name == "After Hot Reload" ]] || [[ $hot_reload_name == "After Hot Reload" ]] ||
fail_with_log "local clone changes reload without an explicit rescan" fail_with_log "installed plugin changes reload without an explicit rescan"
pass "local clone changes reload without an explicit rescan" pass "installed plugin changes reload without an explicit rescan"
[[ $(shell_ipc shell setPluginEnabled local.hot-reload true) == "ok" ]] || [[ $(shell_ipc shell setPluginEnabled "$hot_reload_id" true) == "ok" ]] ||
fail_with_log "local clone could not be enabled" fail_with_log "installed plugin could not be enabled"
[[ $(shell_ipc shell summon omarchy.emojis "{}") == "ok" ]] || [[ $(shell_ipc shell summon omarchy.emojis "{}") == "ok" ]] ||
fail_with_log "calls to a cloned source id do not reach its enabled clone" 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 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" pass "shell IPC routes built-in ids to enabled clones"
shell_config=$(shell_ipc shell listShellConfig) shell_config=$(shell_ipc shell listShellConfig)