From 06088bb120a10bb765858f17d425b41fa9c70372 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 01/40] release: start 0.1.1-beta.15.4 and admit the beta.15.3 tag and merge checkouts --- VERSION | 2 +- contracts/lifecycle-predecessors-v1.json | 2 + contracts/plugin-suite-v1.json | 2 +- hancore.shibumi.ai/Service.qml | 2 +- hancore.shibumi.ai/manifest.json | 2 +- hancore.shibumi.audio/Service.qml | 2 +- hancore.shibumi.audio/manifest.json | 2 +- hancore.shibumi.bar/Bar.qml | 2 +- hancore.shibumi.bar/manifest.json | 2 +- hancore.shibumi.battery/manifest.json | 2 +- hancore.shibumi.bluetooth/Service.qml | 2 +- hancore.shibumi.bluetooth/manifest.json | 2 +- hancore.shibumi.brightness/Service.qml | 2 +- hancore.shibumi.brightness/manifest.json | 2 +- hancore.shibumi.center/Service.qml | 2 +- hancore.shibumi.center/manifest.json | 2 +- .../PluginUpdateService.qml | 2 +- hancore.shibumi.control-center/manifest.json | 2 +- hancore.shibumi.cpu/Service.qml | 2 +- hancore.shibumi.cpu/manifest.json | 2 +- hancore.shibumi.gpu/manifest.json | 2 +- hancore.shibumi.media/Service.qml | 2 +- hancore.shibumi.media/manifest.json | 2 +- hancore.shibumi.memory/manifest.json | 2 +- hancore.shibumi.network/Service.qml | 2 +- hancore.shibumi.network/manifest.json | 2 +- hancore.shibumi.power-profile/manifest.json | 2 +- hancore.shibumi.power-state/Service.qml | 2 +- hancore.shibumi.power-state/manifest.json | 2 +- hancore.shibumi.quick-access/Service.qml | 2 +- hancore.shibumi.quick-access/manifest.json | 2 +- hancore.shibumi.reactor/Service.qml | 2 +- hancore.shibumi.reactor/manifest.json | 2 +- hancore.shibumi.state/Service.qml | 2 +- hancore.shibumi.state/manifest.json | 2 +- hancore.shibumi.state/runtime/Runtime.qml | 2 +- hancore.shibumi.status/Service.qml | 2 +- hancore.shibumi.status/manifest.json | 2 +- hancore.shibumi.storage/Service.qml | 2 +- hancore.shibumi.storage/manifest.json | 2 +- hancore.shibumi.telemetry/Service.qml | 2 +- hancore.shibumi.telemetry/manifest.json | 2 +- hancore.shibumi.temperature/manifest.json | 2 +- hancore.shibumi.update-center/Service.qml | 2 +- hancore.shibumi.update-center/manifest.json | 2 +- .../WorkspaceService.qml | 2 +- hancore.shibumi.workspaces/manifest.json | 2 +- packaging/aur/.SRCINFO | 4 +- packaging/aur/PKGBUILD | 2 +- packaging/package-metadata.json | 2 +- .../audio-native-backend-seam-regression.qml | 2 +- tests/audio-runtime-smoke.qml | 6 +-- tests/bar-catalog-consumer-smoke.qml | 18 +++---- tests/bar-visibility-ipc-smoke.qml | 2 +- tests/catalog-service-smoke.qml | 2 +- tests/core-services-smoke.qml | 6 +-- tests/fixtures/native-runtime/probe.py | 2 +- tests/gpu-selection-state-smoke.qml | 4 +- tests/host-registry-prime-smoke.qml | 8 +-- tests/media-spectrum-service-smoke.qml | 2 +- tests/native-runtime-regression.py | 4 +- tests/plugin-update-service-smoke.qml | 2 +- tests/power-deferred-smoke.qml | 2 +- tests/power-runtime-smoke.qml | 6 +-- tests/power-service-runtime-smoke.qml | 2 +- tests/reactor-plugin-smoke.qml | 4 +- tests/reactor-runtime-smoke.qml | 10 ++-- tests/scoped-state-regression.qml | 2 +- tests/shared-runtime-import-regression.py | 2 +- tests/shared-runtime-regression.qml | 16 +++--- tests/state-service-smoke.qml | 2 +- tests/status-plugin-smoke.qml | 6 +-- tests/telemetry-plugins-smoke.qml | 4 +- tests/telemetry-runtime-smoke.qml | 4 +- tests/test_lifecycle_admission.py | 5 ++ tests/test_package_release.py | 12 +++-- tests/test_shibumi_suite.py | 52 +++++++++++++------ tests/theme-palette-runtime-smoke.qml | 2 +- tests/third-party-integration-smoke.qml | 4 +- tests/widget-pipeline-diagnostics-smoke.qml | 6 +-- tests/workspaces-plugin-smoke.qml | 2 +- tests/workspaces-runtime-smoke.qml | 6 +-- 82 files changed, 169 insertions(+), 140 deletions(-) diff --git a/VERSION b/VERSION index 4ed6225..a9f4988 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.1-beta.15.3 +0.1.1-beta.15.4 diff --git a/contracts/lifecycle-predecessors-v1.json b/contracts/lifecycle-predecessors-v1.json index f8e33b4..02bf092 100644 --- a/contracts/lifecycle-predecessors-v1.json +++ b/contracts/lifecycle-predecessors-v1.json @@ -624,6 +624,8 @@ ], "settingsStorageVersion": 1, "sourceRevisions": [ + "5b1d21f0cea73bb9e7997278a828b3ef295e6c94", + "db579163a4b1004a7cc09ee565cac616db9ff627", "package:0.1.1-beta.15.3" ], "suiteVersion": "0.1.1-beta.15.3" diff --git a/contracts/plugin-suite-v1.json b/contracts/plugin-suite-v1.json index c40655a..da2d4d2 100644 --- a/contracts/plugin-suite-v1.json +++ b/contracts/plugin-suite-v1.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "suiteId": "hancore.shibumi", - "suiteVersion": "0.1.1-beta.15.3", + "suiteVersion": "0.1.1-beta.15.4", "settingsStorageVersion": 1, "hostContract": "contracts/host-facade-v1.json", "retiredPlugins": [ diff --git a/hancore.shibumi.ai/Service.qml b/hancore.shibumi.ai/Service.qml index 1573fbf..c575c02 100644 --- a/hancore.shibumi.ai/Service.qml +++ b/hancore.shibumi.ai/Service.qml @@ -19,7 +19,7 @@ Item { SuiteRuntime.HostShell { id: suiteShell; host: root.shell } SuiteRuntime.Provider { pluginId: "hancore.shibumi.ai" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.ai/manifest.json b/hancore.shibumi.ai/manifest.json index 9bb6179..63c0156 100644 --- a/hancore.shibumi.ai/manifest.json +++ b/hancore.shibumi.ai/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.ai", "name": "Shibumi AI Usage", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi Claude, Codex, and OpenCode usage presentation.", diff --git a/hancore.shibumi.audio/Service.qml b/hancore.shibumi.audio/Service.qml index bdb20e9..4119f19 100644 --- a/hancore.shibumi.audio/Service.qml +++ b/hancore.shibumi.audio/Service.qml @@ -14,7 +14,7 @@ Item { SuiteRuntime.Provider { id: runtimeProvider pluginId: "hancore.shibumi.audio" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.audio/manifest.json b/hancore.shibumi.audio/manifest.json index a5f4ada..672a14e 100644 --- a/hancore.shibumi.audio/manifest.json +++ b/hancore.shibumi.audio/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.audio", "name": "Shibumi Audio", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi audio presentation over Omarchy's PipeWire owner.", diff --git a/hancore.shibumi.bar/Bar.qml b/hancore.shibumi.bar/Bar.qml index 3f1d15c..8c39675 100644 --- a/hancore.shibumi.bar/Bar.qml +++ b/hancore.shibumi.bar/Bar.qml @@ -27,7 +27,7 @@ Item { SuiteRuntime.Provider { id: barRuntimeProvider pluginId: "hancore.shibumi.bar" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: suiteHostShell.host manifest: root.manifest diff --git a/hancore.shibumi.bar/manifest.json b/hancore.shibumi.bar/manifest.json index 1c2f61f..775840f 100644 --- a/hancore.shibumi.bar/manifest.json +++ b/hancore.shibumi.bar/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.bar", "name": "Shibumi Bar", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "The Shibumi full-bar host for Omarchy Shell.", diff --git a/hancore.shibumi.battery/manifest.json b/hancore.shibumi.battery/manifest.json index ff1e4f8..1d215db 100644 --- a/hancore.shibumi.battery/manifest.json +++ b/hancore.shibumi.battery/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.battery", "name": "Shibumi Battery", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi battery status and detailed battery panel.", diff --git a/hancore.shibumi.bluetooth/Service.qml b/hancore.shibumi.bluetooth/Service.qml index 9d585c8..e0b9a65 100644 --- a/hancore.shibumi.bluetooth/Service.qml +++ b/hancore.shibumi.bluetooth/Service.qml @@ -15,7 +15,7 @@ Item { SuiteRuntime.HostShell { id: suiteShell; host: root.shell } SuiteRuntime.Provider { pluginId: "hancore.shibumi.bluetooth" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.bluetooth/manifest.json b/hancore.shibumi.bluetooth/manifest.json index 8420c5d..9d6dc04 100644 --- a/hancore.shibumi.bluetooth/manifest.json +++ b/hancore.shibumi.bluetooth/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.bluetooth", "name": "Shibumi Bluetooth", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi Bluetooth presentation with a native BlueZ and audio backend.", diff --git a/hancore.shibumi.brightness/Service.qml b/hancore.shibumi.brightness/Service.qml index 1f12200..27a0715 100644 --- a/hancore.shibumi.brightness/Service.qml +++ b/hancore.shibumi.brightness/Service.qml @@ -13,7 +13,7 @@ Item { property var manifest: null SuiteRuntime.Provider { pluginId: "hancore.shibumi.brightness" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.brightness/manifest.json b/hancore.shibumi.brightness/manifest.json index 12248b6..b9bf750 100644 --- a/hancore.shibumi.brightness/manifest.json +++ b/hancore.shibumi.brightness/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.brightness", "name": "Shibumi Brightness", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi brightness and display presentation over Omarchy's monitor owner.", diff --git a/hancore.shibumi.center/Service.qml b/hancore.shibumi.center/Service.qml index 1d2e392..27d0b16 100644 --- a/hancore.shibumi.center/Service.qml +++ b/hancore.shibumi.center/Service.qml @@ -10,7 +10,7 @@ Item { property var manifest: null SuiteRuntime.Provider { pluginId: "hancore.shibumi.center" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.center/manifest.json b/hancore.shibumi.center/manifest.json index 58672c9..f4b3b8e 100644 --- a/hancore.shibumi.center/manifest.json +++ b/hancore.shibumi.center/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.center", "name": "Shibumi Center", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi clock, calendar, weather, status, and update center.", diff --git a/hancore.shibumi.control-center/PluginUpdateService.qml b/hancore.shibumi.control-center/PluginUpdateService.qml index 8699876..7ce4e2e 100644 --- a/hancore.shibumi.control-center/PluginUpdateService.qml +++ b/hancore.shibumi.control-center/PluginUpdateService.qml @@ -20,7 +20,7 @@ Item { SuiteRuntime.Provider { id: runtimeProvider pluginId: "hancore.shibumi.control-center" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.control-center/manifest.json b/hancore.shibumi.control-center/manifest.json index ce0d1ad..fa01a3e 100644 --- a/hancore.shibumi.control-center/manifest.json +++ b/hancore.shibumi.control-center/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.control-center", "name": "Shibumi Control Center", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi G1 presentation and bar configuration panel.", diff --git a/hancore.shibumi.cpu/Service.qml b/hancore.shibumi.cpu/Service.qml index 955a6d6..8074b98 100644 --- a/hancore.shibumi.cpu/Service.qml +++ b/hancore.shibumi.cpu/Service.qml @@ -13,7 +13,7 @@ Item { SuiteRuntime.Provider { id: runtimeProvider pluginId: "hancore.shibumi.cpu" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.cpu/manifest.json b/hancore.shibumi.cpu/manifest.json index eaa51a8..21e6aac 100644 --- a/hancore.shibumi.cpu/manifest.json +++ b/hancore.shibumi.cpu/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.cpu", "name": "Shibumi CPU", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi CPU widget, GPU sampler, and detailed usage panel.", diff --git a/hancore.shibumi.gpu/manifest.json b/hancore.shibumi.gpu/manifest.json index 39d23a6..5f345fe 100644 --- a/hancore.shibumi.gpu/manifest.json +++ b/hancore.shibumi.gpu/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.gpu", "name": "Shibumi GPU", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Lazy GPU load, hardware, driver, temperature, and VRAM widget.", diff --git a/hancore.shibumi.media/Service.qml b/hancore.shibumi.media/Service.qml index 68822e9..968f010 100644 --- a/hancore.shibumi.media/Service.qml +++ b/hancore.shibumi.media/Service.qml @@ -18,7 +18,7 @@ Item { SuiteRuntime.Provider { id: runtimeProvider pluginId: "hancore.shibumi.media" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.media/manifest.json b/hancore.shibumi.media/manifest.json index 2f696f2..067ec1e 100644 --- a/hancore.shibumi.media/manifest.json +++ b/hancore.shibumi.media/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.media", "name": "Shibumi Media", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi media controls over Omarchy's MPRIS service.", diff --git a/hancore.shibumi.memory/manifest.json b/hancore.shibumi.memory/manifest.json index 1f1b3ab..a6c6ced 100644 --- a/hancore.shibumi.memory/manifest.json +++ b/hancore.shibumi.memory/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.memory", "name": "Shibumi Memory", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi memory widget and detailed memory panel.", diff --git a/hancore.shibumi.network/Service.qml b/hancore.shibumi.network/Service.qml index 492d7c2..6a2f271 100644 --- a/hancore.shibumi.network/Service.qml +++ b/hancore.shibumi.network/Service.qml @@ -15,7 +15,7 @@ Item { property var manifest: null SuiteRuntime.Provider { pluginId: "hancore.shibumi.network" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.network/manifest.json b/hancore.shibumi.network/manifest.json index 5c4a249..ef75e8f 100644 --- a/hancore.shibumi.network/manifest.json +++ b/hancore.shibumi.network/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.network", "name": "Shibumi Network", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Native Shibumi NetworkManager ownership, telemetry, actions, QR, and speed testing.", diff --git a/hancore.shibumi.power-profile/manifest.json b/hancore.shibumi.power-profile/manifest.json index 597aaf4..abaa1a8 100644 --- a/hancore.shibumi.power-profile/manifest.json +++ b/hancore.shibumi.power-profile/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.power-profile", "name": "Shibumi Power Profile", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi power-profile status and profile selector.", diff --git a/hancore.shibumi.power-state/Service.qml b/hancore.shibumi.power-state/Service.qml index d8ceeb8..47f3e02 100644 --- a/hancore.shibumi.power-state/Service.qml +++ b/hancore.shibumi.power-state/Service.qml @@ -23,7 +23,7 @@ Item { SuiteRuntime.Provider { id: runtimeProvider pluginId: "hancore.shibumi.power-state" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.power-state/manifest.json b/hancore.shibumi.power-state/manifest.json index b3679b9..c0c624f 100644 --- a/hancore.shibumi.power-state/manifest.json +++ b/hancore.shibumi.power-state/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.power-state", "name": "Shibumi Power State", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shared battery and power-profile state for Shibumi widgets.", diff --git a/hancore.shibumi.quick-access/Service.qml b/hancore.shibumi.quick-access/Service.qml index ba7c513..cad04f4 100644 --- a/hancore.shibumi.quick-access/Service.qml +++ b/hancore.shibumi.quick-access/Service.qml @@ -18,7 +18,7 @@ Item { SuiteRuntime.HostShell { id: suiteShell; host: root.shell } SuiteRuntime.Provider { pluginId: "hancore.shibumi.quick-access" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.quick-access/manifest.json b/hancore.shibumi.quick-access/manifest.json index 9978be0..162c509 100644 --- a/hancore.shibumi.quick-access/manifest.json +++ b/hancore.shibumi.quick-access/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.quick-access", "name": "Shibumi Quick Access", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi idle inhibitor and theme, wallpaper, screenshot, and video pickers.", diff --git a/hancore.shibumi.reactor/Service.qml b/hancore.shibumi.reactor/Service.qml index e6f4bff..6956308 100644 --- a/hancore.shibumi.reactor/Service.qml +++ b/hancore.shibumi.reactor/Service.qml @@ -18,7 +18,7 @@ Item { SuiteRuntime.Provider { id: runtimeProvider pluginId: "hancore.shibumi.reactor" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.reactor/manifest.json b/hancore.shibumi.reactor/manifest.json index c031082..c74344b 100644 --- a/hancore.shibumi.reactor/manifest.json +++ b/hancore.shibumi.reactor/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.reactor", "name": "Shibumi Reactor", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi event and quote Reactor service for compatible bar styles.", diff --git a/hancore.shibumi.state/Service.qml b/hancore.shibumi.state/Service.qml index c93ff1f..a46f5b2 100644 --- a/hancore.shibumi.state/Service.qml +++ b/hancore.shibumi.state/Service.qml @@ -16,7 +16,7 @@ Item { SuiteRuntime.Provider { id: runtimeProvider pluginId: "hancore.shibumi.state" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.state/manifest.json b/hancore.shibumi.state/manifest.json index c55d643..d90f8f6 100644 --- a/hancore.shibumi.state/manifest.json +++ b/hancore.shibumi.state/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.state", "name": "Shibumi State", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Normalized Shibumi configuration and theme palette state.", diff --git a/hancore.shibumi.state/runtime/Runtime.qml b/hancore.shibumi.state/runtime/Runtime.qml index 09c62cc..12492e6 100644 --- a/hancore.shibumi.state/runtime/Runtime.qml +++ b/hancore.shibumi.state/runtime/Runtime.qml @@ -10,7 +10,7 @@ QtObject { id: runtime readonly property int contractVersion: 1 - readonly property string suiteVersion: "0.1.1-beta.15.3" + readonly property string suiteVersion: "0.1.1-beta.15.4" readonly property var providerIds: [ "hancore.shibumi.state", "hancore.shibumi.control-center", "hancore.shibumi.reactor", "hancore.shibumi.telemetry", diff --git a/hancore.shibumi.status/Service.qml b/hancore.shibumi.status/Service.qml index b34f361..051e0dc 100644 --- a/hancore.shibumi.status/Service.qml +++ b/hancore.shibumi.status/Service.qml @@ -15,7 +15,7 @@ Item { SuiteRuntime.HostShell { id: suiteShell; host: root.shell } SuiteRuntime.Provider { pluginId: "hancore.shibumi.status" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.status/manifest.json b/hancore.shibumi.status/manifest.json index bacc088..597b056 100644 --- a/hancore.shibumi.status/manifest.json +++ b/hancore.shibumi.status/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.status", "name": "Shibumi Status", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shibumi tray and notification composition with shared desktop status state.", diff --git a/hancore.shibumi.storage/Service.qml b/hancore.shibumi.storage/Service.qml index 0788cb1..3bc08f0 100644 --- a/hancore.shibumi.storage/Service.qml +++ b/hancore.shibumi.storage/Service.qml @@ -13,7 +13,7 @@ Item { SuiteRuntime.Provider { id: runtimeProvider pluginId: "hancore.shibumi.storage" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.storage/manifest.json b/hancore.shibumi.storage/manifest.json index 7ea9235..7163993 100644 --- a/hancore.shibumi.storage/manifest.json +++ b/hancore.shibumi.storage/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.storage", "name": "Shibumi Storage", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Read-only root usage and physical drive inventory.", diff --git a/hancore.shibumi.telemetry/Service.qml b/hancore.shibumi.telemetry/Service.qml index 2678261..a06dbb8 100644 --- a/hancore.shibumi.telemetry/Service.qml +++ b/hancore.shibumi.telemetry/Service.qml @@ -15,7 +15,7 @@ Item { SuiteRuntime.Provider { id: runtimeProvider pluginId: "hancore.shibumi.telemetry" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.telemetry/manifest.json b/hancore.shibumi.telemetry/manifest.json index 1265123..8ea771e 100644 --- a/hancore.shibumi.telemetry/manifest.json +++ b/hancore.shibumi.telemetry/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.telemetry", "name": "Shibumi Telemetry", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Demand-driven CPU and memory telemetry shared by Shibumi widgets.", diff --git a/hancore.shibumi.temperature/manifest.json b/hancore.shibumi.temperature/manifest.json index d46f0a4..dc3d636 100644 --- a/hancore.shibumi.temperature/manifest.json +++ b/hancore.shibumi.temperature/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.temperature", "name": "Shibumi Temperature", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Demand-driven hardware temperature widget and sensor panel.", diff --git a/hancore.shibumi.update-center/Service.qml b/hancore.shibumi.update-center/Service.qml index 621828d..d33d3dc 100644 --- a/hancore.shibumi.update-center/Service.qml +++ b/hancore.shibumi.update-center/Service.qml @@ -12,7 +12,7 @@ Item { property var manifest: null SuiteRuntime.Provider { pluginId: "hancore.shibumi.update-center" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.update-center/manifest.json b/hancore.shibumi.update-center/manifest.json index 403e1f8..12f311c 100644 --- a/hancore.shibumi.update-center/manifest.json +++ b/hancore.shibumi.update-center/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.update-center", "name": "Shibumi Update Center", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Safe repository package and user theme update status for Shibumi.", diff --git a/hancore.shibumi.workspaces/WorkspaceService.qml b/hancore.shibumi.workspaces/WorkspaceService.qml index 5dd1c78..d75e4da 100644 --- a/hancore.shibumi.workspaces/WorkspaceService.qml +++ b/hancore.shibumi.workspaces/WorkspaceService.qml @@ -14,7 +14,7 @@ Item { SuiteRuntime.Provider { id: runtimeProvider pluginId: "hancore.shibumi.workspaces" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: root host: root.shell manifest: root.manifest diff --git a/hancore.shibumi.workspaces/manifest.json b/hancore.shibumi.workspaces/manifest.json index 81121b7..4febb6e 100644 --- a/hancore.shibumi.workspaces/manifest.json +++ b/hancore.shibumi.workspaces/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "hancore.shibumi.workspaces", "name": "Shibumi Workspaces", - "version": "0.1.1-beta.15.3", + "version": "0.1.1-beta.15.4", "author": "HANCORE", "license": "MIT", "description": "Shared Hyprland workspace state with the Shibumi workspace presentation.", diff --git a/packaging/aur/.SRCINFO b/packaging/aur/.SRCINFO index 8d14b0b..61e6f04 100644 --- a/packaging/aur/.SRCINFO +++ b/packaging/aur/.SRCINFO @@ -1,6 +1,6 @@ pkgbase = shibumi-shell pkgdesc = Transactional Shibumi desktop shell plugins for Omarchy Quattro - pkgver = 0.1.1beta.15.3 + pkgver = 0.1.1beta.15.4 pkgrel = 1 url = https://github.com/HANCORE-linux/Shibumi-Shell arch = any @@ -30,7 +30,7 @@ pkgbase = shibumi-shell optdepends = btop: battery panel system monitor action optdepends = nvidia-utils: NVIDIA GPU telemetry optdepends = git: source-checkout diagnostics and migration - source = shibumi-shell-0.1.1-beta.15.3.tar.gz::https://github.com/HANCORE-linux/Shibumi-Shell/releases/download/v0.1.1-beta.15.3/shibumi-shell-0.1.1-beta.15.3.tar.gz + source = shibumi-shell-0.1.1-beta.15.4.tar.gz::https://github.com/HANCORE-linux/Shibumi-Shell/releases/download/v0.1.1-beta.15.4/shibumi-shell-0.1.1-beta.15.4.tar.gz sha256sums = c92e40a26557740ac0263e56604ce693585005ce3646224a1885f31b185ab5b6 pkgname = shibumi-shell diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index ef92b06..07960eb 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: HANCORE Linux pkgname=shibumi-shell -_upstream_version=0.1.1-beta.15.3 +_upstream_version=0.1.1-beta.15.4 _source_sha256=c92e40a26557740ac0263e56604ce693585005ce3646224a1885f31b185ab5b6 pkgver=${_upstream_version//-/} pkgrel=1 diff --git a/packaging/package-metadata.json b/packaging/package-metadata.json index 9e82a1b..468f6dd 100644 --- a/packaging/package-metadata.json +++ b/packaging/package-metadata.json @@ -1,5 +1,5 @@ { "schemaVersion": 1, "packageName": "shibumi-shell", - "version": "0.1.1-beta.15.3" + "version": "0.1.1-beta.15.4" } diff --git a/tests/audio-native-backend-seam-regression.qml b/tests/audio-native-backend-seam-regression.qml index 3adc06c..03dd6ad 100644 --- a/tests/audio-native-backend-seam-regression.qml +++ b/tests/audio-native-backend-seam-regression.qml @@ -174,7 +174,7 @@ ShellRoot { Audio.Service { id: service shell: serviceHost - manifest: ({id: "hancore.shibumi.audio", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.audio", version: "0.1.1-beta.15.4", kinds: ["service"]}) nativeBackendEnabled: true nativeBackendOverride: fakeBackend } diff --git a/tests/audio-runtime-smoke.qml b/tests/audio-runtime-smoke.qml index 4ff8b85..e025bef 100644 --- a/tests/audio-runtime-smoke.qml +++ b/tests/audio-runtime-smoke.qml @@ -11,7 +11,7 @@ ShellRoot { property var savedPanel: null property real originalRadius: 0 property var audioManifest: ({id: "hancore.shibumi.audio", - version: "0.1.1-beta.15.3", kinds: ["service", "bar-widget"]}) + version: "0.1.1-beta.15.4", kinds: ["service", "bar-widget"]}) function check(value, message) { if (value) return console.error("audio-runtime-smoke:", message) @@ -48,9 +48,9 @@ ShellRoot { SuiteRuntime.Provider { id: stateProvider pluginId: "hancore.shibumi.state" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: tokenState - manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"]}) } Fixtures.AudioRuntimeBackend { id: backend } Audio.Service { id: service; nativeBackendOverride: backend } diff --git a/tests/bar-catalog-consumer-smoke.qml b/tests/bar-catalog-consumer-smoke.qml index 9198e6c..ccfa61e 100644 --- a/tests/bar-catalog-consumer-smoke.qml +++ b/tests/bar-catalog-consumer-smoke.qml @@ -67,10 +67,10 @@ ShellRoot { SuiteRuntime.Provider { id: stateProvider pluginId: "hancore.shibumi.state" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: stateService host: stateHost - manifest: ({ id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", + manifest: ({ id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"] }) } @@ -92,7 +92,7 @@ ShellRoot { id: fakePluginRegistry property string pluginId: "hancore.shibumi.bar" property var manifest: ({ id: "hancore.shibumi.bar", - version: "0.1.1-beta.15.3", kinds: ["bar"] }) + version: "0.1.1-beta.15.4", kinds: ["bar"] }) property bool enabled: true property var _entryPointUrl: null readonly property var installedPlugins: { @@ -138,11 +138,11 @@ ShellRoot { property int catalogReadSerial: currentObservation ? currentObservation.serial : 0 property int catalogGeneration: currentObservation ? currentObservation.generation : 0 property var providerManifest: ({ id: "hancore.shibumi.control-center", - version: "0.1.1-beta.15.3", kinds: ["service"] }) + version: "0.1.1-beta.15.4", kinds: ["service"] }) QtObject { id: serviceHost; property string pluginId: "hancore.shibumi.control-center" } SuiteRuntime.Provider { pluginId: "hancore.shibumi.control-center" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: service host: serviceHost manifest: service.providerManifest @@ -192,7 +192,7 @@ ShellRoot { root.bar = barFactory.createObject(root, { omarchyPath: "/fixture/omarchy", shell: scopedBarHost, - manifest: ({ id: "hancore.shibumi.bar", version: "0.1.1-beta.15.3", + manifest: ({ id: "hancore.shibumi.bar", version: "0.1.1-beta.15.4", kinds: ["bar"] }), pluginRegistry: fakePluginRegistry, barWidgetRegistry: fakeWidgetRegistry, @@ -243,7 +243,7 @@ ShellRoot { // pluginsChanged signal for Bar's legacy listener to receive. fakePluginRegistry.enabled = false fakePluginRegistry.manifest = ({ id: "hancore.shibumi.bar", - version: "0.1.1-beta.15.3-metadata", kinds: ["bar"] }) + version: "0.1.1-beta.15.4-metadata", kinds: ["bar"] }) root.stage = 20; root.waits = 0 } else if (root.stage === 20) { if (root.waits < 3) return @@ -339,7 +339,7 @@ ShellRoot { } else if (root.stage === 4) { if (root.firstService.acquireCalls !== 2) return root.firstService.providerManifest = ({ id: "invalid", - version: "0.1.1-beta.15.3", kinds: ["service"] }) + version: "0.1.1-beta.15.4", kinds: ["service"] }) root.stage = 5; root.waits = 0 } else if (root.stage === 5) { if (root.firstService.releaseCalls !== 2) return @@ -365,7 +365,7 @@ ShellRoot { || root.replacementService.acquireCalls !== 1) root.fail("shutdown retained or reacquired the catalog consumer") fakePluginRegistry.manifest = ({ id: "hancore.shibumi.bar", - version: "0.1.1-beta.15.3-shutdown", kinds: ["bar"] }) + version: "0.1.1-beta.15.4-shutdown", kinds: ["bar"] }) if (root.bar.providerRegistryRevision !== providerRevision) root.fail("shutdown accepted a late scoped registry publication") root.stage = 8; root.waits = 0 diff --git a/tests/bar-visibility-ipc-smoke.qml b/tests/bar-visibility-ipc-smoke.qml index 8a32229..522c703 100644 --- a/tests/bar-visibility-ipc-smoke.qml +++ b/tests/bar-visibility-ipc-smoke.qml @@ -60,7 +60,7 @@ ShellRoot { shell: fakeShell manifest: ({ id: "hancore.shibumi.bar", - version: "0.1.1-beta.15.3", + version: "0.1.1-beta.15.4", kinds: ["bar"] }) pluginRegistry: fakePluginRegistry diff --git a/tests/catalog-service-smoke.qml b/tests/catalog-service-smoke.qml index 6702fb6..f0a4599 100644 --- a/tests/catalog-service-smoke.qml +++ b/tests/catalog-service-smoke.qml @@ -62,7 +62,7 @@ Scope { } } function manifest(valid) { - return {id: "hancore.shibumi.control-center", version: valid ? "0.1.1-beta.15.3" : "invalid", + return {id: "hancore.shibumi.control-center", version: valid ? "0.1.1-beta.15.4" : "invalid", kinds: ["service", "bar-widget"]} } function changedCatalogOutput() { diff --git a/tests/core-services-smoke.qml b/tests/core-services-smoke.qml index 714b0d0..18bac16 100644 --- a/tests/core-services-smoke.qml +++ b/tests/core-services-smoke.qml @@ -20,14 +20,14 @@ ShellRoot { id: telemetry shell: fakeShell thermalProbeEnabled: false - manifest: ({id: "hancore.shibumi.telemetry", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.telemetry", version: "0.1.1-beta.15.4", kinds: ["service"]}) } Cpu.Service { id: cpu shell: fakeShell gpuProbeEnabled: false - manifest: ({id: "hancore.shibumi.cpu", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.cpu", version: "0.1.1-beta.15.4", kinds: ["service"]}) } QtObject { @@ -40,7 +40,7 @@ ShellRoot { PowerState.Service { id: power shell: fakeShell - manifest: ({id: "hancore.shibumi.power-state", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.power-state", version: "0.1.1-beta.15.4", kinds: ["service"]}) batterySnapshotOverride: ({ready: false}) commandOverrides: ({}) // This pure projection smoke may never launch helpers. } diff --git a/tests/fixtures/native-runtime/probe.py b/tests/fixtures/native-runtime/probe.py index a0973d4..7edc276 100644 --- a/tests/fixtures/native-runtime/probe.py +++ b/tests/fixtures/native-runtime/probe.py @@ -132,7 +132,7 @@ def catalog_probe(): "actual native catalog identity DTO mismatch") control = ready["snapshot"]["byId"]["hancore.shibumi.control-center"] check(control["description"] and control["author"] == "HANCORE" - and control["version"] == "0.1.1-beta.15.3" + and control["version"] == "0.1.1-beta.15.4" and control["barWidget"]["defaultSection"] == "left", "package-bound catalog presentation metadata was lost") check(host_config() == original, "read-only catalog changed native config") diff --git a/tests/gpu-selection-state-smoke.qml b/tests/gpu-selection-state-smoke.qml index b8a22bf..ddfe107 100644 --- a/tests/gpu-selection-state-smoke.qml +++ b/tests/gpu-selection-state-smoke.qml @@ -25,13 +25,13 @@ ShellRoot { id: stateService shell: fakeShell omarchyPath: Quickshell.env("OMARCHY_PATH") - manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"]}) } Cpu.Service { id: cpuService shell: fakeShell - manifest: ({id: "hancore.shibumi.cpu", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.cpu", version: "0.1.1-beta.15.4", kinds: ["service"]}) gpuProbeEnabled: false } diff --git a/tests/host-registry-prime-smoke.qml b/tests/host-registry-prime-smoke.qml index 5ce8332..d008fc7 100644 --- a/tests/host-registry-prime-smoke.qml +++ b/tests/host-registry-prime-smoke.qml @@ -50,11 +50,11 @@ ShellRoot { Shared.Provider { id: stateProvider pluginId: "hancore.shibumi.state" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: stateOwner host: stateHost manifest: ({ id: "hancore.shibumi.state", - version: "0.1.1-beta.15.3", kinds: ["service"] }) + version: "0.1.1-beta.15.4", kinds: ["service"] }) } QtObject { @@ -77,11 +77,11 @@ ShellRoot { } Shared.Provider { pluginId: "hancore.shibumi.bar" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: owner host: owner.providerHost manifest: ({ id: "hancore.shibumi.bar", - version: "0.1.1-beta.15.3", kinds: ["bar"] }) + version: "0.1.1-beta.15.4", kinds: ["bar"] }) } } } diff --git a/tests/media-spectrum-service-smoke.qml b/tests/media-spectrum-service-smoke.qml index 4b18a54..f8a0ad3 100644 --- a/tests/media-spectrum-service-smoke.qml +++ b/tests/media-spectrum-service-smoke.qml @@ -42,7 +42,7 @@ ShellRoot { Media.Service { id: spectrum shell: fakeShell - manifest: ({id: "hancore.shibumi.media", version: "0.1.1-beta.15.3", kinds: ["service", "bar-widget"]}) + manifest: ({id: "hancore.shibumi.media", version: "0.1.1-beta.15.4", kinds: ["service", "bar-widget"]}) } Timer { diff --git a/tests/native-runtime-regression.py b/tests/native-runtime-regression.py index 85fcd06..0864b0d 100644 --- a/tests/native-runtime-regression.py +++ b/tests/native-runtime-regression.py @@ -94,7 +94,7 @@ def stage(base, native): "suiteId": "hancore.shibumi", "suitePayloadDigest": "a" * 64})) (shibumi_audio / "manifest.json").write_text(json.dumps({ "schemaVersion": 1, "id": "hancore.shibumi.audio", "name": "Shibumi Audio", - "version": "0.1.1-beta.15.3", "kinds": ["bar-widget"], + "version": "0.1.1-beta.15.4", "kinds": ["bar-widget"], "entryPoints": {"barWidget": "Widget.qml"}, "x-shibumi": {"suiteId": "hancore.shibumi"}, "barWidget": {"displayName": "Shibumi Audio", "category": "Audio", @@ -109,7 +109,7 @@ def stage(base, native): "suiteId": "hancore.shibumi", "suitePayloadDigest": "a" * 64})) (shibumi_cpu / "manifest.json").write_text(json.dumps({ "schemaVersion": 1, "id": "hancore.shibumi.cpu", "name": "Shibumi CPU", - "version": "0.1.1-beta.15.3", "kinds": ["bar-widget"], + "version": "0.1.1-beta.15.4", "kinds": ["bar-widget"], "entryPoints": {"barWidget": "Widget.qml"}, "x-shibumi": {"suiteId": "hancore.shibumi"}, "barWidget": {"displayName": "Shibumi CPU", "category": "System", diff --git a/tests/plugin-update-service-smoke.qml b/tests/plugin-update-service-smoke.qml index e4449a8..e66fb9f 100644 --- a/tests/plugin-update-service-smoke.qml +++ b/tests/plugin-update-service-smoke.qml @@ -209,7 +209,7 @@ ShellRoot { if (service.available || service.check(true)) return root.fail("scoped update worker started before manifest admission") service.manifest = {id: "hancore.shibumi.control-center", - version: "0.1.1-beta.15.3", kinds: ["service", "bar-widget"]} + version: "0.1.1-beta.15.4", kinds: ["service", "bar-widget"]} root.phase++ root.ticks = 0 return diff --git a/tests/power-deferred-smoke.qml b/tests/power-deferred-smoke.qml index 6f7a245..65d3bb6 100644 --- a/tests/power-deferred-smoke.qml +++ b/tests/power-deferred-smoke.qml @@ -57,7 +57,7 @@ ShellRoot { PowerState.Service { id: power shell: host - manifest: root.allowed ? ({id: "hancore.shibumi.power-state", version: "0.1.1-beta.15.3", kinds: ["service"]}) : null + manifest: root.allowed ? ({id: "hancore.shibumi.power-state", version: "0.1.1-beta.15.4", kinds: ["service"]}) : null batterySnapshotOverride: battery commandOverrides: ({ profiles: ["/usr/bin/python3", "-I", root.helper, root.directory, "profiles"], diff --git a/tests/power-runtime-smoke.qml b/tests/power-runtime-smoke.qml index 06afd00..6925cb7 100644 --- a/tests/power-runtime-smoke.qml +++ b/tests/power-runtime-smoke.qml @@ -24,7 +24,7 @@ ShellRoot { readonly property var power: serviceLoader.item readonly property string fixturePath: Quickshell.env("SHIBUMI_POWER_SCOPE_DIR") readonly property string helper: decodeURIComponent(String(Qt.resolvedUrl("fixtures/power-runtime-helper.py")).substring(7)) - readonly property var validManifest: ({ id: "hancore.shibumi.power-state", version: "0.1.1-beta.15.3", kinds: ["service"], entryPoints: { service: "Service.qml" } }) + readonly property var validManifest: ({ id: "hancore.shibumi.power-state", version: "0.1.1-beta.15.4", kinds: ["service"], entryPoints: { service: "Service.qml" } }) readonly property var fakeCommands: ({ profiles: ["/usr/bin/python3", "-I", helper, fixturePath, "profiles"], activeProfile: ["/usr/bin/python3", "-I", helper, fixturePath, "activeProfile"], @@ -55,10 +55,10 @@ ShellRoot { } SuiteRuntime.Provider { pluginId: "hancore.shibumi.state" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: state host: root.stateEnabled ? stateHost : null - manifest: ({ id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", kinds: ["service"] }) + manifest: ({ id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"] }) } QtObject { id: battery diff --git a/tests/power-service-runtime-smoke.qml b/tests/power-service-runtime-smoke.qml index 127e481..130733b 100644 --- a/tests/power-service-runtime-smoke.qml +++ b/tests/power-service-runtime-smoke.qml @@ -14,7 +14,7 @@ ShellRoot { PowerState.Service { id: power shell: scopedHost - manifest: ({ id: "hancore.shibumi.power-state", version: "0.1.1-beta.15.3", + manifest: ({ id: "hancore.shibumi.power-state", version: "0.1.1-beta.15.4", kinds: ["service"], entryPoints: { service: "Service.qml" } }) batterySnapshotOverride: ({ ready: false }) commandOverrides: ({ diff --git a/tests/reactor-plugin-smoke.qml b/tests/reactor-plugin-smoke.qml index 9bd897a..de5d842 100644 --- a/tests/reactor-plugin-smoke.qml +++ b/tests/reactor-plugin-smoke.qml @@ -76,7 +76,7 @@ ShellRoot { Audio.Service { id: fakeAudio shell: fakeShell - manifest: ({id: "hancore.shibumi.audio", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.audio", version: "0.1.1-beta.15.4", kinds: ["service"]}) nativeBackendEnabled: false } @@ -115,7 +115,7 @@ ShellRoot { Reactor.Service { id: reactorService - manifest: ({id: "hancore.shibumi.reactor", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.reactor", version: "0.1.1-beta.15.4", kinds: ["service"]}) shell: fakeShell runtimeProbesEnabled: false } diff --git a/tests/reactor-runtime-smoke.qml b/tests/reactor-runtime-smoke.qml index 4eb1b85..e298116 100644 --- a/tests/reactor-runtime-smoke.qml +++ b/tests/reactor-runtime-smoke.qml @@ -16,7 +16,7 @@ ShellRoot { property int beforeLoss: 0 property var cachedMode: null property var cachedTest: null - readonly property var manifest: ({id: "hancore.shibumi.reactor", version: "0.1.1-beta.15.3", kinds: ["service"]}) + readonly property var manifest: ({id: "hancore.shibumi.reactor", version: "0.1.1-beta.15.4", kinds: ["service"]}) function check(value, message) { if (value) return console.error("reactor-runtime:", message) @@ -44,14 +44,14 @@ ShellRoot { property bool outputMuted: true } SuiteRuntime.Provider { - pluginId: "hancore.shibumi.state"; implementationVersion: "0.1.1-beta.15.3" + pluginId: "hancore.shibumi.state"; implementationVersion: "0.1.1-beta.15.4" owner: state; host: root.stateEnabled ? stateHost : null - manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"]}) } SuiteRuntime.Provider { - pluginId: "hancore.shibumi.audio"; implementationVersion: "0.1.1-beta.15.3" + pluginId: "hancore.shibumi.audio"; implementationVersion: "0.1.1-beta.15.4" owner: audio; host: root.audioEnabled ? audioHost : null - manifest: ({id: "hancore.shibumi.audio", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.audio", version: "0.1.1-beta.15.4", kinds: ["service"]}) } Reactor.Service { id: service; runtimeProbesEnabled: false } Reactor.Service { id: duplicate; runtimeProbesEnabled: false } diff --git a/tests/scoped-state-regression.qml b/tests/scoped-state-regression.qml index ce49d67..f7fd806 100644 --- a/tests/scoped-state-regression.qml +++ b/tests/scoped-state-regression.qml @@ -36,7 +36,7 @@ ShellRoot { id: state shell: host omarchyPath: Quickshell.env("OMARCHY_PATH") - manifest: ({ id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", + manifest: ({ id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"], __sourceDir: Quickshell.env("DECOY_MARKER_DIR") }) } BarProbe.Marker { diff --git a/tests/shared-runtime-import-regression.py b/tests/shared-runtime-import-regression.py index 47385b5..3960a30 100644 --- a/tests/shared-runtime-import-regression.py +++ b/tests/shared-runtime-import-regression.py @@ -111,7 +111,7 @@ def test_scoped_bar_services_publish_their_real_owner(self): self.assertEqual(len(owned), 1) for contract in ("owner: root", "host: root.shell", "manifest: root.manifest", - 'implementationVersion: "0.1.1-beta.15.3"'): + 'implementationVersion: "0.1.1-beta.15.4"'): self.assertIn(contract, owned[0]) def test_near_neighbors_receive_no_exception(self): diff --git a/tests/shared-runtime-regression.qml b/tests/shared-runtime-regression.qml index b0dabe0..5798a79 100644 --- a/tests/shared-runtime-regression.qml +++ b/tests/shared-runtime-regression.qml @@ -54,7 +54,7 @@ ShellRoot { id: state shell: stateApi omarchyPath: Quickshell.env("OMARCHY_PATH") - manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"]}) } BarProbe.Marker { id: markerBar } Item { @@ -66,10 +66,10 @@ ShellRoot { Shared.Provider { id: barProvider pluginId: "hancore.shibumi.bar" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: barOwner host: barOwner.injectedHost - manifest: ({id: "hancore.shibumi.bar", version: "0.1.1-beta.15.3", kinds: ["bar"]}) + manifest: ({id: "hancore.shibumi.bar", version: "0.1.1-beta.15.4", kinds: ["bar"]}) } } Shared.HostShell { id: client; host: stateApi } @@ -82,7 +82,7 @@ ShellRoot { property int value: 0 Shared.Provider { pluginId: "hancore.shibumi.audio" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: peer host: peer.injectedHost manifest: peer.injectedManifest @@ -152,7 +152,7 @@ ShellRoot { check(left.state === null && right.state === null && !state.ready && !state.writePending, "version drift retained State authority or queued intent") check(!state.setGroupSetting("G4", "compact", true), "revoked State accepted intent") - state.manifest = {id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", kinds: ["service"]} + state.manifest = {id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"]} check(left.state === state && Shared.Runtime._selected("hancore.shibumi.state") !== oldLease, "State did not register a new lifetime") Shared.Runtime.unregisterProvider(oldLease) @@ -161,10 +161,10 @@ ShellRoot { check(first !== null && client.serviceFor("hancore.shibumi.audio") === null, "parentless service published before injection") first.injectedHost = audioApi - first.injectedManifest = {id: "hancore.shibumi.audio", version: "0.1.1-beta.15.3", kinds: ["service"]} + first.injectedManifest = {id: "hancore.shibumi.audio", version: "0.1.1-beta.15.4", kinds: ["service"]} check(client.serviceFor("hancore.shibumi.audio") === first, "provider registration failed") second = serviceFactory.createObject(null, {injectedHost: audioApi, - injectedManifest: {id: "hancore.shibumi.audio", version: "0.1.1-beta.15.3", kinds: ["service"]}}) + injectedManifest: {id: "hancore.shibumi.audio", version: "0.1.1-beta.15.4", kinds: ["service"]}}) check(client.serviceFor("hancore.shibumi.audio") === null, "duplicate provider was last-writer-wins: second=" + JSON.stringify({ manifest: second.injectedManifest, kindsArray: Array.isArray(second.injectedManifest.kinds), @@ -192,7 +192,7 @@ ShellRoot { first.injectedHost = audioApi check(client.serviceFor("hancore.shibumi.audio") === first, "scope recovery stayed unavailable") let getterReached = false - const malformed = {version: "0.1.1-beta.15.3", kinds: ["service"]} + const malformed = {version: "0.1.1-beta.15.4", kinds: ["service"]} Object.defineProperty(malformed, "id", {get: function() { getterReached = true throw new Error("controlled malformed metadata getter") diff --git a/tests/state-service-smoke.qml b/tests/state-service-smoke.qml index bf70f93..79f9762 100644 --- a/tests/state-service-smoke.qml +++ b/tests/state-service-smoke.qml @@ -55,7 +55,7 @@ ShellRoot { id: state omarchyPath: Quickshell.env("OMARCHY_PATH") shell: fakeShell - manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"]}) } Component.onCompleted: steps = [ function() { diff --git a/tests/status-plugin-smoke.qml b/tests/status-plugin-smoke.qml index 03a0237..c17c6e4 100644 --- a/tests/status-plugin-smoke.qml +++ b/tests/status-plugin-smoke.qml @@ -198,11 +198,11 @@ ShellRoot { SuiteRuntime.Provider { id: leasedBarProvider pluginId: "hancore.shibumi.bar" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: leasedBarOwner host: leasedBarOwner.providerHost manifest: ({ id: "hancore.shibumi.bar", - version: "0.1.1-beta.15.3", kinds: ["bar"] }) + version: "0.1.1-beta.15.4", kinds: ["bar"] }) } } @@ -220,7 +220,7 @@ ShellRoot { Status.Service { shell: rawScopedStatusShell manifest: ({ id: "hancore.shibumi.status", - version: "0.1.1-beta.15.3", kinds: ["service"] }) + version: "0.1.1-beta.15.4", kinds: ["service"] }) actionRunner: actionRecorder runtimeProbesEnabled: false } diff --git a/tests/telemetry-plugins-smoke.qml b/tests/telemetry-plugins-smoke.qml index 4cffbb1..ce47bb4 100644 --- a/tests/telemetry-plugins-smoke.qml +++ b/tests/telemetry-plugins-smoke.qml @@ -25,7 +25,7 @@ ShellRoot { Telemetry.Service { id: telemetryService shell: fakeShell - manifest: ({id: "hancore.shibumi.telemetry", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.telemetry", version: "0.1.1-beta.15.4", kinds: ["service"]}) // Keep this fixture isolated from machine-specific hwmon inventory. thermalProbeEnabled: false } @@ -33,7 +33,7 @@ ShellRoot { Cpu.Service { id: cpuService shell: fakeShell - manifest: ({id: "hancore.shibumi.cpu", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.cpu", version: "0.1.1-beta.15.4", kinds: ["service"]}) gpuProbeEnabled: false } diff --git a/tests/telemetry-runtime-smoke.qml b/tests/telemetry-runtime-smoke.qml index ecdb4ba..39982c2 100644 --- a/tests/telemetry-runtime-smoke.qml +++ b/tests/telemetry-runtime-smoke.qml @@ -21,7 +21,7 @@ ShellRoot { Qt.exit(1) throw new Error(message) } - function manifest(id) { return {id: id, version: "0.1.1-beta.15.3", kinds: ["service"]} } + function manifest(id) { return {id: id, version: "0.1.1-beta.15.4", kinds: ["service"]} } component ScopedHost: QtObject { property string pluginId: "" function serviceFor(id) { root.check(false, "raw scoped lookup: " + id); return null } @@ -42,7 +42,7 @@ ShellRoot { } SuiteRuntime.Provider { pluginId: "hancore.shibumi.state" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" host: stateHost owner: state manifest: root.manifest("hancore.shibumi.state") diff --git a/tests/test_lifecycle_admission.py b/tests/test_lifecycle_admission.py index 8d04f21..8deff00 100644 --- a/tests/test_lifecycle_admission.py +++ b/tests/test_lifecycle_admission.py @@ -310,6 +310,11 @@ def test_published_beta15_checkout_revisions_keep_exact_identity(self) -> None: "aaf7611d66ed5f99078fc5419bc3ba4db6164bed", "package:0.1.1-beta.15.2", ), + "public-beta.15.3": ( + "5b1d21f0cea73bb9e7997278a828b3ef295e6c94", + "db579163a4b1004a7cc09ee565cac616db9ff627", + "package:0.1.1-beta.15.3", + ), } for identity_id, revisions in published.items(): identity = next( diff --git a/tests/test_package_release.py b/tests/test_package_release.py index 5a3c2d6..9623790 100644 --- a/tests/test_package_release.py +++ b/tests/test_package_release.py @@ -72,7 +72,7 @@ def test_versions_and_all_plugin_manifests_agree(self) -> None: marker = json.loads( (ROOT / "packaging/package-metadata.json").read_text(encoding="utf-8") ) - self.assertEqual(version, "0.1.1-beta.15.3") + self.assertEqual(version, "0.1.1-beta.15.4") self.assertEqual(suite["suiteVersion"], version) self.assertEqual(marker["version"], version) for plugin in suite["plugins"]: @@ -193,7 +193,11 @@ def test_lifecycle_contract_pins_exact_release_identities(self) -> None: }, "public-beta.15.3": { "suiteVersion": "0.1.1-beta.15.3", - "sourceRevisions": ["package:0.1.1-beta.15.3"], + "sourceRevisions": [ + "5b1d21f0cea73bb9e7997278a828b3ef295e6c94", + "db579163a4b1004a7cc09ee565cac616db9ff627", + "package:0.1.1-beta.15.3", + ], "settingsStorageVersion": 1, "payloadDigest": "60e1904b23e98317e896fdd0bbf98daf44ba504f5eb22f3a8ae86ab5ab72a342", }, @@ -224,11 +228,11 @@ def test_lifecycle_contract_pins_exact_release_identities(self) -> None: for plugin_id, spec in suite.plugins.items() } self.assertEqual( - states["public-beta.15.3"]["pluginDigests"], + states["public-beta.15.4"]["pluginDigests"], current_plugin_digests, ) self.assertEqual( - states["public-beta.15.3"]["payloadDigest"], + states["public-beta.15.4"]["payloadDigest"], suite_payload_digest(current_plugin_digests), ) self.assertNotIn( diff --git a/tests/test_shibumi_suite.py b/tests/test_shibumi_suite.py index 0afd0a0..d8f376f 100755 --- a/tests/test_shibumi_suite.py +++ b/tests/test_shibumi_suite.py @@ -27,6 +27,7 @@ PUBLISHED_BETA15_REVISION = "4e91c26ebf4da07476d4be6176f29d7662fed9c1" PUBLISHED_BETA151_REVISION = "36e4b9f0de428c17248d40592461a9e3f3f750f8" PUBLISHED_BETA152_REVISION = "c45af77c8333b691ac36522247b6e5b5481a3666" +PUBLISHED_BETA153_REVISION = "5b1d21f0cea73bb9e7997278a828b3ef295e6c94" sys.path.insert(0, str(REPO_ROOT / "scripts")) from shibumi_suite.admission import ( # noqa: E402 @@ -1292,6 +1293,11 @@ def test_published_beta152_checkout_can_update_from_its_tag(self) -> None: "v0.1.1-beta.15.2", PUBLISHED_BETA152_REVISION, "public-beta.15.2" ) + def test_published_beta153_checkout_can_update_from_its_tag(self) -> None: + self.assert_published_checkout_can_update_from_tag( + "v0.1.1-beta.15.3", PUBLISHED_BETA153_REVISION, "public-beta.15.3" + ) + def test_admitted_beta15_merge_payloads_match_their_exact_tags(self) -> None: releases = ( (PUBLISHED_BETA15_REVISION, ( @@ -1304,6 +1310,9 @@ def test_admitted_beta15_merge_payloads_match_their_exact_tags(self) -> None: (PUBLISHED_BETA152_REVISION, ( "aaf7611d66ed5f99078fc5419bc3ba4db6164bed", )), + (PUBLISHED_BETA153_REVISION, ( + "db579163a4b1004a7cc09ee565cac616db9ff627", + )), ) payload_paths = ["contracts/plugin-suite-v1.json", *self.suite.plugins] for tag_revision, merges in releases: @@ -1386,17 +1395,26 @@ def test_exact_beta152_package_identity_is_admitted(self) -> None: self.assertFalse(self.paths.config_file.exists()) self.assertEqual(self.runtime.events, []) - def test_exact_beta153_package_identity_is_admitted(self) -> None: - packaged_suite = self.packaged_suite() + def test_published_beta153_package_identity_is_admitted(self) -> None: + packaged_suite = self.packaged_suite( + version="0.1.1-beta.15.3", + source_revision=PUBLISHED_BETA153_REVISION, + ) self.assertEqual( require_current_payload_identity(packaged_suite), "public-beta.15.3" ) + + def test_exact_beta154_package_identity_is_admitted(self) -> None: + packaged_suite = self.packaged_suite() + self.assertEqual( + require_current_payload_identity(packaged_suite), "public-beta.15.4" + ) self.assertFalse(self.paths.state_dir.exists()) self.assertFalse(self.paths.plugin_dir.exists()) self.assertFalse(self.paths.config_file.exists()) self.assertEqual(self.runtime.events, []) - def test_beta153_package_rejects_byte_and_mode_drift(self) -> None: + def test_beta154_package_rejects_byte_and_mode_drift(self) -> None: packaged_suite = self.packaged_suite() drift_path = self.source / "hancore.shibumi.bar/Bar.qml" payload = drift_path.read_bytes() @@ -1404,7 +1422,7 @@ def test_beta153_package_rejects_byte_and_mode_drift(self) -> None: for mutation in ("bytes", "mode"): with self.subTest(mutation=mutation): self.assertEqual( - require_current_payload_identity(packaged_suite), "public-beta.15.3" + require_current_payload_identity(packaged_suite), "public-beta.15.4" ) try: if mutation == "bytes": @@ -1424,7 +1442,7 @@ def test_beta153_package_rejects_byte_and_mode_drift(self) -> None: drift_path.write_bytes(payload) drift_path.chmod(mode) self.assertEqual( - require_current_payload_identity(packaged_suite), "public-beta.15.3" + require_current_payload_identity(packaged_suite), "public-beta.15.4" ) def test_mutated_beta151_package_bytes_are_rejected(self) -> None: @@ -1492,7 +1510,7 @@ def test_mutated_beta141_package_identity_is_rejected(self) -> None: self.assertEqual(self.runtime.events, []) def test_unknown_future_package_identity_is_rejected(self) -> None: - packaged_suite = self.packaged_suite("0.1.1-beta.15.4") + packaged_suite = self.packaged_suite("0.1.1-beta.15.5") with self.assertRaisesRegex( AdmissionError, "exact declared revision identity" @@ -1725,7 +1743,7 @@ def inject_retired_app_menu(self) -> None: def packaged_suite( self, - version: str = "0.1.1-beta.15.3", + version: str = "0.1.1-beta.15.4", source_revision: str | None = None, ) -> Suite: suite_contract_path = self.source / "contracts/plugin-suite-v1.json" @@ -2028,8 +2046,8 @@ def test_package_install_records_authoritative_package_origin(self) -> None: state = load_install_state(self.paths, suite) self.assertEqual(state["installOrigin"], "package") self.assertEqual(state["packageName"], "shibumi-shell") - self.assertEqual(state["packageVersion"], "0.1.1-beta.15.3") - self.assertEqual(state["sourceRevision"], "package:0.1.1-beta.15.3") + self.assertEqual(state["packageVersion"], "0.1.1-beta.15.4") + self.assertEqual(state["sourceRevision"], "package:0.1.1-beta.15.4") self.assertNotIn("sourceRoot", state) self.assertEqual(state["payloadRoot"], str(self.source.resolve())) @@ -2048,7 +2066,7 @@ def test_update_migrates_checkout_install_to_package_origin(self) -> None: package_state = load_install_state(self.paths, suite) self.assertEqual(package_state["installOrigin"], "package") self.assertEqual(package_state["packageName"], "shibumi-shell") - self.assertEqual(package_state["packageVersion"], "0.1.1-beta.15.3") + self.assertEqual(package_state["packageVersion"], "0.1.1-beta.15.4") self.assertNotIn("sourceRoot", package_state) def test_sandbox_update_advances_beta_7_to_beta_9(self) -> None: @@ -2120,7 +2138,7 @@ def test_sandbox_update_advances_beta_7_to_beta_9(self) -> None: plugin_id: spec.payload_digest() for plugin_id, spec in self.suite.plugins.items() } - self.assertEqual(updated["suiteVersion"], "0.1.1-beta.15.3") + self.assertEqual(updated["suiteVersion"], "0.1.1-beta.15.4") self.assertEqual(updated["sourceRoot"], str(self.source.resolve())) self.assertEqual(updated["pluginDigests"], expected_digests) self.assertEqual(len(updated["plugins"]), 24) @@ -2138,7 +2156,7 @@ def test_sandbox_update_advances_beta_7_to_beta_9(self) -> None: encoding="utf-8" ) ) - self.assertEqual(manifest["version"], "0.1.1-beta.15.3") + self.assertEqual(manifest["version"], "0.1.1-beta.15.4") def test_locked_update_discards_staging_without_live_reconciliation(self) -> None: self.install() @@ -2347,7 +2365,7 @@ def test_update_and_repair_accept_semver_build_metadata(self) -> None: for operation in (command_update, command_repair): with self.subTest(operation=operation.__name__): state = json.loads(state_path.read_text(encoding="utf-8")) - state["suiteVersion"] = "0.1.1-beta.15.3+installed.9" + state["suiteVersion"] = "0.1.1-beta.15.4+installed.9" state_path.write_text( json.dumps(state, indent=2) + "\n", encoding="utf-8" ) @@ -2356,7 +2374,7 @@ def test_update_and_repair_accept_semver_build_metadata(self) -> None: 0, ) updated = json.loads(state_path.read_text(encoding="utf-8")) - self.assertEqual(updated["suiteVersion"], "0.1.1-beta.15.3") + self.assertEqual(updated["suiteVersion"], "0.1.1-beta.15.4") self.assertEqual( version_key("1.0.0+build.7"), @@ -2419,9 +2437,9 @@ def test_explicit_package_rollback_stages_older_payload_transactionally(self) -> ) rolled_back = load_install_state(self.paths, suite) - self.assertEqual(rolled_back["suiteVersion"], "0.1.1-beta.15.3") - self.assertEqual(rolled_back["packageVersion"], "0.1.1-beta.15.3") - self.assertEqual(rolled_back["sourceRevision"], "package:0.1.1-beta.15.3") + self.assertEqual(rolled_back["suiteVersion"], "0.1.1-beta.15.4") + self.assertEqual(rolled_back["packageVersion"], "0.1.1-beta.15.4") + self.assertEqual(rolled_back["sourceRevision"], "package:0.1.1-beta.15.4") def test_rescan_uses_shell_ipc_contract(self) -> None: runtime = OmarchyRuntime() diff --git a/tests/theme-palette-runtime-smoke.qml b/tests/theme-palette-runtime-smoke.qml index 84002a2..2e4144a 100644 --- a/tests/theme-palette-runtime-smoke.qml +++ b/tests/theme-palette-runtime-smoke.qml @@ -26,7 +26,7 @@ ShellRoot { id: state shell: fakeShell omarchyPath: Quickshell.env("OMARCHY_PATH") - manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"]}) } function colorText(value) { diff --git a/tests/third-party-integration-smoke.qml b/tests/third-party-integration-smoke.qml index fbaa619..9a33590 100644 --- a/tests/third-party-integration-smoke.qml +++ b/tests/third-party-integration-smoke.qml @@ -38,14 +38,14 @@ ShellRoot { AudioOwner.Service { shell: fakeShell nativeBackendEnabled: false // Controlled peak stub; this fixture tests IPC. - manifest: ({ id: "hancore.shibumi.audio", version: "0.1.1-beta.15.3", kinds: ["service"] }) + manifest: ({ id: "hancore.shibumi.audio", version: "0.1.1-beta.15.4", kinds: ["service"] }) } StateOwner.Service { shell: fakeShell omarchyPath: "" manifest: ({ - id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", kinds: ["service"] + id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"] }) } diff --git a/tests/widget-pipeline-diagnostics-smoke.qml b/tests/widget-pipeline-diagnostics-smoke.qml index 86ae264..08d533e 100644 --- a/tests/widget-pipeline-diagnostics-smoke.qml +++ b/tests/widget-pipeline-diagnostics-smoke.qml @@ -237,11 +237,11 @@ ShellRoot { Shared.Provider { id: stateProvider pluginId: "hancore.shibumi.state" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: stateOwner host: stateHost manifest: ({ id: "hancore.shibumi.state", - version: "0.1.1-beta.15.3", kinds: ["service"] }) + version: "0.1.1-beta.15.4", kinds: ["service"] }) } QtObject { @@ -290,7 +290,7 @@ ShellRoot { barWidgetRegistry: scopedWidgetRegistry manifest: ({ id: "hancore.shibumi.bar", - version: "0.1.1-beta.15.3", + version: "0.1.1-beta.15.4", kinds: ["bar"] }) barConfig: ({ diff --git a/tests/workspaces-plugin-smoke.qml b/tests/workspaces-plugin-smoke.qml index 178805c..a25a6eb 100644 --- a/tests/workspaces-plugin-smoke.qml +++ b/tests/workspaces-plugin-smoke.qml @@ -125,7 +125,7 @@ ShellRoot { Workspaces.WorkspaceService { id: workspaceState shell: fakeShell - manifest: ({id: "hancore.shibumi.workspaces", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.workspaces", version: "0.1.1-beta.15.4", kinds: ["service"]}) stateService: preferenceState backendOverride: QtObject { property var focusedWorkspace: ({ id: 8 }) diff --git a/tests/workspaces-runtime-smoke.qml b/tests/workspaces-runtime-smoke.qml index 841c716..29ce650 100644 --- a/tests/workspaces-runtime-smoke.qml +++ b/tests/workspaces-runtime-smoke.qml @@ -15,7 +15,7 @@ ShellRoot { property var cachedPreference: null property int callsBeforeLoss: 0 property int writesBeforeLoss: 0 - readonly property var validManifest: ({id: "hancore.shibumi.workspaces", version: "0.1.1-beta.15.3", kinds: ["service"]}) + readonly property var validManifest: ({id: "hancore.shibumi.workspaces", version: "0.1.1-beta.15.4", kinds: ["service"]}) function check(value, message) { if (value) return console.error("workspaces-runtime:", message) @@ -44,10 +44,10 @@ ShellRoot { } SuiteRuntime.Provider { pluginId: "hancore.shibumi.state" - implementationVersion: "0.1.1-beta.15.3" + implementationVersion: "0.1.1-beta.15.4" owner: state host: root.stateEnabled ? stateHost : null - manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.3", kinds: ["service"]}) + manifest: ({id: "hancore.shibumi.state", version: "0.1.1-beta.15.4", kinds: ["service"]}) } QtObject { id: backend From ea7713dfe100f9e0405b90569c4e78747f976405 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 02/40] fix: render Hearthstone picker card titles sharply while cards are transformed --- hancore.shibumi.quick-access/HearthstonePickerView.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hancore.shibumi.quick-access/HearthstonePickerView.qml b/hancore.shibumi.quick-access/HearthstonePickerView.qml index 5ad96a3..8f738a4 100644 --- a/hancore.shibumi.quick-access/HearthstonePickerView.qml +++ b/hancore.shibumi.quick-access/HearthstonePickerView.qml @@ -184,7 +184,7 @@ Item { horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight maximumLineCount: 1 - renderType: Text.NativeRendering + renderType: Text.QtRendering } Rectangle { From 16850c887cb7321f1723629a7427a3961bccfcbe Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 03/40] fix: keep the Health route visible when the compact Control Center shows Health --- .../ControlSettings.qml | 3 +- tests/control-center-regression.sh | 2 ++ tests/control-center-smoke.qml | 35 +++++++++++++++++-- tests/fixtures/ControlCenterTestPanel.qml | 16 +++++++++ tests/panel-window-geometry-regression.sh | 7 ++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/hancore.shibumi.control-center/ControlSettings.qml b/hancore.shibumi.control-center/ControlSettings.qml index 07feaaf..fa0bda1 100644 --- a/hancore.shibumi.control-center/ControlSettings.qml +++ b/hancore.shibumi.control-center/ControlSettings.qml @@ -166,9 +166,10 @@ Item { && configureDetailPage === "health" && settingsQuery.trim() === "" && pageLoader.item !== null + // Short reports must not shrink away the left navigation's Health route. readonly property real compactHealthPanelHeight: configureDetailPanelChromeHeight - + Math.max(1, Number(pageLoader.item + + Math.max(1, configureLanding.implicitHeight, Number(pageLoader.item ? pageLoader.item.implicitHeight : 1)) readonly property bool compactPickersPage: currentPage === "configure" diff --git a/tests/control-center-regression.sh b/tests/control-center-regression.sh index a38f1cd..496c403 100755 --- a/tests/control-center-regression.sh +++ b/tests/control-center-regression.sh @@ -35,6 +35,8 @@ install -Dm0644 "$repo_root/tests/fixtures/PluginUpdateTestService.qml" \ install -Dm0755 "$repo_root/tests/fixtures/slow-health-report" \ "$tmpdir/home/.config/omarchy/plugins/hancore.shibumi.control-center/manager/shibumi-health" mkdir -m 700 "$tmpdir/runtime" +"$repo_root/tests/panel-window-geometry-regression.sh" \ + --stage-control-center "$tmpdir/control" set +e output=$(timeout 8 env \ diff --git a/tests/control-center-smoke.qml b/tests/control-center-smoke.qml index e115b2c..9fed684 100644 --- a/tests/control-center-smoke.qml +++ b/tests/control-center-smoke.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import qs.Commons as Commons import "hancore.shibumi.state" as State import "control" as Control import "control/HostIdentity.js" as HostIdentity @@ -14,6 +15,9 @@ ShellRoot { property var clickTargets: [] property int barsRouteStep: 0 property int iconsNoScrollStep: 0 + property var healthFitProbe: null + property bool healthFitOk: true + Component { id: healthFitComponent; Control.ControlCenterPanel {} } property bool panelIdempotenceStarted: false property bool requestedPreviewChecked: false property var stablePanelItem: null @@ -118,6 +122,7 @@ ShellRoot { id: fakeBar property var shell: fakeShell + property var layoutController: ({v2Mode: false}) property bool vertical: false property int barSize: 35 property string position: "top" @@ -143,6 +148,10 @@ ShellRoot { property var visualTokens: ({ shellStyle: "shibumi", v2Shell: false, + ink: "#eeeeee", + seal: "#d75f5f", + fontFamily: "monospace", + tileRadius: 6, pillHeight: 24, pillRadius: 12, pill: "#332f2f", @@ -1594,9 +1603,9 @@ ShellRoot { const v1HealthPanelHeight = panel.compactHealthPanelHeight panel.v2LayoutActive = true if (!panel.compactHealthPage - || Math.abs(panel.compactHealthPanelHeight + || (panel.compactHealthPanelHeight - health.implicitHeight - - panel.configureDetailPanelChromeHeight) > 0.5 + - panel.configureDetailPanelChromeHeight) < -0.5 || Math.abs(panel.compactHealthPanelHeight - v1HealthPanelHeight) > 0.5) return root.fail("Health did not fit its content height") @@ -2321,6 +2330,28 @@ ShellRoot { return root.fail("V2 Icons selection requires scrolling" + " actual=" + panel.compactIconsSelectionPanelHeight + " required=" + requiredV2) + if (!root.healthFitProbe) { + Commons.Style.fontBaseSize = 11 + root.healthFitProbe = healthFitComponent.createObject(root, { + anchorItem: widget, bar: fakeBar, ownerWidget: widget, stateService: stateService, + healthService: panel.healthService, switchService: panel.switchService, pluginUpdateService: pluginUpdateService, open: true + }) + if (!root.healthFitProbe) return root.fail("real Health fit component unavailable") + root.healthFitProbe.showSettingsPage("health") + root.ticks = 0; return + } + if (root.ticks < 9) return + console.log("HEALTH_FIT_CONTEXT", Commons.Style.fontBaseSize, root.healthFitProbe.availableCardHeight, root.healthFitProbe.settingsPage) + root.healthFitOk = panel.healthGeometryFits(root.healthFitProbe) && root.healthFitOk + if (!root.healthFitProbe.v2LayoutActive) { + fakeBar.layoutController = {v2Mode: true} + fakeBar.visualTokens = Object.assign({}, fakeBar.visualTokens, {v2Shell: true, shellStyle: "full"}) + root.ticks = 0; return + } + if (!root.healthFitOk) return root.fail("Health route/page clipped at actual 11px V1/V2 fit height") + root.healthFitProbe.destroy(); Commons.Style.fontBaseSize = 12 + fakeBar.layoutController = {v2Mode: false} + fakeBar.visualTokens = Object.assign({}, fakeBar.visualTokens, {v2Shell: false, shellStyle: "shibumi"}) appearance.controller.resetGroupAppearance("G4") panel.v2LayoutActive = false fakeShell.activeBarId = "" diff --git a/tests/fixtures/ControlCenterTestPanel.qml b/tests/fixtures/ControlCenterTestPanel.qml index aae3a72..52c7c30 100644 --- a/tests/fixtures/ControlCenterTestPanel.qml +++ b/tests/fixtures/ControlCenterTestPanel.qml @@ -648,6 +648,22 @@ Item { return null } + function healthGeometryFits(panel) { + const label = Array.from(panel.panelContent) + .map(item => findTextItem(item, "Health")).find(item => item) + if (!label || !panel.settingsPageItem) return false + let fits = true + for (const item of [label.parent.parent.parent, panel.settingsPageItem]) + for (let clip = item.parent; clip; clip = clip.parent) + if (clip.clip) { + const bottom = item.mapToItem(clip, 0, item.height).y + console.log("HEALTH_FIT", panel.v2LayoutActive, panel.contentHeight, + item === panel.settingsPageItem ? "page" : "route", bottom, clip.height) + fits = fits && bottom <= clip.height + 0.5 + } + return fits + } + function findTextInput(item) { if (!item) return null if (item.text !== undefined && item.cursorPosition !== undefined diff --git a/tests/panel-window-geometry-regression.sh b/tests/panel-window-geometry-regression.sh index 97c858f..6f20b97 100755 --- a/tests/panel-window-geometry-regression.sh +++ b/tests/panel-window-geometry-regression.sh @@ -206,6 +206,13 @@ for directory in (base / "Ui", base / "shared"): qmldir.write_text(current + "\nFixturePanelWindow 1.0 FixturePanelWindow.qml\n", encoding="utf-8") PY +if [[ ${1:-} == --stage-control-center ]]; then + [[ $# -eq 2 && $2 == /tmp/shibumi-control-center.*/control && -d $2 ]] \ + || fail 'expected a private Control Center staging directory' + cp -- "$base/shared/ShibumiPanel.qml" "$base/shared/FixturePanelWindow.qml" "$2/" + exit 0 +fi + run_case() { local name=$1 expected=$2 diagnostic=$3 local root="$tmpdir/$name" From 970e445be0935ef97c08b572690557d7a931e85d Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 04/40] feat: show every AI usage window with reset countdown, pace and daily tokens --- hancore.shibumi.ai/AgentUsageModel.js | 16 ++- hancore.shibumi.ai/AiUsagePanel.qml | 167 ++++++++++++++++---------- hancore.shibumi.ai/Service.qml | 113 +++++++---------- tests/ai-plugin-regression.sh | 10 +- tests/ai-plugin-smoke.qml | 58 +++++++-- tests/fixtures/AiPanelHost.qml | 2 +- 6 files changed, 214 insertions(+), 152 deletions(-) diff --git a/hancore.shibumi.ai/AgentUsageModel.js b/hancore.shibumi.ai/AgentUsageModel.js index dd35d8b..9623ea5 100644 --- a/hancore.shibumi.ai/AgentUsageModel.js +++ b/hancore.shibumi.ai/AgentUsageModel.js @@ -72,7 +72,7 @@ function validRecordShape(record, expectedId) { function hasDisplayData(record, limits) { // All-time counters establish that this fresh record represents an actually - // used provider, but they are never copied into Shibumi's current model rows. + // used provider; all-time model rows do not imply current activity. return limits.length > 0 || nonNegativeNumber(record.todayPrompts) > 0 || nonNegativeNumber(record.todaySessions) > 0 @@ -109,6 +109,7 @@ function limitSnapshot(limits, index) { label: boundedString(limit.label, index === 0 ? "Primary" : "Secondary"), percent: limit.percent, + title: typeof limit.title === "string" ? boundedString(limit.title, "") : "", resetsAt: boundedString(limit.resetsAt, "") } } @@ -133,7 +134,7 @@ function parseRecord(raw, expectedId, nowMs) { for (var index = 0; index < rawLimits.length; index++) { var limit = limitSnapshot(rawLimits, index) if (!limit) return null - if (limits.length < 2) limits.push(limit) + if (limits.length < 6) limits.push(limit) } if (!hasDisplayData(record, limits)) return null @@ -143,6 +144,13 @@ function parseRecord(raw, expectedId, nowMs) { providerId: boundedString(record.id, ""), providerName: boundedString(record.name, record.id), ready: record.ready === true, + limits: limits, + recentDays: (Array.isArray(record.recentDays) ? record.recentDays : []) + .filter(day => isPlainObject(day) && typeof day.date === "string" && validTimestamp(day.date + "T00:00:00Z") + && isNonNegativeRecordNumber(day.messageCount)) + .map(day => ({ date: day.date, messageCount: day.messageCount })) + .sort((a, b) => a.date.localeCompare(b.date)).slice(-7), + modelUsage: record.modelUsage, rateLimitPercent: primary ? primary.percent : -1, rateLimitLabel: primary ? primary.label : "", rateLimitResetAt: primary ? primary.resetsAt : "", @@ -157,9 +165,7 @@ function parseRecord(raw, expectedId, nowMs) { activeDays: nonNegativeNumber(record.activeDays), windowTokens: 0, hourlyTokens: 0, - // Keep the existing Shibumi presentation stable. Omarchy's agents record - // exposes all-time model aggregates, while the current panel's model rows - // are explicitly labeled as recent activity. + // The service formats modelUsage separately as all-time aggregates. models: [], tierLabel: boundedString(record.tierLabel, ""), usageStatusText: boundedString(record.usageStatusText, ""), diff --git a/hancore.shibumi.ai/AiUsagePanel.qml b/hancore.shibumi.ai/AiUsagePanel.qml index d5a09c9..dbcf454 100644 --- a/hancore.shibumi.ai/AiUsagePanel.qml +++ b/hancore.shibumi.ai/AiUsagePanel.qml @@ -12,8 +12,31 @@ ShibumiPanel { required property var aiService readonly property var provider: aiService ? aiService.selectedProvider : null readonly property int renderedProviderCount: providerTabs.count - readonly property bool primaryUsageVisible: primaryUsage.visible - readonly property bool secondaryUsageVisible: secondaryUsage.visible + readonly property var limitWindows: aiService ? aiService.limitWindows(provider) : [] + readonly property var headline: aiService ? aiService.bindingWindow(provider) : null + readonly property bool headlineAlarm: !!headline && headline.percent >= 90 + readonly property color headlineColor: headlineUsage.usageColor + readonly property bool primaryUsageVisible: limitWindows.length > 0 + readonly property bool secondaryUsageVisible: limitWindows.length > 1 + readonly property var days: provider ? provider.recentDays || [] : [] + readonly property int renderedDayCount: dayRepeater.count + readonly property bool clockRunning: panelClock.running + property double nowMs: Date.now() + onOpenChanged: if (open) nowMs = Date.now() + property Timer presentationClock: Timer { + id: panelClock + interval: 30000; repeat: true; running: panel.open + onTriggered: panel.nowMs = Date.now() + } + readonly property bool limitsUnavailableVisible: limitsUnavailable.visible + readonly property string todaySummary: provider && aiService ? [ + Number(provider.todayTotalTokens) > 0 + ? aiService.formatTokens(provider.todayTotalTokens) + " tokens" : "", + Number(provider.todayPrompts) > 0 ? Math.round(Number(provider.todayPrompts)) + + (Number(provider.todayPrompts) === 1 ? " prompt" : " prompts") : "", + Number(provider.todaySessions) > 0 ? Math.round(Number(provider.todaySessions)) + + (Number(provider.todaySessions) === 1 ? " session" : " sessions") : "" + ].filter(value => value !== "").join(" · ") : "" readonly property var providerModels: provider && Array.isArray(provider.models) ? provider.models : [] readonly property int renderedModelCount: modelRepeater.count @@ -55,7 +78,17 @@ ShibumiPanel { function resetText(timestamp) { return provider && aiService - ? aiService.resetText(provider, timestamp) : "" + ? aiService.resetText(provider, timestamp, nowMs) : "" + } + + function paceText(limit) { + const label = String(limit.label || "") + " " + String(limit.title || "") + const match = /\b(\d+(?:\.\d+)?)\s*-?\s*(hours?|h|days?|d)\b/i.exec(label) + const span = match ? Number(match[1]) * (/^h/i.test(match[2]) ? 3600000 : 86400000) + : /\bweekly\b/i.test(label) ? 604800000 : /\bmonthly\b/i.test(label) ? 2592000000 : 0 + const remaining = Date.parse(limit.resetsAt) - nowMs, elapsed = span - remaining + return providerReady && span > 0 && remaining > 0 && elapsed > 0 + ? "Pace: " + (limit.percent / 100 * span / elapsed).toFixed(1) + "×" : "" } function providerTabLabel(providerValue) { @@ -117,7 +150,10 @@ ShibumiPanel { Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - text: "AI USAGE" + width: parent.width - actionRow.width - Commons.Style.space(8) + elide: Text.ElideRight + text: panel.provider ? panel.providerHeading(panel.provider) + + " · " + panel.providerStatusLabel : "AI USAGE" color: panel.controlForeground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: Commons.Style.font.subtitle @@ -208,33 +244,20 @@ ShibumiPanel { } Item { - visible: panel.provider !== null + visible: panel.tierLabel(panel.provider) !== "" width: parent.width height: visible ? Commons.Style.space(16) : 0 Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - text: panel.provider ? panel.providerHeading(panel.provider) - + (panel.tierLabel(panel.provider) !== "" - ? " · " + panel.tierLabel(panel.provider) : "") : "" + text: panel.tierLabel(panel.provider) color: panel.controlForeground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: Commons.Style.font.body font.weight: Font.Medium renderType: Text.NativeRendering } - - Text { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - text: panel.providerStatusLabel - color: panel.providerStatusLabel === "live" ? panel.controlMuted - : panel.controlAccent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } } Text { @@ -249,36 +272,36 @@ ShibumiPanel { } UsageRow { - id: primaryUsage - visible: panel.provider && Number(panel.provider.rateLimitPercent) >= 0 - label: panel.provider ? String(panel.provider.rateLimitLabel || "Primary") : "" - value: panel.provider ? panel.displayPercent(panel.provider, - panel.provider.rateLimitPercent) : 0 - dimmed: !panel.providerReady - } - - UsageRow { - id: secondaryUsage - visible: panel.provider && Number(panel.provider.secondaryRateLimitPercent) >= 0 - label: panel.provider - ? String(panel.provider.secondaryRateLimitLabel || "Secondary") : "" - value: panel.provider ? panel.displayPercent(panel.provider, - panel.provider.secondaryRateLimitPercent) : 0 + id: headlineUsage + visible: !!panel.headline + label: panel.headline ? panel.headline.title : "" + value: panel.headline ? panel.headline.percent : 0 dimmed: !panel.providerReady } - - DetailRow { - visible: primaryUsage.visible && panel.resetText( - panel.provider.rateLimitResetAt) !== "" - label: primaryUsage.label + " resets in" - value: visible ? panel.resetText(panel.provider.rateLimitResetAt) : "" + Repeater { + model: panel.limitWindows + delegate: Column { + id: windowRow + required property var modelData + width: contentColumn.width + spacing: Commons.Style.space(4) + UsageRow { + label: windowRow.modelData.title + value: windowRow.modelData.percent + dimmed: !panel.providerReady + } + DetailRow { + label: panel.resetText(windowRow.modelData.resetsAt) ? "Resets in" : "" + value: [panel.resetText(windowRow.modelData.resetsAt), panel.paceText(windowRow.modelData)].filter(Boolean).join(" · ") + wrapValue: true + } + } } DetailRow { - visible: secondaryUsage.visible && panel.resetText( - panel.provider.secondaryRateLimitResetAt) !== "" - label: secondaryUsage.label + " resets in" - value: visible ? panel.resetText( - panel.provider.secondaryRateLimitResetAt) : "" + id: limitsUnavailable + visible: panel.provider && !panel.providerHasUsage + label: "Limits" + value: "Unavailable" } DetailRow { visible: panel.provider && String(panel.provider.usageStatusText || "") !== "" @@ -301,22 +324,10 @@ ShibumiPanel { ? panel.aiService.formatTokens(panel.provider.hourlyTokens) + "/h" : "" } DetailRow { - visible: panel.provider && Number(panel.provider.todayTotalTokens) > 0 + visible: panel.todaySummary !== "" label: "Today" - value: visible && panel.aiService - ? panel.aiService.formatTokens(panel.provider.todayTotalTokens) + " tokens" : "" - } - DetailRow { - visible: panel.provider && Number(panel.provider.todayPrompts) > 0 - label: "Today prompts" - value: visible ? String(Math.round( - Number(panel.provider.todayPrompts) || 0)) : "" - } - DetailRow { - visible: panel.provider && Number(panel.provider.todaySessions) > 0 - label: "Today sessions" - value: visible ? String(Math.round( - Number(panel.provider.todaySessions) || 0)) : "" + value: panel.todaySummary + wrapValue: true } DetailRow { visible: panel.provider && String(panel.provider.latestModel || "") !== "" @@ -325,6 +336,21 @@ ShibumiPanel { value: panel.provider ? String(panel.provider.latestModel || "") : "" } + DetailRow { + visible: panel.days.length > 0 + label: "TOKENS BY DAY" + value: "" + } + Repeater { + id: dayRepeater + model: panel.days + delegate: ModelUsageRow { + required property var modelData + width: contentColumn.width + entry: ({ name: modelData.date, totalLabel: panel.aiService.formatTokens(modelData.messageCount), + pct: modelData.messageCount / Math.max(1, ...panel.days.map(day => day.messageCount)) * 100, detail: "" }) + } + } Item { visible: panel.providerModels.length > 0 width: parent.width @@ -344,7 +370,7 @@ ShibumiPanel { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: panel.provider && String(panel.provider.providerId || "") - === "opencode" ? "today" : "recent" + === "opencode" ? "today" : panel.provider && panel.provider.backend === "omarchy.agents" ? "all time" : "recent" color: panel.controlMuted font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: Commons.Style.font.caption @@ -404,6 +430,8 @@ ShibumiPanel { required property string label required property real value property bool dimmed: false + readonly property color usageColor: dimmed ? panel.controlMuted + : value >= 90 ? Commons.Color.urgent : panel.controlAccent width: parent.width height: Commons.Style.space(16) @@ -412,6 +440,9 @@ ShibumiPanel { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter text: usageRow.label + textFormat: Text.PlainText + width: Math.min(implicitWidth, parent.width * 0.45) + elide: Text.ElideRight color: panel.controlMutedHigh font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: Commons.Style.font.bodySmall @@ -424,7 +455,7 @@ ShibumiPanel { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: Math.round(usageRow.value) + "%" - color: usageRow.dimmed ? panel.controlMuted : panel.controlAccent + color: usageRow.usageColor font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: Commons.Style.font.bodySmall font.weight: Font.Medium @@ -444,7 +475,7 @@ ShibumiPanel { width: parent.width * Math.max(0, Math.min(100, usageRow.value)) / 100 height: parent.height radius: height / 2 - color: panel.controlAccent + color: usageRow.usageColor Behavior on width { NumberAnimation { duration: 300; easing.type: Easing.OutCubic } } } } @@ -453,8 +484,10 @@ ShibumiPanel { component DetailRow: Row { required property string label required property string value + property bool wrapValue: false width: parent.width - height: Commons.Style.space(16) + height: wrapValue ? Math.max(Commons.Style.space(16), detailValue.implicitHeight) + : Commons.Style.space(16) Text { width: parent.width * 0.45 @@ -465,7 +498,9 @@ ShibumiPanel { renderType: Text.NativeRendering } Text { + id: detailValue width: parent.width * 0.55 + wrapMode: parent.wrapValue ? Text.WordWrap : Text.NoWrap horizontalAlignment: Text.AlignRight elide: Text.ElideLeft text: parent.value @@ -489,6 +524,7 @@ ShibumiPanel { anchors.top: parent.top width: parent.width * 0.68 text: String(modelRow.entry && modelRow.entry.name || "") + textFormat: Text.PlainText elide: Text.ElideRight color: panel.controlForeground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family @@ -530,7 +566,8 @@ ShibumiPanel { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - text: "I " + String(modelRow.entry && modelRow.entry.inputLabel || "0") + text: modelRow.entry && modelRow.entry.detail !== undefined ? modelRow.entry.detail + : "I " + String(modelRow.entry && modelRow.entry.inputLabel || "0") + " O " + String(modelRow.entry && modelRow.entry.outputLabel || "0") + (String(modelRow.entry && modelRow.entry.reasoningLabel || "0") !== "0" ? " R " + String(modelRow.entry.reasoningLabel) : "") diff --git a/hancore.shibumi.ai/Service.qml b/hancore.shibumi.ai/Service.qml index c575c02..1cded17 100644 --- a/hancore.shibumi.ai/Service.qml +++ b/hancore.shibumi.ai/Service.qml @@ -270,6 +270,35 @@ Item { return result } + function agentModels(usage) { + const rows = Object.keys(usage || {}).map(name => { + const data = usage[name] || {}, row = { name: name }, fields = { + input: "inputTokens", output: "outputTokens", cacheRead: "cacheReadInputTokens", cacheWrite: "cacheCreationInputTokens" } + let total = 0 + for (const key in fields) { + const value = AgentUsageModel.nonNegativeNumber(data[fields[key]]) + total += value; row[key + "Label"] = formatTokens(value) + } + return Object.assign(row, { total: total, totalLabel: formatTokens(total) }) + }).filter(row => row.total > 0).sort((a, b) => b.total - a.total).slice(0, 16) + return rows.map(row => Object.assign(row, { pct: row.total / rows[0].total * 100 })) + } + + function limitWindows(provider) { + if (!provider) return [] + const limits = provider.limits || [ + { label: provider.rateLimitLabel || "Primary", percent: provider.rateLimitPercent, resetsAt: provider.rateLimitResetAt }, + { label: provider.secondaryRateLimitLabel || "Secondary", percent: provider.secondaryRateLimitPercent, resetsAt: provider.secondaryRateLimitResetAt }] + return limits.slice(0, 6).map(limit => Object.assign({}, limit, { + title: limit.title || String(limit.label).replace(/\s*\(.*\)\s*/, "").trim(), + percent: displayPercent(provider, limit.percent) + })).filter(limit => limit.percent >= 0) + } + + function bindingWindow(provider) { + return limitWindows(provider).reduce((best, limit) => !best || limit.percent > best.percent ? limit : best, null) + } + function agentRecordFresh(record, nowMs) { if (!record || String(record.backend || "") !== "omarchy.agents") return true @@ -306,7 +335,9 @@ Item { todaySessions: Math.max(0, Number(provider.todaySessions) || 0), windowTokens: Math.max(0, Number(provider.windowTokens) || 0), hourlyTokens: Math.max(0, Number(provider.hourlyTokens) || 0), - models: cloneModels(provider.models), + limits: provider.limits, + recentDays: provider.recentDays || [], + models: cloneModels(provider.backend === "omarchy.agents" ? agentModels(provider.modelUsage) : provider.models), tierLabel: String(provider.tierLabel || "").slice(0, 512), usageStatusText: String(provider.usageStatusText || "").slice(0, 512), authHelpText: String(provider.authHelpText || "").slice(0, 512), @@ -403,7 +434,7 @@ Item { || Number(provider.todaySessions) > 0 || Number(provider.windowTokens) > 0 || Number(provider.hourlyTokens) > 0 - || (Array.isArray(provider.models) && provider.models.length > 0) + || (provider.backend !== "omarchy.agents" && Array.isArray(provider.models) && provider.models.length > 0) || String(provider.latestModel || "") !== "" } @@ -437,10 +468,10 @@ Item { return label } - function resetText(_provider, timestamp) { + function resetText(_provider, timestamp, nowMs) { if (!timestamp) return "" const reset = new Date(timestamp) - const diffMs = reset.getTime() - Date.now() + const diffMs = reset.getTime() - (nowMs === undefined ? Date.now() : nowMs) if (!isFinite(diffMs)) return "" if (diffMs <= 0) return "now" const hours = Math.floor(diffMs / 3600000) @@ -463,70 +494,16 @@ Item { } function tooltipText() { - if (providers.length === 0) return "No AI usage providers detected" - const lines = [] - for (let index = 0; index < providers.length; index++) { - const provider = providers[index] - if (lines.length) lines.push("") - let heading = String(provider.providerName || provider.providerId || "AI") - if (displayTierLabel(provider.tierLabel)) - heading += " (" + displayTierLabel(provider.tierLabel) + ")" - const status = providerStatusText(provider) - if (status !== "live") heading += " · " + status - lines.push(heading) - if (Number(provider.rateLimitPercent) >= 0) { - const reset = resetText(provider, provider.rateLimitResetAt) - lines.push(String(provider.rateLimitLabel || "Primary") + ": " - + Math.round(displayPercent(provider, provider.rateLimitPercent)) + "%" - + (reset ? " · resets in " + reset : "")) - } - if (Number(provider.secondaryRateLimitPercent) >= 0) { - const reset = resetText(provider, provider.secondaryRateLimitResetAt) - lines.push(String(provider.secondaryRateLimitLabel || "Secondary") + ": " - + Math.round(displayPercent(provider, - provider.secondaryRateLimitPercent)) + "%" - + (reset ? " · resets in " + reset : "")) - } - if (String(provider.providerId || "") === "codex" - && !providerReportsFiveHour(provider)) - lines.push("5h: not reported by Codex RPC") - if (String(provider.usageStatusText || "") - && String(provider.providerId || "") === "codex") - lines.push("General limit: " + String(provider.usageStatusText)) - const providerId = String(provider.providerId || "") - if (providerId === "opencode") { - if (Number(provider.windowTokens) > 0) - lines.push("5h tokens: " + formatTokens(provider.windowTokens)) - if (Number(provider.hourlyTokens) > 0) - lines.push("1h rate: " + formatTokens(provider.hourlyTokens) + "/h") - } else if (Number(provider.windowTokens) > 0) { - let activity = formatTokens(provider.windowTokens) + " tokens" - if (Number(provider.hourlyTokens) > 0) - activity += " · " + formatTokens(provider.hourlyTokens) + "/h" - lines.push(activity) - } else if (Number(provider.hourlyTokens) > 0) { - lines.push("Rate: " + formatTokens(provider.hourlyTokens) + "/h") - } - if (provider.todayTotalTokens !== undefined - && Number(provider.todayTotalTokens) > 0) - lines.push("Today: " + formatTokens(provider.todayTotalTokens) + " tokens") - const todayPrompts = Math.max(0, Number(provider.todayPrompts) || 0) - const todaySessions = Math.max(0, Number(provider.todaySessions) || 0) - if (todayPrompts > 0 || todaySessions > 0) { - const activity = [] - if (todayPrompts > 0) - activity.push(todayPrompts + (todayPrompts === 1 ? " prompt" : " prompts")) - if (todaySessions > 0) - activity.push(todaySessions + (todaySessions === 1 ? " session" : " sessions")) - lines.push("Today activity: " + activity.join(" · ")) - } - if (String(provider.latestModel || "")) - lines.push((providerId === "opencode" ? "Latest today: " : "") - + String(provider.latestModel)) - const currentDataMessage = providerCurrentDataMessage(provider) - if (currentDataMessage) lines.push(currentDataMessage) - } - return lines.join("\n") + const provider = selectedProvider + if (!provider) return "No AI usage providers detected" + const heading = String(provider.providerName || provider.providerId || "AI") + + " · " + providerStatusText(provider) + if (provider.ready === false) return heading + const limit = bindingWindow(provider) + if (!limit) return heading + "\nLimits: Unavailable" + const reset = resetText(provider, limit.resetsAt) + return heading + "\n" + limit.title + ": " + Math.round(limit.percent) + "%" + + (reset ? " · resets in " + reset : "") } function formatTokens(value) { diff --git a/tests/ai-plugin-regression.sh b/tests/ai-plugin-regression.sh index ff4846d..9c9b00c 100755 --- a/tests/ai-plugin-regression.sh +++ b/tests/ai-plugin-regression.sh @@ -311,8 +311,8 @@ rg -q 'providerReportsFiveHour' "$service" \ rg -q 'displayPercent' "$service" \ || fail "AI service lost provider percentage normalization" panel="$repo_root/hancore.shibumi.ai/AiUsagePanel.qml" -rg -q 'text: "AI USAGE"' "$panel" \ - || fail "AI panel lost the V1 heading" +rg -Fq 'text: panel.provider ? panel.providerHeading(panel.provider)' "$panel" \ + || fail "AI panel lost the selected-provider heading" rg -q 'font\.pixelSize: Commons\.Style\.font\.subtitle' "$panel" \ || fail "AI panel heading does not retain the V1 13px role" rg -q 'color: selected \? panel\.controlActiveFillColor' "$panel" \ @@ -330,10 +330,12 @@ rg -q 'renderType: Text\.NativeRendering' "$panel" \ if rg -q 'rgba\([^\n]*urgent[^\n]*0\.22' "$panel"; then fail "AI panel reintroduced an ad-hoc active fill" fi -if rg -q 'Process \{|Timer \{|FileView \{' \ - "$widget" "$panel"; then +if rg -q 'Process \{|FileView \{' "$widget" "$panel" \ + || rg -q 'Timer \{' "$widget"; then fail "AI screen-local views own provider workers" fi +[[ $(rg -c 'Timer \{' "$panel") -eq 1 ]] && grep -Fq 'interval: 30000; repeat: true; running: panel.open' "$panel" \ + || fail "AI panel clock must be singular, bounded and open-only" [[ $(rg -c 'Process \{' "$service") -eq 1 ]] \ || fail "AI providers do not have exactly one process owner" [[ $(rg -c 'FileView \{' "$service") -eq 2 ]] \ diff --git a/tests/ai-plugin-smoke.qml b/tests/ai-plugin-smoke.qml index 7a5bbd9..4d6c841 100644 --- a/tests/ai-plugin-smoke.qml +++ b/tests/ai-plugin-smoke.qml @@ -41,6 +41,48 @@ ShellRoot { Qt.exit(1) } + function compactTooltipContract() { + claudeProvider.rateLimitResetAt = "2000-01-01T00:00:00Z" + const checks = { selectedReset: aiService.tooltipText() === "Claude Code · live\nSession: 3% · resets in now" } + claudeProvider.ready = false; checks.stale = aiService.tooltipText() === "Claude Code · stale" + claudeProvider.ready = true; claudeProvider.rateLimitPercent = -1 + claudeProvider.secondaryRateLimitPercent = 0; claudeProvider.secondaryRateLimitLabel = "Weekly" + checks.secondaryZero = aiService.tooltipText() === "Claude Code · live\nWeekly: 0%" + claudeProvider.secondaryRateLimitPercent = -1 + checks.missingLimit = aiService.tooltipText() === "Claude Code · live\nLimits: Unavailable" + emptyAgentsPanel.aiService = aiService; checks.missingPanel = emptyAgentsPanel.limitsUnavailableVisible === true + claudeProvider.rateLimitPercent = 0.025; claudeProvider.rateLimitResetAt = "invalid" + checks.invalidReset = aiService.tooltipText() === "Claude Code · live\nSession: 3%" + claudeProvider.rateLimitResetAt = ""; claudeProvider.secondaryRateLimitLabel = "" + checks.emptyTooltip = missingStateService.tooltipText() === "No AI usage providers detected" + emptyAgentsPanel.aiService = missingStateService; checks.emptyPanel = emptyAgentsPanel.providerEmptyStateText === "No supported AI usage data was found." + emptyAgentsPanel.aiService = agentsService + const original = agentsService.agentsClaudeRecord, now = Date.now(), record = JSON.parse(codexStatusRecord(true, true)) + record.id = "claude"; record.name = "Claude Code" + record.limits = [{ label: "Session (5-hour)", percent: 0.5, resetsAt: new Date(now + 13500000).toISOString() }, { label: "Weekly (7-day)", percent: 0.8, resetsAt: "" }, { label: "Weekly (7-day)", title: "Fable Weekly", percent: 0.9, resetsAt: new Date(now + 302400000).toISOString() }] + record.recentDays = Array.from({length: 8}, (_, i) => ({date: "2026-09-" + (19 + i), messageCount: i * 100})) + agentsService.applyAgentRecord("claude", JSON.stringify(record)); if ("nowMs" in emptyAgentsPanel) emptyAgentsPanel.nowMs = now + const p = agentsService.providerFor("claude") + checks.windows = !!p.limits && p.limits.length === 3 && p.limits[2].title === "Fable Weekly" + checks.headline = !!emptyAgentsPanel.headline && emptyAgentsPanel.headline.title === "Fable Weekly" && emptyAgentsPanel.headlineAlarm && agentsService.tooltipText().indexOf("Fable Weekly: 90% · resets in ") > 0 + const alarmColor = String(emptyAgentsPanel.headlineColor) + checks.pace = typeof emptyAgentsPanel.paceText === "function" && emptyAgentsPanel.paceText({label: "Session (5-hour)", percent: 50, resetsAt: record.limits[0].resetsAt}) === "Pace: 2.0×" + checks.longPace = typeof emptyAgentsPanel.paceText === "function" && ["Weekly", "Monthly"].every((label, i) => emptyAgentsPanel.paceText({label: label, percent: 50, resetsAt: new Date(now + [302400000, 1296000000][i]).toISOString()}) === "Pace: 1.0×") + checks.noPace = typeof emptyAgentsPanel.paceText === "function" && [{label: "Unknown", resetsAt: record.limits[0].resetsAt}, {label: "Weekly", resetsAt: ""}, {label: "Weekly", resetsAt: new Date(now - 1).toISOString()}, {label: "1-hour", resetsAt: record.limits[0].resetsAt}].every(w => emptyAgentsPanel.paceText(Object.assign({percent: 50}, w)) === "") + checks.days = !!p.recentDays && p.recentDays.length === 7 && p.recentDays[0].date === "2026-09-20" && p.recentDays[6].messageCount === 700 && emptyAgentsPanel.renderedDayCount === 7 + checks.codex = typeof agentsService.limitWindows === "function" && agentsService.limitWindows(agentsService.providerFor("codex")).length === 1 && agentsService.providerFor("codex").models[0].totalLabel === "1.03M" + record.limits[2].percent = 0.899; agentsService.applyAgentRecord("claude", JSON.stringify(record)) + checks.threshold = emptyAgentsPanel.headlineAlarm === false && String(emptyAgentsPanel.headlineColor) !== String(alarmColor) + record.limits = record.limits.concat(Array.from({length: 4}, () => record.limits[0])); agentsService.applyAgentRecord("claude", JSON.stringify(record)) + checks.cap = !!agentsService.providerFor("claude").limits && agentsService.providerFor("claude").limits.length === 6 && emptyAgentsPanel.limitWindows.length === 6 + agentsService.agentsClaudeRecord = original; agentsService.providerRevision++ + checks.clock = emptyAgentsPanel.clockRunning === false; emptyPanelOwner.opened = true + checks.clock = checks.clock && emptyAgentsPanel.clockRunning === true; emptyPanelOwner.opened = false + checks.clock = checks.clock && emptyAgentsPanel.clockRunning === false + console.log("AI_PRESENTATION_CASES", JSON.stringify(checks)) + return Object.values(checks).every(value => value) + } + function linearChannel(value) { return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4) @@ -81,8 +123,8 @@ ShellRoot { && agentsService.usagePercent(codex) === 24 && claude.rateLimitPercent === -1 && claude.secondaryRateLimitPercent === -1 - && claude.models.length === 0 - && codex.models.length === 0 + && claude.models.length === 1 + && codex.models.length === 1 && codex.todayTotalTokens === 1031649 && codex.latestModel === "" && claude.ready && codex.ready @@ -141,6 +183,7 @@ ShellRoot { if (codexStatusProbeStage === 1) { if (agentsService.providerStatusText(provider) !== "partial" || emptyAgentsPanel.providerStatusLabel !== "partial" + || !emptyAgentsPanel.limitsUnavailableVisible || agentsService.tooltipText().indexOf("Codex · partial") < 0) { fail("Codex partial status did not propagate") return false @@ -204,7 +247,7 @@ ShellRoot { const provider = agentsService.providerFor("claude") const accepted = provider && provider.ready && agentsService.usagePercent(provider) === -1 - && provider.models.length === 0 + && provider.models.length === 1 && provider.latestModel === "" && agentsService.providerCurrentDataMessage(provider) === "Authenticate Claude" @@ -928,6 +971,7 @@ ShellRoot { || delayedSelectionState.confirmedTool !== "claude") return root.fail("AI selection did not preview before persistence readback") delayedSelectionState.previewTool = "claude" + if (!root.compactTooltipContract()) return root.fail("compact selected-provider tooltip or unavailable panel") } if (!first.visible || !second.visible || first.aiService !== aiService || second.aiService !== aiService @@ -954,13 +998,9 @@ ShellRoot { !== "Run `claude auth login` to restore authoritative usage." || emptyAgentsPanel.primaryUsageVisible || emptyAgentsPanel.secondaryUsageVisible - || emptyAgentsPanel.renderedModelCount !== 0 + || emptyAgentsPanel.renderedModelCount !== 1 || first.childPanelWidget("omarchy.agents") !== first - || first.tooltipText.indexOf("5h: not reported by Codex RPC") < 0 - || first.tooltipText.indexOf("Codex (Pro Lite)") < 0 - || first.tooltipText.indexOf("5h tokens: 2.3K") < 0 - || first.tooltipText.indexOf("1h rate: 180/h") < 0 - || first.tooltipText.indexOf("Latest today: local-test") < 0 + || first.tooltipText !== "Claude Code · live\nSession: 3%" || !root.openCodeParserContract())) return root.fail("Claude/agents provider metadata") if (root.phase === 0) { diff --git a/tests/fixtures/AiPanelHost.qml b/tests/fixtures/AiPanelHost.qml index 2b5a54e..b032964 100644 --- a/tests/fixtures/AiPanelHost.qml +++ b/tests/fixtures/AiPanelHost.qml @@ -25,7 +25,7 @@ Item { readonly property color controlActiveFillColor: "#442222" readonly property color dividerColor: controlBorderColor - default property alias panelContent: content.data + default property alias panelContent: content.children function fittedContentWidth(preferred) { return preferred } function fittedContentHeight(preferred, maximum) { From 09a9e40b202391a50dd4fb2c0f556f90122c32b3 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 05/40] docs: clarify the restrictive-umask recovery in the beta.15.3 release notes --- .github/release-notes/v0.1.1-beta.15.3.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/release-notes/v0.1.1-beta.15.3.md b/.github/release-notes/v0.1.1-beta.15.3.md index 94157ed..722caaa 100644 --- a/.github/release-notes/v0.1.1-beta.15.3.md +++ b/.github/release-notes/v0.1.1-beta.15.3.md @@ -34,10 +34,15 @@ git checkout v0.1.1-beta.15.3 - Beta.15.1 was tagged but not published as a GitHub release. - Source checkouts cloned with a restrictive `umask` such as 077 now keep a stable payload identity. If an earlier release was installed from such a - checkout and `update` is refused, run - `./scripts/shibumi-suite uninstall --keep-settings --yes` from that checkout, - then check out `v0.1.1-beta.15.3` and run - `./scripts/shibumi-suite install --yes`. + checkout and `update` is refused, uninstall with the release you had + installed while keeping your settings, then install Beta.15.3: + + ```bash + git checkout v0.1.1-beta.15.2 # the tag you had installed + ./scripts/shibumi-suite uninstall --keep-settings --yes + git checkout v0.1.1-beta.15.3 + ./scripts/shibumi-suite install --yes + ``` - No visible interface changes are included. - The notes of 0.1.1-beta.15 remain valid. - AUR publication remains deferred. From ee02f2b57f9a4bb538b5bc25f7f03393ff1fd87c Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 06/40] test: keep two fixtures independent of the caller's umask --- tests/shibumi-suite-quattro-runtime.sh | 5 +++-- tests/test_package_release.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/shibumi-suite-quattro-runtime.sh b/tests/shibumi-suite-quattro-runtime.sh index 1dc9b04..8dc126d 100755 --- a/tests/shibumi-suite-quattro-runtime.sh +++ b/tests/shibumi-suite-quattro-runtime.sh @@ -147,10 +147,11 @@ for source_spec in \ done # Package fixtures mirror the root metadata projection performed by PKGBUILD:62. +# Preserve archived package modes below the private 0700 fixture parent. git --no-replace-objects -C "$repo_root" archive "$package_predecessor_revision" \ - | tar -x -C "$package_predecessor_root" + | tar -x --same-permissions -C "$package_predecessor_root" git --no-replace-objects -C "$repo_root" archive "$package_candidate_revision" \ - | tar -x -C "$package_candidate_root" + | tar -x --same-permissions -C "$package_candidate_root" for package_root in "$package_predecessor_root" "$package_candidate_root"; do [[ -d $package_root && ! -L $package_root ]] \ || fail "package fixture root is not an isolated real directory: $package_root" diff --git a/tests/test_package_release.py b/tests/test_package_release.py index 9623790..179038e 100644 --- a/tests/test_package_release.py +++ b/tests/test_package_release.py @@ -1263,6 +1263,8 @@ def create_checkout(parent: Path, name: str) -> tuple[Path, str]: ["git", "-C", str(candidate), "checkout", "-q", _candidate_revision], check=True, ) + # Git recreated this fixture file using the caller's umask. + candidate_tracked.chmod(0o644) replacement_program = "\n".join(( "import subprocess", f"repo = {str(candidate)!r}", From 2c679768557e370c5227c9e86135526f74a7de29 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:16 +0200 Subject: [PATCH 07/40] feat: count Live and Recent notifications without opening the panel --- hancore.shibumi.status/BarWidget.qml | 16 +++--- .../NotificationAdapter.qml | 52 +++++++++++++++++-- hancore.shibumi.status/NotificationPanel.qml | 6 ++- .../NotificationStatusView.qml | 22 +++++--- tests/notification-adapter-regression.sh | 2 + tests/notification-adapter-smoke.qml | 32 +++++++++--- 6 files changed, 100 insertions(+), 30 deletions(-) diff --git a/hancore.shibumi.status/BarWidget.qml b/hancore.shibumi.status/BarWidget.qml index b837509..7c720bd 100644 --- a/hancore.shibumi.status/BarWidget.qml +++ b/hancore.shibumi.status/BarWidget.qml @@ -77,11 +77,7 @@ Item { readonly property int pendingCount: notificationView.pendingCount readonly property int recentCount: notificationView.recentCount readonly property int notificationCount: notificationView.notificationCount - readonly property string textLabel: notificationService - && notificationService.doNotDisturb === true ? "DND" - : notificationCount > 0 - ? notificationCount + (notificationCount === 1 - ? " NOTIFICATION" : " NOTIFICATIONS") : "CLEAR" + readonly property string textLabel: notificationView.tooltipText readonly property bool ready: updateWidget !== null || trayWidget !== null || notificationService !== null readonly property bool opened: trayDrawerOpen || trayAppMenuOpen @@ -656,6 +652,10 @@ Item { } } + Connections { + target: root + function onTextLabelChanged() { if (textStatusMouse.containsMouse && root.bar) root.bar.showTooltip(textStatus, root.textLabel) } + } onVisibleChanged: syncRegistration() Component.onCompleted: syncRegistration() Component.onDestruction: if (root.bar && registered) @@ -674,14 +674,12 @@ Item { } MouseArea { + id: textStatusMouse anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton hoverEnabled: true cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) root.bar.showTooltip(textStatus, - root.notificationCount > 0 - ? root.notificationCount + (root.notificationCount === 1 - ? " notification" : " notifications") : root.textLabel) + onEntered: if (root.bar) root.bar.showTooltip(textStatus, root.textLabel) onExited: if (root.bar) root.bar.hideTooltip(textStatus) onClicked: function(mouse) { if (root.bar) root.bar.hideTooltip(textStatus) diff --git a/hancore.shibumi.status/NotificationAdapter.qml b/hancore.shibumi.status/NotificationAdapter.qml index 098f774..fb1379a 100644 --- a/hancore.shibumi.status/NotificationAdapter.qml +++ b/hancore.shibumi.status/NotificationAdapter.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import QtQml.Models import Quickshell import Quickshell.Io @@ -35,6 +36,7 @@ Item { && state.hostService.doNotDisturb === true readonly property int pendingCount: pendingRows.count readonly property int recentCount: pastRows.count + property string historyState: "unavailable" readonly property bool liveAvailable: sourceModel() !== null readonly property bool historyAvailable: available readonly property bool pastDismissAvailable: available && @@ -52,6 +54,35 @@ Item { ListModel { id: pendingRows } ListModel { id: pastRows } + // Watch creation/removal and in-place writes, never poll or replay host popups. + // Trailing slash keeps FileView's implicit parent inside notifications. + readonly property var historyWatchPaths: { + const paths = [] + if (!available || historySourceModel()) return paths + paths.push(historyDir, historyDir.slice(0, historyDir.lastIndexOf("/") + 1)) + for (let index = 0; index < Math.min(10, pastRows.count); index++) { + const name = pastRows.get(index).fileName + if (validHistoryFileName(name)) paths.push(historyDir + "/" + name) + } + return paths + } + Instantiator { + model: root.historyWatchPaths + delegate: FileView { + required property string modelData + path: modelData + preload: false + watchChanges: true + onFileChanged: if (!historyRefresh.running) historyRefresh.start() + } + } + Timer { + id: historyRefresh + interval: 100 + repeat: false + onTriggered: root.showHistory() + } + function attachShell(shellValue) { const service = shellValue && typeof shellValue.firstPartyServiceFor === "function" @@ -61,8 +92,10 @@ Item { syncModels() return } + historyRefresh.stop() state.hostGeneration++ state.hostService = nextService + historyState = nextService ? "loading" : "unavailable" state.historyOutput = "" state.queuedHistoryHost = null state.queuedHistoryGeneration = -1 @@ -108,7 +141,8 @@ Item { function rebuild(target, model) { target.clear() if (!model || typeof model.get !== "function") return - for (let index = 0; index < model.count; index++) { + const limit = target === pastRows ? Math.min(10, model.count) : model.count + for (let index = 0; index < limit; index++) { const entry = model.get(index) if (!entry || Number(entry.originalId || entry.id || 0) < 0) continue @@ -119,7 +153,10 @@ Item { function syncModels() { rebuild(pendingRows, sourceModel()) const archived = historySourceModel() - if (archived) rebuild(pastRows, archived) + if (archived) { + rebuild(pastRows, archived) + historyState = "ready" + } else if (available && !historyRefresh.running) historyRefresh.start() } // Derived from MIT-licensed Omarchy v4.0.3 NotificationLogic.historyRows: @@ -285,6 +322,7 @@ Item { } function startHistoryRead() { + historyState = "loading" state.historyOutput = "" state.historyReadHost = state.hostService state.historyReadGeneration = state.hostGeneration @@ -325,8 +363,10 @@ Item { } Process { id: historyReader - command: ["sh", "-c", "awk '{ print FILENAME \"\\t\" $0 }' \"$1\"/*.json", - "--", historyDir] + command: ["timeout", "--foreground", "--kill-after=1", "2", "sh", "-c", + "dir=$1; set -- \"$dir\"/*.json; " + + "if [ -d \"$dir\" ] && [ -r \"$dir\" ] && [ -x \"$dir\" ] && [ ! -e \"$1\" ]; then exit 0; fi; " + + "exec awk '{ print FILENAME \"\\t\" $0 }' \"$@\"", "--", historyDir] stdout: StdioCollector { waitForEnd: true onStreamFinished: state.historyOutput = text @@ -339,8 +379,10 @@ Item { && state.hostGeneration === state.historyReadGeneration state.historyReadHost = null state.historyReadGeneration = -1 - if (current) + if (current && !root.historySourceModel()) { + root.historyState = exitCode === 0 ? "ready" : "unavailable" root.applyHistory(exitCode === 0 ? state.historyOutput : "") + } } } diff --git a/hancore.shibumi.status/NotificationPanel.qml b/hancore.shibumi.status/NotificationPanel.qml index 072bc6e..f16d451 100644 --- a/hancore.shibumi.status/NotificationPanel.qml +++ b/hancore.shibumi.status/NotificationPanel.qml @@ -37,7 +37,7 @@ ShibumiPanel { readonly property int displayedCount: showingRecent ? recentCount : pendingCount // Omarchy 4.0.3 exposes no history model. The adapter reads the same - // host-owned compact JSON directory when Recent is selected. + // host-owned compact JSON directory independently of this panel. readonly property var activeRows: { const rows = [] function append(model, bucket) { @@ -451,7 +451,9 @@ ShibumiPanel { id: emptyLabel anchors.centerIn: parent visible: notificationList.count === 0 - text: "No notifications" + text: panel.showingRecent && notificationService.historyState !== "ready" + ? notificationService.historyState === "loading" ? "Loading recent notifications…" + : "Recent notifications unavailable" : "No notifications" color: Commons.Util.alpha(panel.controlForeground, 0.3) font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family diff --git a/hancore.shibumi.status/NotificationStatusView.qml b/hancore.shibumi.status/NotificationStatusView.qml index 38c3805..5377ff2 100644 --- a/hancore.shibumi.status/NotificationStatusView.qml +++ b/hancore.shibumi.status/NotificationStatusView.qml @@ -23,6 +23,15 @@ Item { && notificationService.pastModel ? Math.max(0, Number(notificationService.pastModel.count) || 0) : 0 readonly property int notificationCount: pendingCount + recentCount + readonly property bool countsKnown: notificationService + && notificationService.liveAvailable === true && notificationService.historyState === "ready" + readonly property string tooltipText: (notificationService && notificationService.liveAvailable + ? pendingCount + " Live" : "Live: Unavailable") + " · " + + (notificationService && notificationService.historyState === "ready" + ? recentCount + " Recent" : "Recent: " + (notificationService + && notificationService.historyState === "loading" ? "Loading" : "Unavailable")) + + (notificationService && notificationService.doNotDisturb ? " · DND" : "") + onTooltipTextChanged: if (tooltipHovered && bar) bar.showTooltip(root, tooltipText) readonly property bool presented: notificationService !== null readonly property color badgeFillColor: notificationBadge.color readonly property color badgeTextColor: badgeText.color @@ -68,7 +77,7 @@ Item { Rectangle { id: notificationBadge - visible: root.notificationCount > 0 + visible: root.notificationCount > 0 || !root.countsKnown width: Math.max(Commons.Style.space(12), badgeText.implicitWidth + 6) height: Commons.Style.space(12) radius: height / 2 @@ -84,8 +93,9 @@ Item { Text { id: badgeText anchors.centerIn: parent - text: root.notificationCount > 99 ? "99" - : String(root.notificationCount) + text: !root.countsKnown ? (root.notificationService + && root.notificationService.historyState === "loading" ? "…" : "?") + : root.notificationCount > 99 ? "99" : String(root.notificationCount) color: root.customToneActive ? root.badgeContrastColor : root.bar ? root.bar.background : Commons.Color.background @@ -101,11 +111,7 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton hoverEnabled: true cursorShape: Qt.PointingHandCursor - onEntered: if (root.bar) root.bar.showTooltip(root, - root.notificationCount > 0 - ? root.notificationCount + (root.notificationCount === 1 - ? " notification" : " notifications") - : "No notifications") + onEntered: if (root.bar) root.bar.showTooltip(root, root.tooltipText) onExited: if (root.bar) root.bar.hideTooltip(root) onClicked: function(mouse) { if (root.bar) root.bar.hideTooltip(root) diff --git a/tests/notification-adapter-regression.sh b/tests/notification-adapter-regression.sh index cb9d8c9..699d2f7 100755 --- a/tests/notification-adapter-regression.sh +++ b/tests/notification-adapter-regression.sh @@ -118,6 +118,7 @@ EOF SHIBUMI_EXPECTED_HISTORY_COUNT="$expected" \ SHIBUMI_HISTORY_RACE="$race" \ SHIBUMI_HISTORY_MUTATION="$mutation" \ + SHIBUMI_HISTORY_MODE="$history_mode" \ SHIBUMI_HELPER_STATE="$root/helper-state" \ QML_IMPORT_PATH="$omarchy_path/shell" \ QML2_IMPORT_PATH="$omarchy_path/shell" \ @@ -163,6 +164,7 @@ EOF } run_case populated 10 populated +run_case external-clear 10 populated 'notification history clear passed' '' external-clear run_case empty 0 empty run_case read-failure 0 missing run_case detach-race 10 populated 'notification history detach race passed' detach diff --git a/tests/notification-adapter-smoke.qml b/tests/notification-adapter-smoke.qml index 1967dc9..2b8f7e4 100644 --- a/tests/notification-adapter-smoke.qml +++ b/tests/notification-adapter-smoke.qml @@ -1,5 +1,6 @@ import QtQuick import Quickshell +import Quickshell.Io import "status" as Status ShellRoot { @@ -102,12 +103,16 @@ ShellRoot { Status.NotificationAdapter { id: dndOnlyAdapter } Status.NotificationAdapter { id: legacyAdapter } Status.NotificationAdapter { id: unavailableAdapter } + Status.NotificationStatusView { id: counts; bar: null; notificationService: dndOnlyAdapter } + Process { id: externalClear; command: ["sh", "-c", "rm -f \"$1\"/*.json", "--", dndOnlyAdapter.historyDir] } Component.onCompleted: { - adapter.attachShell(currentShell) dndOnlyAdapter.attachShell(dndOnlyShell) + adapter.attachShell(currentShell) legacyAdapter.attachShell(legacyShell) unavailableAdapter.attachShell(null) + if (dndOnlyAdapter.historyState !== "loading" || counts.tooltipText !== "Live: Unavailable · Recent: Loading · DND") + root.fail("initial history state is not loading without hover/open") } Timer { @@ -126,7 +131,7 @@ ShellRoot { || unavailableAdapter.available || unavailableAdapter.historyAvailable) return root.fail("capabilities do not match the host models") - if (!dndOnlyAdapter.showHistory()) + if (root.historyRace && !dndOnlyAdapter.showHistory()) return root.fail("Recent did not start the private history read") if (root.historyRace === "refresh") { dndOnlyAdapter.attachShell(currentShell) @@ -137,7 +142,7 @@ ShellRoot { return root.fail("same-host refresh was not accepted") dndOnlyAdapter.attachShell(dndOnlyShell) } else { - if (!adapter.showHistory()) + if (root.historyRace && !adapter.showHistory()) return root.fail("live-host history read was not accepted") if (root.historyRace === "detach") { dndOnlyAdapter.attachShell(null) @@ -149,12 +154,17 @@ ShellRoot { } } if (root.ticks < (root.historyRace ? 40 : 12)) return + if (!root.historyRace && !root.mutationPhase + && (adapter.historyState !== (Quickshell.env("SHIBUMI_HISTORY_MODE") === "missing" ? "unavailable" : "ready") + || counts.tooltipText !== "Live: Unavailable · " + (adapter.historyState === "ready" + ? root.expectedHistoryCount + " Recent" : "Recent: Unavailable") + " · DND")) + return root.fail("eager snapshot/tooltip state without hover/open") if (root.historyRace && root.historyRace !== "mutation-replace") { const refresh = root.historyRace === "refresh" || root.historyRace === "same-refresh" const expected = root.historyRace === "replace" || refresh ? 1 : 0 const summary = refresh ? "New host history" : "Legacy recent" - if (adapter.recentCount !== (refresh ? 0 : root.expectedHistoryCount) + if (adapter.recentCount !== (refresh ? 1 : root.expectedHistoryCount) || dndOnlyAdapter.recentCount !== expected || (expected && dndOnlyAdapter.pastModel.get(0).summary !== summary) @@ -173,14 +183,15 @@ ShellRoot { return } if (root.mutationPhase === 2) { - if (dndOnlyAdapter.recentCount !== 0) return + if (dndOnlyAdapter.recentCount !== 0 || dndOnlyAdapter.historyState !== "ready") return console.log("notification history clear passed") Qt.exit(0) return } if (root.mutationPhase === 3) { if (root.ticks < root.mutationStartTick + 20) return - if (dndOnlyAdapter.recentCount !== 0 + if (dndOnlyAdapter.recentCount !== root.expectedHistoryCount + || dndOnlyAdapter.pastModel.get(0).summary !== "History 11" || dndOnlyAdapter.pendingCount !== 1) return root.fail("stale mutation callback crossed host replacement") console.log("notification history mutation replace race passed") @@ -197,6 +208,11 @@ ShellRoot { if (adapter.pendingCount !== 1 || adapter.pendingModel.get(0).summary !== "Current notification") return root.fail("history read mutated the live popup model") + if (root.historyMutation === "external-clear") { + externalClear.running = true + root.mutationPhase = 2 + return + } if (!dndOnlyAdapter.setDoNotDisturb(false) || dndOnlyAdapter.doNotDisturb) return root.fail("DND-only proxy action failed") @@ -228,6 +244,10 @@ ShellRoot { root.mutationPhase = 2 return } + counts.notificationService = adapter + adapter.setDoNotDisturb(true) + if (counts.tooltipText !== "1 Live · " + (adapter.historyState === "ready" ? root.expectedHistoryCount + " Recent" : "Recent: Unavailable") + " · DND") + return root.fail("live/recent tooltip with DND") liveRows.append({ id: 8, originalId: 8, app: "Live fixture", appIcon: "", summary: "Second notification", body: "Reactive row", image: "", From af9fc2a89ca9da7c3d61dd1ed7bd7a8d5d75b3c7 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:17 +0200 Subject: [PATCH 08/40] feat: group notifications by app with sanitized bodies and relative times --- .../NotificationAdapter.qml | 2 +- hancore.shibumi.status/NotificationPanel.qml | 76 +++++++++++++++++-- tests/status-plugin-regression.sh | 1 + tests/status-plugin-smoke.qml | 28 +++++++ 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/hancore.shibumi.status/NotificationAdapter.qml b/hancore.shibumi.status/NotificationAdapter.qml index fb1379a..f815c83 100644 --- a/hancore.shibumi.status/NotificationAdapter.qml +++ b/hancore.shibumi.status/NotificationAdapter.qml @@ -131,7 +131,7 @@ Item { image: String(value.image || ""), glyph: String(value.glyph || ""), exec: String(value.exec || ""), - urgency: Number(value.urgency || 0), + urgency: typeof value.urgency === "number" ? value.urgency : -1, expireTimeout: Number(value.expireTimeout || 0), timestamp: Number(value.timestamp || 0), fileName: String(value.fileName || "") diff --git a/hancore.shibumi.status/NotificationPanel.qml b/hancore.shibumi.status/NotificationPanel.qml index f16d451..c0280d2 100644 --- a/hancore.shibumi.status/NotificationPanel.qml +++ b/hancore.shibumi.status/NotificationPanel.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import Quickshell import qs.Commons as Commons import qs.Ui as Ui import "../hancore.shibumi.state/lib/presentation" as Presentation @@ -13,6 +14,14 @@ ShibumiPanel { readonly property bool liveAvailable: notificationService && notificationService.liveAvailable === true property bool showingRecent: !liveAvailable + property double nowMs: Date.now() + onOpenChanged: if (open) nowMs = Date.now() + property Timer relativeClock: Timer { + interval: 60000 + repeat: true + running: panel.open + onTriggered: panel.nowMs = Date.now() + } readonly property bool historyAvailable: notificationService && notificationService.historyAvailable === true function paletteColor(id, fallback) { @@ -38,20 +47,24 @@ ShibumiPanel { ? recentCount : pendingCount // Omarchy 4.0.3 exposes no history model. The adapter reads the same // host-owned compact JSON directory independently of this panel. + readonly property var notificationListView: notificationList readonly property var activeRows: { - const rows = [] + const groups = new Map() function append(model, bucket) { if (!model) return for (let index = 0; index < model.count; index++) { const entry = model.get(index) if (!entry) continue - rows.push({ + const app = String(entry.app || "").trim() + if (!groups.has(app)) groups.set(app, []) + groups.get(app).push({ bucket: bucket, sourceIndex: index, - app: String(entry.app || ""), + app: app, appIcon: String(entry.appIcon || ""), summary: String(entry.summary || ""), - body: String(entry.body || "") + body: String(entry.body || ""), + details: rowDetails(entry) }) } } @@ -59,6 +72,9 @@ ShibumiPanel { ? (notificationService ? notificationService.pastModel : null) : (notificationService ? notificationService.pendingModel : null) append(model, showingRecent ? "past" : "pending") + const rows = [] + groups.forEach(group => group.forEach((row, index) => + rows.push(Object.assign(row, { groupCount: group.length, groupStart: index === 0 })))) return rows } @@ -135,6 +151,31 @@ ShibumiPanel { closePanel() } + function safeIconSource(icon) { + let value = String(icon || "") + if (value.startsWith("file:///")) { + try { value = decodeURIComponent(value.slice(7)) } catch (_error) { return "" } + } + return /^(?:[a-z0-9][a-z0-9._-]*|\/[^\x00-\x1f?#]+)$/i.test(value) + ? Quickshell.iconPath(value, true) : "" + } + + function relativeTime(timestamp) { + const stamp = Number(timestamp), age = nowMs - stamp + if (!(stamp > 0) || !isFinite(age) || age < 0) return "" + if (age < 60000) return "now" + if (age < 3600000) return Math.floor(age / 60000) + "m ago" + if (age < 86400000) return Math.floor(age / 3600000) + "h ago" + if (age < 604800000) return Math.floor(age / 86400000) + "d ago" + const date = new Date(stamp) + return ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getMonth()] + + " " + date.getDate() + (date.getFullYear() === new Date(nowMs).getFullYear() ? "" : ", " + date.getFullYear()) + } + + function rowDetails(entry) { + return [relativeTime(entry.timestamp), ["Low", "Normal", "Critical"][entry.urgency] || ""].filter(Boolean).join(" · ") + } + function sanitizedBody(body, app, appIcon) { let text = String(body || "").replace(/]*>/gi, "") const source = (String(app || "") + "\n" @@ -144,10 +185,12 @@ ShibumiPanel { || source.indexOf("vivaldi") >= 0 || source.indexOf("microsoft-edge") >= 0 || source.indexOf("opera") >= 0 - if (!chromium) return text - return text + if (chromium) text = text .replace(/^\s*]*>\s*(?:https?:\/\/|www\.)?(?:[a-z0-9-]+\.)+[a-z]{2,}(?::\d+)?(?:\/[^<\s]*)?\s*<\/a>\s*/i, "") .replace(/^\s*(?:https?:\/\/|www\.)?(?:[a-z0-9-]+\.)+[a-z]{2,}(?::\d+)?(?:\/\S*)?\s+/i, "") + return text.replace(//gi, "\n").replace(/<[^>]*>/g, "") + .replace(/&(amp|lt|gt|quot|apos|nbsp);/g, (_match, key) => + ({ amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " })[key]).trim() } Ui.PanelKeyCatcher { @@ -327,6 +370,9 @@ ShibumiPanel { required property string appIcon required property string summary required property string body + required property string details + required property int groupCount + required property bool groupStart readonly property string cleanBody: panel.sanitizedBody( body, app, appIcon) @@ -349,6 +395,16 @@ ShibumiPanel { notificationRow.sourceIndex) } + Image { + id: appImage + anchors.left: parent.left; anchors.top: parent.top; anchors.margins: 8 + width: 18; height: 18 + source: panel.safeIconSource(notificationRow.appIcon) + visible: status === Image.Ready + fillMode: Image.PreserveAspectFit + asynchronous: true + } + Column { id: rowContent anchors.left: parent.left @@ -356,11 +412,14 @@ ShibumiPanel { anchors.top: parent.top anchors.margins: 8 anchors.rightMargin: 26 + anchors.leftMargin: appImage.visible ? 32 : 8 spacing: 3 Text { width: parent.width - text: notificationRow.bucket === "past" ? "RECENT" : "LIVE" + text: (notificationRow.bucket === "past" ? "RECENT" : "LIVE") + + (notificationRow.details ? " · " + notificationRow.details : "") + elide: Text.ElideRight color: notificationRow.bucket === "past" ? panel.recentHighlight : panel.liveHighlight font.family: panel.bar ? panel.bar.fontFamily @@ -373,7 +432,8 @@ ShibumiPanel { Text { width: parent.width - text: notificationRow.app || "App" + visible: notificationRow.groupStart + text: (notificationRow.app || "App") + " · " + notificationRow.groupCount textFormat: Text.PlainText color: panel.controlMutedHigh font.family: panel.bar ? panel.bar.fontFamily diff --git a/tests/status-plugin-regression.sh b/tests/status-plugin-regression.sh index 2ae85d2..69504f4 100755 --- a/tests/status-plugin-regression.sh +++ b/tests/status-plugin-regression.sh @@ -40,6 +40,7 @@ install -m 0644 "$repo_root/tests/fixtures/StatusTestWidget.qml" \ "$repo_root/tests/fixtures/NotificationPanelTestView.qml" \ "$tmpdir/fixtures/" +printf '\n' >"$tmpdir/fixtures/app.svg" set +e output=$(timeout 8 env -i \ HOME="$tmpdir/home" \ diff --git a/tests/status-plugin-smoke.qml b/tests/status-plugin-smoke.qml index c17c6e4..88289a8 100644 --- a/tests/status-plugin-smoke.qml +++ b/tests/status-plugin-smoke.qml @@ -17,6 +17,7 @@ ShellRoot { property var statusSettings: ({ displayMode: "full" }) readonly property var scopedStatus: scopedStatusLoader.item readonly property var scopedDiagnosticStatus: scopedDiagnosticStatusLoader.item + property bool groupingPrepared: false property bool scopedServiceStarted: false property int scopedServicePhase: 0 property int scopedStayAwakeChanges: 0 @@ -730,6 +731,33 @@ ShellRoot { || !notificationPanel.notificationService.pastClearAvailable || fakeBar.activePopout !== status) return root.fail("notification panel injection/popout ownership") + if (!root.groupingPrepared) { + pendingNotifications.set(0, { appIcon: Qt.resolvedUrl("fixtures/app.svg").toString(), + body: "Hello & bye
next", urgency: 2, + timestamp: new Date(2026, 8, 26, 10, 30).getTime() }) + pendingNotifications.setProperty(1, "app", "Other") + root.groupingPrepared = true; root.phaseTicks = 0; return + } + if ("nowMs" in notificationPanel) notificationPanel.nowMs = new Date(2026, 8, 26, 10, 35).getTime() + const rows = notificationPanel.activeRows + const first = notificationPanel.notificationListView ? notificationPanel.notificationListView.itemAtIndex(0) : null + const checks = { + groups: rows.map(r => r.sourceIndex).join() === "0,2,1" && rows[0].groupCount === 2 && rows[0].groupStart && !rows[1].groupStart && rows[2].groupStart, + body: notificationPanel.sanitizedBody(pendingNotifications.get(0).body, "Fixture", "") === "Hello & bye\nnext", + icons: typeof notificationPanel.safeIconSource === "function" && notificationPanel.safeIconSource(pendingNotifications.get(0).appIcon) !== "" + && ["https://invalid.test/pixel", "data:image/png;base64,AA", "file://remote/icon", "/no-such-shibumi-icon"].every(v => notificationPanel.safeIconSource(v) === ""), + metadata: rows[0].details === "5m ago · Critical" && typeof notificationPanel.rowDetails === "function" + && notificationPanel.rowDetails(notificationPanel.notificationService.primitiveEntry({})) === "", + relative: typeof notificationPanel.relativeTime === "function" && [0, 59999, 60000, 300000, 7200000, 259200000, 604800000].map(age => notificationPanel.relativeTime(notificationPanel.nowMs - age)).join("|") === "now|now|1m ago|5m ago|2h ago|3d ago|Sep 19", + unknownTime: typeof notificationPanel.relativeTime === "function" && [0, -1, NaN, notificationPanel.nowMs + 1].every(stamp => notificationPanel.relativeTime(stamp) === "") + && notificationPanel.relativeTime(new Date(2025, 8, 19).getTime()) === "Sep 19, 2025", + rendered: first && first.cleanBody === "Hello & bye\nnext" && first.children.some(c => "source" in c && c.status === Image.Ready) + } + notificationPanel.selectTab("recent") + checks.recent = notificationPanel.activeRows[0].bucket === "past" && notificationPanel.activeRows[0].groupCount === 1 + notificationPanel.selectTab("live") + console.log("234_NOTIFICATION_GROUPING", JSON.stringify(checks)) + if (Object.values(checks).some(ok => !ok)) return root.fail("grouping/body/icon/metadata presentation") notificationPanel.setDnd(false) notificationPanel.clearActive() notificationPanel.dismiss("pending", 0) From c30058d941f95b7e19614ca97e32578842a8fe7f Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:17 +0200 Subject: [PATCH 09/40] fix: dispatch Live notification clicks only to the exact current row --- .../NotificationAdapter.qml | 26 +++++++++++++++- hancore.shibumi.status/NotificationPanel.qml | 30 +++++++------------ tests/notification-adapter-smoke.qml | 15 ++++++++++ tests/status-plugin-smoke.qml | 5 ++++ 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/hancore.shibumi.status/NotificationAdapter.qml b/hancore.shibumi.status/NotificationAdapter.qml index f815c83..3380080 100644 --- a/hancore.shibumi.status/NotificationAdapter.qml +++ b/hancore.shibumi.status/NotificationAdapter.qml @@ -22,6 +22,8 @@ Item { id: state property var hostService: null property int hostGeneration: 0 + property real liveRevision: 0 + function token(entry) { return hostGeneration + ":" + liveRevision + ":" + JSON.stringify(entry) } property var historyReadHost: null property int historyReadGeneration: -1 property var queuedHistoryHost: null @@ -131,6 +133,7 @@ Item { image: String(value.image || ""), glyph: String(value.glyph || ""), exec: String(value.exec || ""), + execArgv: String(value.execArgv || ""), urgency: typeof value.urgency === "number" ? value.urgency : -1, expireTimeout: Number(value.expireTimeout || 0), timestamp: Number(value.timestamp || 0), @@ -146,11 +149,14 @@ Item { const entry = model.get(index) if (!entry || Number(entry.originalId || entry.id || 0) < 0) continue - target.append(primitiveEntry(entry)) + const copy = primitiveEntry(entry) + if (target === pendingRows) copy.liveToken = state.token(copy) + target.append(copy) } } function syncModels() { + state.liveRevision++ rebuild(pendingRows, sourceModel()) const archived = historySourceModel() if (archived) { @@ -206,6 +212,23 @@ Item { return -1 } + function invokeLive(token) { + const service = state.hostService, revision = state.liveRevision + const model = service ? service.popupModel : null + if (!token || !model || model !== sourceModel() + || typeof service.invokePopupDefault !== "function") return false + let match = -1 + for (let index = 0; index < model.count; index++) { + const row = primitiveEntry(model.get(index)) + if (row.originalId <= 0 || !isFinite(row.timestamp) || row.timestamp <= 0 + || state.token(row) !== token) continue + if (match >= 0) return false + match = index + } + if (match < 0 || revision !== state.liveRevision || service !== state.hostService) return false + try { return service.invokePopupDefault(match) !== false } catch (_error) { return false } + } + function setDoNotDisturb(value) { const service = state.hostService if (!service) return false @@ -389,6 +412,7 @@ Item { Connections { target: root.sourceModel() ignoreUnknownSignals: true + function onRowsMoved() { root.syncModels() } function onRowsInserted() { root.syncModels() } function onRowsRemoved() { root.syncModels() } function onDataChanged() { root.syncModels() } diff --git a/hancore.shibumi.status/NotificationPanel.qml b/hancore.shibumi.status/NotificationPanel.qml index c0280d2..fb7514d 100644 --- a/hancore.shibumi.status/NotificationPanel.qml +++ b/hancore.shibumi.status/NotificationPanel.qml @@ -64,7 +64,8 @@ ShibumiPanel { appIcon: String(entry.appIcon || ""), summary: String(entry.summary || ""), body: String(entry.body || ""), - details: rowDetails(entry) + details: rowDetails(entry), + liveToken: bucket === "pending" ? String(entry.liveToken || "") : "" }) } } @@ -133,22 +134,10 @@ ShibumiPanel { notificationService.clearPending() } - function openNotification(bucket, index) { - if (!notificationService) { - closePanel() - return - } - const model = bucket === "past" ? notificationService.pastModel - : notificationService.pendingModel - if (!model || index < 0 || index >= model.count) { - closePanel() - return - } - const entry = model.get(index) - if (bucket === "pending" && entry - && typeof notificationService.focusApp === "function") - notificationService.focusApp(entry) - closePanel() + // The entry is the identity captured on press, never a current model index. + function openNotification(bucket, entry) { + if (bucket === "pending" && entry && notificationService + && notificationService.invokeLive(entry)) closePanel() } function safeIconSource(icon) { @@ -370,6 +359,7 @@ ShibumiPanel { required property string appIcon required property string summary required property string body + required property string liveToken required property string details required property int groupCount required property bool groupStart @@ -388,11 +378,13 @@ ShibumiPanel { MouseArea { id: rowHover + property string pressedToken: "" + enabled: notificationRow.bucket === "pending" anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: panel.openNotification(notificationRow.bucket, - notificationRow.sourceIndex) + onPressed: pressedToken = notificationRow.liveToken + onClicked: panel.openNotification(notificationRow.bucket, pressedToken) } Image { diff --git a/tests/notification-adapter-smoke.qml b/tests/notification-adapter-smoke.qml index 2b8f7e4..22e5211 100644 --- a/tests/notification-adapter-smoke.qml +++ b/tests/notification-adapter-smoke.qml @@ -16,6 +16,7 @@ ShellRoot { property int mutationPhase: 0 property int mutationStartTick: 0 + function clickLive(token) { return typeof adapter.invokeLive === "function" ? adapter.invokeLive(token) : adapter.focusApp(adapter.pendingModel.get(0)) } function fail(message) { console.error("notification-adapter-smoke:", message) Qt.exit(1) @@ -60,6 +61,8 @@ ShellRoot { property int dismissCount: 0 property int clearCount: 0 property string focusedSummary: "" + property string clickResult: "" + function invokePopupDefault(index) { if (clickResult === "error") throw Error("fixture action"); clickResult = popupModel.get(index).summary; focusApp(popupModel.get(index)) } function setDoNotDisturb(value) { doNotDisturb = value === true } function dismissPopup(index) { if (index < 0 || index >= popupModel.count) return @@ -254,6 +257,18 @@ ShellRoot { glyph: "", exec: "", urgency: 1, expireTimeout: 8000, timestamp: 101 }) + const token = String(adapter.pendingModel.get(0).liveToken || ""), checks = {} + checks.defaultPath = clickLive(token) && currentHost.clickResult === "Current notification" + liveRows.move(0, 1, 1); checks.reorder = !clickLive(token) && clickLive(String(adapter.pendingModel.get(1).liveToken || "")) && currentHost.clickResult === "Current notification" + const reordered = String(adapter.pendingModel.get(1).liveToken || ""), replacement = adapter.primitiveEntry(liveRows.get(1)); liveRows.remove(1); liveRows.insert(1, replacement) + checks.replacement = !clickLive(reordered) && JSON.stringify(adapter.primitiveEntry(liveRows.get(1))) === JSON.stringify(replacement) + const latest = String(adapter.pendingModel.get(1).liveToken || ""); currentHost.clickResult = "error"; currentHost.focusedSummary = "" + checks.error = !clickLive(latest) && currentHost.focusedSummary === ""; currentHost.clickResult = "" + checks.fallback = clickLive(latest) && currentHost.focusedSummary === "Current notification" + adapter.attachShell(null); adapter.attachShell(currentShell); checks.generation = !clickLive(latest) + const dying = String(adapter.pendingModel.get(0).liveToken || ""), removed = adapter.primitiveEntry(liveRows.get(0)); liveRows.remove(0) + checks.expiry = !clickLive(dying); checks.missing = !clickLive("missing"); liveRows.append(removed) + console.log("235_LIVE_CLICK", JSON.stringify(checks)); if (Object.values(checks).some(ok => !ok)) return root.fail("live click identity/default/error") if (adapter.pendingCount !== 2 || !adapter.focusApp(adapter.pendingModel.get(0)) || currentHost.focusedSummary !== "Current notification" diff --git a/tests/status-plugin-smoke.qml b/tests/status-plugin-smoke.qml index 88289a8..89c27f0 100644 --- a/tests/status-plugin-smoke.qml +++ b/tests/status-plugin-smoke.qml @@ -80,6 +80,9 @@ ShellRoot { id: fakeNotifications property bool doNotDisturb: false property var pendingModel: pendingNotifications + property var popupModel: pendingNotifications + function focusApp(_entry) {} + function invokePopupDefault(index) { dismissPendingCount += index + 1 } property var pastModel: pastNotifications property int dndToggleCount: 0 property int markAllSeenCount: 0 @@ -765,6 +768,8 @@ ShellRoot { || fakeNotifications.markAllSeenCount !== 1 || fakeNotifications.dismissPendingCount !== 1) return root.fail("notification actions bypassed official service") + pendingNotifications.setProperty(0, "originalId", 7); notificationPanel.openNotification("pending", String(notificationPanel.activeRows[0].liveToken || "")) + if (fakeNotifications.dismissPendingCount !== 2) return root.fail("panel did not use exact Live dispatch") status.trayWidget.managePopupOpen = true status.close() root.phase++ From 9e8bf501079f98734c6f4eef0dc31701c4bca6b0 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:17 +0200 Subject: [PATCH 10/40] feat: focus the source app when a Recent notification is clicked --- hancore.shibumi.status/NotificationAdapter.qml | 3 ++- hancore.shibumi.status/NotificationPanel.qml | 7 +++++-- tests/status-plugin-smoke.qml | 10 +++++++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/hancore.shibumi.status/NotificationAdapter.qml b/hancore.shibumi.status/NotificationAdapter.qml index 3380080..ff71e7f 100644 --- a/hancore.shibumi.status/NotificationAdapter.qml +++ b/hancore.shibumi.status/NotificationAdapter.qml @@ -330,13 +330,14 @@ Item { return clearPending() } - function focusApp(entry) { + function focusApp(entry, focusOnly) { const service = state.hostService if (!service || !entry) return false if (typeof service.focusApp === "function") { service.focusApp(entry) return true } + if (focusOnly === true) return false // Recent must never invoke a Live action. const source = sourceIndex(entry, sourceModel()) if (source < 0 || typeof service.invokePopupDefault !== "function") return false diff --git a/hancore.shibumi.status/NotificationPanel.qml b/hancore.shibumi.status/NotificationPanel.qml index fb7514d..c220372 100644 --- a/hancore.shibumi.status/NotificationPanel.qml +++ b/hancore.shibumi.status/NotificationPanel.qml @@ -138,6 +138,9 @@ ShibumiPanel { function openNotification(bucket, entry) { if (bucket === "pending" && entry && notificationService && notificationService.invokeLive(entry)) closePanel() + else if (bucket === "past" && typeof entry === "string" + && entry.trim() && !/[\x00-\x1f\x7f]/.test(entry) + && notificationService && notificationService.focusApp({ app: entry.trim() }, true)) closePanel() } function safeIconSource(icon) { @@ -379,11 +382,11 @@ ShibumiPanel { MouseArea { id: rowHover property string pressedToken: "" - enabled: notificationRow.bucket === "pending" anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onPressed: pressedToken = notificationRow.liveToken + onPressed: pressedToken = notificationRow.bucket === "pending" + ? notificationRow.liveToken : notificationRow.app onClicked: panel.openNotification(notificationRow.bucket, pressedToken) } diff --git a/tests/status-plugin-smoke.qml b/tests/status-plugin-smoke.qml index 89c27f0..f1bd2cc 100644 --- a/tests/status-plugin-smoke.qml +++ b/tests/status-plugin-smoke.qml @@ -81,7 +81,8 @@ ShellRoot { property bool doNotDisturb: false property var pendingModel: pendingNotifications property var popupModel: pendingNotifications - function focusApp(_entry) {} + property var focusedApps: [] + function focusApp(entry) { focusedApps = focusedApps.concat([entry.app]) } function invokePopupDefault(index) { dismissPendingCount += index + 1 } property var pastModel: pastNotifications property int dndToggleCount: 0 @@ -724,6 +725,13 @@ ShellRoot { } else if (root.phase === 3) { if (!status.notificationPanelLoaded || root.phaseTicks < 3) return const notificationPanel = status.notificationPanelItem + if (!fakeNotifications.focusedApps.length) { + ["", " ", "bad\napp", null, {}].forEach(app => notificationPanel.openNotification("past", app)) + if (fakeNotifications.focusedApps.length) return root.fail("invalid Recent app dispatched") + notificationPanel.openNotification("past", notificationPanel.notificationService.pastModel.get(0).app) + if (fakeNotifications.focusedApps.join() !== "Fixture" || status.notificationPanelOpen || fakeNotifications.dismissPendingCount !== 0) return root.fail("Recent focus/close invoked a Live action") + status.open(); root.phaseTicks = 0; return + } if (notificationPanel.ownerWidget !== status || notificationPanel.notificationService !== status.notificationService From 3e80b96cebb60c047f8f7a9e5f819523becb9c8c Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:17 +0200 Subject: [PATCH 11/40] feat: show CPU history and equal-weight GPU usage in the CPU panel --- hancore.shibumi.cpu/CpuPanel.qml | 42 +++++++++++++++++++++++-------- tests/telemetry-runtime-smoke.qml | 9 +++++++ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/hancore.shibumi.cpu/CpuPanel.qml b/hancore.shibumi.cpu/CpuPanel.qml index 891f46f..6dadc5b 100644 --- a/hancore.shibumi.cpu/CpuPanel.qml +++ b/hancore.shibumi.cpu/CpuPanel.qml @@ -10,6 +10,8 @@ ShibumiPanel { required property var ownerWidget required property var systemTelemetry required property var gpuTelemetry + readonly property var cpuHistoryView: cpuUsage.historyView + readonly property var gpuUsageView: gpuUsage owner: ownerWidget open: ownerWidget.opened @@ -89,13 +91,17 @@ ShibumiPanel { } UsageRow { + id: cpuUsage width: parent.width + history: panel.systemTelemetry ? panel.systemTelemetry.cpuHistory : [] + maxSamples: panel.systemTelemetry ? panel.systemTelemetry.cpuMaxSamples : 30 label: "CPU" value: panel.systemTelemetry ? panel.systemTelemetry.cpuPercent : 0 bar: panel.bar } UsageRow { + id: gpuUsage width: parent.width visible: panel.gpuTelemetry && panel.gpuTelemetry.available label: "GPU" @@ -129,7 +135,8 @@ ShibumiPanel { Row { width: parent.width - visible: panel.gpuTelemetry && panel.gpuTelemetry.memoryTotalMiB > 0 + visible: panel.gpuTelemetry && panel.gpuTelemetry.available + && panel.gpuTelemetry.memoryTotalMiB > 0 Text { width: parent.width * 0.4 @@ -197,13 +204,16 @@ ShibumiPanel { required property string label required property int value required property var bar + property var history: null + property int maxSamples: 30 + readonly property alias historyView: historyGraph - height: 16 + height: Commons.Style.space(72) Text { id: usageLabel anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter + anchors.top: parent.top text: parent.label color: parent.bar ? Qt.rgba(parent.bar.foreground.r, parent.bar.foreground.g, parent.bar.foreground.b, 0.65) : Commons.Color.foreground @@ -216,7 +226,7 @@ ShibumiPanel { Text { id: usageValue anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.top: parent.top text: parent.value + "%" color: parent.bar ? parent.bar.urgent : Commons.Color.accent font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family @@ -225,13 +235,25 @@ ShibumiPanel { renderType: Text.NativeRendering } + // A separate panel instance; the bar's CpuWave defaults stay untouched. + CpuWave { + id: historyGraph + visible: usageRow.history !== null + width: parent.width + height: Commons.Style.space(48) + anchors.bottom: parent.bottom + history: usageRow.history || [] + maxSamples: usageRow.maxSamples + accent: usageRow.bar ? usageRow.bar.urgent : Commons.Color.accent + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + } + Rectangle { - anchors.left: usageLabel.right - anchors.leftMargin: 8 - anchors.right: usageValue.left - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - height: 8 + visible: usageRow.history === null + anchors.bottom: parent.bottom + width: parent.width + height: Commons.Style.space(48) radius: height / 2 color: panel.controlActiveFillColor diff --git a/tests/telemetry-runtime-smoke.qml b/tests/telemetry-runtime-smoke.qml index 39982c2..ee2ee9e 100644 --- a/tests/telemetry-runtime-smoke.qml +++ b/tests/telemetry-runtime-smoke.qml @@ -151,12 +151,21 @@ ShellRoot { root.check(cpu.gpu.consumers === 3 && telemetry.system.cpuConsumers === 1 && telemetry.system.memoryConsumers === 1 && storage.storage.consumers === 1 && telemetry.thermal.consumers === 1, "open panel/widget leases unbalanced") + const cpuPanel = root.panels[0], oldHistory = telemetry.system.cpuHistory + telemetry.system.cpuHistory = [0.12, 0.55, 0.31] + root.check(cpuPanel.cpuHistoryView && cpuPanel.cpuHistoryView.history.join() === "0.12,0.55,0.31" + && cpuPanel.cpuHistoryView.maxSamples === telemetry.system.cpuMaxSamples + && cpuPanel.gpuUsageView.visible && cpuPanel.gpuUsageView.value === 42, + "CPU panel history or equal GPU presentation missing") + telemetry.system.cpuHistory = oldHistory root.firstGpu = cpu.gpu items[0].bar = replacementBar cpu.shell = null; telemetry.shell = null; storage.shell = null root.phase++ } else if (root.phase === 4) { root.samePanels() + root.check(root.panels[0].gpuUsageView && !root.panels[0].gpuUsageView.visible + && root.panels[0].cpuHistoryView.history.length === 0, "revoked CPU panel retained GPU/history") root.check(!cpu.ready && !telemetry.ready && !storage.ready && !cpu.backendLoaded && !telemetry.backendLoaded && !storage.backendLoaded && cpu.gpu === null && telemetry.system === null && storage.storage === null From 1ba8a9f9a87d5b081b6e979801d77edfd19fe93c Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:17 +0200 Subject: [PATCH 12/40] feat: show memory as a ring with GiB values in the Memory panel --- hancore.shibumi.memory/MemoryPanel.qml | 114 +++++++++++-------------- tests/telemetry-runtime-smoke.qml | 8 ++ 2 files changed, 57 insertions(+), 65 deletions(-) diff --git a/hancore.shibumi.memory/MemoryPanel.qml b/hancore.shibumi.memory/MemoryPanel.qml index e87e132..4d7a84b 100644 --- a/hancore.shibumi.memory/MemoryPanel.qml +++ b/hancore.shibumi.memory/MemoryPanel.qml @@ -9,6 +9,7 @@ ShibumiPanel { required property var ownerWidget required property var telemetry + readonly property var usageRing: memoryRing owner: ownerWidget open: ownerWidget.opened @@ -74,63 +75,56 @@ ShibumiPanel { panel.bar.foreground.b, 0.18) : Commons.Color.popups.border } - Item { + Row { width: parent.width - height: 30 - - Text { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - text: (panel.telemetry ? panel.telemetry.memPercent : 0) + "%" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.family: panel.bar ? panel.bar.fontFamily - : Commons.Style.font.family - font.pixelSize: 11 - font.weight: Font.Medium - renderType: Text.NativeRendering - } - - Rectangle { - anchors.bottom: parent.bottom - width: parent.width - height: 8 - radius: height / 2 - color: panel.controlActiveFillColor - - Rectangle { - width: parent.width * (panel.telemetry ? panel.telemetry.memPercent : 0) / 100 - height: parent.height - radius: height / 2 - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - Behavior on width { NumberAnimation { duration: 300 } } + height: memoryRing.height + spacing: Commons.Style.space(16) + + // Panel-local dimensions and colors; MemoryRing's bar defaults stay intact. + MemoryRing { + id: memoryRing + width: Commons.Style.space(80) + height: width + percent: panel.telemetry ? panel.telemetry.memPercent : 0 + foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground + accent: panel.bar ? panel.bar.urgent : Commons.Color.accent + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + + Text { + anchors.centerIn: parent + text: memoryRing.percent + "%" + color: memoryRing.accent + font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family + font.pixelSize: 11 + font.weight: Font.Medium + renderType: Text.NativeRendering } } - } - Column { - width: parent.width - spacing: 4 - - MemoryStatRow { - width: parent.width - label: "Used" - value: (panel.telemetry ? panel.telemetry.memUsedGiB : 0).toFixed(1) + " GiB" - detail: (panel.telemetry ? panel.telemetry.memUsedMiB : 0) + " MiB" - bar: panel.bar - } - MemoryStatRow { - width: parent.width - label: "Available" - value: ((panel.telemetry ? panel.telemetry.memAvailableMiB : 0) / 1024).toFixed(1) + " GiB" - detail: (panel.telemetry ? panel.telemetry.memAvailableMiB : 0) + " MiB" - bar: panel.bar - } - MemoryStatRow { - width: parent.width - label: "Total" - value: (panel.telemetry ? panel.telemetry.memTotalGiB : 0).toFixed(1) + " GiB" - detail: (panel.telemetry ? panel.telemetry.memTotalMiB : 0) + " MiB" - bar: panel.bar + Column { + width: parent.width - memoryRing.width - parent.spacing + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + MemoryStatRow { + width: parent.width + label: "Used" + value: (panel.telemetry ? panel.telemetry.memUsedGiB : 0).toFixed(1) + " GiB" + bar: panel.bar + } + MemoryStatRow { + width: parent.width + label: "Available" + value: ((panel.telemetry ? panel.telemetry.memAvailableMiB : 0) / 1024).toFixed(1) + " GiB" + bar: panel.bar + } + MemoryStatRow { + width: parent.width + label: "Total" + value: (panel.telemetry ? panel.telemetry.memTotalGiB : 0).toFixed(1) + " GiB" + bar: panel.bar + } } } @@ -176,7 +170,6 @@ ShibumiPanel { component MemoryStatRow: Row { required property string label required property string value - required property string detail required property var bar Text { @@ -191,22 +184,13 @@ ShibumiPanel { } Text { id: valueText - width: parent.width * 0.3 + width: parent.width * 0.6 + horizontalAlignment: Text.AlignRight text: parent.value color: parent.bar ? parent.bar.foreground : Commons.Color.foreground font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family font.pixelSize: 11 renderType: Text.NativeRendering } - Text { - id: detailText - width: parent.width * 0.3 - text: parent.detail - color: parent.bar ? Qt.rgba(parent.bar.foreground.r, parent.bar.foreground.g, - parent.bar.foreground.b, 0.58) : Commons.Color.foreground - font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - renderType: Text.NativeRendering - } } } diff --git a/tests/telemetry-runtime-smoke.qml b/tests/telemetry-runtime-smoke.qml index ee2ee9e..433ac01 100644 --- a/tests/telemetry-runtime-smoke.qml +++ b/tests/telemetry-runtime-smoke.qml @@ -158,6 +158,14 @@ ShellRoot { && cpuPanel.gpuUsageView.visible && cpuPanel.gpuUsageView.value === 42, "CPU panel history or equal GPU presentation missing") telemetry.system.cpuHistory = oldHistory + const memoryPanel = root.panels[1], oldMemory = [telemetry.system.memTotalMiB, telemetry.system.memAvailableMiB] + telemetry.system.memTotalMiB = 8192; telemetry.system.memAvailableMiB = 3072 + function texts(item) { return ("text" in item ? [String(item.text)] : []).concat(item.children.reduce((out, child) => out.concat(texts(child)), [])) } + const memoryText = texts(memoryPanel) + root.check(memoryPanel.usageRing && memoryPanel.usageRing.percent === 63 + && ["5.0 GiB", "3.0 GiB", "8.0 GiB"].every(value => memoryText.indexOf(value) >= 0) + && memoryText.every(value => !/\bMiB\b/.test(value)), "Memory ring or GiB-only details missing") + telemetry.system.memTotalMiB = oldMemory[0]; telemetry.system.memAvailableMiB = oldMemory[1] root.firstGpu = cpu.gpu items[0].bar = replacementBar cpu.shell = null; telemetry.shell = null; storage.shell = null From 6890b41da6738cd366d4d1e343a16e5416f202d0 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 11:15:17 +0200 Subject: [PATCH 13/40] feat: restyle the media panel with a spinning vinyl and player metadata --- hancore.shibumi.media/MediaPanel.qml | 150 ++++++++++++++++++--------- tests/media-plugin-smoke.qml | 16 ++- 2 files changed, 118 insertions(+), 48 deletions(-) diff --git a/hancore.shibumi.media/MediaPanel.qml b/hancore.shibumi.media/MediaPanel.qml index d73a091..7068df8 100644 --- a/hancore.shibumi.media/MediaPanel.qml +++ b/hancore.shibumi.media/MediaPanel.qml @@ -30,6 +30,10 @@ ShibumiPanel { readonly property var spectrumThemeColors: spectrumService && spectrumService.themeColors ? spectrumService.themeColors : [] readonly property int renderedSourceCount: sourceRepeater.count + readonly property bool sourcesVisible: sourceList.visible + readonly property bool vinylRotating: vinylArtwork.rotating + readonly property bool albumVisible: albumText.visible + readonly property bool playerNameVisible: playerText.visible readonly property bool spectrumWorkerRunning: spectrumService ? spectrumService.workerRunning === true : false readonly property string spectrumState: spectrumService @@ -199,18 +203,6 @@ ShibumiPanel { anchors.verticalCenter: parent.verticalCenter spacing: Commons.Style.space(8) - Text { - visible: panel.active && panel.playerName !== "" - anchors.verticalCenter: parent.verticalCenter - text: panel.playerName - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.45) - : Commons.Color.foreground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption - renderType: Text.NativeRendering - } - IconAction { anchors.verticalCenter: parent.verticalCenter icon: "close" @@ -229,36 +221,18 @@ ShibumiPanel { spacing: Commons.Style.space(10) visible: panel.active - Rectangle { - width: Commons.Style.space(56) + PanelVinyl { + id: vinylArtwork + width: Commons.Style.space(80) height: width - radius: Commons.Style.space(5) - color: panel.bar ? Qt.rgba(panel.bar.foreground.r, - panel.bar.foreground.g, panel.bar.foreground.b, 0.10) - : Commons.Color.background - clip: true - - Image { - anchors.fill: parent - source: panel.player ? String(panel.player.trackArtUrl || "") : "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - cache: true - retainWhileLoading: true - } - - Presentation.IconText { - anchors.centerIn: parent - visible: !panel.player || !panel.player.trackArtUrl - text: "music_note" - color: panel.bar ? panel.bar.urgent : Commons.Color.accent - font.pixelSize: Commons.Style.font.displayLarge - fill: 1 - } + tint: panel.bar ? panel.bar.urgent : Commons.Color.accent + artSource: panel.player ? String(panel.player.trackArtUrl || "") : "" + spinning: panel.open && panel.playing + revolutionMs: 6000 } Column { - width: parent.width - Commons.Style.space(66) + width: parent.width - vinylArtwork.width - parent.spacing anchors.verticalCenter: parent.verticalCenter spacing: Commons.Style.space(3) @@ -287,6 +261,7 @@ ShibumiPanel { } Text { + id: albumText width: parent.width visible: text !== "" text: panel.player ? String(panel.player.trackAlbum || "") : "" @@ -298,9 +273,30 @@ ShibumiPanel { elide: Text.ElideRight renderType: Text.NativeRendering } + Text { + id: playerText + width: parent.width + visible: text !== "" + text: panel.playerName + color: panel.controlMutedHigh + font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family + font.pixelSize: Commons.Style.font.caption + elide: Text.ElideRight + renderType: Text.NativeRendering + } } } + MediaSpectrum { + width: parent.width + height: Commons.Style.space(40) + visible: panel.active + levels: panel.levels + tint: panel.bar ? panel.bar.urgent : Commons.Color.accent + themeColors: panel.spectrumThemeColors + opacity: panel.playing ? 1 : 0.5 + } + Item { width: parent.width height: Commons.Style.space(18) @@ -357,15 +353,7 @@ ShibumiPanel { Item { width: parent.width height: Commons.Style.space(40) - - MediaSpectrum { - anchors.fill: parent - visible: panel.active - levels: panel.levels - tint: panel.bar ? panel.bar.urgent : Commons.Color.accent - themeColors: panel.spectrumThemeColors - opacity: panel.playing ? 1 : 0.5 - } + visible: !panel.active Column { anchors.centerIn: parent @@ -430,6 +418,7 @@ ShibumiPanel { } Column { + id: sourceList width: parent.width visible: panel.sourcePlayers.length > 1 spacing: Commons.Style.space(2) @@ -525,6 +514,73 @@ ShibumiPanel { } } + // Panel-local Muse-style mark: no bar geometry, band or animation defaults change. + component PanelVinyl: Item { + id: vinyl + required property color tint + required property string artSource + required property bool spinning + property int revolutionMs: 6000 + readonly property bool rotating: spin.running + + Canvas { + anchors.fill: parent + antialiasing: true + property color canvasTint: vinyl.tint + onCanvasTintChanged: requestPaint() + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + Component.onCompleted: requestPaint() + onPaint: { + const ctx = getContext("2d"), radius = Math.min(width, height) * 0.46 + ctx.clearRect(0, 0, width, height) + ctx.save() + ctx.translate(width / 2, height / 2) + ctx.strokeStyle = canvasTint + ctx.lineWidth = Math.max(1, radius * 0.045) + ctx.beginPath(); ctx.arc(0, 0, radius, 0, Math.PI * 2); ctx.stroke() + ctx.globalAlpha = 0.55 + for (let i = 0; i < 2; i++) { + ctx.beginPath() + ctx.arc(0, 0, radius * 0.64, i * Math.PI - 0.35, i * Math.PI + 2.35) + ctx.stroke() + } + ctx.restore() + } + } + Rectangle { + anchors.centerIn: parent + width: parent.width * 0.38 + height: width + radius: width / 2 + color: Commons.Util.alpha(vinyl.tint, 0.2) + Image { + anchors.centerIn: parent + width: parent.width * 0.66 + height: width + source: vinyl.artSource + visible: status === Image.Ready + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: true + } + Rectangle { + anchors.centerIn: parent + width: Commons.Style.space(3) + height: width + radius: width / 2 + color: panel.bar ? panel.bar.background : Commons.Color.background + } + } + NumberAnimation on rotation { + id: spin + from: 0; to: 360 + duration: vinyl.revolutionMs + loops: Animation.Infinite + running: vinyl.visible && vinyl.spinning + } + } + component IconAction: Ui.CursorSurface { id: action property string icon: "" diff --git a/tests/media-plugin-smoke.qml b/tests/media-plugin-smoke.qml index 6d0f952..afcae86 100644 --- a/tests/media-plugin-smoke.qml +++ b/tests/media-plugin-smoke.qml @@ -312,7 +312,8 @@ ShellRoot { return root.fail("open panel did not replace its spectrum provider") fakeShell.serviceAvailable = false } else if (root.rebindPhase === 5) { - if (panel.mediaService !== null || replacementSpectrum.clientCount !== 0) + if (panel.mediaService !== null || replacementSpectrum.clientCount !== 0 + || ("vinylRotating" in panel && panel.vinylRotating)) return root.fail("open panel retained revoked media authority") fakeShell.serviceAvailable = true fakeShell.spectrumBackend = null @@ -324,6 +325,19 @@ ShellRoot { fakeShell.spectrumBackend = fakeSpectrum } if (root.rebindPhase++ < 7) return + const checks = { playing: panel.vinylRotating === true, + metadata: panel.albumVisible === true && panel.playerNameVisible === true, + multiple: panel.sourcesVisible === true } + playerA.isPlaying = false; checks.paused = panel.vinylRotating === false + playerA.isPlaying = true; checks.resumed = panel.vinylRotating === true + mediaState.sourcePlayers = [playerA]; checks.single = panel.sourcesVisible === false + mediaState.sourcePlayers = [playerA, playerB] + const album = playerA.trackAlbum, identity = playerA.identity, desktop = playerA.desktopEntry + playerA.trackAlbum = ""; playerA.identity = ""; playerA.desktopEntry = "" + checks.absentMetadata = panel.albumVisible === false && panel.playerNameVisible === false + playerA.trackAlbum = album; playerA.identity = identity; playerA.desktopEntry = desktop + console.log("241_MEDIA_PANEL", JSON.stringify(checks)) + if (Object.values(checks).some(value => !value)) return root.fail("vinyl, metadata or source-list presentation") if (media.panelItem.renderedSourceCount !== 2 || media.panelItem.spectrumWorkerRunning || media.panelItem.formatTime(61) !== "1:01" From abee838eed5ab74ab54b264878b819ae0384875d Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:54:48 +0200 Subject: [PATCH 14/40] feat: adopt the Omarchy agents panel layout for AI usage --- hancore.shibumi.ai/AiUsagePanel.qml | 226 +++++++++++++++++++--------- hancore.shibumi.ai/Omarchy-LICENSE | 20 +++ tests/ai-plugin-regression.sh | 2 + tests/ai-plugin-smoke.qml | 19 ++- 4 files changed, 192 insertions(+), 75 deletions(-) create mode 100644 hancore.shibumi.ai/Omarchy-LICENSE diff --git a/hancore.shibumi.ai/AiUsagePanel.qml b/hancore.shibumi.ai/AiUsagePanel.qml index dbcf454..c9a1fc6 100644 --- a/hancore.shibumi.ai/AiUsagePanel.qml +++ b/hancore.shibumi.ai/AiUsagePanel.qml @@ -1,10 +1,14 @@ pragma ComponentBehavior: Bound import QtQuick +import QtQuick.Effects +import Qt.labs.folderlistmodel import qs.Commons as Commons import qs.Ui as Ui import "../hancore.shibumi.state/lib/presentation" as Presentation +// Layout derived from MIT-licensed Omarchy v4.0.4 plugins/agents/Panel.qml. +// See Omarchy-LICENSE. Shibumi retains its own data, selection and lifecycle. ShibumiPanel { id: panel @@ -15,7 +19,21 @@ ShibumiPanel { readonly property var limitWindows: aiService ? aiService.limitWindows(provider) : [] readonly property var headline: aiService ? aiService.bindingWindow(provider) : null readonly property bool headlineAlarm: !!headline && headline.percent >= 90 - readonly property color headlineColor: headlineUsage.usageColor + readonly property color headlineColor: !providerReady ? panel.controlMuted + : headlineAlarm ? Commons.Color.urgent : panel.controlForeground + readonly property var providerLogo: heroMark + readonly property string heroTitle: heroName.text + readonly property string heroTier: heroPlan.text + readonly property bool providerSwitchVisible: providerRow.visible + readonly property bool balanceVisible: balanceSection.visible + readonly property var balance: provider && provider.balance + && typeof provider.balance.remaining === "number" && isFinite(provider.balance.remaining) + && provider.balance.remaining >= 0 ? provider.balance : null + readonly property real balanceRatio: balance && typeof balance.funded === "number" + && isFinite(balance.funded) && balance.funded > 0 ? Math.min(1, balance.remaining / balance.funded) : -1 + readonly property string balanceDetail: balanceRatio >= 0 && typeof balance.spent === "number" + && isFinite(balance.spent) && balance.spent >= 0 + ? money(balance.spent) + " spent of " + money(balance.funded) + " funded" + (balance.estimated === true ? " · estimated" : "") : "" readonly property bool primaryUsageVisible: limitWindows.length > 0 readonly property bool secondaryUsageVisible: limitWindows.length > 1 readonly property var days: provider ? provider.recentDays || [] : [] @@ -68,9 +86,9 @@ ShibumiPanel { owner: ownerWidget open: ownerWidget.opened focusTarget: keyCatcher - contentWidth: fittedContentWidth(Commons.Style.space(360)) + contentWidth: fittedContentWidth(Commons.Style.space(380)) contentHeight: fittedContentHeight(contentColumn.implicitHeight, - Commons.Style.space(560)) + Commons.Style.space(640)) function moveProvider(direction) { if (aiService) aiService.cycleTool(direction) @@ -91,6 +109,17 @@ ShibumiPanel { ? "Pace: " + (limit.percent / 100 * span / elapsed).toFixed(1) + "×" : "" } + function dayLabel(date) { + const day = new Date(date + "T00:00:00"), today = new Date(nowMs) + return day.toDateString() === today.toDateString() ? "Today" + : ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][day.getDay()] || date + } + + function money(value) { + const currency = String(balance ? balance.currency || "USD" : "USD") + return ({USD: "$", EUR: "€", GBP: "£"}[currency] || currency + " ") + Number(value).toFixed(2) + } + function providerTabLabel(providerValue) { if (!providerValue) return "AI" const id = String(providerValue.providerId || "") @@ -140,28 +169,75 @@ ShibumiPanel { Column { id: contentColumn width: scroller.width - spacing: Commons.Style.space(8) + spacing: Commons.Style.space(12) Item { id: header width: parent.width - height: Commons.Style.space(28) + height: Commons.Style.space(52) + + Item { + width: Commons.Style.space(36); height: width + anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter + Image { + id: heroMark + anchors.fill: parent + readonly property string hostBase: { + const id = panel.provider ? String(panel.provider.providerId || "") : "", root = panel.aiService ? String(panel.aiService.omarchyPath || "") : "" + return /^[A-Za-z0-9_-]{1,64}$/.test(id) && /^\/(?!\/)/.test(root) && id !== "codex" && id !== "opencode" ? root.replace(/\/$/, "") + "/shell/plugins/agents/assets/" + id : "" + } + property FolderListModel hostMarks: FolderListModel { + folder: heroMark.hostBase ? Commons.Util.fileUrl(heroMark.hostBase.slice(0, heroMark.hostBase.lastIndexOf("/"))) : Qt.resolvedUrl("assets") + nameFilters: ["*.svg"]; showDirs: false } + readonly property color surface: panel.shibumiTokens ? panel.shibumiTokens.panelBackground : Commons.Color.popups.background + readonly property bool light: [surface.r, surface.g, surface.b].map(c => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)) + .reduce((sum, c, i) => sum + c * [0.2126, 0.7152, 0.0722][i], 0) >= 0.5 + visible: status === Image.Ready + source: panel.provider && panel.provider.providerId === "codex" ? Qt.resolvedUrl("assets/codex.svg") + : panel.provider && panel.provider.providerId === "opencode" ? Qt.resolvedUrl("assets/opencode-mark.svg") : !hostBase || hostMarks.status !== FolderListModel.Ready ? "" + : hostMarks.count && light && hostMarks.indexOf(Commons.Util.fileUrl(hostBase + "-light.svg")) >= 0 ? Commons.Util.fileUrl(hostBase + "-light.svg") + : hostMarks.count && hostMarks.indexOf(Commons.Util.fileUrl(hostBase + ".svg")) >= 0 ? Commons.Util.fileUrl(hostBase + ".svg") : "" + fillMode: Image.PreserveAspectFit + layer.enabled: hostBase === "" + layer.effect: MultiEffect { colorization: 1; colorizationColor: panel.controlForeground } + } + Presentation.IconText { + anchors.centerIn: parent + visible: heroMark.status !== Image.Ready + text: "smart_toy" + color: panel.controlForeground + font.pixelSize: Commons.Style.font.display + } + } Text { + id: heroName anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: parent.width - actionRow.width - Commons.Style.space(8) + anchors.leftMargin: Commons.Style.space(48) + anchors.top: parent.top + anchors.topMargin: Commons.Style.space(6) + width: parent.width - actionRow.width - Commons.Style.space(56) elide: Text.ElideRight - text: panel.provider ? panel.providerHeading(panel.provider) - + " · " + panel.providerStatusLabel : "AI USAGE" + textFormat: Text.PlainText + text: panel.provider ? panel.providerHeading(panel.provider) : "AI USAGE" color: panel.controlForeground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: Commons.Style.font.subtitle - font.letterSpacing: 2 - font.weight: Font.Medium + font.weight: Font.DemiBold renderType: Text.NativeRendering } + Text { + id: heroPlan + anchors.left: heroName.left; anchors.top: heroName.bottom + width: heroName.width; elide: Text.ElideRight + textFormat: Text.PlainText + text: panel.tierLabel(panel.provider) + color: panel.controlMutedHigh + font.family: heroName.font.family + font.pixelSize: Commons.Style.font.bodySmall + } + Row { id: actionRow anchors.right: parent.right @@ -186,6 +262,7 @@ ShibumiPanel { Row { id: providerRow + visible: providerTabs.count > 1 width: parent.width height: Commons.Style.space(28) spacing: Commons.Style.space(6) @@ -243,25 +320,8 @@ ShibumiPanel { color: panel.dividerColor } - Item { - visible: panel.tierLabel(panel.provider) !== "" - width: parent.width - height: visible ? Commons.Style.space(16) : 0 - - Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: panel.tierLabel(panel.provider) - color: panel.controlForeground - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.body - font.weight: Font.Medium - renderType: Text.NativeRendering - } - } - Text { - visible: panel.providerEmptyStateText !== "" + visible: panel.providerEmptyStateText !== "" && !panel.balance width: parent.width text: panel.providerEmptyStateText wrapMode: Text.WordWrap @@ -271,13 +331,38 @@ ShibumiPanel { renderType: Text.NativeRendering } - UsageRow { - id: headlineUsage - visible: !!panel.headline - label: panel.headline ? panel.headline.title : "" - value: panel.headline ? panel.headline.percent : 0 - dimmed: !panel.providerReady + Rectangle { + visible: !!panel.provider + width: parent.width; height: statusRow.height + Commons.Style.space(16) + radius: panel.controlRadius; color: panel.controlFillColor + border.width: panel.controlBorderWidth; border.color: panel.controlBorderColor + DetailRow { + id: statusRow + anchors.centerIn: parent; width: parent.width - Commons.Style.space(16) + label: "Status" + value: panel.provider ? String(panel.provider.usageStatusText || panel.providerStatusLabel) : "" + wrapValue: true + } } + Column { + id: balanceSection + visible: !!panel.balance + width: parent.width; spacing: Commons.Style.space(6) + DetailRow { label: "BALANCE"; value: "" } + DetailRow { label: "Prepaid credits"; value: panel.balance ? panel.money(panel.balance.remaining) : "" } + Rectangle { + width: parent.width; height: Commons.Style.space(4); radius: height / 2 + visible: panel.balanceRatio >= 0 + color: panel.controlActiveFillColor + Rectangle { + height: parent.height; radius: parent.radius + width: parent.width * Math.max(0, panel.balanceRatio) + color: width <= parent.width * 0.1 ? Commons.Color.urgent : panel.controlForeground + } + } + } + DetailRow { visible: panel.balanceDetail !== ""; label: ""; value: panel.balanceDetail; wrapValue: true } + DetailRow { visible: panel.limitWindows.length > 0; label: "LIMITS"; value: "" } Repeater { model: panel.limitWindows delegate: Column { @@ -299,16 +384,10 @@ ShibumiPanel { } DetailRow { id: limitsUnavailable - visible: panel.provider && !panel.providerHasUsage + visible: panel.provider && !panel.providerHasUsage && !panel.balance label: "Limits" value: "Unavailable" } - DetailRow { - visible: panel.provider && String(panel.provider.usageStatusText || "") !== "" - label: panel.provider && String(panel.provider.providerId || "") === "codex" - ? "General limit" : "Status" - value: panel.provider ? String(panel.provider.usageStatusText || "") : "" - } DetailRow { visible: panel.provider && Number(panel.provider.windowTokens) > 0 label: panel.provider && String(panel.provider.providerId || "") @@ -347,7 +426,8 @@ ShibumiPanel { delegate: ModelUsageRow { required property var modelData width: contentColumn.width - entry: ({ name: modelData.date, totalLabel: panel.aiService.formatTokens(modelData.messageCount), + daily: true + entry: ({ name: panel.dayLabel(modelData.date), totalLabel: panel.aiService.formatTokens(modelData.messageCount), pct: modelData.messageCount / Math.max(1, ...panel.days.map(day => day.messageCount)) * 100, detail: "" }) } } @@ -359,7 +439,7 @@ ShibumiPanel { Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - text: "MODELS" + text: "TOKENS BY MODEL" color: panel.controlMutedHigh font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: Commons.Style.font.caption @@ -431,17 +511,17 @@ ShibumiPanel { required property real value property bool dimmed: false readonly property color usageColor: dimmed ? panel.controlMuted - : value >= 90 ? Commons.Color.urgent : panel.controlAccent + : value >= 90 ? Commons.Color.urgent : panel.controlForeground width: parent.width - height: Commons.Style.space(16) + height: Commons.Style.space(30) Text { id: usageLabel anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter + anchors.top: parent.top text: usageRow.label textFormat: Text.PlainText - width: Math.min(implicitWidth, parent.width * 0.45) + width: parent.width - usageValue.width - Commons.Style.space(8) elide: Text.ElideRight color: panel.controlMutedHigh font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family @@ -453,7 +533,7 @@ ShibumiPanel { Text { id: usageValue anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.top: parent.top text: Math.round(usageRow.value) + "%" color: usageRow.usageColor font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family @@ -463,12 +543,10 @@ ShibumiPanel { } Rectangle { - anchors.left: usageLabel.right - anchors.leftMargin: Commons.Style.space(8) - anchors.right: usageValue.left - anchors.rightMargin: Commons.Style.space(8) - anchors.verticalCenter: parent.verticalCenter - height: Commons.Style.space(8) + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: Commons.Style.space(4) radius: height / 2 color: Commons.Util.alpha(panel.controlAccent, 0.15) Rectangle { @@ -514,15 +592,17 @@ ShibumiPanel { component ModelUsageRow: Item { id: modelRow required property var entry - height: Commons.Style.space(42) + property bool daily: false + height: Commons.Style.space(daily ? 20 : 30) readonly property real percent: Math.max(0, Math.min(100, Number(entry && entry.pct) || 0)) Text { id: modelName anchors.left: parent.left - anchors.top: parent.top - width: parent.width * 0.68 + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: modelRow.daily ? 0 : Commons.Style.space(8) + width: modelRow.daily ? Commons.Style.space(52) : parent.width * 0.68 text: String(modelRow.entry && modelRow.entry.name || "") textFormat: Text.PlainText elide: Text.ElideRight @@ -534,8 +614,10 @@ ShibumiPanel { } Text { + id: modelValue anchors.right: parent.right - anchors.top: parent.top + anchors.rightMargin: modelRow.daily ? 0 : Commons.Style.space(8) + anchors.verticalCenter: parent.verticalCenter text: String(modelRow.entry && modelRow.entry.totalLabel || "") color: panel.controlAccent font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family @@ -545,11 +627,12 @@ ShibumiPanel { } Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: modelName.bottom - anchors.topMargin: Commons.Style.space(5) - height: Commons.Style.space(6) + z: -1 + anchors.left: modelRow.daily ? modelName.right : parent.left + anchors.right: modelRow.daily ? modelValue.left : parent.right + anchors.margins: modelRow.daily ? Commons.Style.space(8) : 0 + anchors.verticalCenter: parent.verticalCenter + height: modelRow.daily ? Commons.Style.space(4) : parent.height radius: height / 2 color: Commons.Util.alpha(panel.controlAccent, 0.14) @@ -557,15 +640,15 @@ ShibumiPanel { width: parent.width * modelRow.percent / 100 height: parent.height radius: height / 2 - color: panel.controlAccent + color: modelRow.daily ? panel.controlForeground : panel.controlActiveFillColor Behavior on width { NumberAnimation { duration: 300 } } } } - Text { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom + MouseArea { id: modelHover; anchors.fill: parent; hoverEnabled: true; acceptedButtons: Qt.NoButton } + Presentation.ShibumiPillToolTip { + panel: panel + visible: !modelRow.daily && modelHover.containsMouse && text !== "" text: modelRow.entry && modelRow.entry.detail !== undefined ? modelRow.entry.detail : "I " + String(modelRow.entry && modelRow.entry.inputLabel || "0") + " O " + String(modelRow.entry && modelRow.entry.outputLabel || "0") @@ -577,11 +660,6 @@ ShibumiPanel { ? " CW " + String(modelRow.entry.cacheWriteLabel) : "") + (String(modelRow.entry && modelRow.entry.todayLabel || "0") !== "0" ? " today " + String(modelRow.entry.todayLabel) : "") - elide: Text.ElideRight - color: panel.controlMutedHigh - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.space(9) - renderType: Text.NativeRendering } } } diff --git a/hancore.shibumi.ai/Omarchy-LICENSE b/hancore.shibumi.ai/Omarchy-LICENSE new file mode 100644 index 0000000..f12cfa7 --- /dev/null +++ b/hancore.shibumi.ai/Omarchy-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tests/ai-plugin-regression.sh b/tests/ai-plugin-regression.sh index 9c9b00c..bf76838 100755 --- a/tests/ai-plugin-regression.sh +++ b/tests/ai-plugin-regression.sh @@ -32,6 +32,8 @@ mkdir -p "$tmpdir/runtime" "$tmpdir/fixtures" "$tmpdir/home/.claude" \ "$tmpdir/home/.codex" "$tmpdir/state/omarchy/agents/usage" \ "$tmpdir/omarchy/bin" chmod 700 "$tmpdir/runtime" +mkdir -p "$tmpdir/omarchy/shell/plugins/agents/assets" +printf '\n' | tee "$tmpdir/omarchy/shell/plugins/agents/assets/claude.svg" >"$tmpdir/omarchy/shell/plugins/agents/assets/claude-light.svg" shibumi_stage_suite_runtime "$repo_root" "$tmpdir" cp -a -- "$repo_root/hancore.shibumi.ai" "$tmpdir/ai" install -m 0644 "$repo_root/tests/fixtures/AiPanelHost.qml" \ diff --git a/tests/ai-plugin-smoke.qml b/tests/ai-plugin-smoke.qml index 4d6c841..0864c09 100644 --- a/tests/ai-plugin-smoke.qml +++ b/tests/ai-plugin-smoke.qml @@ -8,6 +8,7 @@ ShellRoot { id: root property int phase: 0 + property int logoStage: 0 property int ticks: 0 property real stableProviderWidth: 0 property var clickTargets: [] @@ -80,7 +81,16 @@ ShellRoot { checks.clock = checks.clock && emptyAgentsPanel.clockRunning === true; emptyPanelOwner.opened = false checks.clock = checks.clock && emptyAgentsPanel.clockRunning === false console.log("AI_PRESENTATION_CASES", JSON.stringify(checks)) - return Object.values(checks).every(value => value) + const savedService = emptyAgentsPanel.aiService + const layout = { noBalance: emptyAgentsPanel.balanceVisible === false } + emptyAgentsPanel.aiService = { selectedProvider: { providerId: "fixture", providerName: "Fixture", tierLabel: "Pro", balance: {remaining: 12, funded: 20, spent: 8, currency: "USD"} }, providers: [{providerId: "fixture"}], selectedTool: "fixture", limitWindows: () => [], bindingWindow: () => null, formatTokens: value => String(value) } + layout.hero = emptyAgentsPanel.heroTitle === "Fixture" && emptyAgentsPanel.heroTier === "Pro" + layout.balance = emptyAgentsPanel.balanceVisible === true + layout.single = emptyAgentsPanel.providerSwitchVisible === false + emptyAgentsPanel.aiService = savedService + layout.multiple = emptyAgentsPanel.providerSwitchVisible === true + console.log("243_AI_LAYOUT", JSON.stringify(layout)) + return Object.values(checks).every(value => value) && Object.values(layout).every(value => value) } function linearChannel(value) { @@ -1070,6 +1080,13 @@ ShellRoot { || agentsService.providerFor("claude") === null || agentsService.providerFor("codex") === null) return root.fail("queued Agents update did not become current") + if (root.logoStage < 3) { + const mark = emptyAgentsPanel.providerLogo, ready = mark && mark.status === Image.Ready && mark.visible, glyph = mark && mark.parent.children[1].visible + if (!mark || (root.logoStage < 2 ? !ready || glyph || String(mark.source) !== "file://" + agentsService.omarchyPath + "/shell/plugins/agents/assets/claude" + (root.logoStage === 1 ? "-light" : "") + ".svg" : ready || !glyph || String(mark.source) !== "")) return root.fail("host Claude logo/glyph stage " + root.logoStage) + console.log("244_AI_LOGO", root.logoStage, "image=" + ready, "glyph=" + glyph) + if (root.logoStage++ === 0) { fakeBar.visualTokens = Object.assign({}, fakeBar.visualTokens, {panelBackground: "#ffffff"}); return } + if (root.logoStage === 2) { emptyAgentsPanel.aiService = {selectedProvider: agentsService.providerFor("claude"), omarchyPath: agentsService.omarchyPath + "/absent", providers: [], limitWindows: () => [], bindingWindow: () => null, formatTokens: value => String(value)}; return } + } stop() watchdog.stop() console.log("ai plugin smoke passed") From efa3435820d32a6d38771e2b3993af656672791e Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:54:48 +0200 Subject: [PATCH 15/40] feat: restyle the CPU panel as system monitor cards with load average --- hancore.shibumi.cpu/CpuPanel.qml | 117 +++++++++++------- hancore.shibumi.telemetry/SystemTelemetry.qml | 39 +++++- tests/telemetry-runtime-smoke.qml | 26 ++-- 3 files changed, 127 insertions(+), 55 deletions(-) diff --git a/hancore.shibumi.cpu/CpuPanel.qml b/hancore.shibumi.cpu/CpuPanel.qml index 6dadc5b..8429f5b 100644 --- a/hancore.shibumi.cpu/CpuPanel.qml +++ b/hancore.shibumi.cpu/CpuPanel.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import qs.Commons as Commons import qs.Ui as Ui +import "../hancore.shibumi.state/lib/presentation" as Presentation ShibumiPanel { id: panel @@ -10,7 +11,8 @@ ShibumiPanel { required property var ownerWidget required property var systemTelemetry required property var gpuTelemetry - readonly property var cpuHistoryView: cpuUsage.historyView + readonly property string cpuModelText: cpuModelLabel.text + readonly property string loadAverageText: loadValue.text readonly property var gpuUsageView: gpuUsage owner: ownerWidget @@ -21,7 +23,19 @@ ShibumiPanel { contentHeight: fittedContentHeight(panelColumn.implicitHeight) property var acquiredGpuTelemetry: null + property var acquiredSystemTelemetry: null + function openMonitor() { + const owner = panel.ownerWidget, accepted = panel.ownerWidget.openSystemMonitor() + if (accepted) owner.close() + return accepted + } function syncGpuLease() { + const system = open ? systemTelemetry : null + if (acquiredSystemTelemetry !== system) { + if (acquiredSystemTelemetry) acquiredSystemTelemetry.release("cpuPanel") + acquiredSystemTelemetry = system + if (system) system.acquire("cpuPanel") + } const next = open ? gpuTelemetry : null if (acquiredGpuTelemetry === next) return if (acquiredGpuTelemetry) acquiredGpuTelemetry.release() @@ -29,9 +43,13 @@ ShibumiPanel { if (acquiredGpuTelemetry) acquiredGpuTelemetry.acquire() } onGpuTelemetryChanged: syncGpuLease() + onSystemTelemetryChanged: syncGpuLease() onOpenChanged: syncGpuLease() Component.onCompleted: syncGpuLease() - Component.onDestruction: if (acquiredGpuTelemetry) acquiredGpuTelemetry.release() + Component.onDestruction: { + if (acquiredGpuTelemetry) acquiredGpuTelemetry.release() + if (acquiredSystemTelemetry) acquiredSystemTelemetry.release("cpuPanel") + } Ui.PanelKeyCatcher { id: keyCatcher @@ -45,12 +63,28 @@ ShibumiPanel { spacing: 8 Item { + id: header width: parent.width - height: 24 + height: Commons.Style.space(52) + + Presentation.IconText { + text: "memory"; color: panel.controlAccent + font.pixelSize: Commons.Style.font.icon + anchors.verticalCenter: parent.verticalCenter + } + Text { + id: cpuModelLabel + x: Commons.Style.space(28); anchors.bottom: parent.bottom + width: parent.width - x - monitorButton.width - Commons.Style.space(8); elide: Text.ElideRight; textFormat: Text.PlainText + text: panel.systemTelemetry ? panel.systemTelemetry.cpuModel : "" + color: panel.controlMutedHigh + font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption + } Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Commons.Style.space(28) text: "CPU · GPU" color: panel.bar ? panel.bar.foreground : Commons.Color.foreground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family @@ -63,6 +97,7 @@ ShibumiPanel { Text { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: monitorButton.width + Commons.Style.space(8) text: "\u2715" color: closeMouse.containsMouse ? panel.controlAccent : panel.controlMuted @@ -93,16 +128,23 @@ ShibumiPanel { UsageRow { id: cpuUsage width: parent.width - history: panel.systemTelemetry ? panel.systemTelemetry.cpuHistory : [] - maxSamples: panel.systemTelemetry ? panel.systemTelemetry.cpuMaxSamples : 30 label: "CPU" value: panel.systemTelemetry ? panel.systemTelemetry.cpuPercent : 0 bar: panel.bar + Text { + id: loadValue + x: Commons.Style.space(10); y: Commons.Style.space(46) + text: "Load Average (1/5/15 min)\n" + (panel.systemTelemetry && panel.systemTelemetry.loadAverage.length === 3 + ? panel.systemTelemetry.loadAverage.map(value => value.toFixed(2)).join(" / ") : "—") + color: panel.controlMutedHigh + font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption + } } UsageRow { id: gpuUsage width: parent.width + height: Commons.Style.space(100) visible: panel.gpuTelemetry && panel.gpuTelemetry.available label: "GPU" value: panel.gpuTelemetry ? panel.gpuTelemetry.utilization : 0 @@ -110,7 +152,10 @@ ShibumiPanel { } Row { - width: parent.width + id: temperatureRow + parent: gpuUsage + x: Commons.Style.space(10); y: Commons.Style.space(46) + width: parent.width - 2 * x visible: panel.gpuTelemetry && panel.gpuTelemetry.available && panel.gpuTelemetry.temperatureC > 0 @@ -124,7 +169,7 @@ ShibumiPanel { renderType: Text.NativeRendering } Text { - width: parent.width * 0.3 + width: parent.width * 0.6 text: panel.gpuTelemetry ? panel.gpuTelemetry.temperatureC + "°C" : "" color: panel.bar ? panel.bar.foreground : Commons.Color.foreground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family @@ -134,7 +179,9 @@ ShibumiPanel { } Row { - width: parent.width + parent: gpuUsage + x: Commons.Style.space(10); y: temperatureRow.visible ? temperatureRow.y + temperatureRow.height + Commons.Style.space(8) : Commons.Style.space(46) + width: parent.width - 2 * x visible: panel.gpuTelemetry && panel.gpuTelemetry.available && panel.gpuTelemetry.memoryTotalMiB > 0 @@ -148,7 +195,8 @@ ShibumiPanel { renderType: Text.NativeRendering } Text { - width: parent.width * 0.3 + width: parent.width * 0.6 + elide: Text.ElideRight text: panel.gpuTelemetry ? panel.gpuTelemetry.memoryUsedMiB + " / " + panel.gpuTelemetry.memoryTotalMiB + " MiB" : "" @@ -160,13 +208,10 @@ ShibumiPanel { } Rectangle { - width: parent.width - height: 1 - color: panel.dividerColor - } - - Rectangle { - width: parent.width + id: monitorButton + parent: header + anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter + width: Commons.Style.space(70) height: 28 radius: panel.controlRadius color: monitorMouse.containsMouse @@ -177,7 +222,7 @@ ShibumiPanel { Text { anchors.centerIn: parent - text: "Open btop" + text: "btop" color: panel.bar ? panel.bar.background : Commons.Color.background font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: 11 @@ -189,10 +234,7 @@ ShibumiPanel { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor - onClicked: { - panel.ownerWidget.close() - panel.ownerWidget.openSystemMonitor() - } + onClicked: panel.openMonitor() } } } @@ -204,16 +246,19 @@ ShibumiPanel { required property string label required property int value required property var bar - property var history: null - property int maxSamples: 30 - readonly property alias historyView: historyGraph + height: Commons.Style.space(90) - height: Commons.Style.space(72) + Rectangle { + anchors.fill: parent; z: -1 + radius: panel.controlRadius; color: panel.controlFillColor + border.width: panel.controlBorderWidth; border.color: panel.controlBorderColor + } Text { id: usageLabel anchors.left: parent.left anchors.top: parent.top + anchors.margins: Commons.Style.space(10) text: parent.label color: parent.bar ? Qt.rgba(parent.bar.foreground.r, parent.bar.foreground.g, parent.bar.foreground.b, 0.65) : Commons.Color.foreground @@ -227,6 +272,7 @@ ShibumiPanel { id: usageValue anchors.right: parent.right anchors.top: parent.top + anchors.margins: Commons.Style.space(10) text: parent.value + "%" color: parent.bar ? parent.bar.urgent : Commons.Color.accent font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family @@ -235,25 +281,10 @@ ShibumiPanel { renderType: Text.NativeRendering } - // A separate panel instance; the bar's CpuWave defaults stay untouched. - CpuWave { - id: historyGraph - visible: usageRow.history !== null - width: parent.width - height: Commons.Style.space(48) - anchors.bottom: parent.bottom - history: usageRow.history || [] - maxSamples: usageRow.maxSamples - accent: usageRow.bar ? usageRow.bar.urgent : Commons.Color.accent - onWidthChanged: requestPaint() - onHeightChanged: requestPaint() - } - Rectangle { - visible: usageRow.history === null - anchors.bottom: parent.bottom - width: parent.width - height: Commons.Style.space(48) + x: Commons.Style.space(10); y: Commons.Style.space(30) + width: parent.width - 2 * x + height: Commons.Style.space(6) radius: height / 2 color: panel.controlActiveFillColor diff --git a/hancore.shibumi.telemetry/SystemTelemetry.qml b/hancore.shibumi.telemetry/SystemTelemetry.qml index 44fe02e..d22eb11 100644 --- a/hancore.shibumi.telemetry/SystemTelemetry.qml +++ b/hancore.shibumi.telemetry/SystemTelemetry.qml @@ -7,7 +7,11 @@ Scope { property int cpuConsumers: 0 property int memoryConsumers: 0 - readonly property bool active: cpuConsumers > 0 || memoryConsumers > 0 + property int cpuPanelConsumers: 0 + property string cpuModel: "" + property var loadAverage: [] + property bool cpuModelRead: false + readonly property bool active: cpuConsumers > 0 || memoryConsumers > 0 || cpuPanelConsumers > 0 readonly property int intervalMs: 2000 property int cpuPercent: 0 @@ -35,12 +39,18 @@ Scope { } else if (kind === "memory") { memoryConsumers++ if (memoryConsumers === 1) memoryFile.reload() + } else if (kind === "cpuPanel") { + if (++cpuPanelConsumers === 1) { + if (!cpuModelRead) { cpuModelRead = true; modelFile.reload() } + loadFile.reload() + } } } function release(kind) { if (kind === "cpu") cpuConsumers = Math.max(0, cpuConsumers - 1) else if (kind === "memory") memoryConsumers = Math.max(0, memoryConsumers - 1) + else if (kind === "cpuPanel") cpuPanelConsumers = Math.max(0, cpuPanelConsumers - 1) } function parseCpu(text) { @@ -102,7 +112,18 @@ Scope { memCachedMiB = Math.round(cached / 1024) } + function parseCpuModel(text) { + const match = /^(?:model name|Hardware)\s*:\s*([^\r\n]{1,256})/m.exec(String(text || "").slice(0, 8192)) + cpuModel = match ? match[1].trim() : "" + } + + function parseLoad(text) { + const values = String(text || "").slice(0, 256).trim().split(/\s+/).slice(0, 3).map(Number) + loadAverage = values.length === 3 && values.every(value => isFinite(value) && value >= 0) ? values : [] + } + function refresh() { + if (cpuPanelConsumers > 0) loadFile.reload() if (cpuConsumers > 0) cpuFile.reload() if (memoryConsumers > 0) memoryFile.reload() } @@ -121,6 +142,22 @@ Scope { onLoaded: root.parseMemory(text()) } + FileView { + id: modelFile + path: "/proc/cpuinfo" + preload: false + printErrors: false + onLoaded: root.parseCpuModel(text()) + } + FileView { + id: loadFile + path: "/proc/loadavg" + preload: false + printErrors: false + onLoaded: if (root.cpuPanelConsumers > 0) root.parseLoad(text()) + onLoadFailed: root.loadAverage = [] + } + Timer { interval: root.intervalMs running: root.active diff --git a/tests/telemetry-runtime-smoke.qml b/tests/telemetry-runtime-smoke.qml index 433ac01..4a15a7d 100644 --- a/tests/telemetry-runtime-smoke.qml +++ b/tests/telemetry-runtime-smoke.qml @@ -67,6 +67,8 @@ ShellRoot { } function requestPopout(owner) { activePopout = owner } function releasePopout(owner) { if (activePopout === owner) activePopout = null } + property string lastCommand: "" + function run(command) { lastCommand = command } function showTooltip(target, text) {} function hideTooltip(target) {} function switchPanelFrom(owner, direction) { return false } @@ -140,6 +142,7 @@ ShellRoot { && telemetry.gpuTelemetry === cpu.gpu, "shared scoped owner identity mismatch") root.check(!cpu.gpu.probeEnabled && !telemetry.thermal.probeEnabled && !storage.storage.runtimeProbesEnabled, "fixture probe suppression lost") + root.check(telemetry.system.cpuPanelConsumers === 0 && telemetry.system.cpuModel === "" && telemetry.system.loadAverage.length === 0, "detail probe before panel demand") cpu.gpu.parse("sysfs|42|61|0|0\nstatus|ok") telemetry.thermal.parseDetailed("55|63|80|100|44|70|90|39") storage.storage.parseUsage("/dev/fixture 1000 400 600 40%") @@ -151,13 +154,13 @@ ShellRoot { root.check(cpu.gpu.consumers === 3 && telemetry.system.cpuConsumers === 1 && telemetry.system.memoryConsumers === 1 && storage.storage.consumers === 1 && telemetry.thermal.consumers === 1, "open panel/widget leases unbalanced") - const cpuPanel = root.panels[0], oldHistory = telemetry.system.cpuHistory - telemetry.system.cpuHistory = [0.12, 0.55, 0.31] - root.check(cpuPanel.cpuHistoryView && cpuPanel.cpuHistoryView.history.join() === "0.12,0.55,0.31" - && cpuPanel.cpuHistoryView.maxSamples === telemetry.system.cpuMaxSamples - && cpuPanel.gpuUsageView.visible && cpuPanel.gpuUsageView.value === 42, - "CPU panel history or equal GPU presentation missing") - telemetry.system.cpuHistory = oldHistory + const cpuPanel = root.panels[0] + root.check(typeof telemetry.system.parseLoad === "function", "CPU panel demand metrics missing") + telemetry.system.parseCpuModel("processor : 0\nmodel name : Fixture CPU\n") + telemetry.system.parseLoad("0.25 1.50 2.75 1/300 12345\n") + root.check(cpuPanel.cpuModelText === "Fixture CPU" && cpuPanel.loadAverageText.indexOf("0.25 / 1.50 / 2.75") >= 0 + && telemetry.system.cpuPanelConsumers === 1 && cpuPanel.gpuUsageView.visible && cpuPanel.gpuUsageView.value === 42, "CPU model/load or GPU card missing") + telemetry.system.parseLoad("invalid"); root.check(telemetry.system.loadAverage.length === 0, "invalid load accepted") const memoryPanel = root.panels[1], oldMemory = [telemetry.system.memTotalMiB, telemetry.system.memAvailableMiB] telemetry.system.memTotalMiB = 8192; telemetry.system.memAvailableMiB = 3072 function texts(item) { return ("text" in item ? [String(item.text)] : []).concat(item.children.reduce((out, child) => out.concat(texts(child)), [])) } @@ -173,12 +176,12 @@ ShellRoot { } else if (root.phase === 4) { root.samePanels() root.check(root.panels[0].gpuUsageView && !root.panels[0].gpuUsageView.visible - && root.panels[0].cpuHistoryView.history.length === 0, "revoked CPU panel retained GPU/history") + && root.panels[0].cpuModelText === "", "revoked CPU panel retained GPU/model") root.check(!cpu.ready && !telemetry.ready && !storage.ready && !cpu.backendLoaded && !telemetry.backendLoaded && !storage.backendLoaded && cpu.gpu === null && telemetry.system === null && storage.storage === null && root.panels[0].gpuTelemetry === null && root.panels[0].systemTelemetry === null - && root.panels[0].acquiredGpuTelemetry === null + && root.panels[0].acquiredGpuTelemetry === null && root.panels[0].acquiredSystemTelemetry === null && root.panels[1].telemetry === null && root.panels[2].gpuTelemetry === null && root.panels[3].telemetry === null && root.panels[4].storage === null && root.panels[0].bar === replacementBar && root.panels[1].bar === memoryBar, @@ -190,7 +193,7 @@ ShellRoot { root.samePanels() root.check(cpu.gpu !== root.firstGpu && cpu.gpu.consumers === 3 && root.panels[0].gpuTelemetry === cpu.gpu - && root.panels[0].acquiredGpuTelemetry === cpu.gpu + && root.panels[0].acquiredGpuTelemetry === cpu.gpu && telemetry.system.cpuPanelConsumers === 1 && root.panels[1].telemetry === telemetry.system && root.panels[2].gpuTelemetry === cpu.gpu && root.panels[3].telemetry === telemetry.thermal @@ -212,10 +215,11 @@ ShellRoot { root.samePanels() root.check(cpu.gpu.consumers === 3 && !duplicateCpu.ready, "duplicate removal did not restore the sole GPU owner") + root.check(root.panels[0].openMonitor() && replacementBar.lastCommand === "omarchy-launch-or-focus-tui btop", "panel btop action") for (const item of items) item.close() root.phase++ } else if (root.phase === 8) { - root.check(cpu.gpu.consumers === 2, "CPU panel close retained its GPU lease") + root.check(cpu.gpu.consumers === 2 && telemetry.system.cpuPanelConsumers === 0, "CPU panel close retained GPU/detail lease") for (const item of items) root.check(item.panelItem === null, "closed panel retained") views.active = false root.phase++ From ff8650b9f6f156a01bef9cd8efdb8444c6cdbc2f Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:54:48 +0200 Subject: [PATCH 16/40] feat: add shuffle, repeat and seek controls to the media panel --- ARCHITECTURE.md | 11 +++-- docs/phase2-ownership-map.md | 5 +- hancore.shibumi.media/MediaPanel.qml | 68 ++++++++++++++++++++++++++-- tests/media-plugin-smoke.qml | 27 +++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d40fe9e..df569f0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -238,7 +238,8 @@ The lifecycle contract and uncompleted physical gates are defined in ### Ownership, Performance, And Safety -- Every platform capability has one authoritative state and action owner. +- Every platform capability has one authoritative state and action owner, + except the explicitly scoped media-panel controls documented under G9 below. Quattro or Quickshell remains authoritative when it exposes a sufficient service contract. - Shibumi adds a narrow adapter only when the host contract cannot produce the @@ -843,9 +844,13 @@ Current Phase 2 foundation: repository-owned compatibility contract rather than inferred from mutable installed files. - G9 replaces only the official media presentation. The keep-loaded - `omarchy.media` service remains the sole MPRIS/PipeWire selection and action + `omarchy.media` service remains the MPRIS/PipeWire selection and transport-action owner while Shibumi views supply the default row, FULL/muse row, lazy panel, - progress, cover art, and source selection; + progress, cover art, and source selection. The approved panel-only exception is + direct shuffle, repeat and seek through the selected native MprisPlayer's public + properties, gated by its current capabilities. Seek also requires supported + position/length and the same player/track as at press. No discovery, process, + IPC owner or OSD is added; previous/play/pause/next still use Omarchy `runAction`; - closed media views are worker-free. One lazy process-wide Shibumi spectrum service owns the optional Cava capability/default-sink probe, bounded Cava process, streamed configuration, degraded state, and cleanup. It runs only diff --git a/docs/phase2-ownership-map.md b/docs/phase2-ownership-map.md index fee018e..ce86c97 100644 --- a/docs/phase2-ownership-map.md +++ b/docs/phase2-ownership-map.md @@ -168,7 +168,10 @@ Shibumi component consumes `shell.firstPartyServiceFor("omarchy.media")` and the original `omarchy.media` bar alias is suppressed, but the official service stays loaded and authoritative for player selection, ghost filtering, action routing, source switching, PipeWire stream correlation, and OSD feedback. The -Shibumi views own only presentation and lazy panel state. One lazy process-wide +Shibumi views own presentation and lazy panel state, with the approved panel-only +exception of capability-gated shuffle/repeat/seek on the selected native player. +Transport still delegates to Omarchy; no discovery, IPC owner or OSD is added. +One lazy process-wide Shibumi spectrum service owns the optional Cava probe, process, degraded state, lease accounting, and cleanup. Per-output, per-panel, or per-view Cava owners are prohibited even though the spectrum is not a second media-state owner. diff --git a/hancore.shibumi.media/MediaPanel.qml b/hancore.shibumi.media/MediaPanel.qml index 7068df8..9eba630 100644 --- a/hancore.shibumi.media/MediaPanel.qml +++ b/hancore.shibumi.media/MediaPanel.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import Quickshell.Services.Mpris import qs.Commons as Commons import qs.Ui as Ui import "../hancore.shibumi.state/lib/presentation" as Presentation @@ -34,6 +35,12 @@ ShibumiPanel { readonly property bool vinylRotating: vinylArtwork.rotating readonly property bool albumVisible: albumText.visible readonly property bool playerNameVisible: playerText.visible + readonly property var shuffleControl: shuffleButton + readonly property var repeatControl: repeatButton + readonly property var seekControl: seekMouse + readonly property bool directAvailable: open && active && !!player && player.canControl === true && sourcePlayers.indexOf(player) >= 0 + readonly property bool seekAvailable: directAvailable && !!player && player.canSeek === true + && player.positionSupported === true && player.lengthSupported === true && isFinite(player.length) && player.length > 0 readonly property bool spectrumWorkerRunning: spectrumService ? spectrumService.workerRunning === true : false readonly property string spectrumState: spectrumService @@ -64,6 +71,28 @@ ShibumiPanel { playerKey(player)) === true : false } + // Approved exception: only shuffle, repeat and seek mutate the selected native + // player directly. Transport still belongs to Omarchy; no discovery, IPC or OSD. + function directAction(action, target, value, trackId) { + if (!directAvailable || !target || target !== player || target.canControl !== true) return false + try { + if (action === "shuffle" && target.shuffleSupported === true) { + target.shuffle = !target.shuffle + } else if (action === "repeat" && target.loopSupported === true) { + target.loopState = target.loopState === MprisLoopState.None ? MprisLoopState.Track + : target.loopState === MprisLoopState.Track ? MprisLoopState.Playlist : MprisLoopState.None + } else if (action === "seek" && seekAvailable && target.uniqueId === trackId && typeof value === "number" && isFinite(value)) { + target.position = Math.max(0, Math.min(target.length, value)) // Native API uses seconds. + resetPosition() + } else return false + return true + } catch (_error) { return false } + } + + function controlAction(action) { + return action === "shuffle" || action === "repeat" ? directAction(action, player) : runAction(action) + } + function selectSource(index) { if (index < 0 || index >= sourcePlayers.length || !mediaService || typeof mediaService.selectPlayer !== "function") return false @@ -72,6 +101,8 @@ ShibumiPanel { function activateCursor() { if (focusSection === "sources") return selectSource(cursorIndex) + if (cursorIndex === -1) return controlAction("shuffle") + if (cursorIndex === 3) return controlAction("repeat") if (cursorIndex === 0) return runAction("previous") if (cursorIndex === 2) return runAction("next") return runAction("playPause") @@ -83,7 +114,7 @@ ShibumiPanel { focusSection = "sources" cursorIndex = 0 } else if (dx !== 0) { - cursorIndex = Math.max(0, Math.min(2, cursorIndex + dx)) + cursorIndex = Math.max(-1, Math.min(3, cursorIndex + dx)) } return } @@ -302,6 +333,21 @@ ShibumiPanel { height: Commons.Style.space(18) visible: panel.active && panel.currentLength > 0 + MouseArea { + id: seekMouse + anchors.fill: parent; z: 1 + enabled: panel.seekAvailable + cursorShape: Qt.PointingHandCursor + property var pressedPlayer: null + property var pressedTrack: null + onPressed: { pressedPlayer = panel.player; pressedTrack = pressedPlayer.uniqueId } + onCanceled: pressedPlayer = null + onClicked: function(mouse) { + if (pressedPlayer && pressedPlayer === panel.player && pressedTrack === pressedPlayer.uniqueId) + panel.directAction("seek", pressedPlayer, mouse.x / Math.max(1, width) * pressedPlayer.length, pressedTrack) + pressedPlayer = null + } + } Rectangle { id: progressTrack anchors.left: parent.left @@ -390,6 +436,13 @@ ShibumiPanel { anchors.horizontalCenter: parent.horizontalCenter spacing: Commons.Style.space(12) + MediaPanelButton { + id: shuffleButton + icon: checked ? "shuffle_on" : "shuffle"; action: "shuffle"; controlIndex: -1 + tooltipText: checked ? "Shuffle: On" : "Shuffle: Off" + checked: !!panel.player && panel.player.shuffle === true + enabled: panel.directAvailable && !!panel.player && panel.player.shuffleSupported === true + } MediaPanelButton { icon: "skip_previous" action: "previous" @@ -410,6 +463,14 @@ ShibumiPanel { controlIndex: 2 enabled: panel.player && panel.player.canGoNext === true } + MediaPanelButton { + id: repeatButton + icon: panel.player && panel.player.loopState === MprisLoopState.Track ? "repeat_one" : "repeat" + action: "repeat"; controlIndex: 3 + tooltipText: "Repeat: " + (!checked ? "Off" : panel.player.loopState === MprisLoopState.Track ? "Track" : "Playlist") + checked: !!panel.player && (panel.player.loopState === MprisLoopState.Track || panel.player.loopState === MprisLoopState.Playlist) + enabled: panel.directAvailable && !!panel.player && panel.player.loopSupported === true + } } Ui.PanelSeparator { @@ -620,9 +681,10 @@ ShibumiPanel { required property string action required property int controlIndex property bool accent: false + property bool checked: false iconText: icon - foreground: accent && panel.bar + foreground: checked ? panel.controlAccent : accent && panel.bar ? panel.bar.urgent : panel.bar ? panel.bar.foreground : Commons.Color.foreground hoverColor: panel.bar ? panel.bar.urgent : Commons.Color.accent fontFamily: "Material Symbols Rounded" @@ -636,6 +698,6 @@ ShibumiPanel { panel.cursorIndex = controlIndex } } - onClicked: panel.runAction(action) + onClicked: panel.controlAction(action) } } diff --git a/tests/media-plugin-smoke.qml b/tests/media-plugin-smoke.qml index afcae86..c82e73d 100644 --- a/tests/media-plugin-smoke.qml +++ b/tests/media-plugin-smoke.qml @@ -34,6 +34,10 @@ ShellRoot { property bool canTogglePlaying: true property bool canPlay: true property bool canPause: true + property bool canControl: true + property bool shuffleSupported: true; property bool loopSupported: true + property bool canSeek: true; property bool positionSupported: true; property bool lengthSupported: true + property bool shuffle: false; property int loopState: 0; property int uniqueId: 1 property real position: 61 property real length: 245 } @@ -53,6 +57,10 @@ ShellRoot { property bool canTogglePlaying: true property bool canPlay: true property bool canPause: true + property bool canControl: true + property bool shuffleSupported: false; property bool loopSupported: false + property bool canSeek: false; property bool positionSupported: true; property bool lengthSupported: true + property bool shuffle: false; property int loopState: 0; property int uniqueId: 2 property real position: 0 property real length: 180 } @@ -70,6 +78,7 @@ ShellRoot { function playerKey(player) { return player ? String(player.key || "") : "" } function runAction(action, showFeedback, targetKey) { + if (showFeedback) root.fail("unexpected media OSD") lastAction = String(action || "") lastTarget = String(targetKey || "") actionCount++ @@ -338,6 +347,24 @@ ShellRoot { playerA.trackAlbum = album; playerA.identity = identity; playerA.desktopEntry = desktop console.log("241_MEDIA_PANEL", JSON.stringify(checks)) if (Object.values(checks).some(value => !value)) return root.fail("vinyl, metadata or source-list presentation") + if (typeof panel.directAction !== "function") return root.fail("direct MPRIS controls missing") + const actionCount = mediaState.actionCount, direct = { supported: panel.shuffleControl.enabled && panel.repeatControl.enabled && panel.seekControl.enabled } + panel.shuffleControl.clicked(); direct.shuffle = playerA.shuffle && panel.shuffleControl.checked + direct.cycle = [1, 2, 0].every(state => { panel.repeatControl.clicked(); return playerA.loopState === state && panel.repeatControl.checked === (state !== 0) }) + direct.seek = panel.directAction("seek", playerA, -5, 1) && playerA.position === 0 && panel.directAction("seek", playerA, 80, 1) && playerA.position === 80 && panel.directAction("seek", playerA, 999, 1) && playerA.position === 245 + direct.invalid = !panel.directAction("seek", playerA, NaN, 1) && !panel.directAction("seek", playerB, 30, 2) + panel.selectSource(1) + direct.unsupported = !panel.shuffleControl.enabled && !panel.repeatControl.enabled && !panel.seekControl.enabled && !panel.directAction("shuffle", playerB) && !panel.directAction("repeat", playerB) && !panel.directAction("seek", playerB, 30, 2) + playerB.shuffleSupported = true; playerB.loopSupported = true; playerB.canSeek = true + panel.shuffleControl.clicked(); direct.selected = !panel.directAction("shuffle", playerA) && playerB.shuffle && playerA.shuffle && panel.directAction("repeat", playerB) && playerB.loopState === 1 && panel.directAction("seek", playerB, 50, 2) && playerB.position === 50 + playerB.uniqueId = 3; direct.stale = !panel.directAction("seek", playerB, 10, 2) && playerB.position === 50 + playerB.positionSupported = false; direct.noPosition = !panel.seekControl.enabled && !panel.directAction("seek", playerB, 10, 3); playerB.positionSupported = true + playerB.lengthSupported = false; direct.noLength = !panel.seekControl.enabled && !panel.directAction("seek", playerB, 10, 3); playerB.lengthSupported = true + playerB.canControl = false; direct.noControl = !panel.shuffleControl.enabled && !panel.repeatControl.enabled && !panel.seekControl.enabled && !panel.directAction("shuffle", playerB); playerB.canControl = true + mediaState.sourcePlayers = [playerA]; direct.detached = !panel.shuffleControl.enabled && !panel.directAction("shuffle", playerB); mediaState.sourcePlayers = [playerA, playerB] + panel.selectSource(0); direct.owner = mediaState.actionCount === actionCount && panel.runAction("next") && mediaState.actionCount === actionCount + 1 && mediaState.lastTarget === "player-a" + console.log("243_MPRIS_CONTROLS", JSON.stringify(direct)) + if (Object.values(direct).some(value => !value)) return root.fail("direct MPRIS capability, target or owner path") if (media.panelItem.renderedSourceCount !== 2 || media.panelItem.spectrumWorkerRunning || media.panelItem.formatTime(61) !== "1:01" From b4b07d31f9129be40c3861844590fd08cb2251a5 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:10:12 +0200 Subject: [PATCH 17/40] feat: restyle the AI usage panel with a compact token chart and limit rows --- hancore.shibumi.ai/AiUsagePanel.qml | 186 ++++++++++++++-------------- hancore.shibumi.ai/Local-AI-LICENSE | 21 ++++ tests/ai-plugin-regression.sh | 2 - tests/ai-plugin-smoke.qml | 46 +++++-- tests/fixtures/AiPanelHost.qml | 1 + 5 files changed, 154 insertions(+), 102 deletions(-) create mode 100644 hancore.shibumi.ai/Local-AI-LICENSE diff --git a/hancore.shibumi.ai/AiUsagePanel.qml b/hancore.shibumi.ai/AiUsagePanel.qml index c9a1fc6..7670e51 100644 --- a/hancore.shibumi.ai/AiUsagePanel.qml +++ b/hancore.shibumi.ai/AiUsagePanel.qml @@ -1,14 +1,13 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Effects -import Qt.labs.folderlistmodel import qs.Commons as Commons import qs.Ui as Ui import "../hancore.shibumi.state/lib/presentation" as Presentation // Layout derived from MIT-licensed Omarchy v4.0.4 plugins/agents/Panel.qml. // See Omarchy-LICENSE. Shibumi retains its own data, selection and lifecycle. +// Chart/metric structure adapted from 0xSero/omarchy-local-ai; see Local-AI-LICENSE. ShibumiPanel { id: panel @@ -21,7 +20,6 @@ ShibumiPanel { readonly property bool headlineAlarm: !!headline && headline.percent >= 90 readonly property color headlineColor: !providerReady ? panel.controlMuted : headlineAlarm ? Commons.Color.urgent : panel.controlForeground - readonly property var providerLogo: heroMark readonly property string heroTitle: heroName.text readonly property string heroTier: heroPlan.text readonly property bool providerSwitchVisible: providerRow.visible @@ -37,7 +35,19 @@ ShibumiPanel { readonly property bool primaryUsageVisible: limitWindows.length > 0 readonly property bool secondaryUsageVisible: limitWindows.length > 1 readonly property var days: provider ? provider.recentDays || [] : [] - readonly property int renderedDayCount: dayRepeater.count + readonly property int renderedDayCount: days.length + // Omarchy's legacy recentDays.messageCount field contains token totals, not messages. + readonly property var weekDays: Array.from({length: 7}, (_, i) => { + const date = dayKey(i - 6), day = days.find(entry => entry.date === date) + return {date: date, messageCount: day ? day.messageCount : null} + }) + readonly property int reportedDays: weekDays.filter(day => day.messageCount !== null).length + readonly property real messageTotal: weekDays.reduce((sum, day) => sum + (day.messageCount || 0), 0) + readonly property real messageMax: Math.max(1, ...weekDays.map(day => day.messageCount || 0)) + function dayKey(offset) { + const date = new Date(nowMs); date.setDate(date.getDate() + offset) + return Qt.formatDateTime(date, "yyyy-MM-dd") + } readonly property bool clockRunning: panelClock.running property double nowMs: Date.now() onOpenChanged: if (open) nowMs = Date.now() @@ -169,54 +179,19 @@ ShibumiPanel { Column { id: contentColumn width: scroller.width - spacing: Commons.Style.space(12) + spacing: Commons.Style.space(7) Item { id: header width: parent.width - height: Commons.Style.space(52) - - Item { - width: Commons.Style.space(36); height: width - anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter - Image { - id: heroMark - anchors.fill: parent - readonly property string hostBase: { - const id = panel.provider ? String(panel.provider.providerId || "") : "", root = panel.aiService ? String(panel.aiService.omarchyPath || "") : "" - return /^[A-Za-z0-9_-]{1,64}$/.test(id) && /^\/(?!\/)/.test(root) && id !== "codex" && id !== "opencode" ? root.replace(/\/$/, "") + "/shell/plugins/agents/assets/" + id : "" - } - property FolderListModel hostMarks: FolderListModel { - folder: heroMark.hostBase ? Commons.Util.fileUrl(heroMark.hostBase.slice(0, heroMark.hostBase.lastIndexOf("/"))) : Qt.resolvedUrl("assets") - nameFilters: ["*.svg"]; showDirs: false } - readonly property color surface: panel.shibumiTokens ? panel.shibumiTokens.panelBackground : Commons.Color.popups.background - readonly property bool light: [surface.r, surface.g, surface.b].map(c => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)) - .reduce((sum, c, i) => sum + c * [0.2126, 0.7152, 0.0722][i], 0) >= 0.5 - visible: status === Image.Ready - source: panel.provider && panel.provider.providerId === "codex" ? Qt.resolvedUrl("assets/codex.svg") - : panel.provider && panel.provider.providerId === "opencode" ? Qt.resolvedUrl("assets/opencode-mark.svg") : !hostBase || hostMarks.status !== FolderListModel.Ready ? "" - : hostMarks.count && light && hostMarks.indexOf(Commons.Util.fileUrl(hostBase + "-light.svg")) >= 0 ? Commons.Util.fileUrl(hostBase + "-light.svg") - : hostMarks.count && hostMarks.indexOf(Commons.Util.fileUrl(hostBase + ".svg")) >= 0 ? Commons.Util.fileUrl(hostBase + ".svg") : "" - fillMode: Image.PreserveAspectFit - layer.enabled: hostBase === "" - layer.effect: MultiEffect { colorization: 1; colorizationColor: panel.controlForeground } - } - Presentation.IconText { - anchors.centerIn: parent - visible: heroMark.status !== Image.Ready - text: "smart_toy" - color: panel.controlForeground - font.pixelSize: Commons.Style.font.display - } - } + height: Commons.Style.space(44) Text { id: heroName anchors.left: parent.left - anchors.leftMargin: Commons.Style.space(48) anchors.top: parent.top anchors.topMargin: Commons.Style.space(6) - width: parent.width - actionRow.width - Commons.Style.space(56) + width: Math.max(0, parent.width - actionRow.width - Commons.Style.space(8)) elide: Text.ElideRight textFormat: Text.PlainText text: panel.provider ? panel.providerHeading(panel.provider) : "AI USAGE" @@ -233,9 +208,12 @@ ShibumiPanel { width: heroName.width; elide: Text.ElideRight textFormat: Text.PlainText text: panel.tierLabel(panel.provider) - color: panel.controlMutedHigh + visible: text !== "" + color: panel.controlForeground font.family: heroName.font.family - font.pixelSize: Commons.Style.font.bodySmall + font.pixelSize: Commons.Style.font.body + font.weight: Font.DemiBold + renderType: Text.NativeRendering } Row { @@ -263,7 +241,8 @@ ShibumiPanel { Row { id: providerRow visible: providerTabs.count > 1 - width: parent.width + x: panel.controlBorderWidth + width: Math.max(0, parent.width - 2 * x) height: Commons.Style.space(28) spacing: Commons.Style.space(6) @@ -277,11 +256,10 @@ ShibumiPanel { readonly property bool selected: panel.aiService.selectedTool === modelData.providerId readonly property bool hovered: tabMouse.containsMouse - width: (providerRow.width - Math.max(0, - (panel.aiService.providers.length - 1) * providerRow.spacing)) - / Math.max(1, panel.aiService.providers.length) + width: Math.max(0, Math.floor((providerRow.width - Math.max(0, + (providerTabs.count - 1) * providerRow.spacing)) / Math.max(1, providerTabs.count))) height: parent.height - radius: panel.controlRadius + radius: panel.renderedSurfaceRadius color: selected ? panel.controlActiveFillColor : hovered ? panel.controlHoverFillColor : panel.controlFillColor border.width: panel.controlBorderWidth @@ -292,7 +270,9 @@ ShibumiPanel { Behavior on border.color { ColorAnimation { duration: 100 } } Text { - anchors.centerIn: parent + anchors.fill: parent; anchors.margins: Commons.Style.space(6) + horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight; textFormat: Text.PlainText text: panel.providerTabLabel(providerTab.modelData) color: providerTab.selected || providerTab.hovered ? panel.controlAccent : panel.controlForeground @@ -331,17 +311,60 @@ ShibumiPanel { renderType: Text.NativeRendering } - Rectangle { - visible: !!panel.provider - width: parent.width; height: statusRow.height + Commons.Style.space(16) - radius: panel.controlRadius; color: panel.controlFillColor - border.width: panel.controlBorderWidth; border.color: panel.controlBorderColor - DetailRow { - id: statusRow - anchors.centerIn: parent; width: parent.width - Commons.Style.space(16) - label: "Status" - value: panel.provider ? String(panel.provider.usageStatusText || panel.providerStatusLabel) : "" - wrapValue: true + DetailRow { + visible: !!panel.provider; label: "Status" + value: panel.provider ? String(panel.provider.usageStatusText || panel.providerStatusLabel) : ""; wrapValue: true + } + Grid { + id: metrics + visible: !!panel.provider; width: parent.width + columns: 4; columnSpacing: Commons.Style.space(4) + Repeater { + model: [{label:"Today tokens",value:panel.provider && panel.aiService ? panel.aiService.formatTokens(panel.provider.todayTotalTokens || 0) : "—"}, + {label:"Prompts",value:panel.provider ? panel.provider.todayPrompts || 0 : "—"}, {label:"Sessions",value:panel.provider ? panel.provider.todaySessions || 0 : "—"}, + {label:"7D tokens",value:panel.reportedDays && panel.aiService ? panel.aiService.formatTokens(panel.messageTotal) : "—"}] + Column { + required property var modelData + width: (metrics.width - 3 * metrics.columnSpacing) / 4; spacing: Commons.Style.space(3) + Text { text: String(modelData.value); color: panel.controlForeground; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.subtitle; renderType: Text.NativeRendering } + Text { text: modelData.label; color: panel.controlMutedHigh; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption; renderType: Text.NativeRendering } + } + } + } + Column { + visible: panel.reportedDays > 0; width: parent.width; spacing: Commons.Style.space(4) + DetailRow { label: "TOKENS · LAST 7 DAYS"; value: panel.reportedDays < 7 ? panel.reportedDays + "/7 days reported" : "" } + Canvas { + width: parent.width; height: Commons.Style.space(38) + readonly property var values: panel.weekDays + readonly property color ink: panel.controlForeground + onValuesChanged: requestPaint() + onInkChanged: requestPaint() + onWidthChanged: requestPaint() + Component.onCompleted: requestPaint() + onPaint: { + const g = getContext("2d"), step = width / 7; g.clearRect(0, 0, width, height) + g.fillStyle = Commons.Util.alpha(ink, 0.08); g.strokeStyle = Commons.Util.alpha(ink, 0.5); g.lineWidth = 1 + for (let i = 0; i < 7; i++) { + if (values[i].messageCount === null) continue // Missing days remain gaps, never fabricated zeroes. + const y = height - 1 - values[i].messageCount / panel.messageMax * (height - 4) + g.fillRect(i * step, y, step, height - y); g.beginPath(); g.moveTo(i * step, y); g.lineTo((i + 1) * step, y) + if (i < 6 && values[i + 1].messageCount !== null) g.lineTo((i + 1) * step, height - 1 - values[i + 1].messageCount / panel.messageMax * (height - 4)) + g.stroke() + } + } + } + Row { + width: parent.width + Repeater { + model: panel.weekDays + Column { + required property var modelData + width: contentColumn.width / 7; spacing: Commons.Style.space(2) + Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; text: modelData.messageCount === null ? "—" : panel.aiService.formatTokens(modelData.messageCount); color: panel.controlForeground; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption; renderType: Text.NativeRendering } + Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; text: panel.dayLabel(modelData.date); color: panel.controlMutedHigh; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption; renderType: Text.NativeRendering } + } + } } } Column { @@ -402,12 +425,6 @@ ShibumiPanel { value: visible && panel.aiService ? panel.aiService.formatTokens(panel.provider.hourlyTokens) + "/h" : "" } - DetailRow { - visible: panel.todaySummary !== "" - label: "Today" - value: panel.todaySummary - wrapValue: true - } DetailRow { visible: panel.provider && String(panel.provider.latestModel || "") !== "" label: panel.provider && String(panel.provider.providerId || "") @@ -415,22 +432,6 @@ ShibumiPanel { value: panel.provider ? String(panel.provider.latestModel || "") : "" } - DetailRow { - visible: panel.days.length > 0 - label: "TOKENS BY DAY" - value: "" - } - Repeater { - id: dayRepeater - model: panel.days - delegate: ModelUsageRow { - required property var modelData - width: contentColumn.width - daily: true - entry: ({ name: panel.dayLabel(modelData.date), totalLabel: panel.aiService.formatTokens(modelData.messageCount), - pct: modelData.messageCount / Math.max(1, ...panel.days.map(day => day.messageCount)) * 100, detail: "" }) - } - } Item { visible: panel.providerModels.length > 0 width: parent.width @@ -478,15 +479,14 @@ ShibumiPanel { required property var action implicitWidth: Commons.Style.space(28) implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius + radius: panel.renderedSurfaceRadius foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - Presentation.IconText { - anchors.centerIn: parent - text: iconAction.icon - color: iconAction.foreground - font.pixelSize: Commons.Style.font.body + Ui.OpticalGlyph { + anchors.fill: parent + text: iconAction.icon; color: iconAction.foreground + fontFamily: "Material Symbols Rounded"; fontSize: Math.round(Commons.Style.font.body) } MouseArea { @@ -513,7 +513,7 @@ ShibumiPanel { readonly property color usageColor: dimmed ? panel.controlMuted : value >= 90 ? Commons.Color.urgent : panel.controlForeground width: parent.width - height: Commons.Style.space(30) + height: Commons.Style.space(24) Text { id: usageLabel @@ -593,7 +593,7 @@ ShibumiPanel { id: modelRow required property var entry property bool daily: false - height: Commons.Style.space(daily ? 20 : 30) + height: Commons.Style.space(22) readonly property real percent: Math.max(0, Math.min(100, Number(entry && entry.pct) || 0)) @@ -632,8 +632,8 @@ ShibumiPanel { anchors.right: modelRow.daily ? modelValue.left : parent.right anchors.margins: modelRow.daily ? Commons.Style.space(8) : 0 anchors.verticalCenter: parent.verticalCenter - height: modelRow.daily ? Commons.Style.space(4) : parent.height - radius: height / 2 + height: Commons.Style.space(2); anchors.verticalCenterOffset: Commons.Style.space(10) + radius: 0 color: Commons.Util.alpha(panel.controlAccent, 0.14) Rectangle { diff --git a/hancore.shibumi.ai/Local-AI-LICENSE b/hancore.shibumi.ai/Local-AI-LICENSE new file mode 100644 index 0000000..2ddb476 --- /dev/null +++ b/hancore.shibumi.ai/Local-AI-LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 0xSero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/ai-plugin-regression.sh b/tests/ai-plugin-regression.sh index bf76838..9c9b00c 100755 --- a/tests/ai-plugin-regression.sh +++ b/tests/ai-plugin-regression.sh @@ -32,8 +32,6 @@ mkdir -p "$tmpdir/runtime" "$tmpdir/fixtures" "$tmpdir/home/.claude" \ "$tmpdir/home/.codex" "$tmpdir/state/omarchy/agents/usage" \ "$tmpdir/omarchy/bin" chmod 700 "$tmpdir/runtime" -mkdir -p "$tmpdir/omarchy/shell/plugins/agents/assets" -printf '\n' | tee "$tmpdir/omarchy/shell/plugins/agents/assets/claude.svg" >"$tmpdir/omarchy/shell/plugins/agents/assets/claude-light.svg" shibumi_stage_suite_runtime "$repo_root" "$tmpdir" cp -a -- "$repo_root/hancore.shibumi.ai" "$tmpdir/ai" install -m 0644 "$repo_root/tests/fixtures/AiPanelHost.qml" \ diff --git a/tests/ai-plugin-smoke.qml b/tests/ai-plugin-smoke.qml index 0864c09..240f5a8 100644 --- a/tests/ai-plugin-smoke.qml +++ b/tests/ai-plugin-smoke.qml @@ -8,7 +8,7 @@ ShellRoot { id: root property int phase: 0 - property int logoStage: 0 + property int headerStage: 0 property int ticks: 0 property real stableProviderWidth: 0 property var clickTargets: [] @@ -71,6 +71,14 @@ ShellRoot { checks.longPace = typeof emptyAgentsPanel.paceText === "function" && ["Weekly", "Monthly"].every((label, i) => emptyAgentsPanel.paceText({label: label, percent: 50, resetsAt: new Date(now + [302400000, 1296000000][i]).toISOString()}) === "Pace: 1.0×") checks.noPace = typeof emptyAgentsPanel.paceText === "function" && [{label: "Unknown", resetsAt: record.limits[0].resetsAt}, {label: "Weekly", resetsAt: ""}, {label: "Weekly", resetsAt: new Date(now - 1).toISOString()}, {label: "1-hour", resetsAt: record.limits[0].resetsAt}].every(w => emptyAgentsPanel.paceText(Object.assign({percent: 50}, w)) === "") checks.days = !!p.recentDays && p.recentDays.length === 7 && p.recentDays[0].date === "2026-09-20" && p.recentDays[6].messageCount === 700 && emptyAgentsPanel.renderedDayCount === 7 + emptyAgentsPanel.nowMs = Date.parse("2026-09-26T12:00:00Z") + checks.tokenDays = emptyAgentsPanel.messageTotal === 2800 && emptyAgentsPanel.reportedDays === 7 + const days = record.recentDays; record.recentDays = days.slice(0, -1) + agentsService.applyAgentRecord("claude", JSON.stringify(record)) + checks.missingDay = emptyAgentsPanel.reportedDays === 6 && emptyAgentsPanel.messageTotal === 2100 + && emptyAgentsPanel.weekDays[6].messageCount === null + record.recentDays = days; agentsService.applyAgentRecord("claude", JSON.stringify(record)) + emptyAgentsPanel.nowMs = now checks.codex = typeof agentsService.limitWindows === "function" && agentsService.limitWindows(agentsService.providerFor("codex")).length === 1 && agentsService.providerFor("codex").models[0].totalLabel === "1.03M" record.limits[2].percent = 0.899; agentsService.applyAgentRecord("claude", JSON.stringify(record)) checks.threshold = emptyAgentsPanel.headlineAlarm === false && String(emptyAgentsPanel.headlineColor) !== String(alarmColor) @@ -93,6 +101,22 @@ ShellRoot { return Object.values(checks).every(value => value) && Object.values(layout).every(value => value) } + function descendants(item) { + return Array.from(item.children || []).reduce((items, child) => items.concat([child], descendants(child)), []) + } + + function panelHeaderMatches() { + const items = descendants(emptyAgentsPanel), tier = aiService.displayTierLabel(aiService.selectedProvider.tierLabel) + const plan = items.find(item => item.text === tier && "font" in item) + const tabs = items.filter(item => "selected" in item && item.modelData && item.modelData.providerId) + return items.every(item => !("sourceSize" in item)) && emptyAgentsPanel.heroTier === tier + && plan && plan.visible && !plan.truncated && plan.width > 0 && plan.y + plan.height <= plan.parent.height + && tabs.length === 3 && tabs.filter(item => item.selected).length === 1 + && tabs.every(item => item.width > 0 && Number.isInteger(item.width) + && item.mapToItem(item.parent.parent, 0, 0).x >= 0 + && item.mapToItem(item.parent.parent, item.width, 0).x <= item.parent.parent.width) + } + function linearChannel(value) { return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4) @@ -1080,12 +1104,20 @@ ShellRoot { || agentsService.providerFor("claude") === null || agentsService.providerFor("codex") === null) return root.fail("queued Agents update did not become current") - if (root.logoStage < 3) { - const mark = emptyAgentsPanel.providerLogo, ready = mark && mark.status === Image.Ready && mark.visible, glyph = mark && mark.parent.children[1].visible - if (!mark || (root.logoStage < 2 ? !ready || glyph || String(mark.source) !== "file://" + agentsService.omarchyPath + "/shell/plugins/agents/assets/claude" + (root.logoStage === 1 ? "-light" : "") + ".svg" : ready || !glyph || String(mark.source) !== "")) return root.fail("host Claude logo/glyph stage " + root.logoStage) - console.log("244_AI_LOGO", root.logoStage, "image=" + ready, "glyph=" + glyph) - if (root.logoStage++ === 0) { fakeBar.visualTokens = Object.assign({}, fakeBar.visualTokens, {panelBackground: "#ffffff"}); return } - if (root.logoStage === 2) { emptyAgentsPanel.aiService = {selectedProvider: agentsService.providerFor("claude"), omarchyPath: agentsService.omarchyPath + "/absent", providers: [], limitWindows: () => [], bindingWindow: () => null, formatTokens: value => String(value)}; return } + if (root.headerStage > 0) { + if (!root.panelHeaderMatches()) return root.fail("text-only provider header or tab bounds stage " + root.headerStage) + console.log("247_AI_HEADER", root.headerStage, aiService.selectedTool, emptyAgentsPanel.heroTier, emptyAgentsPanel.contentWidth) + } + if (root.headerStage < 6) { + const index = root.headerStage++ % 3 + emptyAgentsPanel.aiService = aiService; emptyPanelOwner.opened = true + emptyAgentsPanel.contentWidth = [347, 320, 273][index] + fakeBar.v2ShellMode = root.headerStage > 3 + claudeProvider.tierLabel = root.headerStage > 3 ? "Max 20x" : "Max 5x" + const tab = root.descendants(emptyAgentsPanel).find(item => "selected" in item && item.modelData && item.modelData.providerId === ["claude", "codex", "opencode"][index]) + tab.children.find(item => typeof item.clicked === "function").clicked(null) + if (aiService.selectedTool !== tab.modelData.providerId) return root.fail("provider tab did not dispatch selection") + return } stop() watchdog.stop() diff --git a/tests/fixtures/AiPanelHost.qml b/tests/fixtures/AiPanelHost.qml index b032964..b134811 100644 --- a/tests/fixtures/AiPanelHost.qml +++ b/tests/fixtures/AiPanelHost.qml @@ -13,6 +13,7 @@ Item { property int contentHeight: 560 readonly property var shibumiTokens: bar && "visualTokens" in bar ? bar.visualTokens : null + readonly property real renderedSurfaceRadius: shibumiTokens && shibumiTokens.panelRadius !== undefined ? shibumiTokens.panelRadius : 8 readonly property real controlRadius: 8 readonly property color controlForeground: bar ? bar.foreground : "white" readonly property color controlAccent: bar ? bar.urgent : "red" From 6ce494ab97ee917cdcb356601915ecea82077575 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:10:12 +0200 Subject: [PATCH 18/40] feat: restyle the CPU panel with a history chart and sticky GPU row --- hancore.shibumi.cpu/BarWidget.qml | 7 +++ hancore.shibumi.cpu/CpuPanel.qml | 93 +++++++++++++++------------- hancore.shibumi.cpu/Local-AI-LICENSE | 21 +++++++ tests/fixtures/ShibumiPanelTest.qml | 1 + tests/telemetry-runtime-smoke.qml | 15 ++++- 5 files changed, 92 insertions(+), 45 deletions(-) create mode 100644 hancore.shibumi.cpu/Local-AI-LICENSE diff --git a/hancore.shibumi.cpu/BarWidget.qml b/hancore.shibumi.cpu/BarWidget.qml index c4a787a..810a09b 100644 --- a/hancore.shibumi.cpu/BarWidget.qml +++ b/hancore.shibumi.cpu/BarWidget.qml @@ -41,6 +41,13 @@ Ui.Panel { bar ? bar.urgent : Commons.Color.accent) : (bar ? bar.urgent : Commons.Color.accent) readonly property var gpuTelemetry: cpuService ? cpuService.gpu : null + // Presentation latch survives lazy panel recreation, but not owner replacement. + property bool gpuActivitySeen: false + onGpuTelemetryChanged: gpuActivitySeen = !!gpuTelemetry && gpuTelemetry.utilization > 0 + Connections { + target: root.gpuTelemetry + function onUtilizationChanged() { if (root.gpuTelemetry.utilization > 0) root.gpuActivitySeen = true } + } readonly property string displayMode: String( setting("displayMode", setting("compact", false) ? "icon" : "full")) readonly property bool compact: displayMode === "icon" diff --git a/hancore.shibumi.cpu/CpuPanel.qml b/hancore.shibumi.cpu/CpuPanel.qml index 8429f5b..785ce2e 100644 --- a/hancore.shibumi.cpu/CpuPanel.qml +++ b/hancore.shibumi.cpu/CpuPanel.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import qs.Commons as Commons import qs.Ui as Ui -import "../hancore.shibumi.state/lib/presentation" as Presentation +// Compact chart/metric layout inspired by 0xSero/omarchy-local-ai; see Local-AI-LICENSE. ShibumiPanel { id: panel @@ -12,7 +12,8 @@ ShibumiPanel { required property var systemTelemetry required property var gpuTelemetry readonly property string cpuModelText: cpuModelLabel.text - readonly property string loadAverageText: loadValue.text + readonly property string loadAverageText: systemTelemetry && systemTelemetry.loadAverage.length === 3 + ? systemTelemetry.loadAverage.map(value => value.toFixed(2)).join(" / ") : "—" readonly property var gpuUsageView: gpuUsage owner: ownerWidget @@ -65,25 +66,26 @@ ShibumiPanel { Item { id: header width: parent.width - height: Commons.Style.space(52) + height: Commons.Style.space(42) - Presentation.IconText { - text: "memory"; color: panel.controlAccent - font.pixelSize: Commons.Style.font.icon + Ui.OpticalGlyph { + text: "memory"; color: panel.controlForeground + fontFamily: "Material Symbols Rounded"; fontSize: Math.round(Commons.Style.font.icon) + width: Commons.Style.space(20); height: width anchors.verticalCenter: parent.verticalCenter } Text { id: cpuModelLabel x: Commons.Style.space(28); anchors.bottom: parent.bottom - width: parent.width - x - monitorButton.width - Commons.Style.space(8); elide: Text.ElideRight; textFormat: Text.PlainText + width: parent.width - x; elide: Text.ElideRight; textFormat: Text.PlainText text: panel.systemTelemetry ? panel.systemTelemetry.cpuModel : "" - color: panel.controlMutedHigh + color: panel.controlMutedHigh; renderType: Text.NativeRendering font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption } Text { anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter + anchors.top: parent.top anchors.leftMargin: Commons.Style.space(28) text: "CPU · GPU" color: panel.bar ? panel.bar.foreground : Commons.Color.foreground @@ -97,7 +99,6 @@ ShibumiPanel { Text { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.rightMargin: monitorButton.width + Commons.Style.space(8) text: "\u2715" color: closeMouse.containsMouse ? panel.controlAccent : panel.controlMuted @@ -125,27 +126,36 @@ ShibumiPanel { panel.bar.foreground.b, 0.18) : Commons.Color.popups.border } - UsageRow { - id: cpuUsage + CpuWave { + width: parent.width; height: Commons.Style.space(64) + history: panel.systemTelemetry ? panel.systemTelemetry.cpuHistory : [] + maxSamples: panel.systemTelemetry ? panel.systemTelemetry.cpuMaxSamples : 30 + accent: panel.controlMutedHigh + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + } + Row { width: parent.width - label: "CPU" - value: panel.systemTelemetry ? panel.systemTelemetry.cpuPercent : 0 - bar: panel.bar - Text { - id: loadValue - x: Commons.Style.space(10); y: Commons.Style.space(46) - text: "Load Average (1/5/15 min)\n" + (panel.systemTelemetry && panel.systemTelemetry.loadAverage.length === 3 - ? panel.systemTelemetry.loadAverage.map(value => value.toFixed(2)).join(" / ") : "—") - color: panel.controlMutedHigh - font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption + Repeater { + model: ["CPU", "LOAD 1 MIN", "LOAD 5 MIN", "LOAD 15 MIN"] + Column { + required property int index; required property string modelData + width: panelColumn.width / 4; spacing: Commons.Style.space(3) + Text { + text: index === 0 ? (panel.systemTelemetry ? panel.systemTelemetry.cpuPercent : 0) + "%" + : panel.systemTelemetry && panel.systemTelemetry.loadAverage.length === 3 ? panel.systemTelemetry.loadAverage[index - 1].toFixed(2) : "—" + color: panel.controlForeground; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.subtitle; renderType: Text.NativeRendering + } + Text { text: modelData; color: panel.controlMutedHigh; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption; renderType: Text.NativeRendering } + } } } UsageRow { id: gpuUsage width: parent.width - height: Commons.Style.space(100) - visible: panel.gpuTelemetry && panel.gpuTelemetry.available + height: Commons.Style.space(80) + visible: !!panel.gpuTelemetry && panel.gpuTelemetry.available && panel.ownerWidget.gpuActivitySeen === true label: "GPU" value: panel.gpuTelemetry ? panel.gpuTelemetry.utilization : 0 bar: panel.bar @@ -154,7 +164,7 @@ ShibumiPanel { Row { id: temperatureRow parent: gpuUsage - x: Commons.Style.space(10); y: Commons.Style.space(46) + x: 0; y: Commons.Style.space(34) width: parent.width - 2 * x visible: panel.gpuTelemetry && panel.gpuTelemetry.available && panel.gpuTelemetry.temperatureC > 0 @@ -180,7 +190,7 @@ ShibumiPanel { Row { parent: gpuUsage - x: Commons.Style.space(10); y: temperatureRow.visible ? temperatureRow.y + temperatureRow.height + Commons.Style.space(8) : Commons.Style.space(46) + x: 0; y: temperatureRow.visible ? temperatureRow.y + temperatureRow.height + Commons.Style.space(4) : Commons.Style.space(34) width: parent.width - 2 * x visible: panel.gpuTelemetry && panel.gpuTelemetry.available && panel.gpuTelemetry.memoryTotalMiB > 0 @@ -208,12 +218,15 @@ ShibumiPanel { } Rectangle { - id: monitorButton - parent: header - anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter - width: Commons.Style.space(70) + width: parent.width + height: 1 + color: panel.dividerColor + } + + Rectangle { + width: parent.width height: 28 - radius: panel.controlRadius + radius: panel.renderedSurfaceRadius color: monitorMouse.containsMouse ? panel.controlPrimaryHoverColor : panel.bar ? panel.bar.urgent : Commons.Color.accent @@ -222,7 +235,7 @@ ShibumiPanel { Text { anchors.centerIn: parent - text: "btop" + text: "Open btop" color: panel.bar ? panel.bar.background : Commons.Color.background font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: 11 @@ -248,17 +261,11 @@ ShibumiPanel { required property var bar height: Commons.Style.space(90) - Rectangle { - anchors.fill: parent; z: -1 - radius: panel.controlRadius; color: panel.controlFillColor - border.width: panel.controlBorderWidth; border.color: panel.controlBorderColor - } - Text { id: usageLabel anchors.left: parent.left anchors.top: parent.top - anchors.margins: Commons.Style.space(10) + anchors.margins: 0 text: parent.label color: parent.bar ? Qt.rgba(parent.bar.foreground.r, parent.bar.foreground.g, parent.bar.foreground.b, 0.65) : Commons.Color.foreground @@ -272,7 +279,7 @@ ShibumiPanel { id: usageValue anchors.right: parent.right anchors.top: parent.top - anchors.margins: Commons.Style.space(10) + anchors.margins: 0 text: parent.value + "%" color: parent.bar ? parent.bar.urgent : Commons.Color.accent font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family @@ -282,9 +289,9 @@ ShibumiPanel { } Rectangle { - x: Commons.Style.space(10); y: Commons.Style.space(30) - width: parent.width - 2 * x - height: Commons.Style.space(6) + x: 0; y: Commons.Style.space(22) + width: parent.width + height: Commons.Style.space(3) radius: height / 2 color: panel.controlActiveFillColor diff --git a/hancore.shibumi.cpu/Local-AI-LICENSE b/hancore.shibumi.cpu/Local-AI-LICENSE new file mode 100644 index 0000000..2ddb476 --- /dev/null +++ b/hancore.shibumi.cpu/Local-AI-LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 0xSero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/fixtures/ShibumiPanelTest.qml b/tests/fixtures/ShibumiPanelTest.qml index bed9d74..045fb0f 100644 --- a/tests/fixtures/ShibumiPanelTest.qml +++ b/tests/fixtures/ShibumiPanelTest.qml @@ -19,6 +19,7 @@ Item { readonly property string shellStyle: "shibumi" readonly property string popoutScreenName: "" readonly property color renderedSurfaceColor: "#181818" + readonly property real renderedSurfaceRadius: shibumiTokens.panelRadius readonly property color controlForeground: "#eeeeee" readonly property color controlFillColor: "transparent" readonly property color controlAccent: "#d75f5f" diff --git a/tests/telemetry-runtime-smoke.qml b/tests/telemetry-runtime-smoke.qml index 4a15a7d..61181da 100644 --- a/tests/telemetry-runtime-smoke.qml +++ b/tests/telemetry-runtime-smoke.qml @@ -143,7 +143,7 @@ ShellRoot { root.check(!cpu.gpu.probeEnabled && !telemetry.thermal.probeEnabled && !storage.storage.runtimeProbesEnabled, "fixture probe suppression lost") root.check(telemetry.system.cpuPanelConsumers === 0 && telemetry.system.cpuModel === "" && telemetry.system.loadAverage.length === 0, "detail probe before panel demand") - cpu.gpu.parse("sysfs|42|61|0|0\nstatus|ok") + cpu.gpu.parse("sysfs||61|0|0\nstatus|ok") telemetry.thermal.parseDetailed("55|63|80|100|44|70|90|39") storage.storage.parseUsage("/dev/fixture 1000 400 600 40%") for (const item of items) item.open() @@ -159,7 +159,17 @@ ShellRoot { telemetry.system.parseCpuModel("processor : 0\nmodel name : Fixture CPU\n") telemetry.system.parseLoad("0.25 1.50 2.75 1/300 12345\n") root.check(cpuPanel.cpuModelText === "Fixture CPU" && cpuPanel.loadAverageText.indexOf("0.25 / 1.50 / 2.75") >= 0 - && telemetry.system.cpuPanelConsumers === 1 && cpuPanel.gpuUsageView.visible && cpuPanel.gpuUsageView.value === 42, "CPU model/load or GPU card missing") + && telemetry.system.cpuPanelConsumers === 1 && !cpuPanel.gpuUsageView.visible, "CPU model/load or initially hidden GPU row missing") + cpu.gpu.parse("sysfs||61|0|0\nstatus|ok") + root.check(cpu.gpu.available && !cpuPanel.gpuUsageView.visible, "GPU without reported utilization stayed visible") + cpu.gpu.parse("sysfs|0|61|0|0\nstatus|ok") + root.check(!cpuPanel.gpuUsageView.visible, "zero-utilization GPU stayed visible") + cpu.gpu.parse("sysfs|42|61|0|0\nstatus|ok") + root.check(cpuPanel.gpuUsageView.visible && cpuPanel.gpuUsageView.value === 42, "GPU utilization did not restore its row") + cpu.gpu.parse("sysfs|0|61|0|0\nstatus|ok") + root.check(cpuPanel.gpuUsageView.visible && cpuPanel.gpuUsageView.value === 0, "observed GPU row disappeared at idle") + items[0].close(); items[0].open(); root.panels[0] = items[0].panelItem + root.check(root.panels[0] && root.panels[0].gpuUsageView.visible, "GPU activity latch lost on panel reopen") telemetry.system.parseLoad("invalid"); root.check(telemetry.system.loadAverage.length === 0, "invalid load accepted") const memoryPanel = root.panels[1], oldMemory = [telemetry.system.memTotalMiB, telemetry.system.memAvailableMiB] telemetry.system.memTotalMiB = 8192; telemetry.system.memAvailableMiB = 3072 @@ -191,6 +201,7 @@ ShellRoot { } else if (root.phase === 5) { if (!cpu.ready || !telemetry.ready || !storage.ready) return root.samePanels() + root.check(!root.panels[0].gpuUsageView.visible, "GPU activity latch leaked to replacement owner") root.check(cpu.gpu !== root.firstGpu && cpu.gpu.consumers === 3 && root.panels[0].gpuTelemetry === cpu.gpu && root.panels[0].acquiredGpuTelemetry === cpu.gpu && telemetry.system.cpuPanelConsumers === 1 From 1aa8e1d8b0db915f23d955eade38dd9eeb4d3692 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:10:12 +0200 Subject: [PATCH 19/40] feat: restyle the Memory panel with a symbol ring and metric grid --- hancore.shibumi.memory/Local-AI-LICENSE | 21 ++++++++++++ hancore.shibumi.memory/MemoryPanel.qml | 43 ++++++++++++------------- 2 files changed, 41 insertions(+), 23 deletions(-) create mode 100644 hancore.shibumi.memory/Local-AI-LICENSE diff --git a/hancore.shibumi.memory/Local-AI-LICENSE b/hancore.shibumi.memory/Local-AI-LICENSE new file mode 100644 index 0000000..2ddb476 --- /dev/null +++ b/hancore.shibumi.memory/Local-AI-LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 0xSero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/hancore.shibumi.memory/MemoryPanel.qml b/hancore.shibumi.memory/MemoryPanel.qml index 4d7a84b..7f2ed38 100644 --- a/hancore.shibumi.memory/MemoryPanel.qml +++ b/hancore.shibumi.memory/MemoryPanel.qml @@ -4,6 +4,7 @@ import QtQuick import qs.Commons as Commons import qs.Ui as Ui +// Compact metric layout inspired by 0xSero/omarchy-local-ai; see Local-AI-LICENSE. ShibumiPanel { id: panel @@ -36,7 +37,7 @@ ShibumiPanel { Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - text: "Memory" + text: "MEMORY" color: panel.bar ? panel.bar.foreground : Commons.Color.foreground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: 13 @@ -91,36 +92,32 @@ ShibumiPanel { onWidthChanged: requestPaint() onHeightChanged: requestPaint() - Text { - anchors.centerIn: parent - text: memoryRing.percent + "%" - color: memoryRing.accent - font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 - font.weight: Font.Medium - renderType: Text.NativeRendering + Ui.OpticalGlyph { + anchors.centerIn: parent; width: Commons.Style.space(32); height: width + text: "memory_alt"; color: panel.controlForeground + fontFamily: "Material Symbols Rounded"; fontSize: Math.round(Commons.Style.space(26)) } } - Column { + Grid { width: parent.width - memoryRing.width - parent.spacing anchors.verticalCenter: parent.verticalCenter - spacing: 4 - + columns: 2; columnSpacing: Commons.Style.space(8); rowSpacing: Commons.Style.space(8) + MemoryStatRow { width: (parent.width - parent.columnSpacing) / 2; label: "Usage"; value: memoryRing.percent + "%"; bar: panel.bar } MemoryStatRow { - width: parent.width + width: (parent.width - parent.columnSpacing) / 2 label: "Used" value: (panel.telemetry ? panel.telemetry.memUsedGiB : 0).toFixed(1) + " GiB" bar: panel.bar } MemoryStatRow { - width: parent.width + width: (parent.width - parent.columnSpacing) / 2 label: "Available" value: ((panel.telemetry ? panel.telemetry.memAvailableMiB : 0) / 1024).toFixed(1) + " GiB" bar: panel.bar } MemoryStatRow { - width: parent.width + width: (parent.width - parent.columnSpacing) / 2 label: "Total" value: (panel.telemetry ? panel.telemetry.memTotalGiB : 0).toFixed(1) + " GiB" bar: panel.bar @@ -137,7 +134,7 @@ ShibumiPanel { Rectangle { width: parent.width height: 28 - radius: panel.controlRadius + radius: panel.renderedSurfaceRadius color: monitorMouse.containsMouse ? panel.controlPrimaryHoverColor : panel.bar ? panel.bar.urgent : Commons.Color.accent @@ -167,29 +164,29 @@ ShibumiPanel { } } - component MemoryStatRow: Row { + component MemoryStatRow: Item { required property string label required property string value required property var bar + implicitHeight: valueText.height + Commons.Style.space(3) + labelText.height Text { id: labelText - width: parent.width * 0.4 - text: parent.label + y: valueText.height + Commons.Style.space(3); width: parent.width + text: parent.label.toUpperCase() color: parent.bar ? Qt.rgba(parent.bar.foreground.r, parent.bar.foreground.g, parent.bar.foreground.b, 0.65) : Commons.Color.foreground font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 + font.pixelSize: Commons.Style.font.caption renderType: Text.NativeRendering } Text { id: valueText - width: parent.width * 0.6 - horizontalAlignment: Text.AlignRight + width: parent.width text: parent.value color: parent.bar ? parent.bar.foreground : Commons.Color.foreground font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family - font.pixelSize: 11 + font.pixelSize: Commons.Style.font.subtitle renderType: Text.NativeRendering } } From 89108bd3a44241212150b8f72e087092f8afd75f Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:10:13 +0200 Subject: [PATCH 20/40] feat: show media controls as rounded buttons in one row --- hancore.shibumi.media/Local-AI-LICENSE | 21 ++++++++++++++ hancore.shibumi.media/MediaPanel.qml | 40 +++++++++++++------------- 2 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 hancore.shibumi.media/Local-AI-LICENSE diff --git a/hancore.shibumi.media/Local-AI-LICENSE b/hancore.shibumi.media/Local-AI-LICENSE new file mode 100644 index 0000000..2ddb476 --- /dev/null +++ b/hancore.shibumi.media/Local-AI-LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 0xSero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/hancore.shibumi.media/MediaPanel.qml b/hancore.shibumi.media/MediaPanel.qml index 9eba630..ddb6165 100644 --- a/hancore.shibumi.media/MediaPanel.qml +++ b/hancore.shibumi.media/MediaPanel.qml @@ -434,11 +434,11 @@ ShibumiPanel { Row { visible: panel.active anchors.horizontalCenter: parent.horizontalCenter - spacing: Commons.Style.space(12) + spacing: Commons.Style.space(8) MediaPanelButton { id: shuffleButton - icon: checked ? "shuffle_on" : "shuffle"; action: "shuffle"; controlIndex: -1 + icon: "shuffle"; action: "shuffle"; controlIndex: -1 tooltipText: checked ? "Shuffle: On" : "Shuffle: Off" checked: !!panel.player && panel.player.shuffle === true enabled: panel.directAvailable && !!panel.player && panel.player.shuffleSupported === true @@ -499,7 +499,7 @@ ShibumiPanel { width: parent.width height: Commons.Style.space(34) - radius: panel.controlRadius + radius: panel.renderedSurfaceRadius color: cursor || sourceMouse.containsMouse ? panel.bar ? Qt.rgba(panel.bar.urgent.r, panel.bar.urgent.g, panel.bar.urgent.b, 0.14) : Commons.Color.background @@ -649,7 +649,7 @@ ShibumiPanel { signal clicked() implicitWidth: Commons.Style.space(28) implicitHeight: Commons.Style.space(28) - radius: panel.controlRadius + radius: panel.renderedSurfaceRadius foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground accent: panel.bar ? panel.bar.urgent : Commons.Color.accent @@ -676,28 +676,28 @@ ShibumiPanel { } } - component MediaPanelButton: Ui.PanelActionButton { + // Outline button structure inspired by 0xSero/omarchy-local-ai; see Local-AI-LICENSE. + component MediaPanelButton: Rectangle { + id: button required property string icon required property string action required property int controlIndex property bool accent: false property bool checked: false - - iconText: icon - foreground: checked ? panel.controlAccent : accent && panel.bar - ? panel.bar.urgent : panel.bar ? panel.bar.foreground : Commons.Color.foreground - hoverColor: panel.bar ? panel.bar.urgent : Commons.Color.accent - fontFamily: "Material Symbols Rounded" - fontSize: accent ? Commons.Style.font.iconLarge : Commons.Style.font.icon - size: accent ? Commons.Style.space(34) : Commons.Style.space(28) - radius: panel.controlRadius - hasCursor: panel.focusSection === "controls" && panel.cursorIndex === controlIndex - onHovered: function(isHovered) { - if (isHovered) { - panel.focusSection = "controls" - panel.cursorIndex = controlIndex - } + property string tooltipText: "" + signal clicked() + readonly property bool hot: enabled && (buttonMouse.containsMouse || panel.focusSection === "controls" && panel.cursorIndex === controlIndex) + width: Commons.Style.space(36); height: width; radius: panel.renderedSurfaceRadius; opacity: enabled ? 1 : 0.35 + color: hot ? panel.controlHoverFillColor : checked ? panel.controlActiveFillColor : "transparent" + border.width: 1; border.color: checked || hot ? panel.controlAccent : panel.controlMutedHigh + Ui.OpticalGlyph { anchors.fill: parent; text: button.icon; color: button.checked || button.accent ? panel.controlAccent : panel.controlForeground + fontFamily: "Material Symbols Rounded"; fontSize: Math.round(Commons.Style.font.icon) } + MouseArea { + id: buttonMouse; anchors.fill: parent; enabled: button.enabled; hoverEnabled: true; cursorShape: Qt.PointingHandCursor + onContainsMouseChanged: if (containsMouse) { panel.focusSection = "controls"; panel.cursorIndex = button.controlIndex } + onClicked: button.clicked() } + Presentation.ShibumiPillToolTip { panel: panel; visible: button.tooltipText !== "" && buttonMouse.containsMouse; text: button.tooltipText } onClicked: panel.controlAction(action) } } From 5e2039845a71b9237eedfe9193b721d4ab96042b Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:58:00 +0200 Subject: [PATCH 21/40] fix: use one font size per text role in the AI usage panel --- hancore.shibumi.ai/AiUsagePanel.qml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/hancore.shibumi.ai/AiUsagePanel.qml b/hancore.shibumi.ai/AiUsagePanel.qml index 7670e51..6bacbdd 100644 --- a/hancore.shibumi.ai/AiUsagePanel.qml +++ b/hancore.shibumi.ai/AiUsagePanel.qml @@ -211,7 +211,7 @@ ShibumiPanel { visible: text !== "" color: panel.controlForeground font.family: heroName.font.family - font.pixelSize: Commons.Style.font.body + font.pixelSize: Commons.Style.font.bodySmall font.weight: Font.DemiBold renderType: Text.NativeRendering } @@ -326,14 +326,14 @@ ShibumiPanel { Column { required property var modelData width: (metrics.width - 3 * metrics.columnSpacing) / 4; spacing: Commons.Style.space(3) - Text { text: String(modelData.value); color: panel.controlForeground; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.subtitle; renderType: Text.NativeRendering } - Text { text: modelData.label; color: panel.controlMutedHigh; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption; renderType: Text.NativeRendering } + Text { text: String(modelData.value); color: panel.controlForeground; font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family; font.pixelSize: Commons.Style.font.subtitle; renderType: Text.NativeRendering } + Text { text: modelData.label; color: panel.controlMutedHigh; font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption; renderType: Text.NativeRendering } } } } Column { visible: panel.reportedDays > 0; width: parent.width; spacing: Commons.Style.space(4) - DetailRow { label: "TOKENS · LAST 7 DAYS"; value: panel.reportedDays < 7 ? panel.reportedDays + "/7 days reported" : "" } + DetailRow { label: "TOKENS · LAST 7 DAYS"; value: panel.reportedDays < 7 ? panel.reportedDays + "/7 days reported" : ""; textSize: Commons.Style.font.caption } Canvas { width: parent.width; height: Commons.Style.space(38) readonly property var values: panel.weekDays @@ -361,8 +361,8 @@ ShibumiPanel { Column { required property var modelData width: contentColumn.width / 7; spacing: Commons.Style.space(2) - Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; text: modelData.messageCount === null ? "—" : panel.aiService.formatTokens(modelData.messageCount); color: panel.controlForeground; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption; renderType: Text.NativeRendering } - Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; text: panel.dayLabel(modelData.date); color: panel.controlMutedHigh; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption; renderType: Text.NativeRendering } + Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; text: modelData.messageCount === null ? "—" : panel.aiService.formatTokens(modelData.messageCount); color: panel.controlForeground; font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family; font.pixelSize: Commons.Style.font.bodySmall; renderType: Text.NativeRendering } + Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; text: panel.dayLabel(modelData.date); color: panel.controlMutedHigh; font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family; font.pixelSize: Commons.Style.font.bodySmall; renderType: Text.NativeRendering } } } } @@ -371,7 +371,7 @@ ShibumiPanel { id: balanceSection visible: !!panel.balance width: parent.width; spacing: Commons.Style.space(6) - DetailRow { label: "BALANCE"; value: "" } + DetailRow { label: "BALANCE"; value: ""; textSize: Commons.Style.font.caption } DetailRow { label: "Prepaid credits"; value: panel.balance ? panel.money(panel.balance.remaining) : "" } Rectangle { width: parent.width; height: Commons.Style.space(4); radius: height / 2 @@ -385,7 +385,7 @@ ShibumiPanel { } } DetailRow { visible: panel.balanceDetail !== ""; label: ""; value: panel.balanceDetail; wrapValue: true } - DetailRow { visible: panel.limitWindows.length > 0; label: "LIMITS"; value: "" } + DetailRow { visible: panel.limitWindows.length > 0; label: "LIMITS"; value: ""; textSize: Commons.Style.font.caption } Repeater { model: panel.limitWindows delegate: Column { @@ -563,6 +563,7 @@ ShibumiPanel { required property string label required property string value property bool wrapValue: false + property int textSize: Commons.Style.font.bodySmall width: parent.width height: wrapValue ? Math.max(Commons.Style.space(16), detailValue.implicitHeight) : Commons.Style.space(16) @@ -572,7 +573,7 @@ ShibumiPanel { text: parent.label color: panel.controlMutedHigh font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall + font.pixelSize: parent.textSize renderType: Text.NativeRendering } Text { @@ -584,7 +585,7 @@ ShibumiPanel { text: parent.value color: panel.controlForeground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.bodySmall + font.pixelSize: parent.textSize renderType: Text.NativeRendering } } @@ -608,7 +609,7 @@ ShibumiPanel { elide: Text.ElideRight color: panel.controlForeground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption + font.pixelSize: Commons.Style.font.bodySmall font.weight: Font.Medium renderType: Text.NativeRendering } @@ -621,7 +622,7 @@ ShibumiPanel { text: String(modelRow.entry && modelRow.entry.totalLabel || "") color: panel.controlAccent font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family - font.pixelSize: Commons.Style.font.caption + font.pixelSize: Commons.Style.font.bodySmall font.weight: Font.Medium renderType: Text.NativeRendering } From 9bb53d72f02d7260473b96a799a154595299204c Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 01:36:29 +0200 Subject: [PATCH 22/40] feat: show CPU and memory load as water-filled rings --- hancore.shibumi.cpu/CpuPanel.qml | 109 +++++++++++++++++-------- hancore.shibumi.cpu/CpuRing.qml | 65 +++++++++++++++ hancore.shibumi.memory/MemoryPanel.qml | 6 +- hancore.shibumi.memory/MemoryRing.qml | 23 ++++++ 4 files changed, 163 insertions(+), 40 deletions(-) create mode 100644 hancore.shibumi.cpu/CpuRing.qml diff --git a/hancore.shibumi.cpu/CpuPanel.qml b/hancore.shibumi.cpu/CpuPanel.qml index 785ce2e..73df5b0 100644 --- a/hancore.shibumi.cpu/CpuPanel.qml +++ b/hancore.shibumi.cpu/CpuPanel.qml @@ -64,29 +64,12 @@ ShibumiPanel { spacing: 8 Item { - id: header width: parent.width - height: Commons.Style.space(42) - - Ui.OpticalGlyph { - text: "memory"; color: panel.controlForeground - fontFamily: "Material Symbols Rounded"; fontSize: Math.round(Commons.Style.font.icon) - width: Commons.Style.space(20); height: width - anchors.verticalCenter: parent.verticalCenter - } - Text { - id: cpuModelLabel - x: Commons.Style.space(28); anchors.bottom: parent.bottom - width: parent.width - x; elide: Text.ElideRight; textFormat: Text.PlainText - text: panel.systemTelemetry ? panel.systemTelemetry.cpuModel : "" - color: panel.controlMutedHigh; renderType: Text.NativeRendering - font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption - } + height: 24 Text { anchors.left: parent.left - anchors.top: parent.top - anchors.leftMargin: Commons.Style.space(28) + anchors.verticalCenter: parent.verticalCenter text: "CPU · GPU" color: panel.bar ? panel.bar.foreground : Commons.Color.foreground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family @@ -119,6 +102,15 @@ ShibumiPanel { } } + Text { + id: cpuModelLabel + width: parent.width; elide: Text.ElideRight; textFormat: Text.PlainText + text: panel.systemTelemetry ? panel.systemTelemetry.cpuModel : "" + color: panel.controlMutedHigh; renderType: Text.NativeRendering + font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family + font.pixelSize: Commons.Style.font.caption + } + Rectangle { width: parent.width height: 1 @@ -126,6 +118,42 @@ ShibumiPanel { panel.bar.foreground.b, 0.18) : Commons.Color.popups.border } + Row { + width: parent.width + height: cpuRing.height + spacing: Commons.Style.space(16) + + CpuRing { + id: cpuRing + width: Commons.Style.space(80) + height: width + percent: panel.systemTelemetry ? panel.systemTelemetry.cpuPercent : 0 + foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground + accent: panel.bar ? panel.bar.urgent : Commons.Color.accent + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + + water: true + } + + Grid { + width: parent.width - cpuRing.width - parent.spacing + anchors.verticalCenter: parent.verticalCenter + columns: 2; columnSpacing: Commons.Style.space(8); rowSpacing: Commons.Style.space(8) + Repeater { + model: ["Usage", "Load 1 min", "Load 5 min", "Load 15 min"] + CpuStatRow { + required property int index; required property string modelData + width: (parent.width - parent.columnSpacing) / 2 + label: modelData + value: index === 0 ? cpuRing.percent + "%" + : panel.systemTelemetry && panel.systemTelemetry.loadAverage.length === 3 ? panel.systemTelemetry.loadAverage[index - 1].toFixed(2) : "—" + bar: panel.bar + } + } + } + } + CpuWave { width: parent.width; height: Commons.Style.space(64) history: panel.systemTelemetry ? panel.systemTelemetry.cpuHistory : [] @@ -134,22 +162,6 @@ ShibumiPanel { onWidthChanged: requestPaint() onHeightChanged: requestPaint() } - Row { - width: parent.width - Repeater { - model: ["CPU", "LOAD 1 MIN", "LOAD 5 MIN", "LOAD 15 MIN"] - Column { - required property int index; required property string modelData - width: panelColumn.width / 4; spacing: Commons.Style.space(3) - Text { - text: index === 0 ? (panel.systemTelemetry ? panel.systemTelemetry.cpuPercent : 0) + "%" - : panel.systemTelemetry && panel.systemTelemetry.loadAverage.length === 3 ? panel.systemTelemetry.loadAverage[index - 1].toFixed(2) : "—" - color: panel.controlForeground; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.subtitle; renderType: Text.NativeRendering - } - Text { text: modelData; color: panel.controlMutedHigh; font.family: Commons.Style.font.family; font.pixelSize: Commons.Style.font.caption; renderType: Text.NativeRendering } - } - } - } UsageRow { id: gpuUsage @@ -253,6 +265,33 @@ ShibumiPanel { } } + component CpuStatRow: Item { + required property string label + required property string value + required property var bar + implicitHeight: valueText.height + Commons.Style.space(3) + labelText.height + + Text { + id: labelText + y: valueText.height + Commons.Style.space(3); width: parent.width + text: parent.label.toUpperCase() + color: parent.bar ? Qt.rgba(parent.bar.foreground.r, parent.bar.foreground.g, + parent.bar.foreground.b, 0.65) : Commons.Color.foreground + font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family + font.pixelSize: Commons.Style.font.caption + renderType: Text.NativeRendering + } + Text { + id: valueText + width: parent.width + text: parent.value + color: parent.bar ? parent.bar.foreground : Commons.Color.foreground + font.family: parent.bar ? parent.bar.fontFamily : Commons.Style.font.family + font.pixelSize: Commons.Style.font.subtitle + renderType: Text.NativeRendering + } + } + component UsageRow: Item { id: usageRow diff --git a/hancore.shibumi.cpu/CpuRing.qml b/hancore.shibumi.cpu/CpuRing.qml new file mode 100644 index 0000000..f2e0dc8 --- /dev/null +++ b/hancore.shibumi.cpu/CpuRing.qml @@ -0,0 +1,65 @@ +// Plugin-local copy of Shibumi's MemoryRing; no cross-plugin presentation import. +import QtQuick +import qs.Commons as Commons + +Canvas { + id: root + + required property int percent + required property color foreground + required property color accent + property bool water: false + + width: Commons.Style.space(16) + height: width + + onPercentChanged: requestPaint() + onWaterChanged: requestPaint() + onForegroundChanged: requestPaint() + onAccentChanged: requestPaint() + Component.onCompleted: requestPaint() + + onPaint: { + const context = getContext("2d") + context.clearRect(0, 0, width, height) + const center = width / 2 + const radius = width / 2 - 1.5 + const ratio = Math.max(0, Math.min(1, percent / 100)) + const start = -Math.PI / 2 + + // Static waterline: height follows the clamped value; no clock or animation. + if (water && ratio > 0) { + const inner = radius - 3 + const left = center - inner, right = center + inner, bottom = center + inner + const level = bottom - 2 * inner * ratio + const wave = Math.min(Commons.Style.space(3), 2 * inner * ratio, 2 * inner * (1 - ratio)) + context.save() + context.beginPath() + context.arc(center, center, inner, 0, Math.PI * 2) + context.clip() + context.beginPath() + context.moveTo(left, level) + context.bezierCurveTo(center - inner / 3, level - wave, center + inner / 3, level + wave, right, level) + context.lineTo(right, bottom) + context.lineTo(left, bottom) + context.closePath() + context.fillStyle = Qt.rgba(accent.r, accent.g, accent.b, 0.35) + context.fill() + context.restore() + } + + context.lineWidth = 1.8 + context.lineCap = "round" + context.beginPath() + context.arc(center, center, radius, 0, Math.PI * 2) + context.strokeStyle = Qt.rgba(foreground.r, foreground.g, foreground.b, 0.2) + context.stroke() + + if (ratio > 0) { + context.beginPath() + context.arc(center, center, radius, start, start + Math.PI * 2 * ratio) + context.strokeStyle = accent + context.stroke() + } + } +} diff --git a/hancore.shibumi.memory/MemoryPanel.qml b/hancore.shibumi.memory/MemoryPanel.qml index 7f2ed38..4a4507e 100644 --- a/hancore.shibumi.memory/MemoryPanel.qml +++ b/hancore.shibumi.memory/MemoryPanel.qml @@ -92,11 +92,7 @@ ShibumiPanel { onWidthChanged: requestPaint() onHeightChanged: requestPaint() - Ui.OpticalGlyph { - anchors.centerIn: parent; width: Commons.Style.space(32); height: width - text: "memory_alt"; color: panel.controlForeground - fontFamily: "Material Symbols Rounded"; fontSize: Math.round(Commons.Style.space(26)) - } + water: true } Grid { diff --git a/hancore.shibumi.memory/MemoryRing.qml b/hancore.shibumi.memory/MemoryRing.qml index c3567db..09b623b 100644 --- a/hancore.shibumi.memory/MemoryRing.qml +++ b/hancore.shibumi.memory/MemoryRing.qml @@ -7,11 +7,13 @@ Canvas { required property int percent required property color foreground required property color accent + property bool water: false width: Commons.Style.space(16) height: width onPercentChanged: requestPaint() + onWaterChanged: requestPaint() onForegroundChanged: requestPaint() onAccentChanged: requestPaint() Component.onCompleted: requestPaint() @@ -24,6 +26,27 @@ Canvas { const ratio = Math.max(0, Math.min(1, percent / 100)) const start = -Math.PI / 2 + // Static waterline: height follows the clamped value; no clock or animation. + if (water && ratio > 0) { + const inner = radius - 3 + const left = center - inner, right = center + inner, bottom = center + inner + const level = bottom - 2 * inner * ratio + const wave = Math.min(Commons.Style.space(3), 2 * inner * ratio, 2 * inner * (1 - ratio)) + context.save() + context.beginPath() + context.arc(center, center, inner, 0, Math.PI * 2) + context.clip() + context.beginPath() + context.moveTo(left, level) + context.bezierCurveTo(center - inner / 3, level - wave, center + inner / 3, level + wave, right, level) + context.lineTo(right, bottom) + context.lineTo(left, bottom) + context.closePath() + context.fillStyle = Qt.rgba(accent.r, accent.g, accent.b, 0.35) + context.fill() + context.restore() + } + context.lineWidth = 1.8 context.lineCap = "round" context.beginPath() From fafc7cdd97a0ae04f38b870b24edf9d5d4eb8d4e Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 10:01:48 +0200 Subject: [PATCH 23/40] fix: read the CPU model and load average when the CPU panel opens --- hancore.shibumi.telemetry/SystemTelemetry.qml | 6 ++++-- tests/telemetry-runtime-smoke.qml | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/hancore.shibumi.telemetry/SystemTelemetry.qml b/hancore.shibumi.telemetry/SystemTelemetry.qml index d22eb11..c4789c7 100644 --- a/hancore.shibumi.telemetry/SystemTelemetry.qml +++ b/hancore.shibumi.telemetry/SystemTelemetry.qml @@ -41,8 +41,10 @@ Scope { if (memoryConsumers === 1) memoryFile.reload() } else if (kind === "cpuPanel") { if (++cpuPanelConsumers === 1) { - if (!cpuModelRead) { cpuModelRead = true; modelFile.reload() } + // Lazy FileViews start async reads on text(), not on reload() alone. + if (!cpuModelRead) { cpuModelRead = true; modelFile.text() } loadFile.reload() + loadFile.text() } } } @@ -123,7 +125,7 @@ Scope { } function refresh() { - if (cpuPanelConsumers > 0) loadFile.reload() + if (cpuPanelConsumers > 0) { loadFile.reload(); loadFile.text() } if (cpuConsumers > 0) cpuFile.reload() if (memoryConsumers > 0) memoryFile.reload() } diff --git a/tests/telemetry-runtime-smoke.qml b/tests/telemetry-runtime-smoke.qml index 61181da..dc6078c 100644 --- a/tests/telemetry-runtime-smoke.qml +++ b/tests/telemetry-runtime-smoke.qml @@ -13,6 +13,7 @@ ShellRoot { id: root property int phase: 0 property int ticks: 0 + property int detailReadStartedTick: 0 property var panels: [] property var firstGpu: null function check(value, message) { @@ -146,6 +147,7 @@ ShellRoot { cpu.gpu.parse("sysfs||61|0|0\nstatus|ok") telemetry.thermal.parseDetailed("55|63|80|100|44|70|90|39") storage.storage.parseUsage("/dev/fixture 1000 400 600 40%") + root.detailReadStartedTick = root.ticks for (const item of items) item.open() root.phase++ } else if (root.phase === 3) { @@ -155,10 +157,17 @@ ShellRoot { && telemetry.system.memoryConsumers === 1 && storage.storage.consumers === 1 && telemetry.thermal.consumers === 1, "open panel/widget leases unbalanced") const cpuPanel = root.panels[0] - root.check(typeof telemetry.system.parseLoad === "function", "CPU panel demand metrics missing") - telemetry.system.parseCpuModel("processor : 0\nmodel name : Fixture CPU\n") - telemetry.system.parseLoad("0.25 1.50 2.75 1/300 12345\n") - root.check(cpuPanel.cpuModelText === "Fixture CPU" && cpuPanel.loadAverageText.indexOf("0.25 / 1.50 / 2.75") >= 0 + // Exercise the lazy FileView path, not synthetic parser success calls. + if (telemetry.system.cpuModel.length === 0 || telemetry.system.loadAverage.length !== 3) { + root.check(root.ticks - root.detailReadStartedTick < 25, + "CPU panel real /proc read missing after acquire(cpuPanel): model=" + + JSON.stringify(telemetry.system.cpuModel) + " load=" + JSON.stringify(telemetry.system.loadAverage)) + return + } + console.log("252_CPU_PROC_READ", "ticks", root.ticks - root.detailReadStartedTick, + "model", telemetry.system.cpuModel, "load", JSON.stringify(telemetry.system.loadAverage)) + root.check(cpuPanel.cpuModelText === telemetry.system.cpuModel + && cpuPanel.loadAverageText === telemetry.system.loadAverage.map(value => value.toFixed(2)).join(" / ") && telemetry.system.cpuPanelConsumers === 1 && !cpuPanel.gpuUsageView.visible, "CPU model/load or initially hidden GPU row missing") cpu.gpu.parse("sysfs||61|0|0\nstatus|ok") root.check(cpu.gpu.available && !cpuPanel.gpuUsageView.visible, "GPU without reported utilization stayed visible") From cf4c26211df910e8ffd8efdf0d0f4ae8621c8d3d Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 10:01:48 +0200 Subject: [PATCH 24/40] feat: remove the history chart from the CPU panel --- hancore.shibumi.cpu/CpuPanel.qml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/hancore.shibumi.cpu/CpuPanel.qml b/hancore.shibumi.cpu/CpuPanel.qml index 73df5b0..cba67e0 100644 --- a/hancore.shibumi.cpu/CpuPanel.qml +++ b/hancore.shibumi.cpu/CpuPanel.qml @@ -154,15 +154,6 @@ ShibumiPanel { } } - CpuWave { - width: parent.width; height: Commons.Style.space(64) - history: panel.systemTelemetry ? panel.systemTelemetry.cpuHistory : [] - maxSamples: panel.systemTelemetry ? panel.systemTelemetry.cpuMaxSamples : 30 - accent: panel.controlMutedHigh - onWidthChanged: requestPaint() - onHeightChanged: requestPaint() - } - UsageRow { id: gpuUsage width: parent.width From 03da9fe38dbc29017c90bd6c5a5e9040319426ef Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 10:20:23 +0200 Subject: [PATCH 25/40] feat: name the GPU in the CPU panel title only when its row is shown --- hancore.shibumi.cpu/CpuPanel.qml | 2 +- tests/telemetry-plugins-regression.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hancore.shibumi.cpu/CpuPanel.qml b/hancore.shibumi.cpu/CpuPanel.qml index cba67e0..4a3a304 100644 --- a/hancore.shibumi.cpu/CpuPanel.qml +++ b/hancore.shibumi.cpu/CpuPanel.qml @@ -70,7 +70,7 @@ ShibumiPanel { Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - text: "CPU · GPU" + text: gpuUsage.visible ? "CPU · GPU" : "CPU" color: panel.bar ? panel.bar.foreground : Commons.Color.foreground font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family font.pixelSize: 13 diff --git a/tests/telemetry-plugins-regression.sh b/tests/telemetry-plugins-regression.sh index 2956466..4151457 100755 --- a/tests/telemetry-plugins-regression.sh +++ b/tests/telemetry-plugins-regression.sh @@ -150,9 +150,9 @@ for plugin in memory cpu; do "$repo_root/hancore.shibumi.$plugin/${plugin^}Panel.qml" \ || fail "$plugin panel bypasses its owner action" done -rg -q 'text: "CPU · GPU"' \ +rg -Fq 'text: gpuUsage.visible ? "CPU · GPU" : "CPU"' \ "$repo_root/hancore.shibumi.cpu/CpuPanel.qml" \ - || fail "CPU panel lost the V1 CPU/GPU heading" + || fail "CPU panel heading does not follow GPU row visibility" rg -q 'visible: panel\.gpuTelemetry && panel\.gpuTelemetry\.available' \ "$repo_root/hancore.shibumi.cpu/CpuPanel.qml" \ || fail "CPU panel does not gate GPU data on a real telemetry backend" From ec7bfe39268f3430c1372eb080be2446edd8d2bf Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:23:40 +0200 Subject: [PATCH 26/40] feat: show the RAM type and speed in the Memory panel --- hancore.shibumi.memory/MemoryPanel.qml | 24 +++++++++ hancore.shibumi.telemetry/SystemTelemetry.qml | 51 +++++++++++++++++++ tests/telemetry-plugins-regression.sh | 1 + tests/telemetry-runtime-smoke.qml | 21 ++++++-- 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/hancore.shibumi.memory/MemoryPanel.qml b/hancore.shibumi.memory/MemoryPanel.qml index 4a4507e..1b60461 100644 --- a/hancore.shibumi.memory/MemoryPanel.qml +++ b/hancore.shibumi.memory/MemoryPanel.qml @@ -11,6 +11,7 @@ ShibumiPanel { required property var ownerWidget required property var telemetry readonly property var usageRing: memoryRing + readonly property var hardwareInfoView: hardwareInfo owner: ownerWidget open: ownerWidget.opened @@ -19,6 +20,19 @@ ShibumiPanel { contentWidth: fittedContentWidth(320) contentHeight: fittedContentHeight(panelColumn.implicitHeight) + property var acquiredTelemetry: null + function syncHardwareLease() { + const next = open ? telemetry : null + if (acquiredTelemetry === next) return + if (acquiredTelemetry) acquiredTelemetry.release("memoryPanel") + acquiredTelemetry = next + if (next) next.acquire("memoryPanel") + } + onTelemetryChanged: syncHardwareLease() + onOpenChanged: syncHardwareLease() + Component.onCompleted: syncHardwareLease() + Component.onDestruction: if (acquiredTelemetry) acquiredTelemetry.release("memoryPanel") + Ui.PanelKeyCatcher { id: keyCatcher anchors.fill: parent @@ -69,6 +83,16 @@ ShibumiPanel { } } + Text { + id: hardwareInfo + width: parent.width; elide: Text.ElideRight; textFormat: Text.PlainText + text: panel.telemetry ? panel.telemetry.memoryHardwareInfo : "" + visible: text.length > 0 + color: panel.controlMutedHigh; renderType: Text.NativeRendering + font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family + font.pixelSize: Commons.Style.font.caption + } + Rectangle { width: parent.width height: 1 diff --git a/hancore.shibumi.telemetry/SystemTelemetry.qml b/hancore.shibumi.telemetry/SystemTelemetry.qml index c4789c7..b076c64 100644 --- a/hancore.shibumi.telemetry/SystemTelemetry.qml +++ b/hancore.shibumi.telemetry/SystemTelemetry.qml @@ -11,6 +11,10 @@ Scope { property string cpuModel: "" property var loadAverage: [] property bool cpuModelRead: false + property int memoryPanelConsumers: 0 + property bool memoryHardwareRead: false + property bool memoryHardwareReady: false + property string memoryHardwareInfo: "" readonly property bool active: cpuConsumers > 0 || memoryConsumers > 0 || cpuPanelConsumers > 0 readonly property int intervalMs: 2000 @@ -39,6 +43,9 @@ Scope { } else if (kind === "memory") { memoryConsumers++ if (memoryConsumers === 1) memoryFile.reload() + } else if (kind === "memoryPanel") { + memoryPanelConsumers++ + if (!memoryHardwareRead) { memoryHardwareRead = true; memoryHardwareFile.text() } } else if (kind === "cpuPanel") { if (++cpuPanelConsumers === 1) { // Lazy FileViews start async reads on text(), not on reload() alone. @@ -52,6 +59,7 @@ Scope { function release(kind) { if (kind === "cpu") cpuConsumers = Math.max(0, cpuConsumers - 1) else if (kind === "memory") memoryConsumers = Math.max(0, memoryConsumers - 1) + else if (kind === "memoryPanel") memoryPanelConsumers = Math.max(0, memoryPanelConsumers - 1) else if (kind === "cpuPanel") cpuPanelConsumers = Math.max(0, cpuPanelConsumers - 1) } @@ -114,6 +122,40 @@ Scope { memCachedMiB = Math.round(cached / 1024) } + function parseMemoryHardware(text) { + memoryHardwareInfo = "" + const input = String(text || "") + if (input.length > 131072) return + // systemd's internal udev database: accept only these E: properties. + const modules = {} + for (const line of input.split("\n")) { + const match = /^E:MEMORY_DEVICE_(\d+)_(SIZE|TYPE|CONFIGURED_SPEED_MTS|SPEED_MTS)=(.*)$/.exec(line) + if (!match) continue + const index = Number(match[1]) + if (index >= 256) return + if (!modules[index]) modules[index] = {} + if (modules[index][match[2]] !== undefined) return + modules[index][match[2]] = match[3] + } + function positive(value) { + const number = Number(value) + return /^\d+$/.test(value || "") && Number.isSafeInteger(number) && number > 0 ? number : 0 + } + let type = "", speed = Infinity + for (const index of Object.keys(modules).sort((a, b) => Number(a) - Number(b))) { + const module = modules[index] + if (!positive(module.SIZE)) continue + if (!type) { + type = String(module.TYPE || "").trim() + if (!/^[A-Za-z][A-Za-z0-9 ()-]{0,31}$/.test(type) || /^(Unknown|Other|Not Specified)$/i.test(type)) return + } + const rate = positive(module.CONFIGURED_SPEED_MTS) || positive(module.SPEED_MTS) + if (!rate) return + speed = Math.min(speed, rate) + } + if (type && isFinite(speed)) memoryHardwareInfo = type + " · " + speed + " MT/s" + } + function parseCpuModel(text) { const match = /^(?:model name|Hardware)\s*:\s*([^\r\n]{1,256})/m.exec(String(text || "").slice(0, 8192)) cpuModel = match ? match[1].trim() : "" @@ -144,6 +186,15 @@ Scope { onLoaded: root.parseMemory(text()) } + FileView { + id: memoryHardwareFile + path: "/run/udev/data/+dmi:id" + preload: false + printErrors: false + onLoaded: { root.parseMemoryHardware(text()); root.memoryHardwareReady = true } + onLoadFailed: { root.memoryHardwareInfo = ""; root.memoryHardwareReady = true } + } + FileView { id: modelFile path: "/proc/cpuinfo" diff --git a/tests/telemetry-plugins-regression.sh b/tests/telemetry-plugins-regression.sh index 4151457..a6df008 100755 --- a/tests/telemetry-plugins-regression.sh +++ b/tests/telemetry-plugins-regression.sh @@ -115,6 +115,7 @@ install -m 0644 "$repo_root/tests/telemetry-runtime-smoke.qml" "$tmpdir/runtime- mkdir -m 700 "$tmpdir/slice-runtime" set +e slice_output=$(timeout 8 env \ + SHIBUMI_TEST_MEMORY_PRESENT="$(grep -qE '^E:MEMORY_DEVICE_[0-9]+_TYPE=' /run/udev/data/+dmi:id 2>/dev/null && printf 1 || printf 0)" \ QT_QPA_PLATFORM=offscreen WAYLAND_DISPLAY= XDG_RUNTIME_DIR="$tmpdir/slice-runtime" \ QML_IMPORT_PATH="$omarchy_path/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \ QML2_IMPORT_PATH="$omarchy_path/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \ diff --git a/tests/telemetry-runtime-smoke.qml b/tests/telemetry-runtime-smoke.qml index dc6078c..76d66a4 100644 --- a/tests/telemetry-runtime-smoke.qml +++ b/tests/telemetry-runtime-smoke.qml @@ -144,6 +144,8 @@ ShellRoot { root.check(!cpu.gpu.probeEnabled && !telemetry.thermal.probeEnabled && !storage.storage.runtimeProbesEnabled, "fixture probe suppression lost") root.check(telemetry.system.cpuPanelConsumers === 0 && telemetry.system.cpuModel === "" && telemetry.system.loadAverage.length === 0, "detail probe before panel demand") + root.check(telemetry.system.memoryPanelConsumers === 0 && !telemetry.system.memoryHardwareRead + && !telemetry.system.memoryHardwareReady && telemetry.system.memoryHardwareInfo === "", "memory hardware read before panel demand") cpu.gpu.parse("sysfs||61|0|0\nstatus|ok") telemetry.thermal.parseDetailed("55|63|80|100|44|70|90|39") storage.storage.parseUsage("/dev/fixture 1000 400 600 40%") @@ -156,6 +158,10 @@ ShellRoot { root.check(cpu.gpu.consumers === 3 && telemetry.system.cpuConsumers === 1 && telemetry.system.memoryConsumers === 1 && storage.storage.consumers === 1 && telemetry.thermal.consumers === 1, "open panel/widget leases unbalanced") + if (!telemetry.system.memoryHardwareReady) { + root.check(root.ticks - root.detailReadStartedTick < 25, "Memory panel real udev read did not complete after acquire(memoryPanel)") + return + } const cpuPanel = root.panels[0] // Exercise the lazy FileView path, not synthetic parser success calls. if (telemetry.system.cpuModel.length === 0 || telemetry.system.loadAverage.length !== 3) { @@ -181,6 +187,13 @@ ShellRoot { root.check(root.panels[0] && root.panels[0].gpuUsageView.visible, "GPU activity latch lost on panel reopen") telemetry.system.parseLoad("invalid"); root.check(telemetry.system.loadAverage.length === 0, "invalid load accepted") const memoryPanel = root.panels[1], oldMemory = [telemetry.system.memTotalMiB, telemetry.system.memAvailableMiB] + const hasInfo = Quickshell.env("SHIBUMI_TEST_MEMORY_PRESENT") === "1", info = memoryPanel.hardwareInfoView + root.check(telemetry.system.memoryHardwareRead && telemetry.system.memoryPanelConsumers === 1 + && info && info.text === telemetry.system.memoryHardwareInfo + && (hasInfo ? /^.+ · [1-9][0-9]* MT\/s$/.test(info.text) && info.visible : !info.visible), + "Memory real udev data/visibility mismatch: " + JSON.stringify(telemetry.system.memoryHardwareInfo)) + console.log("257_MEMORY_UDEV_READ", "present", hasInfo, "ticks", root.ticks - root.detailReadStartedTick, + "info", JSON.stringify(info.text), "visible", info.visible) telemetry.system.memTotalMiB = 8192; telemetry.system.memAvailableMiB = 3072 function texts(item) { return ("text" in item ? [String(item.text)] : []).concat(item.children.reduce((out, child) => out.concat(texts(child)), [])) } const memoryText = texts(memoryPanel) @@ -201,7 +214,7 @@ ShellRoot { && cpu.gpu === null && telemetry.system === null && storage.storage === null && root.panels[0].gpuTelemetry === null && root.panels[0].systemTelemetry === null && root.panels[0].acquiredGpuTelemetry === null && root.panels[0].acquiredSystemTelemetry === null - && root.panels[1].telemetry === null && root.panels[2].gpuTelemetry === null + && root.panels[1].telemetry === null && root.panels[1].acquiredTelemetry === null && root.panels[2].gpuTelemetry === null && root.panels[3].telemetry === null && root.panels[4].storage === null && root.panels[0].bar === replacementBar && root.panels[1].bar === memoryBar, "scope loss retained panel backend or crossed local bar") @@ -214,7 +227,8 @@ ShellRoot { root.check(cpu.gpu !== root.firstGpu && cpu.gpu.consumers === 3 && root.panels[0].gpuTelemetry === cpu.gpu && root.panels[0].acquiredGpuTelemetry === cpu.gpu && telemetry.system.cpuPanelConsumers === 1 - && root.panels[1].telemetry === telemetry.system + && root.panels[1].telemetry === telemetry.system && root.panels[1].acquiredTelemetry === telemetry.system + && telemetry.system.memoryPanelConsumers === 1 && root.panels[2].gpuTelemetry === cpu.gpu && root.panels[3].telemetry === telemetry.thermal && root.panels[4].storage === storage.storage, @@ -239,7 +253,8 @@ ShellRoot { for (const item of items) item.close() root.phase++ } else if (root.phase === 8) { - root.check(cpu.gpu.consumers === 2 && telemetry.system.cpuPanelConsumers === 0, "CPU panel close retained GPU/detail lease") + root.check(cpu.gpu.consumers === 2 && telemetry.system.cpuPanelConsumers === 0 + && telemetry.system.memoryPanelConsumers === 0, "panel close retained GPU/detail lease") for (const item of items) root.check(item.panelItem === null, "closed panel retained") views.active = false root.phase++ From 862a92ff35fd5b36f9fa2dd9062b3f8bc7f50082 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:37:50 +0200 Subject: [PATCH 27/40] fix: count and focus Recent notifications without Live access --- .../NotificationAdapter.qml | 11 +++++- .../NotificationStatusView.qml | 4 +-- hancore.shibumi.status/Service.qml | 2 ++ tests/notification-adapter-regression.sh | 18 ++++++++++ tests/notification-adapter-smoke.qml | 34 ++++++++++++++++++- 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/hancore.shibumi.status/NotificationAdapter.qml b/hancore.shibumi.status/NotificationAdapter.qml index ff71e7f..a370594 100644 --- a/hancore.shibumi.status/NotificationAdapter.qml +++ b/hancore.shibumi.status/NotificationAdapter.qml @@ -33,6 +33,7 @@ Item { property int mutationGeneration: -1 } + property string omarchyPath: Quickshell.env("OMARCHY_PATH") readonly property bool available: state.hostService !== null readonly property bool doNotDisturb: available && state.hostService.doNotDisturb === true @@ -337,7 +338,14 @@ Item { service.focusApp(entry) return true } - if (focusOnly === true) return false // Recent must never invoke a Live action. + if (focusOnly === true) { + const app = typeof entry.app === "string" ? entry.app.trim() : "" + if (!app || /[\x00-\x1f\x7f]/.test(entry.app) + || !omarchyPath.startsWith("/") || focusProcess.running) return false + focusProcess.command = [omarchyPath + "/bin/omarchy-hyprland-focus-app", app] + focusProcess.running = true + return true // Request accepted, not confirmation of a focused window. + } const source = sourceIndex(entry, sourceModel()) if (source < 0 || typeof service.invokePopupDefault !== "function") return false @@ -375,6 +383,7 @@ Item { return true } + Process { id: focusProcess } Process { id: mutationProcess onExited: function(exitCode, _exitStatus) { diff --git a/hancore.shibumi.status/NotificationStatusView.qml b/hancore.shibumi.status/NotificationStatusView.qml index 5377ff2..c1dacc3 100644 --- a/hancore.shibumi.status/NotificationStatusView.qml +++ b/hancore.shibumi.status/NotificationStatusView.qml @@ -17,14 +17,14 @@ Item { readonly property real iconHorizontalOffset: Commons.Style.space(1) property var notificationService: null readonly property int pendingCount: notificationService - && notificationService.pendingModel + && notificationService.liveAvailable === true && notificationService.pendingModel ? Math.max(0, Number(notificationService.pendingModel.count) || 0) : 0 readonly property int recentCount: notificationService && notificationService.pastModel ? Math.max(0, Number(notificationService.pastModel.count) || 0) : 0 readonly property int notificationCount: pendingCount + recentCount readonly property bool countsKnown: notificationService - && notificationService.liveAvailable === true && notificationService.historyState === "ready" + && notificationService.historyState === "ready" readonly property string tooltipText: (notificationService && notificationService.liveAvailable ? pendingCount + " Live" : "Live: Unavailable") + " · " + (notificationService && notificationService.historyState === "ready" diff --git a/hancore.shibumi.status/Service.qml b/hancore.shibumi.status/Service.qml index 051e0dc..8d75adf 100644 --- a/hancore.shibumi.status/Service.qml +++ b/hancore.shibumi.status/Service.qml @@ -11,6 +11,7 @@ Item { id: root property var shell: null + property string omarchyPath: Quickshell.env("OMARCHY_PATH") property var manifest: null SuiteRuntime.HostShell { id: suiteShell; host: root.shell } SuiteRuntime.Provider { @@ -33,6 +34,7 @@ Item { NotificationAdapter { id: notificationAdapter + omarchyPath: root.omarchyPath } onShellChanged: notificationAdapter.attachShell(suiteShell) diff --git a/tests/notification-adapter-regression.sh b/tests/notification-adapter-regression.sh index 699d2f7..0f9b08d 100755 --- a/tests/notification-adapter-regression.sh +++ b/tests/notification-adapter-regression.sh @@ -47,6 +47,7 @@ run_case() { local marker=${4:-notification adapter smoke passed} local race=${5:-} mutation=${6:-} path=/usr/bin:/bin local root="$tmpdir/$name" output="$tmpdir/$name.log" + local proxy_app='Proxy App $(printf unsafe); "quoted"' mkdir -p "$root"/{home,config,cache,state,data,runtime,tmp,pipewire} chmod 700 "$root/runtime" if [[ -n $race ]]; then @@ -98,6 +99,15 @@ EOF >"$root/state/omarchy/notifications/history/poison.json" fi + if [[ $history_mode == proxy ]]; then + mkdir -p "$root/omarchy fixture/bin" + printf '#!/bin/sh\nprintf '\''%%s\\0'\'' "$0" "$@" >"$SHIBUMI_FOCUS_ARGV"\n' \ + >"$root/omarchy fixture/bin/omarchy-hyprland-focus-app" + chmod 755 "$root/omarchy fixture/bin/omarchy-hyprland-focus-app" + : >"$root/focus.argv" + printf '%s\0' "$root/omarchy fixture/bin/omarchy-hyprland-focus-app" "$proxy_app" >"$root/expected.argv" + fi + setsid env -i \ HOME="$root/home" \ XDG_CONFIG_HOME="$root/config" \ @@ -120,6 +130,9 @@ EOF SHIBUMI_HISTORY_MUTATION="$mutation" \ SHIBUMI_HISTORY_MODE="$history_mode" \ SHIBUMI_HELPER_STATE="$root/helper-state" \ + OMARCHY_PATH="$root/omarchy fixture" \ + SHIBUMI_FOCUS_ARGV="$root/focus.argv" \ + SHIBUMI_PROXY_APP="$proxy_app" \ QML_IMPORT_PATH="$omarchy_path/shell" \ QML2_IMPORT_PATH="$omarchy_path/shell" \ "$quickshell_bin" -p "$tmpdir" --no-color >"$output" 2>&1 & @@ -145,6 +158,10 @@ EOF if grep -Eq 'Binding loop|TypeError|ReferenceError|is not a type|failed to load' "$output"; then fail "$name runtime log contains a composition error" fi + if [[ $history_mode == proxy ]]; then + cmp -s "$root/expected.argv" "$root/focus.argv" || fail 'proxy focus helper argv differs' + printf '259_PROXY_HELPER_ARGV exact [helper, app]\n' + fi if [[ $mutation == dismiss ]]; then [[ ! -e $history/12.json && -e $history/11.json && -e $history/1.json \ && -e "$history/"$'bad\tname.json' ]] \ @@ -163,6 +180,7 @@ EOF fi } +run_case proxy 0 proxy 'notification proxy badge/focus passed' run_case populated 10 populated run_case external-clear 10 populated 'notification history clear passed' '' external-clear run_case empty 0 empty diff --git a/tests/notification-adapter-smoke.qml b/tests/notification-adapter-smoke.qml index 22e5211..0a4afb9 100644 --- a/tests/notification-adapter-smoke.qml +++ b/tests/notification-adapter-smoke.qml @@ -15,6 +15,9 @@ ShellRoot { Quickshell.env("SHIBUMI_HISTORY_MUTATION") property int mutationPhase: 0 property int mutationStartTick: 0 + readonly property bool proxyCase: Quickshell.env("SHIBUMI_HISTORY_MODE") === "proxy" + property int proxyPhase: 0 + property var proxyChecks: ({}) function clickLive(token) { return typeof adapter.invokeLive === "function" ? adapter.invokeLive(token) : adapter.focusApp(adapter.pendingModel.get(0)) } function fail(message) { @@ -75,7 +78,7 @@ ShellRoot { } } - // The 4.0.3 host proxy can expose DND while withholding popupModel. + // The real notification proxy exposes only DND, not models or focus actions. QtObject { id: dndOnlyHost property bool doNotDisturb: true @@ -108,6 +111,8 @@ ShellRoot { Status.NotificationAdapter { id: unavailableAdapter } Status.NotificationStatusView { id: counts; bar: null; notificationService: dndOnlyAdapter } Process { id: externalClear; command: ["sh", "-c", "rm -f \"$1\"/*.json", "--", dndOnlyAdapter.historyDir] } + FileView { id: proxyHistory; path: dndOnlyAdapter.historyDir + "/proxy.json"; preload: false; printErrors: false } + FileView { id: proxyReceipt; path: Quickshell.env("SHIBUMI_FOCUS_ARGV"); preload: false; printErrors: false } Component.onCompleted: { dndOnlyAdapter.attachShell(dndOnlyShell) @@ -124,6 +129,33 @@ ShellRoot { running: true onTriggered: { root.ticks++ + if (root.proxyCase) { + if (root.ticks > 100) return root.fail("proxy badge/focus deadline") + if (dndOnlyAdapter.historyState !== "ready") return + const badge = Array.from(counts.children).find(child => child.z === counts.badgeLayer) + if (!badge) return root.fail("proxy badge missing") + if (root.proxyPhase === 0) { + root.proxyChecks.emptyReady = !dndOnlyAdapter.liveAvailable && counts.countsKnown + && !badge.visible && badge.children[0].text !== "?" + proxyHistory.setText(JSON.stringify({app: Quickshell.env("SHIBUMI_PROXY_APP"), summary: "Proxy recent", timestamp: 1})) + root.proxyPhase = 1 + return + } + if (root.proxyPhase === 1) { + if (dndOnlyAdapter.recentCount !== 1) return + root.proxyChecks.one = badge.visible && counts.notificationCount === 1 && badge.children[0].text === "1" + root.proxyChecks.emptyApp = !dndOnlyAdapter.focusApp({app: ""}, true) + root.proxyChecks.focus = dndOnlyAdapter.focusApp(dndOnlyAdapter.pastModel.get(0), true) + console.log("259_PROXY", JSON.stringify(root.proxyChecks)) + if (Object.values(root.proxyChecks).some(ok => !ok)) return root.fail("proxy badge/focus") + root.proxyPhase = 2 + } + proxyReceipt.reload() + if (!proxyReceipt.text().length) return + console.log("notification proxy badge/focus passed") + Qt.exit(0) + return + } if (root.ticks === 2) { if (!adapter.available || !adapter.liveAvailable || !adapter.historyAvailable || adapter.pendingCount !== 1 From cbe65fecfc08f3b8723c7f177373363719518681 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:11:26 +0200 Subject: [PATCH 28/40] fix: omit the unavailable Live count from the notification tooltip --- hancore.shibumi.status/NotificationStatusView.qml | 4 ++-- tests/notification-adapter-smoke.qml | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/hancore.shibumi.status/NotificationStatusView.qml b/hancore.shibumi.status/NotificationStatusView.qml index c1dacc3..9732d9b 100644 --- a/hancore.shibumi.status/NotificationStatusView.qml +++ b/hancore.shibumi.status/NotificationStatusView.qml @@ -25,8 +25,8 @@ Item { readonly property int notificationCount: pendingCount + recentCount readonly property bool countsKnown: notificationService && notificationService.historyState === "ready" - readonly property string tooltipText: (notificationService && notificationService.liveAvailable - ? pendingCount + " Live" : "Live: Unavailable") + " · " + readonly property string tooltipText: (notificationService && notificationService.liveAvailable === true + ? pendingCount + " Live · " : "") + (notificationService && notificationService.historyState === "ready" ? recentCount + " Recent" : "Recent: " + (notificationService && notificationService.historyState === "loading" ? "Loading" : "Unavailable")) diff --git a/tests/notification-adapter-smoke.qml b/tests/notification-adapter-smoke.qml index 0a4afb9..1c46ca0 100644 --- a/tests/notification-adapter-smoke.qml +++ b/tests/notification-adapter-smoke.qml @@ -119,7 +119,7 @@ ShellRoot { adapter.attachShell(currentShell) legacyAdapter.attachShell(legacyShell) unavailableAdapter.attachShell(null) - if (dndOnlyAdapter.historyState !== "loading" || counts.tooltipText !== "Live: Unavailable · Recent: Loading · DND") + if (dndOnlyAdapter.historyState !== "loading" || counts.tooltipText !== "Recent: Loading · DND") root.fail("initial history state is not loading without hover/open") } @@ -137,6 +137,7 @@ ShellRoot { if (root.proxyPhase === 0) { root.proxyChecks.emptyReady = !dndOnlyAdapter.liveAvailable && counts.countsKnown && !badge.visible && badge.children[0].text !== "?" + && counts.tooltipText === "0 Recent · DND" proxyHistory.setText(JSON.stringify({app: Quickshell.env("SHIBUMI_PROXY_APP"), summary: "Proxy recent", timestamp: 1})) root.proxyPhase = 1 return @@ -144,6 +145,7 @@ ShellRoot { if (root.proxyPhase === 1) { if (dndOnlyAdapter.recentCount !== 1) return root.proxyChecks.one = badge.visible && counts.notificationCount === 1 && badge.children[0].text === "1" + && counts.tooltipText === "1 Recent · DND" root.proxyChecks.emptyApp = !dndOnlyAdapter.focusApp({app: ""}, true) root.proxyChecks.focus = dndOnlyAdapter.focusApp(dndOnlyAdapter.pastModel.get(0), true) console.log("259_PROXY", JSON.stringify(root.proxyChecks)) @@ -191,7 +193,7 @@ ShellRoot { if (root.ticks < (root.historyRace ? 40 : 12)) return if (!root.historyRace && !root.mutationPhase && (adapter.historyState !== (Quickshell.env("SHIBUMI_HISTORY_MODE") === "missing" ? "unavailable" : "ready") - || counts.tooltipText !== "Live: Unavailable · " + (adapter.historyState === "ready" + || counts.tooltipText !== (adapter.historyState === "ready" ? root.expectedHistoryCount + " Recent" : "Recent: Unavailable") + " · DND")) return root.fail("eager snapshot/tooltip state without hover/open") if (root.historyRace && root.historyRace !== "mutation-replace") { From 99b30c5d49c08e6f53e4560317171d848afd274f Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 19:25:40 +0200 Subject: [PATCH 29/40] build: align the production boundary with the approved beta.15.4 payload --- contracts/backend-boundary-v1.json | 5 +++-- hancore.shibumi.ai/AiUsagePanel.qml | 2 +- hancore.shibumi.cpu/CpuPanel.qml | 2 +- hancore.shibumi.media/MediaPanel.qml | 2 +- hancore.shibumi.memory/MemoryPanel.qml | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/contracts/backend-boundary-v1.json b/contracts/backend-boundary-v1.json index 0fe04b5..7e5a4f1 100644 --- a/contracts/backend-boundary-v1.json +++ b/contracts/backend-boundary-v1.json @@ -44,7 +44,7 @@ {"scope":"rule","source":"hancore.shibumi.power-state/Service.qml","kind":"helper","kindValue":"omarchy-powerprofiles-list","category":"omarchyBackend","owner":"hancore.shibumi.power-state","capability":"power-profile","count":1,"backend":"omarchy.power","removalStep":"Step 6"}, {"scope":"rule","source":"hancore.shibumi.status/Service.qml","kind":"helper","kindValue":"/tmp/omarchy-screenrecord-filename","category":"omarchyBackend","owner":"hancore.shibumi.status","capability":"recording-voxtype","count":1,"backend":"omarchy.recording","removalStep":"Step 8"}, {"scope":"rule","source":"hancore.shibumi.status/Service.qml","kind":"helper","kindValue":"omarchy-voxtype-status","category":"omarchyBackend","owner":"hancore.shibumi.status","capability":"recording-voxtype","count":1,"backend":"omarchy.voxtype","removalStep":"Step 8"}, - {"scope":"hostIdRule","target":"omarchy.agents","category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","count":10,"backend":"omarchy.agents","removalStep":"Step 8"}, + {"scope":"hostIdRule","target":"omarchy.agents","category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","count":13,"backend":"omarchy.agents","removalStep":"Step 8"}, {"scope":"hostIdRule","target":"omarchy.audio","category":"omarchyBackend","owner":"hancore.shibumi.audio","capability":"audio","count":13,"backend":"omarchy.audio","removalStep":"Step 4A"}, {"scope":"hostIdRule","target":"omarchy.media","category":"omarchyBackend","owner":"hancore.shibumi.media","capability":"media","count":9,"backend":"omarchy.media","removalStep":"Step 7"}, {"scope":"hostIdRule","target":"omarchy.model-usage","category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","count":8,"backend":"omarchy.model-usage","removalStep":"Step 8"}, @@ -361,7 +361,7 @@ ], "hostIdRules": [ {"target":"omarchy.active-window","count":2,"category":"hostIntegration","owner":"quattro.host","capability":"bar-host-routing"}, - {"target":"omarchy.agents","count":10,"category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","backend":"omarchy.agents","removalStep":"Step 8","reason":"Remove private Agents provider identifiers after the Shibumi provider adapter is authoritative."}, + {"target":"omarchy.agents","count":13,"category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","backend":"omarchy.agents","removalStep":"Step 8","reason":"Remove private Agents provider identifiers after the Shibumi provider adapter is authoritative."}, {"target":"omarchy.audio","count":13,"category":"omarchyBackend","owner":"hancore.shibumi.audio","capability":"audio","backend":"omarchy.audio","removalStep":"Step 4A","reason":"Remove the official audio component and its compatibility identity after native Audio activation."}, {"target":"omarchy.bar","count":12,"category":"hostIntegration","owner":"quattro.host","capability":"bar-host-routing"}, {"target":"omarchy.bluetooth","count":8,"category":"hostIntegration","owner":"hancore.shibumi.bluetooth","capability":"bluetooth"}, @@ -399,6 +399,7 @@ {"value":"omarchy-cmd-missing","count":1,"category":"hostIntegration","owner":"quattro.host","capability":"bar-host-routing"}, {"value":"omarchy-cmd-present","count":4,"category":"hostIntegration","owner":"quattro.host","capability":"bar-host-routing"}, {"value":"omarchy-dev","count":2,"category":"hostIntegration","owner":"quattro.host","capability":"bar-host-routing"}, + {"value":"omarchy-hyprland-focus-app","count":1,"category":"omarchyAction","owner":"quattro.host","capability":"notifications","reason":"NotificationAdapter.qml invokes the public focus helper through an argv-only Process for Recent entries; it neither launches apps nor replays notification actions."}, {"value":"omarchy-keyboard-panel","count":18,"category":"hostIntegration","owner":"quattro.host","capability":"bar-host-routing"}, {"value":"omarchy-launch-floating-terminal-with-presentation","count":3,"category":"omarchyAction","owner":"quattro.host","capability":"omarchy-update-action"}, {"value":"omarchy-launch-or-focus-tui","count":4,"category":"omarchyAction","owner":"quattro.host","capability":"bar-host-routing"}, diff --git a/hancore.shibumi.ai/AiUsagePanel.qml b/hancore.shibumi.ai/AiUsagePanel.qml index 6bacbdd..cad5b5c 100644 --- a/hancore.shibumi.ai/AiUsagePanel.qml +++ b/hancore.shibumi.ai/AiUsagePanel.qml @@ -7,7 +7,7 @@ import "../hancore.shibumi.state/lib/presentation" as Presentation // Layout derived from MIT-licensed Omarchy v4.0.4 plugins/agents/Panel.qml. // See Omarchy-LICENSE. Shibumi retains its own data, selection and lifecycle. -// Chart/metric structure adapted from 0xSero/omarchy-local-ai; see Local-AI-LICENSE. +// Chart/metric structure adapted from 0xSero's Omarchy Local AI plugin (MIT); see Local-AI-LICENSE. ShibumiPanel { id: panel diff --git a/hancore.shibumi.cpu/CpuPanel.qml b/hancore.shibumi.cpu/CpuPanel.qml index 4a3a304..2553710 100644 --- a/hancore.shibumi.cpu/CpuPanel.qml +++ b/hancore.shibumi.cpu/CpuPanel.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import qs.Commons as Commons import qs.Ui as Ui -// Compact chart/metric layout inspired by 0xSero/omarchy-local-ai; see Local-AI-LICENSE. +// Compact chart/metric layout inspired by 0xSero's Omarchy Local AI plugin (MIT); see Local-AI-LICENSE. ShibumiPanel { id: panel diff --git a/hancore.shibumi.media/MediaPanel.qml b/hancore.shibumi.media/MediaPanel.qml index ddb6165..142d533 100644 --- a/hancore.shibumi.media/MediaPanel.qml +++ b/hancore.shibumi.media/MediaPanel.qml @@ -676,7 +676,7 @@ ShibumiPanel { } } - // Outline button structure inspired by 0xSero/omarchy-local-ai; see Local-AI-LICENSE. + // Outline button structure inspired by 0xSero's Omarchy Local AI plugin (MIT); see Local-AI-LICENSE. component MediaPanelButton: Rectangle { id: button required property string icon diff --git a/hancore.shibumi.memory/MemoryPanel.qml b/hancore.shibumi.memory/MemoryPanel.qml index 1b60461..7f9dd96 100644 --- a/hancore.shibumi.memory/MemoryPanel.qml +++ b/hancore.shibumi.memory/MemoryPanel.qml @@ -4,7 +4,7 @@ import QtQuick import qs.Commons as Commons import qs.Ui as Ui -// Compact metric layout inspired by 0xSero/omarchy-local-ai; see Local-AI-LICENSE. +// Compact metric layout inspired by 0xSero's Omarchy Local AI plugin (MIT); see Local-AI-LICENSE. ShibumiPanel { id: panel From a9ebae91bcac125a06e72cf6123578f909da3ddf Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 19:25:40 +0200 Subject: [PATCH 30/40] release: pin beta.15.4 and cover beta.15.3 checkout updates --- contracts/lifecycle-predecessors-v1.json | 61 ++++++++++++++++++++++++ scripts/check-aur-package | 2 +- tests/shibumi-suite-quattro-runtime.sh | 33 ++++++++----- tests/test_package_release.py | 12 +++-- 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/contracts/lifecycle-predecessors-v1.json b/contracts/lifecycle-predecessors-v1.json index 02bf092..b29680e 100644 --- a/contracts/lifecycle-predecessors-v1.json +++ b/contracts/lifecycle-predecessors-v1.json @@ -629,6 +629,67 @@ "package:0.1.1-beta.15.3" ], "suiteVersion": "0.1.1-beta.15.3" + }, + { + "id": "public-beta.15.4", + "payloadDigest": "977af8d8622042c2348f522016d6655a89e8e80e51e55343acc953ea7993fc5c", + "pluginDigests": { + "hancore.shibumi.bar": "fd1d69f365d08ad06dd52e0cb19892ac935109752f6be4b80e2a20adaf3e7fb9", + "hancore.shibumi.state": "8bdbe450c842f7bc6b6483edb652389370201babdef52ae792a9700b51269af4", + "hancore.shibumi.control-center": "2ee96573dc0ea3835a21bbe5acc8f9dfaa8acfe811e00f220598f427352c54cf", + "hancore.shibumi.reactor": "b32cc5ac9d1394254d0c092f83ee2067d17e3d6339dbd63290aedb4f595cf436", + "hancore.shibumi.telemetry": "c0a2d79bfe338e33bfbf492bcbbbc333aba2f723057268265cfab2e556acb332", + "hancore.shibumi.power-state": "b0a25028a9ef266e0593dcad6e5d9133478f6b4754b7a3fa0756e927f754d88d", + "hancore.shibumi.workspaces": "9c19d443cca3e15c3977296fd1dea6c85aa27ba13d2c36991524fdb115dee629", + "hancore.shibumi.update-center": "2382455e00ded224236931fe9ca892bbe7f913fbb8f020b56511d3fb736e700f", + "hancore.shibumi.status": "1716e7ceaff65c45a78c45cdf99b27d59536e76a4f27b889e2168bc8ab3d8a3e", + "hancore.shibumi.memory": "f3f29bdc73796002bf4e59eeea7845ffe69a007f09df1d2bea1c31e1f85c9d83", + "hancore.shibumi.cpu": "f54b3afe5df6bc63bd8e943de36507ffc2a2aedaca99bdf31dacae369261c68d", + "hancore.shibumi.audio": "6e39beda5d952e7828ef727f07dd1300228c8c1715a4c32ca3571b3df889404e", + "hancore.shibumi.ai": "fc5852d74397cf6be90a9dc84941942411d0a5a5ad64e770c2d3ef0a391205b9", + "hancore.shibumi.center": "11952ccdb8379bb54fdcf7196b5b3324792a0236d8b9fd3a5c3dcab7c65cac7d", + "hancore.shibumi.media": "6b7804f6221079ef74abe6ae82eaae66bfe546e3c3127857b09399d125494277", + "hancore.shibumi.quick-access": "1cfe183baa98f3cd662b841f38188ec4a01c6937ced45790318d4c13dfeb484d", + "hancore.shibumi.network": "d885f84b76fa3219767910d49edb9adb79b20c676e05ff0a104e7dfa2d7d6e8a", + "hancore.shibumi.battery": "420bb2f196310d48d679c661605eded0489e1928d1ca81fa8801b7893a88dd25", + "hancore.shibumi.brightness": "655e1c7a89c010ff35031ce48f61c167a680b40b3e91cbd1d1d5cab344cf5fb7", + "hancore.shibumi.power-profile": "9bbd0a75a4ed39c6491bb81de0c407fefef14886626f8f25678f207a96414bca", + "hancore.shibumi.bluetooth": "b76fd398a2398a71086a4fc74bc6896386499e6b9bbca83e99e25f456a5c3953", + "hancore.shibumi.temperature": "fbffe15654561999dab251c27f88702c5273d4e323258f39820406e7d2d9eeca", + "hancore.shibumi.gpu": "6b7b40ae466dad7b127a219750675d023b2849b9e4d3337a58505b776a667e3a", + "hancore.shibumi.storage": "7ec9ba216960a12a5a65664da36d215acc0b5cc170bcc05b4930316b2fd8ac44" + }, + "pluginIds": [ + "hancore.shibumi.bar", + "hancore.shibumi.state", + "hancore.shibumi.control-center", + "hancore.shibumi.reactor", + "hancore.shibumi.telemetry", + "hancore.shibumi.power-state", + "hancore.shibumi.workspaces", + "hancore.shibumi.update-center", + "hancore.shibumi.status", + "hancore.shibumi.memory", + "hancore.shibumi.cpu", + "hancore.shibumi.audio", + "hancore.shibumi.ai", + "hancore.shibumi.center", + "hancore.shibumi.media", + "hancore.shibumi.quick-access", + "hancore.shibumi.network", + "hancore.shibumi.battery", + "hancore.shibumi.brightness", + "hancore.shibumi.power-profile", + "hancore.shibumi.bluetooth", + "hancore.shibumi.temperature", + "hancore.shibumi.gpu", + "hancore.shibumi.storage" + ], + "settingsStorageVersion": 1, + "sourceRevisions": [ + "package:0.1.1-beta.15.4" + ], + "suiteVersion": "0.1.1-beta.15.4" } ] } diff --git a/scripts/check-aur-package b/scripts/check-aur-package index 9b1276d..7d4e414 100755 --- a/scripts/check-aur-package +++ b/scripts/check-aur-package @@ -44,7 +44,7 @@ jq -e '.schemaVersion == 1 and (.states | map(.id)) == [ "public-beta.11", "step-5-tip", "public-beta.12", "public-beta.13", "source-beta.14", "public-beta.14.1", "public-beta.15", "public-beta.15.1", - "public-beta.15.2", "public-beta.15.3" + "public-beta.15.2", "public-beta.15.3", "public-beta.15.4" ]' \ contracts/lifecycle-predecessors-v1.json >/dev/null grep -Fq 'contracts/lifecycle-predecessors-v1.json' "$pkgbuild" || { diff --git a/tests/shibumi-suite-quattro-runtime.sh b/tests/shibumi-suite-quattro-runtime.sh index 8dc126d..c11a418 100755 --- a/tests/shibumi-suite-quattro-runtime.sh +++ b/tests/shibumi-suite-quattro-runtime.sh @@ -98,6 +98,7 @@ package_predecessor_root="$tmpdir/predecessor-package" package_candidate_root="$tmpdir/candidate-package" source_predecessor_root="$tmpdir/predecessor-source" source_beta15_root="$tmpdir/beta15-source" +source_beta153_root="$tmpdir/beta153-source" source_candidate_root="$tmpdir/candidate-source" source_root="" stub_bin="$tmpdir/bin" @@ -107,10 +108,12 @@ mkdir -p "$fresh_home" "$package_predecessor_root" \ package_predecessor_revision=2760cdb8272255790d5e4613fed8a48cb63c3555 # Published beta.14.1; lift to the next tag at release pin. package_candidate_revision=7a6c853b1947d303bad9a5b640c224c01b669106 -# Exercise the current installed checkout's published beta.15.2 tag. +# Retain the published beta.15.2 checkout path. source_predecessor_revision=c45af77c8333b691ac36522247b6e5b5481a3666 # Also cover the original documented update path that beta.15.2 rejected. source_beta15_revision=4e91c26ebf4da07476d4be6176f29d7662fed9c1 +# Exercise the documented beta.15.3 tag-checkout update path. +source_beta153_revision=5b1d21f0cea73bb9e7997278a828b3ef295e6c94 # Only committed payload is tested; uncommitted plugin changes are invisible to this gate. candidate_revision=$(git --no-replace-objects -C "$repo_root" rev-parse HEAD) [[ $candidate_revision =~ ^[0-9a-f]{40}$ ]] \ @@ -118,12 +121,14 @@ candidate_revision=$(git --no-replace-objects -C "$repo_root" rev-parse HEAD) [[ $package_predecessor_revision != "$package_candidate_revision" ]] \ || fail 'package predecessor and candidate revisions must differ' [[ $source_predecessor_revision != "$candidate_revision" \ - && $source_beta15_revision != "$candidate_revision" ]] \ + && $source_beta15_revision != "$candidate_revision" \ + && $source_beta153_revision != "$candidate_revision" ]] \ || fail 'source predecessors and candidate revisions must differ' for source_spec in \ "$source_predecessor_root:$source_predecessor_revision" \ "$source_beta15_root:$source_beta15_revision" \ + "$source_beta153_root:$source_beta153_revision" \ "$source_candidate_root:$candidate_revision"; do checkout_root=${source_spec%%:*} checkout_revision=${source_spec#*:} @@ -211,7 +216,7 @@ mapfile -t units < <(awk 'NF && !seen[$0]++' "$SHIBUMI_TEST_SERVICE_FILE") [[ $SHIBUMI_TEST_SERVICE_PREFIX =~ ^shibumi-runtime-[A-Za-z0-9]{6}$ ]] \ || exit 1 for unit in "${units[@]}"; do - if [[ ! $unit =~ ^${SHIBUMI_TEST_SERVICE_PREFIX}-([1-9]|1[0-9]|2[0-5])\.service$ \ + if [[ ! $unit =~ ^${SHIBUMI_TEST_SERVICE_PREFIX}-([1-9]|[12][0-9]|3[0-2])\.service$ \ && $unit != "$SHIBUMI_TEST_SERVICE_PREFIX-cleanup-probe.service" ]]; then printf 'refusing foreign fixture service: %s\n' "$unit" >&2 exit 1 @@ -280,11 +285,11 @@ mapfile -t units < <(awk 'NF && !seen[$0]++' "$SHIBUMI_TEST_SERVICE_FILE") [[ $SHIBUMI_TEST_SERVICE_PREFIX =~ ^shibumi-runtime-[A-Za-z0-9]{6}$ ]] \ || exit 1 for existing in "${units[@]}"; do - [[ $existing =~ ^${SHIBUMI_TEST_SERVICE_PREFIX}-([1-9]|1[0-9]|2[0-5])\.service$ ]] \ + [[ $existing =~ ^${SHIBUMI_TEST_SERVICE_PREFIX}-([1-9]|[12][0-9]|3[0-2])\.service$ ]] \ || exit 1 done -# package update (5) + fresh round trip (6) + two source round trips (7 each) = 25 shells. -(( ${#units[@]} < 25 )) || { +# package update (5) + fresh round trip (6) + three source round trips (7 each) = 32 shells. +(( ${#units[@]} < 32 )) || { printf 'isolated shell service generation limit exceeded\n' >&2 exit 1 } @@ -549,6 +554,7 @@ run_keep_settings_cycle() { fresh) style=shibumi ;; # V1: foreign widget remains in the Extra-Deck, without a V1 slot. source-beta15) style=full ;; source-beta152) style=notch ;; + source-beta153) style=shibumi ;; *) fail "unexpected keep-settings arm: $arm" ;; esac # Supported native and State IPC only once the fixture shell is running. @@ -703,7 +709,7 @@ run_update_arm() { || fail "$arm candidate state service did not confirm its payload digest" suite_cli status >/dev/null || fail "$arm candidate status is not clean" - # The historical package arm stays beta.13 -> beta.14.1, not the 15.3 candidate. + # The historical package arm stays beta.13 -> beta.14.1, not the current candidate. if [[ $origin == checkout ]]; then run_keep_settings_cycle "$arm" "$candidate_version" "$origin" \ "$candidate_identity" "$candidate_digest" @@ -801,8 +807,11 @@ run_update_arm "$source_beta15_root" "$source_candidate_root" checkout \ # Arm 4: beta.15.2 source checkout update run_update_arm "$source_predecessor_root" "$source_candidate_root" checkout \ source-beta152 0.1.1-beta.15.2 "$source_predecessor_revision" -[[ $(service_generation_count) -eq 25 ]] \ - || fail 'runtime arms did not use the exact 25-shell generation budget' +# Arm 5: beta.15.3 source checkout update +run_update_arm "$source_beta153_root" "$source_candidate_root" checkout \ + source-beta153 0.1.1-beta.15.3 "$source_beta153_revision" +[[ $(service_generation_count) -eq 32 ]] \ + || fail 'runtime arms did not use the exact 32-shell generation budget' if grep -Eq \ 'hancore\.shibumi[^ ]*.*(Binding loop|TypeError|ReferenceError|is not a type|failed to load)|plugin hancore\.shibumi.*failed|bar option hancore\.shibumi.*failed' \ @@ -839,7 +848,7 @@ printf 'Fresh arm preflight/verifyPayload: %s/%s deactivated=%s\n' \ "$fresh_preflight" "$fresh_reply" "$fresh_deactivated_reply" printf 'Fresh arm timing/generations: %ss/%s\n' \ "$fresh_elapsed" "$fresh_generations" -for arm in source-beta15 source-beta152; do +for arm in source-beta15 source-beta152 source-beta153; do printf '%s update arm verifyPayload: predecessor=%s candidate=%s deactivated=%s\n' \ "$arm" "${arm_predecessor_reply[$arm]}" "${arm_candidate_reply[$arm]}" \ "${arm_deactivated_reply[$arm]}" @@ -850,7 +859,7 @@ printf 'Cleanup evidence: normal=%s; deliberate-probe=%s; fixture-services=inact "$normal_cleanup_records" "$probe_cleanup_record" printf 'Package revisions: predecessor=%s candidate=%s\n' \ "$package_predecessor_revision" "$package_candidate_revision" -printf 'Source revisions: beta.15=%s beta.15.2=%s candidate=%s; total elapsed: %ss\n' \ +printf 'Source revisions: beta.15=%s beta.15.2=%s beta.15.3=%s candidate=%s; total elapsed: %ss\n' \ "$source_beta15_revision" "$source_predecessor_revision" \ - "$candidate_revision" "$total_elapsed" + "$source_beta153_revision" "$candidate_revision" "$total_elapsed" printf 'Shibumi suite Quattro runtime passed\n' diff --git a/tests/test_package_release.py b/tests/test_package_release.py index 179038e..33d77a5 100644 --- a/tests/test_package_release.py +++ b/tests/test_package_release.py @@ -201,6 +201,12 @@ def test_lifecycle_contract_pins_exact_release_identities(self) -> None: "settingsStorageVersion": 1, "payloadDigest": "60e1904b23e98317e896fdd0bbf98daf44ba504f5eb22f3a8ae86ab5ab72a342", }, + "public-beta.15.4": { + "suiteVersion": "0.1.1-beta.15.4", + "sourceRevisions": ["package:0.1.1-beta.15.4"], + "settingsStorageVersion": 1, + "payloadDigest": "977af8d8622042c2348f522016d6655a89e8e80e51e55343acc953ea7993fc5c", + }, } self.assertEqual(set(states), set(expected)) for identity_id, pinned in expected.items(): @@ -743,7 +749,7 @@ def test_quattro_cleanup_rejects_foreign_units_without_systemctl(self) -> None: "SHIBUMI_TEST_SERVICE_PREFIX": "shibumi-runtime-Ab12Cd", }) for unit in ("production-user.service", "shibumi-runtime-Ab12Cd-0.service", - "shibumi-runtime-Ab12Cd-26.service"): + "shibumi-runtime-Ab12Cd-33.service"): with self.subTest(unit=unit): service_file.write_text(unit + "\n") result = subprocess.run( @@ -753,14 +759,14 @@ def test_quattro_cleanup_rejects_foreign_units_without_systemctl(self) -> None: self.assertEqual(result.returncode, 1) self.assertIn("refusing foreign fixture service", result.stderr) self.assertFalse(systemctl_log.exists()) - # All 25 owned identities pass the start allowlist; a 26th generation + # All 32 owned identities pass the start allowlist; a 33rd generation # must be refused before any launch or service-file append. start_script = runtime.split( "cat >\"$start_shell\" <<'START_SHELL'\n", 1 )[1].split("\nSTART_SHELL", 1)[0] script.write_text(start_script) full_budget = "".join(f"shibumi-runtime-Ab12Cd-{i}.service\n" - for i in range(1, 26)) + for i in range(1, 33)) service_file.write_text(full_budget) result = subprocess.run( [str(script)], text=True, capture_output=True, From 03405781d40cdb2da4cb2cdcdeac601509a4e3ed Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:43:41 +0200 Subject: [PATCH 31/40] build: remove the Local AI attribution from four panels --- contracts/lifecycle-predecessors-v1.json | 10 +++++----- hancore.shibumi.ai/AiUsagePanel.qml | 1 - hancore.shibumi.ai/Local-AI-LICENSE | 21 --------------------- hancore.shibumi.cpu/CpuPanel.qml | 1 - hancore.shibumi.cpu/Local-AI-LICENSE | 21 --------------------- hancore.shibumi.media/Local-AI-LICENSE | 21 --------------------- hancore.shibumi.media/MediaPanel.qml | 1 - hancore.shibumi.memory/Local-AI-LICENSE | 21 --------------------- hancore.shibumi.memory/MemoryPanel.qml | 1 - tests/test_package_release.py | 2 +- 10 files changed, 6 insertions(+), 94 deletions(-) delete mode 100644 hancore.shibumi.ai/Local-AI-LICENSE delete mode 100644 hancore.shibumi.cpu/Local-AI-LICENSE delete mode 100644 hancore.shibumi.media/Local-AI-LICENSE delete mode 100644 hancore.shibumi.memory/Local-AI-LICENSE diff --git a/contracts/lifecycle-predecessors-v1.json b/contracts/lifecycle-predecessors-v1.json index b29680e..6391d31 100644 --- a/contracts/lifecycle-predecessors-v1.json +++ b/contracts/lifecycle-predecessors-v1.json @@ -632,7 +632,7 @@ }, { "id": "public-beta.15.4", - "payloadDigest": "977af8d8622042c2348f522016d6655a89e8e80e51e55343acc953ea7993fc5c", + "payloadDigest": "db6342698c7ba3e019858ee61431a8de84e0226b19c7cca6c9326cb6f199920c", "pluginDigests": { "hancore.shibumi.bar": "fd1d69f365d08ad06dd52e0cb19892ac935109752f6be4b80e2a20adaf3e7fb9", "hancore.shibumi.state": "8bdbe450c842f7bc6b6483edb652389370201babdef52ae792a9700b51269af4", @@ -643,12 +643,12 @@ "hancore.shibumi.workspaces": "9c19d443cca3e15c3977296fd1dea6c85aa27ba13d2c36991524fdb115dee629", "hancore.shibumi.update-center": "2382455e00ded224236931fe9ca892bbe7f913fbb8f020b56511d3fb736e700f", "hancore.shibumi.status": "1716e7ceaff65c45a78c45cdf99b27d59536e76a4f27b889e2168bc8ab3d8a3e", - "hancore.shibumi.memory": "f3f29bdc73796002bf4e59eeea7845ffe69a007f09df1d2bea1c31e1f85c9d83", - "hancore.shibumi.cpu": "f54b3afe5df6bc63bd8e943de36507ffc2a2aedaca99bdf31dacae369261c68d", + "hancore.shibumi.memory": "6680fd24261f39cff46f2302125040e478b78285370da3ad8b9efbb28aca6820", + "hancore.shibumi.cpu": "2028db7ef14411d905034959c2ae1d7d9d218cda468a09e8ef87b6137896aad9", "hancore.shibumi.audio": "6e39beda5d952e7828ef727f07dd1300228c8c1715a4c32ca3571b3df889404e", - "hancore.shibumi.ai": "fc5852d74397cf6be90a9dc84941942411d0a5a5ad64e770c2d3ef0a391205b9", + "hancore.shibumi.ai": "8c8066abf504759bb381ce4a6e7bf6986109e2b0fe6c7e704ad88f5fe16c2738", "hancore.shibumi.center": "11952ccdb8379bb54fdcf7196b5b3324792a0236d8b9fd3a5c3dcab7c65cac7d", - "hancore.shibumi.media": "6b7804f6221079ef74abe6ae82eaae66bfe546e3c3127857b09399d125494277", + "hancore.shibumi.media": "7423c21d1e248c87e46bfa050427047fae2a69875447ebd1a62ec892d857bd08", "hancore.shibumi.quick-access": "1cfe183baa98f3cd662b841f38188ec4a01c6937ced45790318d4c13dfeb484d", "hancore.shibumi.network": "d885f84b76fa3219767910d49edb9adb79b20c676e05ff0a104e7dfa2d7d6e8a", "hancore.shibumi.battery": "420bb2f196310d48d679c661605eded0489e1928d1ca81fa8801b7893a88dd25", diff --git a/hancore.shibumi.ai/AiUsagePanel.qml b/hancore.shibumi.ai/AiUsagePanel.qml index cad5b5c..cb1c496 100644 --- a/hancore.shibumi.ai/AiUsagePanel.qml +++ b/hancore.shibumi.ai/AiUsagePanel.qml @@ -7,7 +7,6 @@ import "../hancore.shibumi.state/lib/presentation" as Presentation // Layout derived from MIT-licensed Omarchy v4.0.4 plugins/agents/Panel.qml. // See Omarchy-LICENSE. Shibumi retains its own data, selection and lifecycle. -// Chart/metric structure adapted from 0xSero's Omarchy Local AI plugin (MIT); see Local-AI-LICENSE. ShibumiPanel { id: panel diff --git a/hancore.shibumi.ai/Local-AI-LICENSE b/hancore.shibumi.ai/Local-AI-LICENSE deleted file mode 100644 index 2ddb476..0000000 --- a/hancore.shibumi.ai/Local-AI-LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 0xSero - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/hancore.shibumi.cpu/CpuPanel.qml b/hancore.shibumi.cpu/CpuPanel.qml index 2553710..582ed2f 100644 --- a/hancore.shibumi.cpu/CpuPanel.qml +++ b/hancore.shibumi.cpu/CpuPanel.qml @@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound import QtQuick import qs.Commons as Commons import qs.Ui as Ui -// Compact chart/metric layout inspired by 0xSero's Omarchy Local AI plugin (MIT); see Local-AI-LICENSE. ShibumiPanel { id: panel diff --git a/hancore.shibumi.cpu/Local-AI-LICENSE b/hancore.shibumi.cpu/Local-AI-LICENSE deleted file mode 100644 index 2ddb476..0000000 --- a/hancore.shibumi.cpu/Local-AI-LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 0xSero - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/hancore.shibumi.media/Local-AI-LICENSE b/hancore.shibumi.media/Local-AI-LICENSE deleted file mode 100644 index 2ddb476..0000000 --- a/hancore.shibumi.media/Local-AI-LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 0xSero - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/hancore.shibumi.media/MediaPanel.qml b/hancore.shibumi.media/MediaPanel.qml index 142d533..f80ada3 100644 --- a/hancore.shibumi.media/MediaPanel.qml +++ b/hancore.shibumi.media/MediaPanel.qml @@ -676,7 +676,6 @@ ShibumiPanel { } } - // Outline button structure inspired by 0xSero's Omarchy Local AI plugin (MIT); see Local-AI-LICENSE. component MediaPanelButton: Rectangle { id: button required property string icon diff --git a/hancore.shibumi.memory/Local-AI-LICENSE b/hancore.shibumi.memory/Local-AI-LICENSE deleted file mode 100644 index 2ddb476..0000000 --- a/hancore.shibumi.memory/Local-AI-LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 0xSero - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/hancore.shibumi.memory/MemoryPanel.qml b/hancore.shibumi.memory/MemoryPanel.qml index 7f9dd96..fe04d75 100644 --- a/hancore.shibumi.memory/MemoryPanel.qml +++ b/hancore.shibumi.memory/MemoryPanel.qml @@ -4,7 +4,6 @@ import QtQuick import qs.Commons as Commons import qs.Ui as Ui -// Compact metric layout inspired by 0xSero's Omarchy Local AI plugin (MIT); see Local-AI-LICENSE. ShibumiPanel { id: panel diff --git a/tests/test_package_release.py b/tests/test_package_release.py index 33d77a5..a3d3379 100644 --- a/tests/test_package_release.py +++ b/tests/test_package_release.py @@ -205,7 +205,7 @@ def test_lifecycle_contract_pins_exact_release_identities(self) -> None: "suiteVersion": "0.1.1-beta.15.4", "sourceRevisions": ["package:0.1.1-beta.15.4"], "settingsStorageVersion": 1, - "payloadDigest": "977af8d8622042c2348f522016d6655a89e8e80e51e55343acc953ea7993fc5c", + "payloadDigest": "db6342698c7ba3e019858ee61431a8de84e0226b19c7cca6c9326cb6f199920c", }, } self.assertEqual(set(states), set(expected)) From f4469aea864fd9afab4a58199ffe4416f695ffce Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:43:41 +0200 Subject: [PATCH 32/40] fix: keep fresh AI usage visible after a failed provider refresh --- contracts/lifecycle-predecessors-v1.json | 4 +- hancore.shibumi.ai/Service.qml | 19 ++++---- tests/ai-plugin-smoke.qml | 57 ++++++++++++++++++------ tests/test_package_release.py | 2 +- 4 files changed, 55 insertions(+), 27 deletions(-) diff --git a/contracts/lifecycle-predecessors-v1.json b/contracts/lifecycle-predecessors-v1.json index 6391d31..359e003 100644 --- a/contracts/lifecycle-predecessors-v1.json +++ b/contracts/lifecycle-predecessors-v1.json @@ -632,7 +632,7 @@ }, { "id": "public-beta.15.4", - "payloadDigest": "db6342698c7ba3e019858ee61431a8de84e0226b19c7cca6c9326cb6f199920c", + "payloadDigest": "7161850120a5a27feb62ffc3651c940f8f2e77f250975d940e4d3f0ededd9cd7", "pluginDigests": { "hancore.shibumi.bar": "fd1d69f365d08ad06dd52e0cb19892ac935109752f6be4b80e2a20adaf3e7fb9", "hancore.shibumi.state": "8bdbe450c842f7bc6b6483edb652389370201babdef52ae792a9700b51269af4", @@ -646,7 +646,7 @@ "hancore.shibumi.memory": "6680fd24261f39cff46f2302125040e478b78285370da3ad8b9efbb28aca6820", "hancore.shibumi.cpu": "2028db7ef14411d905034959c2ae1d7d9d218cda468a09e8ef87b6137896aad9", "hancore.shibumi.audio": "6e39beda5d952e7828ef727f07dd1300228c8c1715a4c32ca3571b3df889404e", - "hancore.shibumi.ai": "8c8066abf504759bb381ce4a6e7bf6986109e2b0fe6c7e704ad88f5fe16c2738", + "hancore.shibumi.ai": "197d9eb2956dabc7baec4975b383bc114c1eca12c1267abd4804157906c77a71", "hancore.shibumi.center": "11952ccdb8379bb54fdcf7196b5b3324792a0236d8b9fd3a5c3dcab7c65cac7d", "hancore.shibumi.media": "7423c21d1e248c87e46bfa050427047fae2a69875447ebd1a62ec892d857bd08", "hancore.shibumi.quick-access": "1cfe183baa98f3cd662b841f38188ec4a01c6937ced45790318d4c13dfeb484d", diff --git a/hancore.shibumi.ai/Service.qml b/hancore.shibumi.ai/Service.qml index 1cded17..9dc4123 100644 --- a/hancore.shibumi.ai/Service.qml +++ b/hancore.shibumi.ai/Service.qml @@ -55,7 +55,6 @@ Item { expiries.push(Number(agentsCodexRecord.expiresAtMs)) return expiries.length > 0 ? Math.min.apply(null, expiries) : 0 } - property bool agentsUpdateHealthy: false property int providerRevision: 0 property string backendProcessKind: "" property string backendOutput: "" @@ -310,15 +309,16 @@ Item { } function providerSnapshot(provider, nowMs) { - if (!provider || !agentRecordFresh(provider, nowMs)) return null + const currentMs = isFinite(Number(nowMs)) ? Number(nowMs) : Date.now() + if (!provider || !agentRecordFresh(provider, currentMs)) return null const id = String(provider.providerId || "") if (!id || !providerEnabled(id)) return null return { providerId: id, providerName: String(provider.providerName || id).slice(0, 512), - ready: (provider.ready !== undefined ? provider.ready === true : true) - && (String(provider.backend || "") !== "omarchy.agents" - || agentsUpdateHealthy), + ready: provider.ready !== undefined ? provider.ready === true : true, + fresh: String(provider.backend || "") !== "omarchy.agents" + || currentMs - Number(provider.updatedAtMs) <= 2 * agentsRefreshInterval, rateLimitPercent: Number(provider.rateLimitPercent) >= 0 ? Number(provider.rateLimitPercent) : -1, rateLimitLabel: String(provider.rateLimitLabel || "").slice(0, 512), @@ -445,7 +445,7 @@ Item { } function providerStatusText(provider) { - if (!provider || provider.ready === false) return "stale" + if (!provider || provider.ready === false || provider.fresh === false) return "stale" if (String(provider.providerId || "") === "codex" && Number(provider.rateLimitPercent) < 0 && Number(provider.secondaryRateLimitPercent) < 0) @@ -548,7 +548,8 @@ Item { agentsCodexRecord = null changed = true } - if (changed) providerRevision++ + // Recheck status by age even before the record's admission expires. + if (changed || agentsClaudeRecord || agentsCodexRecord) providerRevision++ return changed } @@ -626,7 +627,6 @@ Item { function reconcileBackendLifecycle(force) { providerSettingsGeneration++ - agentsUpdateHealthy = false pendingBackendRefresh = false pendingBackendForce = false if (!runtimeProbesEnabled || !serviceActive || !anyProviderEnabled) { @@ -660,9 +660,6 @@ Item { const error = backendError backendProcessKind = "" if (completedKind === "agents-update") { - agentsUpdateHealthy = Number(exitCode) === 0 - && runningSettingsGeneration === providerSettingsGeneration - && !pendingBackendRefresh if (Number(exitCode) === 66) { agentsBackendUnavailable = true } else { diff --git a/tests/ai-plugin-smoke.qml b/tests/ai-plugin-smoke.qml index 240f5a8..7beb9fb 100644 --- a/tests/ai-plugin-smoke.qml +++ b/tests/ai-plugin-smoke.qml @@ -255,6 +255,47 @@ ShellRoot { return true } + function failedRefreshKeepsFreshUsage() { + if (agentsService.backendRunning || agentsService.backendProcessKind !== "") + return false + const raw = JSON.parse(codexStatusRecord(true, true)) + raw.id = "claude"; raw.name = "Claude Code" + agentsService.applyAgentRecord("claude", JSON.stringify(raw)) + // Deliver the same nonzero completion as a timed-out update, without + // waiting 120 seconds or invoking a real account collector. + agentsService.backendProcessKind = "agents-update" + agentsService.backendError = "" + agentsService.finishBackendProcess(1) + const fresh = agentsService.providerFor("claude") + const checks = { freshAfterFailure: fresh && fresh.ready + && agentsService.usagePercent(fresh) === 24 + && agentsService.providerStatusText(fresh) === "live" + && emptyAgentsPanel.providerStatusLabel === "live" + && agentsService.tooltipText() === "Claude Code · live\nWeekly: 24%" } + const record = agentsService.agentsClaudeRecord + const threshold = 2 * agentsService.agentsRefreshInterval + const boundary = agentsService.providerSnapshot(record, + record.updatedAtMs + threshold) + checks.atThreshold = boundary && boundary.ready + && agentsService.usagePercent(boundary) === 24 + && agentsService.providerStatusText(boundary) === "live" + // Age the same record in place: only the existing expiry/revision path + // may invalidate the already cached provider snapshot. + record.updatedAtMs = Date.now() - threshold - 1 + record.updatedAt = new Date(record.updatedAtMs).toISOString() + record.expiresAtMs = record.updatedAtMs + 24 * 60 * 60 * 1000 + const revision = agentsService.providerRevision + agentsService.expireAgentRecords(Date.now()) + const aged = agentsService.providerFor("claude") + checks.staleByAge = agentsService.providerRevision > revision + && aged && aged.ready && agentsService.usagePercent(aged) === 24 + && agentsService.providerStatusText(aged) === "stale" + && emptyAgentsPanel.providerStatusLabel === "stale" + && agentsService.tooltipText() === "Claude Code · stale\nWeekly: 24%" + console.log("268_AI_REFRESH_AGE", JSON.stringify(checks)) + return Object.values(checks).every(value => value) + } + function readyRecordWithoutCurrentDataAccepted() { const recordNow = Date.now() const currentUpdatedAt = new Date(recordNow).toISOString() @@ -1098,8 +1139,7 @@ ShellRoot { return root.fail("queued backend transition did not complete") return } - if (!agentsService.agentsUpdateHealthy - || agentsService.runningSettingsGeneration + if (agentsService.runningSettingsGeneration !== agentsService.providerSettingsGeneration || agentsService.providerFor("claude") === null || agentsService.providerFor("codex") === null) @@ -1152,20 +1192,11 @@ ShellRoot { return root.fail("loaded agent record expiry timer") return } + if (!root.failedRefreshKeepsFreshUsage()) + return root.fail("failed refresh or stale status hid valid Claude usage") agentsService.agentsClaudeRecord = root.expiryProbeRecord agentsService.providerRevision++ root.waitingExpiryProbe = false - agentsService.agentsUpdateHealthy = false - agentsService.providerRevision++ - if (agentsService.providerFor("claude").ready - || agentsService.providerFor("codex").ready - || agentsService.providerStatusText( - agentsService.providerFor("codex")) !== "stale" - || agentsService.usagePercent( - agentsService.providerFor("claude")) !== -1 - || agentsService.usagePercent( - agentsService.providerFor("codex")) !== -1) - return root.fail("failed agents update did not suppress live quota") const backendGeneration = agentsService.providerSettingsGeneration agentsService.modelUsageSourceOverride = Quickshell.env("SHIBUMI_TEST_MODEL_USAGE_SOURCE") diff --git a/tests/test_package_release.py b/tests/test_package_release.py index a3d3379..4d1c9df 100644 --- a/tests/test_package_release.py +++ b/tests/test_package_release.py @@ -205,7 +205,7 @@ def test_lifecycle_contract_pins_exact_release_identities(self) -> None: "suiteVersion": "0.1.1-beta.15.4", "sourceRevisions": ["package:0.1.1-beta.15.4"], "settingsStorageVersion": 1, - "payloadDigest": "db6342698c7ba3e019858ee61431a8de84e0226b19c7cca6c9326cb6f199920c", + "payloadDigest": "7161850120a5a27feb62ffc3651c940f8f2e77f250975d940e4d3f0ededd9cd7", }, } self.assertEqual(set(states), set(expected)) From ca7148ed28d08354b1b0d2c30890adbb80f9afed Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:43:41 +0200 Subject: [PATCH 33/40] docs: add the beta.15.4 release notes and changelog --- .github/release-notes/v0.1.1-beta.15.4.md | 57 +++++++++++++++++++++++ CHANGELOG.md | 49 ++++++++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 .github/release-notes/v0.1.1-beta.15.4.md diff --git a/.github/release-notes/v0.1.1-beta.15.4.md b/.github/release-notes/v0.1.1-beta.15.4.md new file mode 100644 index 0000000..25e9790 --- /dev/null +++ b/.github/release-notes/v0.1.1-beta.15.4.md @@ -0,0 +1,57 @@ +# 0.1.1-beta.15.4 + +Beta.15.4 refreshes Shibumi's AI, CPU, Memory, and media panels and improves +notification history, picker text, and the Control Center. + +## Highlights + +- The compact AI panel shows a seven-day token chart, reported limit windows + with reset countdowns and pace, and the plan name when available. Text uses + consistent sizes for labels, content, and values. + The AI widget keeps its value when a provider refresh times out. +- The CPU panel now follows the Memory panel's layout, with a water-filled + load ring, CPU model, and load averages. A GPU row appears after reported + activity; the title follows its visibility as "CPU" or "CPU · GPU". +- The Memory panel combines a water-filled load ring with GiB values and, + when available, RAM type and speed in MT/s. +- The media panel has five rounded buttons in one row: Shuffle, Previous, + Play/Pause, Next, and Repeat. Shuffle, Repeat, and seeking are available + where the player supports them. +- Notifications are counted without opening the panel, grouped by app, and + shown with relative times. Clicking a Recent entry focuses an existing + window of its app. +- Hearthstone picker card titles stay sharp while the cards are transformed. +- The compact Control Center no longer clips the Health page. +- Buttons in the redesigned panels follow the panel's corner radius. + +## Compatibility + +Beta.15.4 targets the repository's pinned Omarchy 4.0.4 package and source +baselines with Quickshell 0.3.1. It does not update Omarchy. + +Update a Beta.15 through Beta.15.3 source installation from its existing checkout: + +```bash +git fetch --tags +git checkout v0.1.1-beta.15.4 +./scripts/shibumi-suite update --yes +``` + +## Notes + +- On the pinned Omarchy 4.0.x hosts, the plugin API does not expose Live + popups. Shibumi therefore shows Recent history and omits the Live section + from the tooltip. +- Source checkouts cloned with a restrictive `umask` such as 077 now keep a + stable payload identity. If an earlier release was installed from such a + checkout and `update` is refused, uninstall with the release you had + installed while keeping your settings, then install Beta.15.4: + + ```bash + git checkout v0.1.1-beta.15.2 # the tag you had installed + ./scripts/shibumi-suite uninstall --keep-settings --yes + git checkout v0.1.1-beta.15.4 + ./scripts/shibumi-suite install --yes + ``` +- The notes of 0.1.1-beta.15 remain valid. +- AUR publication remains deferred. diff --git a/CHANGELOG.md b/CHANGELOG.md index 563fabb..c51cc4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,53 @@ # Changelog -## [0.1.1-beta.15.3] - Unreleased +## [0.1.1-beta.15.4] - Unreleased + +### Added + +- Show reported AI limit windows with reset countdowns and pace, a seven-day + token chart, and the provider's plan name when available. +- Show the CPU model and load averages when the CPU panel opens, and optional + RAM type and speed in MT/s when the Memory panel opens. +- Add player-supported Shuffle, Repeat, and seek controls to the media panel. +- Count notifications without opening the panel, group them by app, show + relative times, and focus an existing app window from a Recent entry. +- Admit the exact Beta.15.3 tag and merge checkout revisions as update + predecessors while preserving strict payload and lifecycle checks. + +### Changed + +- Use consistent text roles in the compact AI panel. +- Arrange CPU and Memory metrics around water-filled load rings. Remove the + CPU panel history chart while retaining the bar's history. +- Show the GPU row only after reported activity and retain it while idle; + use "CPU · GPU" as the title only while the row is visible. +- Arrange the five media buttons in one row and make buttons in the + redesigned panels follow the panel radius. +- Omit the notification tooltip's Live section when the host exposes no + Live model; keep Recent counts and Do Not Disturb status. + +### Fixed + +- Keep the AI usage value visible when a slow provider refresh exceeds its + time limit, and mark usage older than two refresh intervals as stale in + the panel. +- Bind Live notification actions to the exact current row and reject stale + clicks after replacement or reordering. +- Keep Hearthstone picker card titles sharp during transforms. +- Keep the Health page visible in the compact Control Center. + +### Validation + +- Pin the final 24-plugin Beta.15.4 payload and package identity without + inventing future tag or merge revisions. +- Add a Beta.15.3 tag-checkout update arm to the isolated Quattro runtime + fixture, retaining the existing package, fresh-install, Beta.15, and + Beta.15.2 arms and their lifecycle checks. +- Cover the changed panels, lazy telemetry reads, notification counts and + actions with scoped regressions. Keep restrictive-umask fixtures independent + of the caller's umask. + +## [0.1.1-beta.15.3] - 2026-09-26 ### Fixed From 93df25c1d9abf147211b72e4185339c4865dff7c Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:36:13 +0200 Subject: [PATCH 34/40] test: allow the approved MPRIS control exception in the media owner guard --- tests/contract-regression.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/contract-regression.sh b/tests/contract-regression.sh index 50d295e..3294958 100755 --- a/tests/contract-regression.sh +++ b/tests/contract-regression.sh @@ -672,10 +672,16 @@ rg -q 'hancore\.shibumi\.media' contracts/plugin-suite-v1.json \ || fail "Shibumi media presentation is not registered" rg -q 'firstPartyServiceFor\("omarchy\.media"\)' hancore.shibumi.media/BarWidget.qml \ || fail "media presentation does not reuse the official service" +# Approved panel-only exception: native shuffle, repeat and seek (ARCHITECTURE.md, G9). if rg -q 'Quickshell\.Services\.(Mpris|Pipewire)|Mpris\.|Pipewire\.' \ - hancore.shibumi.media/BarWidget.qml hancore.shibumi.media/MediaPanel.qml hancore.shibumi.media/MediaPulse.qml \ + hancore.shibumi.media/BarWidget.qml hancore.shibumi.media/MediaPulse.qml \ hancore.shibumi.media/MediaSpectrum.qml hancore.shibumi.media/MediaMuse.qml \ - hancore.shibumi.media/Service.qml; then + hancore.shibumi.media/Service.qml \ + || rg -q 'Quickshell\.Services\.Pipewire|Mpris\.|Pipewire\.' \ + hancore.shibumi.media/MediaPanel.qml \ + || ! awk '/Quickshell[.]Services[.]Mpris/ { + if ($0 != "import Quickshell.Services.Mpris" || ++imports > 1) exit 1 + }' hancore.shibumi.media/MediaPanel.qml; then fail "Shibumi media presentation must not create a second media owner" fi if rg -q 'Process \{|Timer \{|FileView \{' hancore.shibumi.media/BarWidget.qml \ From 595fc6f4d044346bc84c94c1c176d7d2ea28aca8 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 22:53:06 +0200 Subject: [PATCH 35/40] test: align contract guards with the approved beta.15.4 panels --- tests/contract-regression.sh | 6 +++++- tests/panel-window-geometry-regression.sh | 2 +- tests/style-contract-regression.sh | 10 ++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/contract-regression.sh b/tests/contract-regression.sh index 3294958..d581d3b 100755 --- a/tests/contract-regression.sh +++ b/tests/contract-regression.sh @@ -1080,8 +1080,12 @@ if rg -q 'CACHE_FILE|stale_last' \ hancore.shibumi.ai/scripts/opencode-usage; then fail "OpenCode provider must not persist a V1 usage cache" fi -if rg -q 'Process \{|Timer \{|FileView \{' \ +# Approved reset-countdown display: one open-only clock, with no provider work. +if rg -Uq '\b(Process|Timer|FileView)[[:space:]]*\{' \ hancore.shibumi.ai/BarWidget.qml \ + || rg -Uq '\b(Process|FileView)[[:space:]]*\{' hancore.shibumi.ai/AiUsagePanel.qml \ + || [[ $(rg -Uo '\bTimer[[:space:]]*\{' hancore.shibumi.ai/AiUsagePanel.qml | wc -l) -ne 1 ]] \ + || ! rg -Uq '^ property Timer presentationClock: Timer \{\n id: panelClock\n interval: 30000; repeat: true; running: panel\.open\n onTriggered: panel\.nowMs = Date\.now\(\)\n \}$' \ hancore.shibumi.ai/AiUsagePanel.qml; then fail "AI views must not own provider polling or file watchers" fi diff --git a/tests/panel-window-geometry-regression.sh b/tests/panel-window-geometry-regression.sh index 6f20b97..8832688 100755 --- a/tests/panel-window-geometry-regression.sh +++ b/tests/panel-window-geometry-regression.sh @@ -30,7 +30,7 @@ from pathlib import Path repo = Path(sys.argv[1]) expected = { - "hancore.shibumi.ai/AiUsagePanel.qml": "215bc466aff11b593c9731a13e90316b1702aeed19d696654e8f0f555a12f946", + "hancore.shibumi.ai/AiUsagePanel.qml": "adb7e154226a79de1aea92070517c775d2f51144c3c6fef0a38f750c249a7284", "hancore.shibumi.audio/AudioPanel.qml": "fedbe78ccc91b1e86764eaa5234f64b1f2519555b47cc6043ded86e6127d7d4e", "hancore.shibumi.battery/BatteryPanel.qml": "46f3fad436b76672cb1344822f40f6441a95d166aee66ba4ee1b8877d7daf6f9", "hancore.shibumi.bluetooth/BluetoothPanel.qml": "44a5f3dd0346cf31ec470f783a46b97fd7dffbc4aa2e97cc6f8fabee533988fa", diff --git a/tests/style-contract-regression.sh b/tests/style-contract-regression.sh index c20febf..140f455 100755 --- a/tests/style-contract-regression.sh +++ b/tests/style-contract-regression.sh @@ -613,8 +613,14 @@ for radius_owner in \ hancore.shibumi.status/TrayDrawerPanel.qml \ hancore.shibumi.update-center/UpdateCenterPanel.qml \ hancore.shibumi.workspaces/WorkspacePanelContent.qml; do - rg -q 'radius: (panel|controller|root\.controller)\.controlRadius' \ - "$radius_owner" \ + # Approved beta.15.4 panel buttons follow their rendered panel radius. + case "$radius_owner" in + hancore.shibumi.ai/AiUsagePanel.qml|hancore.shibumi.cpu/CpuPanel.qml|\ + hancore.shibumi.media/MediaPanel.qml|hancore.shibumi.memory/MemoryPanel.qml) + radius_pattern='radius: panel\.renderedSurfaceRadius' ;; + *) radius_pattern='radius: (panel|controller|root\.controller)\.controlRadius' ;; + esac + rg -q "$radius_pattern" "$radius_owner" \ || fail "$radius_owner bypasses the dynamic Shibumi control radius" done From 97007a5275ec91e7bb87dad7442f49003d5d04f8 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Sun, 27 Sep 2026 23:58:48 +0200 Subject: [PATCH 36/40] build: update beta.15.4 release archive checksum --- packaging/aur/.SRCINFO | 2 +- packaging/aur/PKGBUILD | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/aur/.SRCINFO b/packaging/aur/.SRCINFO index 61e6f04..31a4bfc 100644 --- a/packaging/aur/.SRCINFO +++ b/packaging/aur/.SRCINFO @@ -31,6 +31,6 @@ pkgbase = shibumi-shell optdepends = nvidia-utils: NVIDIA GPU telemetry optdepends = git: source-checkout diagnostics and migration source = shibumi-shell-0.1.1-beta.15.4.tar.gz::https://github.com/HANCORE-linux/Shibumi-Shell/releases/download/v0.1.1-beta.15.4/shibumi-shell-0.1.1-beta.15.4.tar.gz - sha256sums = c92e40a26557740ac0263e56604ce693585005ce3646224a1885f31b185ab5b6 + sha256sums = eadeb33daf84f29d9b0e1a3c31ceafbad932a9bb47d0a80d001b4c77e691321d pkgname = shibumi-shell diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index 07960eb..b6c961b 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: HANCORE Linux pkgname=shibumi-shell _upstream_version=0.1.1-beta.15.4 -_source_sha256=c92e40a26557740ac0263e56604ce693585005ce3646224a1885f31b185ab5b6 +_source_sha256=eadeb33daf84f29d9b0e1a3c31ceafbad932a9bb47d0a80d001b4c77e691321d pkgver=${_upstream_version//-/} pkgrel=1 pkgdesc="Transactional Shibumi desktop shell plugins for Omarchy Quattro" From 8bb32937ddc0cf1d6d58b4c9d00610e3f3548484 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Mon, 28 Sep 2026 00:21:41 +0200 Subject: [PATCH 37/40] fix: format AI token counts compactly like Omarchy --- contracts/lifecycle-predecessors-v1.json | 4 +-- hancore.shibumi.ai/Service.qml | 3 +- hancore.shibumi.ai/scripts/opencode-usage | 2 ++ tests/ai-plugin-smoke.qml | 37 ++++++++++++++++++++--- tests/opencode-usage-regression.py | 4 +-- tests/test_package_release.py | 2 +- 6 files changed, 42 insertions(+), 10 deletions(-) diff --git a/contracts/lifecycle-predecessors-v1.json b/contracts/lifecycle-predecessors-v1.json index 359e003..b4e4a79 100644 --- a/contracts/lifecycle-predecessors-v1.json +++ b/contracts/lifecycle-predecessors-v1.json @@ -632,7 +632,7 @@ }, { "id": "public-beta.15.4", - "payloadDigest": "7161850120a5a27feb62ffc3651c940f8f2e77f250975d940e4d3f0ededd9cd7", + "payloadDigest": "f838f53ddba2219c3494a0ac5055dbd7760792aa8c653e73c5999f078f89cd73", "pluginDigests": { "hancore.shibumi.bar": "fd1d69f365d08ad06dd52e0cb19892ac935109752f6be4b80e2a20adaf3e7fb9", "hancore.shibumi.state": "8bdbe450c842f7bc6b6483edb652389370201babdef52ae792a9700b51269af4", @@ -646,7 +646,7 @@ "hancore.shibumi.memory": "6680fd24261f39cff46f2302125040e478b78285370da3ad8b9efbb28aca6820", "hancore.shibumi.cpu": "2028db7ef14411d905034959c2ae1d7d9d218cda468a09e8ef87b6137896aad9", "hancore.shibumi.audio": "6e39beda5d952e7828ef727f07dd1300228c8c1715a4c32ca3571b3df889404e", - "hancore.shibumi.ai": "197d9eb2956dabc7baec4975b383bc114c1eca12c1267abd4804157906c77a71", + "hancore.shibumi.ai": "8abd9feb2cc5b9fee5e140ca3c79f4cc9b273ceec1f4ccebc4f6863be7686e68", "hancore.shibumi.center": "11952ccdb8379bb54fdcf7196b5b3324792a0236d8b9fd3a5c3dcab7c65cac7d", "hancore.shibumi.media": "7423c21d1e248c87e46bfa050427047fae2a69875447ebd1a62ec892d857bd08", "hancore.shibumi.quick-access": "1cfe183baa98f3cd662b841f38188ec4a01c6937ced45790318d4c13dfeb484d", diff --git a/hancore.shibumi.ai/Service.qml b/hancore.shibumi.ai/Service.qml index 9dc4123..3bf7a05 100644 --- a/hancore.shibumi.ai/Service.qml +++ b/hancore.shibumi.ai/Service.qml @@ -508,7 +508,8 @@ Item { function formatTokens(value) { const count = Math.max(0, Number(value) || 0) - if (count >= 1000000) return (count / 1000000).toFixed(2) + "M" + if (count >= 1000000000) return (count / 1000000000).toFixed(1) + "B" + if (count >= 1000000) return (count / 1000000).toFixed(1) + "M" if (count >= 1000) return (count / 1000).toFixed(1) + "K" return String(Math.round(count)) } diff --git a/hancore.shibumi.ai/scripts/opencode-usage b/hancore.shibumi.ai/scripts/opencode-usage index 8212787..badebf8 100755 --- a/hancore.shibumi.ai/scripts/opencode-usage +++ b/hancore.shibumi.ai/scripts/opencode-usage @@ -138,6 +138,8 @@ def newest_model(messages: list[dict], today_ms: int, tomorrow_ms: int) -> str: def fmt_tokens(value: int) -> str: + if value >= 1_000_000_000: + return f"{value / 1_000_000_000:.1f}B" if value >= 1_000_000: return f"{value / 1_000_000:.1f}M" if value >= 1_000: diff --git a/tests/ai-plugin-smoke.qml b/tests/ai-plugin-smoke.qml index 7beb9fb..55d5d18 100644 --- a/tests/ai-plugin-smoke.qml +++ b/tests/ai-plugin-smoke.qml @@ -79,7 +79,7 @@ ShellRoot { && emptyAgentsPanel.weekDays[6].messageCount === null record.recentDays = days; agentsService.applyAgentRecord("claude", JSON.stringify(record)) emptyAgentsPanel.nowMs = now - checks.codex = typeof agentsService.limitWindows === "function" && agentsService.limitWindows(agentsService.providerFor("codex")).length === 1 && agentsService.providerFor("codex").models[0].totalLabel === "1.03M" + checks.codex = typeof agentsService.limitWindows === "function" && agentsService.limitWindows(agentsService.providerFor("codex")).length === 1 && agentsService.providerFor("codex").models[0].totalLabel === "1.0M" record.limits[2].percent = 0.899; agentsService.applyAgentRecord("claude", JSON.stringify(record)) checks.threshold = emptyAgentsPanel.headlineAlarm === false && String(emptyAgentsPanel.headlineColor) !== String(alarmColor) record.limits = record.limits.concat(Array.from({length: 4}, () => record.limits[0])); agentsService.applyAgentRecord("claude", JSON.stringify(record)) @@ -105,6 +105,37 @@ ShellRoot { return Array.from(item.children || []).reduce((items, child) => items.concat([child], descendants(child)), []) } + function compactDailyTokenLabels() { + const record = JSON.parse(codexStatusRecord(true, true)) + record.id = "claude"; record.name = "Claude Code" + emptyAgentsPanel.aiService = agentsService + emptyAgentsPanel.contentWidth = 320 + emptyAgentsPanel.nowMs = Date.now() + const values = [1054540000, 1002400000, 1295660000, 1765390000, + 2054540000, 3002400000, 4007230000] + record.recentDays = values.map((value, i) => ({ + date: emptyAgentsPanel.dayKey(i - 6), messageCount: value })) + agentsService.applyAgentRecord("claude", JSON.stringify(record)) + agentsService.selectTool("claude") + Qt.callLater(function() { + const columns = root.descendants(emptyAgentsPanel).filter(item => + item.modelData && typeof item.modelData.messageCount === "number" + && item.modelData.date && item.children.length === 2) + const labels = columns.map(column => ({ value: column.modelData.messageCount, + text: column.children[0].text, implicitWidth: column.children[0].implicitWidth, + columnWidth: column.width })) + const example = agentsService.formatTokens(1054540000) + console.log("AI_DAILY_TOKEN_LABELS", JSON.stringify({example: example, labels: labels})) + if (example !== "1.1B" || labels.length !== 7 || !labels.every(label => + label.value >= 1e9 && label.columnWidth > 0 + && label.implicitWidth <= label.columnWidth)) + return root.fail("daily token labels exceed their columns or compact format drifted") + watchdog.stop() + console.log("ai plugin smoke passed") + Qt.quit() + }) + } + function panelHeaderMatches() { const items = descendants(emptyAgentsPanel), tier = aiService.displayTierLabel(aiService.selectedProvider.tierLabel) const plan = items.find(item => item.text === tier && "font" in item) @@ -1160,9 +1191,7 @@ ShellRoot { return } stop() - watchdog.stop() - console.log("ai plugin smoke passed") - Qt.quit() + root.compactDailyTokenLabels() return } if (first.panelLoaded || secondLoader.item !== null diff --git a/tests/opencode-usage-regression.py b/tests/opencode-usage-regression.py index ffdb373..74fc997 100755 --- a/tests/opencode-usage-regression.py +++ b/tests/opencode-usage-regression.py @@ -104,7 +104,7 @@ def test_models_and_latest_model_use_positive_today_scope(self) -> None: model="current-a", input_tokens=100, output_tokens=50, - cache_read=500, + cache_read=1054540000, ) self.add_message( session="current-b", @@ -147,7 +147,7 @@ def test_models_and_latest_model_use_positive_today_scope(self) -> None: self.assertEqual(current_a["total"], 150) self.assertEqual(current_a["inputLabel"], "100") self.assertEqual(current_a["outputLabel"], "50") - self.assertEqual(current_a["cacheReadLabel"], "500") + self.assertEqual(current_a["cacheReadLabel"], "1.1B") self.assertEqual(current_a["todayLabel"], "0") self.assertEqual(current_a["pct"], 100) self.assertEqual(current_b["total"], 65) diff --git a/tests/test_package_release.py b/tests/test_package_release.py index 4d1c9df..ed7bdda 100644 --- a/tests/test_package_release.py +++ b/tests/test_package_release.py @@ -205,7 +205,7 @@ def test_lifecycle_contract_pins_exact_release_identities(self) -> None: "suiteVersion": "0.1.1-beta.15.4", "sourceRevisions": ["package:0.1.1-beta.15.4"], "settingsStorageVersion": 1, - "payloadDigest": "7161850120a5a27feb62ffc3651c940f8f2e77f250975d940e4d3f0ededd9cd7", + "payloadDigest": "f838f53ddba2219c3494a0ac5055dbd7760792aa8c653e73c5999f078f89cd73", }, } self.assertEqual(set(states), set(expected)) From 4b7098d4a7cc3d1c4ced2ad7a2ac5605a65f18d8 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Mon, 28 Sep 2026 00:54:00 +0200 Subject: [PATCH 38/40] feat: animate the water rings in the CPU and memory panels --- contracts/lifecycle-predecessors-v1.json | 6 ++-- hancore.shibumi.cpu/CpuPanel.qml | 4 +-- hancore.shibumi.cpu/CpuRing.qml | 41 ++++++++++++++++++------ hancore.shibumi.memory/MemoryPanel.qml | 4 +-- hancore.shibumi.memory/MemoryRing.qml | 41 ++++++++++++++++++------ tests/telemetry-plugins-smoke.qml | 34 ++++++++++++++++++++ tests/test_package_release.py | 2 +- 7 files changed, 104 insertions(+), 28 deletions(-) diff --git a/contracts/lifecycle-predecessors-v1.json b/contracts/lifecycle-predecessors-v1.json index b4e4a79..c28f087 100644 --- a/contracts/lifecycle-predecessors-v1.json +++ b/contracts/lifecycle-predecessors-v1.json @@ -632,7 +632,7 @@ }, { "id": "public-beta.15.4", - "payloadDigest": "f838f53ddba2219c3494a0ac5055dbd7760792aa8c653e73c5999f078f89cd73", + "payloadDigest": "5686a127809125ea0b715f41fd4cf9d9bc6817b372ce8b0370dcb42337a616fa", "pluginDigests": { "hancore.shibumi.bar": "fd1d69f365d08ad06dd52e0cb19892ac935109752f6be4b80e2a20adaf3e7fb9", "hancore.shibumi.state": "8bdbe450c842f7bc6b6483edb652389370201babdef52ae792a9700b51269af4", @@ -643,8 +643,8 @@ "hancore.shibumi.workspaces": "9c19d443cca3e15c3977296fd1dea6c85aa27ba13d2c36991524fdb115dee629", "hancore.shibumi.update-center": "2382455e00ded224236931fe9ca892bbe7f913fbb8f020b56511d3fb736e700f", "hancore.shibumi.status": "1716e7ceaff65c45a78c45cdf99b27d59536e76a4f27b889e2168bc8ab3d8a3e", - "hancore.shibumi.memory": "6680fd24261f39cff46f2302125040e478b78285370da3ad8b9efbb28aca6820", - "hancore.shibumi.cpu": "2028db7ef14411d905034959c2ae1d7d9d218cda468a09e8ef87b6137896aad9", + "hancore.shibumi.memory": "761096b841d78a0b75a76577d12d5378a1c38ba9c2bdacdf35fb8d97e0f8ab6a", + "hancore.shibumi.cpu": "1c1ab2bc7a5e0f2b2c7a7ca8921a63dbe765ecef273abda1d4acbe0f7b20b43b", "hancore.shibumi.audio": "6e39beda5d952e7828ef727f07dd1300228c8c1715a4c32ca3571b3df889404e", "hancore.shibumi.ai": "8abd9feb2cc5b9fee5e140ca3c79f4cc9b273ceec1f4ccebc4f6863be7686e68", "hancore.shibumi.center": "11952ccdb8379bb54fdcf7196b5b3324792a0236d8b9fd3a5c3dcab7c65cac7d", diff --git a/hancore.shibumi.cpu/CpuPanel.qml b/hancore.shibumi.cpu/CpuPanel.qml index 582ed2f..12077c3 100644 --- a/hancore.shibumi.cpu/CpuPanel.qml +++ b/hancore.shibumi.cpu/CpuPanel.qml @@ -129,10 +129,8 @@ ShibumiPanel { percent: panel.systemTelemetry ? panel.systemTelemetry.cpuPercent : 0 foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - onWidthChanged: requestPaint() - onHeightChanged: requestPaint() - water: true + panelOpen: panel.open } Grid { diff --git a/hancore.shibumi.cpu/CpuRing.qml b/hancore.shibumi.cpu/CpuRing.qml index f2e0dc8..342debb 100644 --- a/hancore.shibumi.cpu/CpuRing.qml +++ b/hancore.shibumi.cpu/CpuRing.qml @@ -9,25 +9,45 @@ Canvas { required property color foreground required property color accent property bool water: false + property bool panelOpen: false + readonly property bool animationActive: water && panelOpen + readonly property bool animating: waveClock.running + property int waveFrame: 0 + property real displayedPercent: percent + + NumberAnimation on waveFrame { + id: waveClock + from: 0; to: 72; duration: 3600; loops: Animation.Infinite + running: root.animationActive + } + onAnimationActiveChanged: if (animationActive) displayedPercent = percent + // One ~20 Hz clock advances both the wave and the gently settling level. + onWaveFrameChanged: { + if (!animationActive) return + const delta = percent - displayedPercent + displayedPercent = Math.abs(delta) < 0.1 ? percent : displayedPercent + delta * 0.25 + requestPaint() + } width: Commons.Style.space(16) height: width - onPercentChanged: requestPaint() - onWaterChanged: requestPaint() - onForegroundChanged: requestPaint() - onAccentChanged: requestPaint() - Component.onCompleted: requestPaint() + onPercentChanged: if (!water) requestPaint() + onWaterChanged: if (!water) requestPaint() + onForegroundChanged: if (!water) requestPaint() + onAccentChanged: if (!water) requestPaint() + Component.onCompleted: if (!water) requestPaint() onPaint: { + if (water && !panelOpen) return const context = getContext("2d") context.clearRect(0, 0, width, height) const center = width / 2 const radius = width / 2 - 1.5 - const ratio = Math.max(0, Math.min(1, percent / 100)) + const ratio = Math.max(0, Math.min(1, (water ? displayedPercent : percent) / 100)) const start = -Math.PI / 2 - // Static waterline: height follows the clamped value; no clock or animation. + // Two slow crests, with the subtle amplitude of the former cubic waterline. if (water && ratio > 0) { const inner = radius - 3 const left = center - inner, right = center + inner, bottom = center + inner @@ -38,8 +58,11 @@ Canvas { context.arc(center, center, inner, 0, Math.PI * 2) context.clip() context.beginPath() - context.moveTo(left, level) - context.bezierCurveTo(center - inner / 3, level - wave, center + inner / 3, level + wave, right, level) + context.moveTo(left, bottom) + for (let step = 0; step <= 40; step++) { + const phase = step / 40 * Math.PI * 4 - waveFrame / 72 * Math.PI * 2 + context.lineTo(left + (right - left) * step / 40, level + wave * 0.3 * Math.sin(phase)) + } context.lineTo(right, bottom) context.lineTo(left, bottom) context.closePath() diff --git a/hancore.shibumi.memory/MemoryPanel.qml b/hancore.shibumi.memory/MemoryPanel.qml index fe04d75..8583db5 100644 --- a/hancore.shibumi.memory/MemoryPanel.qml +++ b/hancore.shibumi.memory/MemoryPanel.qml @@ -112,10 +112,8 @@ ShibumiPanel { percent: panel.telemetry ? panel.telemetry.memPercent : 0 foreground: panel.bar ? panel.bar.foreground : Commons.Color.foreground accent: panel.bar ? panel.bar.urgent : Commons.Color.accent - onWidthChanged: requestPaint() - onHeightChanged: requestPaint() - water: true + panelOpen: panel.open } Grid { diff --git a/hancore.shibumi.memory/MemoryRing.qml b/hancore.shibumi.memory/MemoryRing.qml index 09b623b..cba18cc 100644 --- a/hancore.shibumi.memory/MemoryRing.qml +++ b/hancore.shibumi.memory/MemoryRing.qml @@ -8,25 +8,45 @@ Canvas { required property color foreground required property color accent property bool water: false + property bool panelOpen: false + readonly property bool animationActive: water && panelOpen + readonly property bool animating: waveClock.running + property int waveFrame: 0 + property real displayedPercent: percent + + NumberAnimation on waveFrame { + id: waveClock + from: 0; to: 72; duration: 3600; loops: Animation.Infinite + running: root.animationActive + } + onAnimationActiveChanged: if (animationActive) displayedPercent = percent + // One ~20 Hz clock advances both the wave and the gently settling level. + onWaveFrameChanged: { + if (!animationActive) return + const delta = percent - displayedPercent + displayedPercent = Math.abs(delta) < 0.1 ? percent : displayedPercent + delta * 0.25 + requestPaint() + } width: Commons.Style.space(16) height: width - onPercentChanged: requestPaint() - onWaterChanged: requestPaint() - onForegroundChanged: requestPaint() - onAccentChanged: requestPaint() - Component.onCompleted: requestPaint() + onPercentChanged: if (!water) requestPaint() + onWaterChanged: if (!water) requestPaint() + onForegroundChanged: if (!water) requestPaint() + onAccentChanged: if (!water) requestPaint() + Component.onCompleted: if (!water) requestPaint() onPaint: { + if (water && !panelOpen) return const context = getContext("2d") context.clearRect(0, 0, width, height) const center = width / 2 const radius = width / 2 - 1.5 - const ratio = Math.max(0, Math.min(1, percent / 100)) + const ratio = Math.max(0, Math.min(1, (water ? displayedPercent : percent) / 100)) const start = -Math.PI / 2 - // Static waterline: height follows the clamped value; no clock or animation. + // Two slow crests, with the subtle amplitude of the former cubic waterline. if (water && ratio > 0) { const inner = radius - 3 const left = center - inner, right = center + inner, bottom = center + inner @@ -37,8 +57,11 @@ Canvas { context.arc(center, center, inner, 0, Math.PI * 2) context.clip() context.beginPath() - context.moveTo(left, level) - context.bezierCurveTo(center - inner / 3, level - wave, center + inner / 3, level + wave, right, level) + context.moveTo(left, bottom) + for (let step = 0; step <= 40; step++) { + const phase = step / 40 * Math.PI * 4 - waveFrame / 72 * Math.PI * 2 + context.lineTo(left + (right - left) * step / 40, level + wave * 0.3 * Math.sin(phase)) + } context.lineTo(right, bottom) context.lineTo(left, bottom) context.closePath() diff --git a/tests/telemetry-plugins-smoke.qml b/tests/telemetry-plugins-smoke.qml index ce47bb4..85df2d9 100644 --- a/tests/telemetry-plugins-smoke.qml +++ b/tests/telemetry-plugins-smoke.qml @@ -186,6 +186,39 @@ ShellRoot { } } + // Keep real panels alive while closed, without acquiring telemetry. + Item { id: waterOwner; property bool opened: false } + Cpu.CpuPanel { + id: cpuWaterPanel + anchorItem: waterOwner; ownerWidget: waterOwner; bar: fakeBar + systemTelemetry: null; gpuTelemetry: null + } + Memory.MemoryPanel { + id: memoryWaterPanel + anchorItem: waterOwner; ownerWidget: waterOwner; bar: fakeBar + telemetry: null + } + + function checkWaterAnimationCost() { + function rings(item) { + return Array.from(item.children || []).reduce((out, child) => + out.concat(child.water === true ? [child] : rings(child)), []) + } + const waterRings = rings(cpuWaterPanel).concat(rings(memoryWaterPanel)) + if (waterRings.length !== 2) return root.fail("panel water rings missing") + const states = [] + for (const open of [false, true, false]) { + waterOwner.opened = open + // Closing must also stop an in-flight level transition. + if (open) waterRings.forEach(ring => ring.percent = 75) + const running = waterRings.map(ring => ring.animating) + states.push({open: open, running: running}) + if (!waterRings.every(ring => ring.panelOpen === open && ring.animating === open)) + return root.fail("water animation escaped panel-open cost guard") + } + console.log("PANEL_WATER_COST", JSON.stringify(states)) + } + Timer { interval: 40 running: true @@ -400,6 +433,7 @@ ShellRoot { || cpuService.gpu.consumers !== 0 || telemetryService.thermal.consumers !== 0) return root.fail("widget destruction leaked telemetry leases") + root.checkWaterAnimationCost() console.log("telemetry plugins smoke passed") Qt.quit() } diff --git a/tests/test_package_release.py b/tests/test_package_release.py index ed7bdda..4c96096 100644 --- a/tests/test_package_release.py +++ b/tests/test_package_release.py @@ -205,7 +205,7 @@ def test_lifecycle_contract_pins_exact_release_identities(self) -> None: "suiteVersion": "0.1.1-beta.15.4", "sourceRevisions": ["package:0.1.1-beta.15.4"], "settingsStorageVersion": 1, - "payloadDigest": "f838f53ddba2219c3494a0ac5055dbd7760792aa8c653e73c5999f078f89cd73", + "payloadDigest": "5686a127809125ea0b715f41fd4cf9d9bc6817b372ce8b0370dcb42337a616fa", }, } self.assertEqual(set(states), set(expected)) From b4e4f514e259f8b61bf55cf5e4e0f78de913447c Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:07:55 +0200 Subject: [PATCH 39/40] docs: note the compact AI token units and animated water rings --- .github/release-notes/v0.1.1-beta.15.4.md | 2 ++ CHANGELOG.md | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.github/release-notes/v0.1.1-beta.15.4.md b/.github/release-notes/v0.1.1-beta.15.4.md index 25e9790..3cc181d 100644 --- a/.github/release-notes/v0.1.1-beta.15.4.md +++ b/.github/release-notes/v0.1.1-beta.15.4.md @@ -14,6 +14,8 @@ notification history, picker text, and the Control Center. activity; the title follows its visibility as "CPU" or "CPU · GPU". - The Memory panel combines a water-filled load ring with GiB values and, when available, RAM type and speed in MT/s. +- The water rings in the CPU and Memory panels move gently while a panel is + open. - The media panel has five rounded buttons in one row: Shuffle, Previous, Play/Pause, Next, and Repeat. Shuffle, Repeat, and seeking are available where the player supports them. diff --git a/CHANGELOG.md b/CHANGELOG.md index c51cc4f..a8fea4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ redesigned panels follow the panel radius. - Omit the notification tooltip's Live section when the host exposes no Live model; keep Recent counts and Do Not Disturb status. +- Show AI token counts in compact K/M/B units, matching Omarchy. +- Animate the water rings in the CPU and Memory panels while a panel is + open. ### Fixed From 6594b989c9f5edfe557669b0413a9bd3e50f8e81 Mon Sep 17 00:00:00 2001 From: HANCORE-linux <230438592+HANCORE-linux@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:49:11 +0200 Subject: [PATCH 40/40] build: update beta.15.4 release archive checksum --- packaging/aur/.SRCINFO | 2 +- packaging/aur/PKGBUILD | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/aur/.SRCINFO b/packaging/aur/.SRCINFO index 31a4bfc..9e9be11 100644 --- a/packaging/aur/.SRCINFO +++ b/packaging/aur/.SRCINFO @@ -31,6 +31,6 @@ pkgbase = shibumi-shell optdepends = nvidia-utils: NVIDIA GPU telemetry optdepends = git: source-checkout diagnostics and migration source = shibumi-shell-0.1.1-beta.15.4.tar.gz::https://github.com/HANCORE-linux/Shibumi-Shell/releases/download/v0.1.1-beta.15.4/shibumi-shell-0.1.1-beta.15.4.tar.gz - sha256sums = eadeb33daf84f29d9b0e1a3c31ceafbad932a9bb47d0a80d001b4c77e691321d + sha256sums = 56ab3ea984861b8fba747061e3ffd59ba4c0ae4b231bbe1c005c6c9e6f0a6d4d pkgname = shibumi-shell diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index b6c961b..ef34442 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: HANCORE Linux pkgname=shibumi-shell _upstream_version=0.1.1-beta.15.4 -_source_sha256=eadeb33daf84f29d9b0e1a3c31ceafbad932a9bb47d0a80d001b4c77e691321d +_source_sha256=56ab3ea984861b8fba747061e3ffd59ba4c0ae4b231bbe1c005c6c9e6f0a6d4d pkgver=${_upstream_version//-/} pkgrel=1 pkgdesc="Transactional Shibumi desktop shell plugins for Omarchy Quattro"