Proof Portal

Project overview

Omarchy

ProbeLabs65 findings · 95 requirements

A proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.

Inbox
ReportR-4941GitHub issuePublicReceived

Plugin hot reload re-runs cached QML: Qt.clearComponentCache does not exist in Quickshell

Reported by janrenz · filed Sep 7, 2026Closed upstream on 2026-09-20 by bjarneo via an automated message, as a duplicate of #6981 (R-1835). GitHub close reason: completed.

Received and recorded verbatim. Nothing has been checked yet.

At a glance

1 · What is claimed?
Only the report text below — no validator has restated it as checkable claims yet.
2 · What does it affect?
Not yet mapped to any requirement or component.
3 · Is it reproducible?
No validation has been run yet.
4 · What remains unclear?
  • Not validated yet: no claims, no reproducer.
5 · What happens next?
Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. No decision has been recorded yet.

Original report

Plugin hot reload re-runs cached QML: Qt.clearComponentCache does not exist in Quickshellrevision 1 · captured 2026-10-04 12:10 UTC via poll

System details

Intel i7-1255U / Iris Xe, Omarchy 4.0.2-1 (/usr/share/omarchy/version 4.0.0.alpha), quickshell 0.3.1-1, qt6-declarative 6.11.2-1, kernel 7.1.9-arch1. Code references below are to quattro (verified there, not just in the packaged copy).

What's wrong?

Plugin hot reload never picks up an edited plugin's QML. It does tear down and rebuild every plugin, so the plugin's state, services and helper processes are reset — and then the old compiled QML runs again. The result looks exactly like "my change silently did nothing", and shell/README.md promises the opposite:

Saving a file anywhere under ~/.config/omarchy/plugins/ reloads plugin code automatically; omarchy-shell shell rescanPlugins remains available to force a reload.

Cause. shell/shell.qml:1459, in finishPluginReload():

if (typeof Qt.clearComponentCache === "function") Qt.clearComponentCache()

Qt.clearComponentCache is not part of the QML Qt object — QQmlEngine::clearComponentCache() is C++ API only — so the guard is always false and the flush never runs. QML's type cache is keyed by URL and does not re-read an edited file, so reloadPlugins() (shell/shell.qml:1441) unloads the panels, services and bar widgets and then rebuilds them from the cached components. rescanPlugins (shell/shell.qml:1592) calls the same function, so the documented manual escape hatch has the same hole. Manifests are unaffected — the rescan cats them in a subprocess — which is why installing, removing and enabling plugins does work live.

Repro 1 — the guard is dead. Any config, e.g. /tmp/p/shell.qml:

import QtQuick
import Quickshell
ShellRoot {
  Component.onCompleted: {
    console.log("typeof Qt.clearComponentCache =", typeof Qt.clearComponentCache)
    Qt.exit(0)
  }
}

QT_QPA_PLATFORM=offscreen qs -p /tmp/p/shell.qml → typeof Qt.clearComponentCache = undefined.

Repro 2 — the cache never re-reads a URL. In one qs run: create a component from A.qml (property string label: "v1"), sed -i s/v1/v2/ A.qml, then create from A.qml again → still v1; create from a copy at a new path → v2.

Repro 3 — end to end in a running shell. Take any installed bar-widget plugin, add property int _probe: { console.log("PROBE"); return 1 } to the root of its entryPoints.barWidget file, save.

  • The journal shows Local plugin changed, reloading: <id> and the whole plugin layer being torn down and rebuilt (panels closed, services restarted, polkit agent re-registered, ~5 s of churn).
  • PROBE is never logged, however many reloads fire. Same after omarchy-shell shell rescanPlugins.
  • Only omarchy-restart-shell shows the change.
  • Copy that file to a new path inside the plugin dir, point entryPoints.barWidget at the copy, save the manifest: PROBE appears ~7 s later with the shell PID unchanged. So URL keying is the whole story, and the reload machinery around it works.

Every save under the plugins dir — .py, README.md, anything — pays the full teardown for no benefit, which is the part that hurts during plugin development.

Suggested fix. Stamp a revision into the entry point URL in PluginRegistry.entryPointUrl() (shell/services/PluginRegistry.qml:118), since a changed URL is a cache miss. It cannot be appended to the path string, because Util.fileUrl() (shell/Commons/Util.qml:42) percent-encodes each segment and would turn ? into %3F; it has to go on after encoding, e.g. Util.fileUrl(resolved) + "?rev=" + registryRevision. I verified a file:///…/A.qml?rev=2 URL loads and does pick up the edited content.

That alone only refreshes the entry file: a relative import resolves against the document URL without the query, so a plugin's sibling QML files stay cached. A complete fix means giving each reload generation its own base URL for the whole plugin (or Quickshell exposing an engine cache flush — happy to file that side upstream too).

