onScreenLockSecureChanged is an edge. After a sessiond restart our lock surface
is already secure, so the edge is in the past and the ack owed by the new
handoff was never sent — sessiond logged 'shell never confirmed its lock after
handoff' and retook the lock 15s later.
SessionEvents asked loginctl for -p ObjectPath, which is not a property — it
returned empty on every boot, so external lock signals were never monitored and
the shell had no unlock ingress. Resolve the graphical session the way
lockhint.rs does; not GetSession(auto) (this shell is a session-less user unit)
and not the /session/auto alias (PropertiesChanged only fires on the concrete
path).
SessiondBridge read root.registered to decide whether to re-register on
reconnect, but the disconnect branch had already cleared it, so the shell never
re-registered after a sessiond restart. Latch it.
Session.qml and Lock.qml both registered target 'session'; quickshell keeps
whichever loads first and drops the other. Lock.qml's handler is a strict
superset, so the duplicate goes.
The page asked the same question twice because two daemons answer it, and
split the blank budget by held-vs-resting — which makes the user arbitrate a
guess the accelerometer is making. Held-ness belongs in the confidence
arithmetic as an adjustment to one budget, not as a second budget here.
The fullscreen overlay sat above squeekboard's layer and owned the whole
screen's input region, so taps in the keyboard area hit the dialog. It now
yields the bottom third while the OSK is up. Exclusive keyboard focus was
tried and reverted — it stopped the keyboard taking touch entirely.
UPower's ChargeType (fork, read off the charger) reaches the lock line as
fast/slow charging. Discharging with the cable in is the charger resting
between top-ups, not a drain — it now reads Charged/Plugged in.
squeekboard hides itself when input-method focus drops, so the password
field was left with no keyboard. A hold re-asserts through that and
releases on dismiss; a manual close drops the hold.
IdleConfig: ConfigSelectionArray presets (30s/1/2/5/10 min for the shell's
timers, 15s/30s/1/5 min/Never for the lock screen) instead of seconds
spinboxes. Nobody reasons about a lock screen in 5s increments; the policy
struct already said "iOS Auto-Lock shape". Wire values stay seconds.
Header comment corrected — hypridle no longer owns screen-off.
set_panel(false) clears pending_blank. A panel darkened by any other route
left it armed, so the next tick emitted a second Blank or hit the
blank-without-lock error path for a blank nobody was waiting on.
SetPolicy refuses dim_grace >= lock_blank_after. dim_at saturates to zero, so
the panel dimmed the instant it went idle with no lit period.
Verified by rendering the page, not by parse alone.
SessiondBridge: handle pushed {"directive":"lock"} on the heartbeat line and
raise the lock surface. Unknown directives are console.error — a silent drop
leaves the daemon blanking unlocked when its budget expires.
SessiondPolicy: new singleton, reads/writes DeviceStatePolicy over its own
short-lived connection (not the bridge socket — that EOF is shell-death
detection).
IdleConfig: new "Lock screen (device authority)" section bound to it. SetPolicy
had zero callers, so the old spinboxes moved a JSON file the daemon never read.
Says so in the error colour when sessiond is unreachable.
New file needs a deploy.sh manifest line or the services qmldir fails whole.
LOCK-DPMS-LESSONS §1 is "Ordering: lock, then off". It held only because
hypridle's 300s lock listener fired before its own 600s screen-off listener —
two numbers in a config file, not a guarantee. Anything that skipped the lock
(idle inhibitor, native coordinator off, dead shell) still met the blank, and
the panel went dark on an unlocked session with nothing said about it.
Every path to a dark panel now routes through request_blank():
- locked -> Blank, unchanged, no added round trip
- unlocked -> Action::Lock, blank withheld, pending_blank deadline armed
- ack lands -> Blank
- budget expires -> Blank anyway + error-security in the forensic trail
The panel fails open because a lit unlocked phone in a pocket is worse than a
dark one (§1); the *claim* fails closed because doctrine §8 forbids pretending
the session locked. Input inside the ack window cancels the blank outright.
Action::Lock does not go through the executor table — the lock is not a shell
command. While a live shell owns steady state the directive goes down the
heartbeat connection and the shell raises its rich surface; with no shell alive
sessiond raises its own. Same relationship DEVICE-STATE-MACHINE §6 gives the
DPMS executor: the authority decides, the surface executes. A directive that
cannot be delivered takes the lock here rather than timing out into a dark
unlocked screen, and is recorded either way.
Policy gains lock_ack_budget (2s, refuses 0) and unlocked_blank_after (None by
default — the shell's IdleCoordinator owns the unlocked idle timer through
ext-idle-notify per doctrine §5, and a second one here would recreate the
competing-owner disease). Both are on the GetPolicy/SetPolicy seam so Settings
is a view over the owning daemon, per TASK-19.
Unknown directives are loud on the shell side: an older shell silently
dropping one would leave the daemon waiting out its budget and blanking
unlocked.
38 tests pass, 5 new covering the ordering.
Speech: prefetch() now sets synthProc._prefetch=true before voiceLookup
so the background synth doesn't auto-play. speak() skips voiceLookup
when the endpoint+voice are already resolved (faster repeat taps).
Ai: addMessage defaults model to Souveraine.currentAgentId so restored
messages (/resume, new conversations) show the agent icon/name instead
of a generic placeholder.
Session: create_with_messages_and_timestamps preserves persisted
created_at/updated_at through load_persisted so /resume sorts by
actual activity time, not server restart time.
TTS: ttsClean strips unclosed think blocks, fixes regex dotAll flag
for older Qt/V8 (phone), adds pronunciation map.
SubconsciousTicker: tap psychology icon to expand inline mini-log,
long-press to open the event panel overlay.
New: workspace-selector.sh + .desktop — wofi dmenu grid for quick
workspace 1-10 jump from the app drawer. Installed to phone via
deploy.sh symlink.
Previous fullscreen detection had two blind spots:
1. Workspace scan used wayland?.fullscreen (unreliable) and missed qs -p windows
2. HyprlandData.activeWindow fallback only saw the focused window — missed
fullscreen apps that lost focus to layer-shell surfaces (dock, notifications)
Now scans HyprlandData.windowList (hyprctl clients -j) for any window with
fullscreen === 2 on the focused monitor. Source of truth for all windows,
not just the active one. Reactive via HyprlandData's event-driven updates.
Co-Authored-By: Claude <noreply@anthropic.com>
Dragging a pinned app now shows an insertion gap indicator when hovering
over other pinned apps. Quick horizontal slide + release = reorder the
pinned apps array. Dwell (500ms) on a target still = combine into stack
(existing behavior). Both gestures share the same drag start and ghost.
TaskbarApps: reorderPinned() splice-moves within Config.options.dock.pinnedApps.
DockApps: shared dragSourceIndex/dragInsertIndex state, delegate passes
parent.index as modelIndex to DockAppButton.
DockAppButton: insertion indicator (2px primary-color line), DropArea
onEntered computes gap position based on drag direction, release handler
commits reorder when no dwell fired.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds a Download-random-wallpaper button and a 3-way content filter
(SFW/Sketchy/NSFW) to our Wallpaper settings page, backed by a new
WallpaperDownload service and its own download script under
surfaces/quickshell/scripts/wallpaper/ — not ii's scripts/colors tree,
which is going away. Only the 'wallpapers apply' IPC target still comes
from ii and moves with it when the base is vendored.
Downloads are named by wallhaven id so the library accumulates instead of
overwriting a single wallhaven_wallpaper.<ext>. Purity is read from and
written to background.wallhaven.purity in config.json (not in the upstream
Config.options schema). Anchors the root scripts/ gitignore to / so shipped
surface scripts are tracked.
The repo qmldirs listed only repo-owned types; composed dirs also carry
ii-borrowed types (ReloadPopup, PanelLoader, Bar, ...) which a qmldir
present in the dir must list or they stop resolving — broke the phone
shell on deploy. Regenerated every deployed qmldir from the composed
config on-device (capitalized .qml -> type line, pragma Singleton ->
singleton line) and pulled back. Shell verified active, zero load
errors.
Speech.qml singleton: speak(text) reads the who->voice mapping live
from souveraine /v1/config (VoiceConfig tts_url + voice_id), POSTs
VibeVoice /audio/speech ({input, voice, model} -> mp3, same contract
as core/voice/client.rs) and plays via mpv/ffplay. The shell picks no
voice of its own. speech.tts.enable is the kill switch (button hidden
when off); speech.tts.endpoint overrides the mapped URL. Sidebar gains
a volume_up/stop toggle beside send that reads the last visible
assistant reply, skipping lock-redacted content.
deploy.sh recomposes ~/.config/quickshell/souveraine with rm -rf each
run; the 19 new qmldir files were hand-placed on the phone and would
be wiped on next deploy. Add them to the manifest.
19 dirs had .qml but no qmldir -> quickshell config load failed
(File not found @ shell.qml[-1:-1]). Corrected singleton decls in
3 existing qmldir. SystemGestureRail: pill->WlrLayer.Top, oskLift
200, drop oskOpen hide.
Floating the pill atop the keyboard stole top-row taps and stacked over
stevia's upward-opening layout menu. 3-finger swipe / stevia's menu
cover dismissal while typing.
speech.stt/speech.tts land in Config so the settings page, the
souveraine-stt CLI, and the keyboard mic key all read one source of
truth. STT defaults to the VPN faster-whisper bridge; the page probes
/health so configured and answering are visibly different states. TTS
is a field waiting for a server.
The session-lock surface was transparent with nothing painting behind it.
TouchLockSurface now owns its backdrop: lock.wallpaperPath when pinned,
else the system wallpaper, else a dark field — plus a scrim for glance
legibility. Glance clock gains lock.twelveHourClock (default on, h:mm ap).
LockConfig grows the wallpaper picker (same FolderListModel grid as the
wallpaper page, no matugen side effects) and the 12-hour switch.
loginctl manages sessions/users/seats only — `loginctl poweroff` exits 1
"Unknown command verb". Preferring it silently broke poweroff/reboot/
suspend/hibernate from every shell surface. Use systemctl; fall back to
loginctl only on elogind systems where it does carry power verbs. The
logind D-Bus capability probe (CanPowerOff etc.) is unchanged — logind
owns policy, systemctl carries out the action.
The C splash now plays only Acts I-III and hands off at ~6.8s; a new
BootBloom layershell overlay (modules/souveraine/boot) resumes Acts IV-V
(Souvie + bloom flower) over the live compositor, clock-aligned to the boot
epoch so the animation is continuous across the process swap. It maps as
Hyprland's first surface (top-level in ShellRoot, before Config.ready),
pokes splash-signal on its first frame to release the GPU, and LockScreen
clears bootBloomActive on secure to fade it out and reveal the lock. Bloom
shader is qsb-compiled from BootBloom.frag at deploy time.
Drop services/Audio.qml from the manifest: it was a pactl mirror written
for native PulseAudio and only tracked the default sink/source. The phone
runs PipeWire now, so ii's native Pipewire Audio service (full graph,
WirePlumber-owned volume) takes over.
Deleted from repo in a prior session while reverting an experiment;
never should have removed a file that wasn't reverted alongside it.
Pulled back from the phone's live ~/.config/quickshell deploy, which
was still running the correct version the whole time.
squeekboard auto-shows/hides itself on input-method focus without
telling the shell; the stale flag left the dock suppressed and the rail
floating at keyboard height. gdbus monitor on its Visible property is
the single truth now.
Wallpaper grid in the settings app applying through the shell's
wallpapers IPC. Lock pad now tracks the finger during the swipe and
settles on release instead of snapping at a threshold.
seat0-active polkit rule so power off/reboot/suspend stop returning
challenge into a session with no polkit agent on the lock surface.
Lock-surface buttons stay gated behind allowPowerFromLock (opt-in) and
requirePasswordToPower. SystemGestureRail progressive gesture + mission
control nav flag.
- packaging/upower-souveraine submodule pinned at fork head (f18b1a4,
souveraine/charge-type): ChargeType + PercentageTrusted on Device
- PKGBUILD.upower.prebuilt: prebuilt meson install tree, provides/
conflicts upower, replaces stock (IgnorePkg=upower on the phone)
- ci.yml: build the fork per-arch against the aarch64 sysroot, assert
binary arch, fold into the souveraine-{arch} pacman db
- LockSurfaceHost: charge line driven by device state, not onBattery —
no more eternal 'Charging N%' on a topped-off pack
- docs/tasks/power-indication.md force-added past the docs/ gitignore
PIN pad hidden until swipe up / tap hint / hardware key; retreats after
25s idle with nothing typed. shouldReFocus no longer implies reveal --
hypridle fires it on every wake and wake must land on glance.
ext-session-lock finished (another client held the lock) drops
WlSessionLock.locked C++-side; the request bool never heard about it.
Stale true lied to redaction/capability gates and blocked re-locking
(the binding only fires on a false->true edge).
Session.lock() notified logind unconditionally; hypridle's lock_cmd fires
on logind's Lock signal, so every lock echoed back through logind ->
hypridle -> lock() forever. Observed: ~450 lock requests/min for 7 minutes,
re-locking the screen seconds after each unlock. Notify only on the
unlocked->locked edge.
sessiond gave up waiting for the compositor lock ack after one dispatch —
at boot Hyprland defers the ack while the splash holds DRM, so the initial
lock silently failed (journal: 'never acknowledged', 0.7s in) and boot ran
on the legacy lock path. Wait up to 60s for locked/finished.
The shell refused to load on the phone — onSocketConnected/
onSocketDisconnected handler-slots aren't reliably attachable across
quickshell builds, so the whole singleton cascade aborted at the bridge.
Switched to the real connectionStateChanged signal, branching on
sock.connected. LockAgentCard needs 'import qs' for the GlobalStates
singleton (root module), not just qs.services.
Redacted messages stayed '[content hidden until unlock]' forever — nothing
restored them on unlock. Redaction now records the message and unlock
restores content from rawContent. Turns finishing while locked are also
redacted and surfaced. New LockAgentCard shows a one-line preview of
replies that landed during the lock window on the lock glance, like a
notification; body stays hidden until unlock.
sessiond auth: getpwuid_r buffer is u8 with a c_char cast so it compiles
on aarch64 (c_char = u8 there) and x86 (c_char = i8).
ext-session-lock client that locks at session start, hands the lock to the
shell over a runtime socket without an unlocked instant (abandon + restore),
and retakes it the moment the shell heartbeat drops. Spartan PIN fallback
surface, PAM via hand-rolled libpam FFI, machined-style sync server.
Shell side: SessiondBridge singleton + LockScreen initIfReady routes the
startup lock decision through the handshake.