feat(tray): surface kept virtual displays in the tray tooltip

Stage 8 polish. `GET /api/v1/local/summary` (the tray's loopback-only unauthenticated status
source) gains `kept_displays` — the count of lingering/pinned virtual displays (held with no live
session), over the already-validated `registry::snapshot()`. The tray shows it in the idle tooltip
("idle · 1 display kept"), so a user knows a display — and, under exclusive topology, their physical
monitors — is being held (e.g. a gaming-rig `forever` pin). Release stays via the console: a
state-changing release can't be an unauthenticated endpoint, and the non-elevated Windows tray
can't read the SYSTEM-DACL'd mgmt token, so a tray release button isn't cleanly cross-platform.
`#[serde(default)]` on the tray side keeps it compatible with an older host. Tray tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 17:54:44 +00:00
parent 468a60c88a
commit 69f4c987f6
3 changed files with 30 additions and 1 deletions
+8 -1
View File
@@ -2671,13 +2671,20 @@
"paired_clients", "paired_clients",
"native_paired_clients", "native_paired_clients",
"pin_pending", "pin_pending",
"pending_approvals" "pending_approvals",
"kept_displays"
], ],
"properties": { "properties": {
"audio_streaming": { "audio_streaming": {
"type": "boolean", "type": "boolean",
"description": "True while the audio stream thread is running." "description": "True while the audio stream thread is running."
}, },
"kept_displays": {
"type": "integer",
"format": "int32",
"description": "Virtual displays being KEPT with no live session — lingering (keep-alive window) or pinned\n(`keep_alive: forever`). Non-zero means a display (and, exclusive, your physical monitors) is\nheld; the tray surfaces it + a one-click release. Active (in-use) displays are not counted.",
"minimum": 0
},
"native_paired_clients": { "native_paired_clients": {
"type": "integer", "type": "integer",
"format": "int32", "format": "int32",
+9
View File
@@ -382,6 +382,10 @@ struct LocalSummary {
pin_pending: bool, pin_pending: bool,
/// Native pairing knocks awaiting the operator's approval (count only). /// Native pairing knocks awaiting the operator's approval (count only).
pending_approvals: u32, pending_approvals: u32,
/// Virtual displays being KEPT with no live session — lingering (keep-alive window) or pinned
/// (`keep_alive: forever`). Non-zero means a display (and, exclusive, your physical monitors) is
/// held; the tray surfaces it + a one-click release. Active (in-use) displays are not counted.
kept_displays: u32,
} }
/// A paired (certificate-pinned) Moonlight client. /// A paired (certificate-pinned) Moonlight client.
@@ -1330,6 +1334,11 @@ async fn get_local_summary(State(st): State<Arc<MgmtState>>) -> Json<LocalSummar
native_paired_clients, native_paired_clients,
pin_pending: st.app.pairing.pin.awaiting_pin(), pin_pending: st.app.pairing.pin.awaiting_pin(),
pending_approvals, pending_approvals,
kept_displays: crate::vdisplay::registry::snapshot()
.displays
.iter()
.filter(|d| d.state == "lingering" || d.state == "pinned")
.count() as u32,
}) })
} }
+13
View File
@@ -33,6 +33,10 @@ pub struct Summary {
pub native_paired_clients: u32, pub native_paired_clients: u32,
pub pin_pending: bool, pub pin_pending: bool,
pub pending_approvals: u32, pub pending_approvals: u32,
/// Virtual displays kept with no live session (lingering/pinned). `#[serde(default)]` so an older
/// host that doesn't send it deserializes as 0.
#[serde(default)]
pub kept_displays: u32,
} }
#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
@@ -71,6 +75,14 @@ impl TrayStatus {
s.version, sess.width, sess.height, sess.fps s.version, sess.width, sess.height, sess.fps
), ),
(_, true) => format!("punktfunk host {} — streaming", s.version), (_, true) => format!("punktfunk host {} — streaming", s.version),
// Idle, but surface a kept (lingering/pinned) display: it — and, under an exclusive
// topology, your physical monitors — is being held. Release it from the console.
_ if s.kept_displays > 0 => format!(
"punktfunk host {} — idle · {} display{} kept",
s.version,
s.kept_displays,
if s.kept_displays == 1 { "" } else { "s" }
),
_ => format!("punktfunk host {} — idle", s.version), _ => format!("punktfunk host {} — idle", s.version),
}, },
} }
@@ -432,6 +444,7 @@ mod tests {
native_paired_clients: 2, native_paired_clients: 2,
pin_pending: false, pin_pending: false,
pending_approvals: 0, pending_approvals: 0,
kept_displays: 0,
} }
} }