Failing either, dropping the dead guard and documenting that plugin code edits need omarchy-restart-shell would at least stop the current behaviour from misleading plugin authors — the reload as it stands is worse than none, because it resets the plugin's state while still running the old code.

View raw text
### System details

Intel i7-1255U / Iris Xe, Omarchy 4.0.2-1 (`/usr/share/omarchy/version` 4.0.0.alpha), quickshell 0.3.1-1, qt6-declarative 6.11.2-1, kernel 7.1.9-arch1. Code references below are to `quattro` (verified there, not just in the packaged copy).

### What's wrong?

Plugin hot reload never picks up an edited plugin's QML. It does tear down and rebuild every plugin, so the plugin's state, services and helper processes are reset — and then the *old* compiled QML runs again. The result looks exactly like "my change silently did nothing", and `shell/README.md` promises the opposite:

> Saving a file anywhere under `~/.config/omarchy/plugins/` reloads plugin code automatically; `omarchy-shell shell rescanPlugins` remains available to force a reload.

**Cause.** `shell/shell.qml:1459`, in `finishPluginReload()`:

```qml
if (typeof Qt.clearComponentCache === "function") Qt.clearComponentCache()
```

`Qt.clearComponentCache` is not part of the QML `Qt` object — `QQmlEngine::clearComponentCache()` is C++ API only — so the guard is always false and the flush never runs. QML's type cache is keyed by URL and does not re-read an edited file, so `reloadPlugins()` (`shell/shell.qml:1441`) unloads the panels, services and bar widgets and then rebuilds them from the cached components. `rescanPlugins` (`shell/shell.qml:1592`) calls the same function, so the documented manual escape hatch has the same hole. Manifests are unaffected — the rescan `cat`s them in a subprocess — which is why installing, removing and enabling plugins does work live.

**Repro 1 — the guard is dead.** Any config, e.g. `/tmp/p/shell.qml`:

```qml
import QtQuick
import Quickshell
ShellRoot {
  Component.onCompleted: {
    console.log("typeof Qt.clearComponentCache =", typeof Qt.clearComponentCache)
    Qt.exit(0)
  }
}
```

`QT_QPA_PLATFORM=offscreen qs -p /tmp/p/shell.qml` → `typeof Qt.clearComponentCache = undefined`.

**Repro 2 — the cache never re-reads a URL.** In one `qs` run: create a component from `A.qml` (`property string label: "v1"`), `sed -i s/v1/v2/ A.qml`, then create from `A.qml` again → still `v1`; create from a copy at a new path → `v2`.

**Repro 3 — end to end in a running shell.** Take any installed bar-widget plugin, add `property int _probe: { console.log("PROBE"); return 1 }` to the root of its `entryPoints.barWidget` file, save.

- The journal shows `Local plugin changed, reloading: <id>` and the whole plugin layer being torn down and rebuilt (panels closed, services restarted, polkit agent re-registered, ~5 s of churn).
- `PROBE` is never logged, however many reloads fire. Same after `omarchy-shell shell rescanPlugins`.
- Only `omarchy-restart-shell` shows the change.
- Copy that file to a new path inside the plugin dir, point `entryPoints.barWidget` at the copy, save the manifest: `PROBE` appears ~7 s later with the shell PID unchanged. So URL keying is the whole story, and the reload machinery around it works.

Every save under the plugins dir — `.py`, `README.md`, anything — pays the full teardown for no benefit, which is the part that hurts during plugin development.

**Suggested fix.** Stamp a revision into the entry point URL in `PluginRegistry.entryPointUrl()` (`shell/services/PluginRegistry.qml:118`), since a changed URL is a cache miss. It cannot be appended to the path string, because `Util.fileUrl()` (`shell/Commons/Util.qml:42`) percent-encodes each segment and would turn `?` into `%3F`; it has to go on after encoding, e.g. `Util.fileUrl(resolved) + "?rev=" + registryRevision`. I verified a `file:///…/A.qml?rev=2` URL loads and does pick up the edited content.

That alone only refreshes the entry file: a relative import resolves against the document URL *without* the query, so a plugin's sibling QML files stay cached. A complete fix means giving each reload generation its own base URL for the whole plugin (or Quickshell exposing an engine cache flush — happy to file that side upstream too).

Failing either, dropping the dead guard and documenting that plugin code edits need `omarchy-restart-shell` would at least stop the current behaviour from misleading plugin authors — the reload as it stands is worse than none, because it resets the plugin's state while still running the old code.

Validation

Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.

Linked

Not linked to any known issue, other report or change yet.

Decision

Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.

No decision has been recorded yet.