Watch
1
0
Fork
You've already forked souveraine
0

docs: quickshell design docs moved to SouveraineOS/docs

They're cross-surface (laptop + phone) OS docs, not shell-tree internals.
Live copies now in ~/Projects/SouveraineOS/docs/ alongside
SESSION-AUTHORITY-DOCTRINE.md and the session dumps.
This commit is contained in:
Fimeg 2026-07-16 08:05:59 -04:00
commit 0f50a3e9c5
7 changed files with 0 additions and 859 deletions

View file

@ -1,55 +0,0 @@
# How the phone shell works
Three surfaces: **bar** (top), **dock** (bottom, above the navigation rail),
and Souveraine's integrated **navigation rail** (bottom edge).
Edit here → `./deploy.sh --phone` → restart. Deployed via symlink; live edits = git-tracked.
## Navigation rail (`modules/souveraine/navigation/SystemGestureRail.qml`)
- **Always visible.** `WlrLayer.Overlay` + `ExclusionMode.Ignore` + `margins.bottom:0`. Survives fullscreen, dock, OSK. Never lower the layer.
- **Owned by Souveraine.** It is loaded by `SouveraineFamily` on phone mode;
there is no second Quickshell configuration, external IPC hop, or Hyprland
layer rule to keep in sync.
- Gestures (MouseArea — pointer handlers don't get touch here):
- **double-tap** → toggles the active app's named `fullscreen` mode (whole
display, no border/gaps). Targets `Hyprland.activeToplevel.address` — not
hyprctl's focus (the tap focuses the shell).
- **swipe up** → reveals the dock.
- **swipe down** → dismisses the dock.
- **No keyboard on the rail.** OSK = 3-finger hyprgrass swipe only.
## Dock (`modules/ii/dock/Dock.qml`)
- **On `WlrLayer.Overlay`** so the navigation rail can reveal it over fullscreen apps.
- **Hidden = layer unmounted** (`visible:false`), not just tucked — else it paints over fullscreen.
- **Visibility** (`computeDockState`, first match wins):
1. fullscreen app on focused monitor → **Hidden** (unless rail-revealed)
2. pinned → **Pinned** (only state that reserves exclusive zone)
3. preview-hover → **Shown**
4. empty desktop / no focused app → **Shown**
5. else (normal app focused) → **Hidden**
- **Reserves the navigation rail height** at the bottom visually *and in its
layer-shell input mask*, so the dock cannot intercept rail touches.
- **Bar height is content-driven**: the window sizes itself to the button row (64px buttons) + row margin + rail height; `Config.options.dock.height` is only a floor. Don't tune the config height to "fix" icon clipping.
- **App list width is capped** to the screen (`DockApps.maxWidth`); past that the icon row scrolls horizontally by touch (flick is only enabled when overflowing, so drag-to-combine keeps working when everything fits).
- **DockStack renders with the same content block as DockAppButton** (icon + half-reserved dot strip, centered as one unit). Keep them structurally identical or they drift apart on the bar.
- Depends on **`GlobalStates.dockRevealed`** — lives in the surface tree (`GlobalStates.qml`, stock ii + patch) and the deploy manifest. If ii updates its GlobalStates, re-diff and re-apply the patch.
## souveraine-settings (planned, not built)
A future settings app for the phone shell. Planned sections:
- **Dock** — pinned apps, height floor, monochrome icons, ignored-app regexes (today: hand-edited in `~/.config/illogical-impulse/config.json`).
- **Stacks** — create/rename/reorder app stacks and their members (today: `dock.stacks` strings in config.json, or drag-to-combine on the dock).
The dock already reserves its entry points: the long-press menu's "App settings…"
and the `dockSettings` IPC target (`openApp(appId)` / `open()`) in `Dock.qml`.
Both are STUBS — they log and pulse the dock, nothing opens. That's deliberate:
the IPC name stays stable so the settings app can take it over without touching
the dock.
## Fullscreen API (Hyprland 0.55, Lua dispatch)
- Dispatch: `hl.dsp.window.fullscreen({ window="address:0x…", mode="fullscreen", action="toggle" })`.
- **`fullscreen`** = whole display, no borders or gaps. ← the navigation rail uses this.
- **`maximized`** = keeps the normal workspace layout margins.

View file

@ -1,182 +0,0 @@
# Phone shell — interface architecture
The phone has four primary interfaces the user actually touches: the **pill**,
the **dock**, the **keyboard**, and **touch/gesture**. This doc names what each
one is *meant to be*, what it is *today*, and the single seam where a hardcode
should become a small framework so the surface can grow without rewrites.
Principle: **clean beats feature-rich.** Each surface has one job and one place
its behaviour is tuned. A new feature should be a new entry in an existing
table, not a new hardcoded branch. Where a value is tuned in more than one
place today, that is the bug — not the value.
Truth hierarchy: this doc describes intent; `HOW-IT-WORKS.md` describes the
current wiring; the code is the reality. When they disagree, fix the code or
fix the doc in the same change — never leave a third story.
---
## 1. Pill — the always-on gesture bar
**Meant to be:** the one surface that is *always* reachable, on any app, in any
state. A thin, dumb input strip at the bottom edge that recognises a small
fixed vocabulary of gestures and forwards them — it owns no app state and makes
no decisions beyond "which gesture was this."
**Today (`pill/shell.qml`):** `WlrLayer.Overlay`, `ExclusionMode.Ignore`,
`margins.bottom:0`. Gesture arbitration is one MouseArea (double-tap, swipe up,
swipe down) → `dock` IPC. This part is clean and well-reasoned.
**The seam / the bug:** the pill's z-order guarantee is *documented but not
enforced*. `pill/shell.qml:23-27` sets namespace `quickshell:pill` and points
at "the `order` layerrule in hyprland.lua" — **that layerrule does not exist**
(hyprland.lua only has a commented-out generic `^my-overlay$` example). So
after an `ii` restart the pill can be buried under the dock and stop taking
touch — the exact failure the comment says is solved. Fix: add the real
`order` layerrule for `quickshell:pill`, or drop the promise from the code and
docs. Do not add more pill gestures until stacking is enforced — an always-on
surface that silently dies is worse than a plain one.
**Growth rule:** new pill gestures are new cases in the one MouseArea, each
forwarding to a *named* IPC target — never doing app logic inline.
---
## 2. Dock — the app switcher and stack surface
**Meant to be:** the surface that shows and switches running/pinned apps and
lets the user group them into stacks. It owns app-presentation state; it does
not own gesture recognition (the pill and hyprgrass feed it via IPC).
**Today:** interaction layer (`Dock.qml`, `DockAppButton.qml`, `DockStack.qml`)
is strong — drag-to-combine, fan-out arc, visibility state machine
(`computeDockState`). The weak layer is **state/config**:
- Stacks are a hand-parsed string `"stackId|appId,appId"` (`TaskbarApps.qml:25`).
A `|` or `,` in a name corrupts it silently; parsed/encoded in ~6 places.
→ move to **one JSON blob per entry** (array-of-strings the adapter tolerates,
but each string is JSON): kills the delimiter-corruption class, keeps the
adapter happy. This is the agreed direction.
- Stack ids are count-based (`nextStackName` = `"Stack " + (count+1)`,
`:87`) → collide after a delete, and `id` is the lookup key. Two stacks can
share an id and cross-contaminate members. → stable uid, display name
separate from id.
- Combining into a new stack suppresses the target's standalone pin at *render*
time (`apps` getter) but never removes it from `pinnedApps` in config
(`:99-108`) → the pin lingers; unstack works by luck. → remove-on-combine so
config matches what's shown.
- Tap-cycle `lastFocused` index (`DockAppButton :196`) is never reconciled when
windows open/close and is also written by hover (`:180`) → tapping can focus
the wrong window. → clamp/reconcile against the live toplevel model.
- Magic numbers live in the components: `pillStripHeight 32` (`Dock.qml:161`),
dwell `500` (twice: `DockAppButton:133`, `DockStack:116`), double-tap `350`,
arc geometry (`DockStack:33-36`). → lift into `Config.options.dock` next to
the `height`/`monochromeIcons`/`hoverRegionHeight` that already live there.
- `reorderStackMember` (`TaskbarApps:113`) is dead — superseded by
`setStackOrder`. → remove, or wire the menu reorder it was meant for.
**The seam:** `Config.options.dock` already exists — the framework anchor is
already there. "More structure" here means *finishing* that namespace (all
tuning knobs + a real stack schema under it), not inventing a new one.
**Growth rule:** dock feel/config knobs go under `Config.options.dock`; stack
members are structured entries, not delimited strings. Rename + ungroup are
table-stakes and should exist before any fancier stack feature.
---
## 3. Keyboard (OSK) — squeekboard
**Meant to be:** an on-screen keyboard that appears on text focus and can be
summoned by one deliberate gesture, driven through **one** IPC target so its
visibility and any focus-grab shield always move together.
**Today:** squeekboard, launched from `hyprland.lua`, auto-shows via
input-method-v2, toggled by 3-finger swipe-up routed through ii's `osk` IPC.
The *routing* is right (single `osk` target).
**The seam / the drift:** the comments and the retained
`quickshell-ii-patches/OnScreenKeyboard.qml` still describe **wvkbd**
(retired 2026-07-10) and its focus-grab shield — the `osk` IPC now drives
squeekboard, but the code comments and the history-only patch file still tell
the wvkbd story. Same doc-vs-reality drift as the pill layerrule. → update the
comments to squeekboard reality; confirm whether `OnScreenKeyboard.qml`'s
GlobalStates.oskOpen plumbing is still the thing `osk toggle` hits, or vestigial.
**Growth rule:** one `osk` IPC target owns show/hide; layout/theme is
squeekboard config, addressable later from souveraine-settings. No second path
to toggle the keyboard.
---
## 4. Touch / gesture — the input spine
**Meant to be:** one routing layer that maps a physical touch gesture to a
named interface action. Today this is the messiest surface because it is spread
across four dispatch styles.
**Today (`hyprland.lua`):**
- `hl.gesture{fingers=3,horizontal}` → workspace (Hyprland-native)
- `hl.plugin.hyprgrass.bind{...}` → close / terminal / edge-swipes →
`qs -c ii ipc call <target>`
- the pill's own MouseArea → `dock` IPC
- `touchdevice.output = "DSI-1"` binds touch to the panel for rotation
Two input sources (pill swipe-up, hyprgrass edge-swipe-up) both drive the dock;
no single arbiter. Gesture → action bindings are inline `exec_cmd` strings
scattered through the file.
**The seam:** this is the memory note "don't hardcode bindings blindly; need a
routing system to avoid overlap." The clean form is **one gesture table**
edge/finger/direction → named IPC target — read top-to-bottom so overlaps are
visible in one place. Every binding already ends in a `qs ... ipc call <target>`;
the framework is just making that table explicit and single-sourced instead of
sprinkled through `hyprland.lua`.
**Growth rule:** a new gesture is a new row in the gesture table pointing at a
named IPC target. Two rows may not target the same action from different edges
without being adjacent in the table (so overlap is obvious).
---
## Cross-cutting: the one recurring defect
Three of four surfaces have the **same** flaw — *a documented guarantee the
code doesn't enforce*: the pill layerrule (doc says pinned, code doesn't pin),
the keyboard (comments say wvkbd, code runs squeekboard), the dock stack pin
(render says suppressed, config says still pinned). Cleaning these up is mostly
making the code match its own stated contract, then giving each surface exactly
one place it's tuned.
## Status (2026-07-13)
1. ~~Pill layerrule~~ DONE — `pill-above-dock` rule added to hyprland.lua
(`order = 1` on `^quickshell:pill$`). VERIFY ON GLASS: `order`
passthrough in the Lua layer_rule wrapper is untested on this build.
2. ~~Gesture table~~ DONE — hyprgrass binds are one `touch_gestures` table
in hyprland.lua; the two other gesture sources (hl.gesture workspace
swipe, the pill's MouseArea) are listed in its header so overlap stays
visible in one read.
3. ~~Dock state layer~~ DONE — JSON-per-entry stacks (legacy pipe entries
still parse, rewritten on next write), minted `stack-N` ids that never
reuse after delete, name separate from id (+ `renameStack()`, no UI
yet), pin actually removed from config on combine, tap-cycle starts
from the truly focused window, dead `reorderStackMember` removed,
`dragDwellMs`/`pillStripHeight` lifted into `Config.options.dock`.
4. ~~Keyboard~~ DONE — hyprland.lua comments tell the squeekboard story;
`OnScreenKeyboard.qml` (the "osk" IPC owner) moved into this surface +
deploy manifest. Remaining hand-deployed patches (BarContent, Network,
QuickSliders, switchwall.sh) noted in overlays/QUICKSHELL-MOVED.md.
## Open decision — two-stage swipe-up was removed, not lost by accident?
Souveraine commit `6e21c50` ("resume agent conversations from server")
reworked the dock state machine: the Peek state, `dockRevealPulse`, and the
two-stage swipeUp (reveal → escalate to overview) all went away in favor of
a flat reveal/hide contract — under a commit title that says nothing about
the dock. Pixel3Arch CLAUDE.md still records two-stage as SHIPPED
(2026-07-11). The replacement comment is articulate, so this reads as a
deliberate redesign, but it needs Casey's confirmation: keep the flat
contract (overview = the dock's apps button), or restore the escalation.
Comments in hyprland.lua/HOW-IT-WORKS now describe the flat behavior that
actually runs.

View file

@ -1,98 +0,0 @@
# Souveraine quickshell surface
The desktop shell as a Souveraine surface — the primary visual frontend for
SouveraineOS, with the TUI remaining the dive-in instrument.
## Architecture
- `services/Souveraine.qml` — the substrate singleton. The ONE server
connection every shell module hangs off: agent inventory, conversation
lifecycle, the SSE turn stream (raw events re-emitted via
`streamEvent(var)`), the backchannel (`cancelTurn()` / `interject(text)`),
and the desktop sensorium — every send carries ambient context (active
window, open apps, cursor position) so she perceives the room she is being
spoken to in. Device sensors (SouveraineOS positional data from the Pixel
3 kernel path) extend `collectAmbient()`.
- `services/Ai.qml` — ii-compat adapter. Keeps the API the illogical-impulse
sidebar expects; owns no transport. Shapes wire events into the message
objects the existing chat UI renders.
- `modules/` (coming) — presence (portrait PNGs from memfs, posture state
machine), cockpit (subconscious pane), agents (masterdetail manager),
settings, schedules. Each subscribes to the Souveraine singleton.
## What changes
- "Models" in the sidebar are **Souveraine agents** (`GET /v1/agents`).
Picking one starts a conversation with that agent — memory, sensors,
subconscious and all.
- Messages stream over the server's SSE endpoint
(`POST /v1/conversations/:id/messages`), authenticated with the agent's
bearer token from `~/.souveraine/server/agents/<id>/api_token`.
- Subconscious **surfacings**, **reflection**, and **archivist** pressure
render in the chat as interface notes (dedicated widgets later).
- Reasoning and sensor activity render inside collapsible `<think>` blocks.
- Keys/providers/temperature are owned by `souveraine.toml` — the sidebar's
`/key` and `/temp` commands now just point there. The keyring path is dead.
- Token pressure is fetched after each turn from
`GET /v1/conversations/:id/tokens`.
## Portability (KDE / non-Hyprland)
`Souveraine.qml` itself is compositor-agnostic: quickshell runs on any
wlroots-ish Wayland compositor and KWin; window sensing uses the
foreign-toplevel protocol (KWin implements it); the cursor read tries
`hyprctl`, then `kdotool`, then degrades to nothing — ambient never blocks
a send. Server autostart is desktop-neutral (systemd user unit, nohup
fallback), so opening any surface summons her.
What is NOT portable yet is the chrome: the chat UI is illogical-impulse's
sidebar. The path for "I run KDE, can I use this?" is a standalone
quickshell config (own ShellRoot + a window hosting the chat/presence
modules) that ships `Souveraine.qml` unchanged — planned once the modules
stop being ii-embedded. Same service, same mappings, different shell.
## Deploy
```bash
./deploy.sh # backs up upstream Ai.qml, symlinks ours in
./deploy.sh -u # restore upstream
```
Requires the server: `souveraine server` (default http://127.0.0.1:8484,
override with `ai.souveraineUrl` in the ii config).
## Wire contract
The server's SSE layer is a full mirror of `BackendEvent` (see
`src/api/models.rs::StreamEvent` — exhaustive `From` impls both ways, so a
new engine event is a compile error at the seam, not a silent skip). The
surface consumes the personification channel: subconscious tokens buffer and
flush as one bubble when the N+1 pass ends (`subconscious_pass`), halts land
as body signals, interstitials render by register (cenno = quiet aside,
her_voice = gutter passage), `primary_complete` releases the input while the
stream stays open for the subconscious, and `context_pressure` drives the
live token counter. `atmosphere`/`outfit`/`itinerary` are logged, awaiting
their shell-chrome layer.
Server-side, the backchannel and verbs exist for every surface:
`POST /v1/conversations/:id/cancel` (interrupt, `*[raised hand]*`
semantics), `.../interject` (mid-turn notes, queued between turns),
`GET .../messages` (transcript backfill), `POST .../fork` (`/btw`
side-quests). `SendMessageRequest.ambient` injects the sensorium note.
RemoteBackend rides all of it, so TUI remote mode gained cancel/interject/
fork/resume in the same stroke.
## Not yet wired
- Sidebar UI hooks for cancel (Esc) and interject (type-while-busy) — the
service functions exist, the ii chat input doesn't call them yet
- Conversation resume in the sidebar (server verb exists; surface always
starts fresh)
- Atmosphere/outfit/itinerary driving actual shell chrome (events arrive;
modules pending)
- File/image attachments (server has an image path; surface doesn't use it yet)
- Regenerate (Souveraine conversations are forward-only by doctrine)
- "Blank LLM mode" — a memoryless passthrough agent for throwaway questions;
needs a server-side agent flavor first
- Dedicated widgets for surfacing/subconscious bubbles instead of interface
notes

View file

@ -1,176 +0,0 @@
# Lock, idle, and wallpaper reference extraction
This is a design extraction, not a dependency list. Souveraine should retain
one session authority across desktop, laptop, and phone; form factor changes
the chosen layout and cards, never the meaning of lock, idle, or auth.
Migration rule: ii is a temporary compatibility substrate, not a source of
new architecture. New behavior belongs in `surfaces/quickshell` under a
Souveraine-owned module or service. Borrowed code is replaced deliberately,
with the replacement's contract documented before the borrowed file leaves.
## What the active shell has today
The phone lock has a credential keypad, fingerprint attempt, battery state,
and guarded power actions. It does **not** yet have lockscreen media,
notifications, timers, weather, calls, agent output policy, a staged idle
coordinator, or a real sleep-inhibitor backend. `Idle.qml` currently controls
hypridle because the tested invisible Wayland idle inhibitor is not honoured
on the Pixel 3 compositor build.
That is a sound small base. It is not yet a phone-grade lock surface.
## Ideas to extract
| Source | Keep | Do not inherit |
| --- | --- | --- |
| Sailfish/Lipstick lockscreen | A lock screen as layered/pannable content: glance surface, credential surface, and event surface. Wake/display-off are separate from lock. | Its device-specific Lipstick and DeviceLock plumbing; source provenance must be audited before copying. |
| Glacier Home | A compact MPRIS card with capability-gated previous/play/next controls; media is loaded only if a player exists. Clock, media, keypad, and notifications are separate components. | Its older Nemo/Amber APIs and visual code verbatim unless copyright notices travel with it. |
| DankMaterialShell | Independent idle monitors for dim/DPMS/lock/suspend, explicit re-arming, AC-vs-battery thresholds, and a wake monitor after screen-off. | Treating its shell state as authority or blindly importing its settings model. |
| ii/iNiR | MPRIS card mechanics, downloaded artwork, palette adaptation, and image "least busy region" placement. ii already calculates content placement against wallpaper pixels. | The current one-file wallpaper assumption and arbitrary widget dimensions. |
| Ambxst | Screen capture at lock, short-lived credential handling, failure lockout feedback, and listening to logind Lock/PrepareForSleep events. | Its AGPL code, polling `axctl` layer, and its separate lockscreen authority. Concepts only. |
DankMaterialShell and iNiR are MIT; Glacier carries MIT/BSD notices. Ambxst is
AGPL and Vast is GPL. Do not copy from the latter two into this repository
without intentionally accepting their licence obligations. Sailfish files in
the local research extraction need per-file provenance review before reuse.
## One lock surface, composable cards
The lock surface should be a host with slots rather than a new shell for each
device:
```
WlSessionLockSurface
└─ LockSurfaceHost
├─ glance slot clock · date · battery · weather
├─ continuity slot timer · ongoing call · navigation · media transport
├─ event slot notification summaries / previews
├─ credential slot PIN/password/fingerprint PAM conversation
└─ ambient-agent slot explicitly safe question/answer cards
```
The host chooses a layout profile:
| Profile | Layout |
| --- | --- |
| phone portrait | One vertical glance surface; a swipe/press brings up credentials; continuity cards occupy the lower third. |
| laptop | Center credentials with a side or lower card rail; multi-monitor cards remain local to their screen. |
| desktop | Center credentials and a sparse, large-screen card rail; no phone gesture assumptions. |
Each card declares a tier and a lock-surface visibility policy. This prevents
an attractive card from silently becoming a data leak.
| Tier | Suitable cards | Rule |
| --- | --- | --- |
| ambient | clock, battery, weather, timer, transport buttons, an ongoing-call *indicator* | no user data beyond the explicitly permitted setting |
| personal | track title/art, notification body, calendar title, agent conversation, call identity | hidden until unlock unless the user opts that field into ambient |
| step-up | send/reply/delete/purchase/unlock a physical thing | never performed by a card tap alone; needs a recent step-up grant |
An ambient agent is not a diluted personal agent. It has a separate tool and
memory allowlist, cannot read conversation history or sensors, and its output
is labelled ambient. If the lock starts while a personal response streams, the
host immediately withdraws it.
## Idle is a transition graph, not a timer
The useful DMS extraction is the vocabulary and re-arm discipline. The actual
authority remains Wayland idle protocols plus logind:
```
active ─idle timeout→ dimmed ─timeout→ lock-requested
WlSessionLock.secure
lock-secure ─timeout→ display-off
│ │
PrepareForSleep(true) ─────────────────────────────────────┘
suspending → asleep → waking → active
```
Input can cancel dimming and display-off before lock. It cannot cancel an
already secure lock; credentials do that. Every transition has an event source
and an observable result. `screenLocked` is the request; `screenLockSecure` is
the compositor acknowledgement.
The implementation sequence is:
1. Add one `IdleCoordinator` that owns the staged state and form-factor
thresholds. It consumes native `IdleMonitor` events when verified on the
target; the Pixel can retain the hypridle adapter behind that interface.
2. Add a logind event watcher for this session's `Lock` and the manager's
`PrepareForSleep` signal.
3. Hold a delay-mode sleep inhibitor while the shell is running. Release it
only after the Wayland lock is secure; re-acquire it on wake. Measure the
actual logind delay budget on the Pixel 3.
4. Make `idle`, `sleep`, `logout`, and `user-switch` explicit inhibitor kinds.
Only expose a kind after it owns a real backend. `idle` is the sole
implemented kind today.
## Wallpaper should be an asset, not a path
The current background path gives every output the same image and asks crop to
solve incompatible aspect ratios. A downloaded landscape image consequently
loses its subject on the phone. Preserve originals, but track a portable asset
record:
```
id, originalPath, origin, licence, palette,
focalPoint(x,y), safeRegions[],
variants: { landscape, portrait, square },
previewPath, generatedAt
```
Selection order for a screen is:
1. Use an explicit per-device assignment when the user set one.
2. Select a sufficiently close aspect-ratio variant.
3. Crop a derived local variant around `focalPoint` while preserving a
`safeRegion`; never overwrite the original.
4. Fall back to a blurred/palette background with the image contained, rather
than crop the only meaningful part away.
ii's existing least-busy-region analysis is useful twice: place a clock/card
where it does not cover the subject, and propose an initial safe region.
However, it must run against the **actual output crop** and card dimensions,
not a fixed 300×300 desktop widget. A manual focal-point editor is the escape
hatch; an analyser may propose, never silently decide.
For the lock surface, prefer a frozen screencopy or an already-derived local
variant over downloading or decoding a remote wallpaper at lock time. Locking
must remain instant and work offline.
## Proposed Souveraine map
```
modules/common/functions/Session.qml lifecycle verbs + IPC projection
services/SessionEvents.qml logind Lock / PrepareForSleep ingress
services/IdleCoordinator.qml transition graph + target adapters
services/LockContentPolicy.qml card tier / field visibility decisions
modules/ii/lock/LockSurfaceHost.qml shared card host
modules/ii/lock/cards/ Clock · Media · Notifications · Continuity · AmbientAgent
services/StepUpAuth.qml independent PamContext + expiring grants
services/WallpaperAssets.qml asset records, variants, safe regions
```
None of these are separate session managers. They are consumers of the single
`session` lifecycle and its state projection.
## Build order
1. **Truth before visuals:** logind event ingress, delay inhibitor, and
`IdleCoordinator` with a visible transition log. [done — SessionEvents.qml, delay inhibitor in SessionEvents, IdleCoordinator extended with sleep states]
2. **First lock cards:** clock/date, battery, transport-only MPRIS. Add the
metadata privacy switch before title/artist/artwork.
3. **Continuity:** timers, ongoing call, navigation, and notification summary
cards with per-field redaction.
4. **Trust:** ambient-agent allowlist, lock-time response redaction, then
`StepUpAuth` and action-family grants. [partially done — StepUpAuth.qml built, minTier wiring and ambient-agent allowlist remain]
5. **Wallpaper assets:** per-device assignments, focal-point editor, derived
portrait/landscape variants, and crop-aware card placement.
The first useful vertical slice is therefore not a huge lockscreen redesign:
one shared `LockSurfaceHost`, a transport-only media card, a real staged idle
state projection, and a wallpaper asset descriptor. It proves the architecture
on both shapes without committing personal data to the lock surface.

View file

@ -1,104 +0,0 @@
# Session and interaction trust model
This is the contract for the Souveraine shell on laptop and phone. It records
the boundary between orchestration we own and the Linux/Wayland authorities we
must consume rather than replace.
## Authorities and projections
| Question | Authority | Souveraine's role |
| --- | --- | --- |
| Can this machine suspend, hibernate, power off, or reboot? | logind `Can*` methods | Query and expose the answer; execute the matching logind/systemd verb. |
| Has the compositor secured the display? | `WlSessionLock.secure` | Mirror it as `screenLockSecure`; never infer it from a button press. |
| Does the shell want its lock surface shown? | `GlobalStates.screenLocked` | Maintain this request state and persist it across a shell crash. |
| Has the user authenticated? | PAM and the lock's `LockContext` | Start a PAM conversation; only it may release `WlSessionLock`. |
| Why is automatic idle sleep suppressed? | The active idle mechanism | Keep a reason/cookie registry, but report only mechanisms that are actually applied. |
`screenLocked` and `screenLockSecure` must stay distinct. The first closes
ordinary shell surfaces immediately. The second is the only proof suitable for
personal-data disclosure. A failed or delayed Wayland lock is therefore
visible as `lockRequested: true, locked: false`, not mistaken for success.
## Session IPC
All structured IPC results are JSON strings. Quickshell IPC only marshals
primitive QML types, so `: var` silently becomes `void` and loses its payload.
`session.lock()` means “the Wayland lock was requested”; it does not claim
the compositor is secure yet. `session.state()` exposes the subsequent
`locked` acknowledgement and the most recent asynchronous power-action result.
Power verbs return `status: "started"` when their command has started; a later
exit is available in `lastAction`. Refusals always log and return a reason.
The current public inhibitor surface implements `idle` and `sleep`. It is
intentionally wrong to accept `shutdown`, `logout`, or `user-switch` until each
owns a real mechanism. The eventual contract is:
| Kind | Backend | Status |
| --- | --- | --- |
| `idle` | Native Wayland idle inhibitor once verified on both targets; until then the tested hypridle control path. | Implemented. |
| `sleep` | A live logind/systemd inhibitor FD or `systemd-inhibit` process, held for the cookie lifetime. | Implemented via SessionEvents delay-mode inhibitor. |
| `logout`, `user-switch` | Shell policy checks at those operations, not fake logind inhibitors. | Not yet implemented. |
Every inhibitor must have a non-empty human reason, a cookie, and a visible
holder in `state()`. Releasing an unknown cookie is a refusal.
## Lock and suspend sequencing
The next lifecycle change is not another cached state machine. It is a logind
event ingress, implemented in `services/SessionEvents.qml`:
1. [done] Subscribe to the current logind session's `Lock` signal and request the
Wayland lock on receipt. This makes external `loginctl lock-session` calls
meaningful without assuming logind implements a screen locker.
2. [done] Hold one delay-mode sleep inhibitor from shell startup.
3. [done] On logind `PrepareForSleep(true)`, request the Wayland lock and wait for
`WlSessionLock.secure`; release the delay inhibitor only then.
4. [done] Reacquire it after `PrepareForSleep(false)`.
The delay budget is logind's `InhibitDelayMaxUSec` (commonly five seconds), so
this must be timed on the Pixel 3 before it becomes a security claim. The
failure policy is fail closed: if the lock is not secure before the deadline,
leave a diagnostic event and do not pretend the session was locked.
## Interaction capability tiers
The lock screen, media controls, and agent are one policy system, not three.
Each operation and each response is labelled with a minimum tier:
| Tier | Requirement | Examples |
| --- | --- | --- |
| `ambient` | None | clock, weather, timers, transport controls, non-personal answers |
| `personal` | No lock is requested and the compositor is not secure (`!screenLocked && !screenLockSecure`) | messages, calendar, memories, fleet state, conversation history |
| `stepUp` | Recent successful reauthentication | sending, pushes, deletion, payments, physical access |
Media metadata is `ambient` by default but must be configurable to `personal`.
When the lock occurs, in-flight personal or step-up agent output is withheld or
redacted; only ambient output remains visible.
Step-up authentication is a separate `PamContext`, using a dedicated PAM
service such as `souveraine-stepup`. It never unlocks the session and it never
accepts a boolean from an agent as proof. A successful result mints a
short-lived, in-memory grant bound to the local action family. The grant must
be cleared on lock, session end, PAM failure, and expiry. The exact freshness
window is a deliberate policy setting, not an implementation accident.
Implemented in `services/StepUpAuth.qml`. The PAM service file
(`/etc/pam.d/souveraine-stepup`) is NOT shipped by the shell — it is
root-owned system configuration and must be installed/audited separately.
Before enabling it, verify that one polkit agent owns the session and that
no legacy desktop power manager is competing for idle or sleep policy.
The trust boundary matrix is documented in `TRUST-BOUNDARY-MATRIX.md`.
## Required verification
- `session.capabilities()` matches logind `Can*` on laptop and phone.
- `session.lock()` reaches `locked: true` only after `WlSessionLock.secure`.
- `loginctl lock-session` and suspend both take the same Wayland lock path.
- Unsupported inhibitors refuse; each supported cookie visibly changes its
mechanism and is released exactly once.
- A lock during personal agent output hides it; a lock during an ambient timer
leaves only the timer visible.
- Step-up grants cannot survive lock, timeout, shell restart, or action-family
changes. [done — StepUpAuth revokes on lock, expiry timer runs every 30s]

View file

@ -1,180 +0,0 @@
# Souveraine shell — plan (2026-07-13)
## What this is now
`qs -c souveraine` — our own quickshell config, composed by `deploy.sh`:
our files (shell.qml, SouveraineFamily, services/, modules/settings/) are
symlinked from this repo; untouched ii directories are borrowed as whole-dir
symlinks; directories where we override any file are composed file-by-file.
ii's tree stays pristine — no overlay symlinks, no `.upstream` backups. We
step on end-4's toes nowhere; we borrow deliberately and vendor per-directory
only when a reason forces it.
`SouveraineFamily.qml` is the convergence scoreboard: one family, both modes
(desktop + phone). Panels that differ by form factor get an
`extraCondition: Config.options.souveraine.phone` gate; a panel is
"homogenized" when its gate is deleted. No `panelFamily` switching — the
config loads SouveraineFamily, period.
Live on laptop + phone as of 2026-07-13. Hyprland flipped: laptop via the
`qsConfig` var, phone via sed across its 4 hardcoded `qs -c ii` sites
(backup: `hyprland.lua.bak-souveraine-config`).
### The Settings app
`souveraine/settings-phone.qml` — purpose-built mobile shell (list → push →
back, 68px finger rows, fullscreen, no titlebar), launched by
`settings-launch.sh` (imports compositor env from the user manager) via
`souveraine-settings.desktop` in the phone's app grid. Page registry shape
`[{name, icon, component}]` is identical to desktop settings.qml, so pages
are interchangeable. Pages edit `Config.options` (JsonAdapter → config.json,
hot-apply + persist).
Current pages: Device (souveraine.phone flag + future per-device overrides),
Lock screen, Dock, Pill, Keyboard, About (ii's stock page verbatim).
## Doctrine — "tie back down"
Two tiers, nothing app-private:
1. **Shell settings**`Config.options.*` → config.json. Idempotent,
survives redeploy, diffable. Zero new persistence.
2. **System settings** → the owning daemon, live: NetworkManager, ModemManager
(mmcli -J), bluez, UPower, brightnessctl. The app is a *view* over system
state — kill it and the system is still the truth. No shadow copies, no
sync jobs. Privileged writes via polkit (`pkexec` — setuid fixed
2026-07-13, Pixel3Arch gap #7), zero sudo.
## Upstream drift — the decision (2026-07-13)
Our overrides were forked against the **phone's vintage** of ii. The laptop
runs a **newer** ii. Drift is large and one-directional. The question is not
"rebase everything" — it's "which upstream additions do we actually want,
and which are desktop features we're deliberately not running on a
phone-first shell."
**Laptop-upstream additions we do NOT have:**
- `background.widgets.{visualizer,stats,systemResources}` — desktop widgets
(the pctrade-style widget system landed upstream). Phone doesn't use these.
BUT: borrowed `Background.qml` reads `Config.options.background.widgets.*`
unconditionally → TypeError spam on laptop. **Fix: backport the keys into
our Config.qml (small surgical add), don't pull the widget machinery.**
- `claudeUsage` — Claude Pro/Max subscription gauge in the bar. Want later.
- `fileSearch` — indexed file search in the launcher. Want later.
- `sidebar.width`/`widthExtended`, AiChat `fontSize` live-binding. Want.
- `dpmsTimeout`, `unlockHook`, `autoIdleInhibit`, `termBgTone` — small knobs.
- GlobalStates super-press timing (`superPressTime`,
`shouldSuppressSuperReleaseSearch`) — press-and-hold-super suppresses
search. Conflicts with our `dockRevealPulse` patch (both touch the
super-release block). **Rebase needed eventually; not breaking anything
today.**
- Ai.qml: OpenAI Responses API strategy, `thoughtSignature`, FileView chat
save. Our Ai.qml is heavily customized (no hardcoded models, Bifrost
server, agent/resume) — **rebase here is the dangerous one; do carefully
or not at all.**
**What we have that upstream DROPPED:**
- `Config.options.dock.stacks` (fan-out stacks, "stackId|appId,appId") —
upstream removed it. Our `DockStack.qml` arc feature is ours alone now,
not just forked. Keep.
### Drift action items (priority order)
1. **Backport `background.widgets.*` keys into our Config.qml** — kills the
laptop TypeError spam. Small, surgical, no widget machinery pulled in.
2. GlobalStates.qml — rebase `dockRevealPulse` onto upstream's super-press
rewrite. Both touch the same block.
3. Ai.qml — decide: carry our customizations forward on the old base (safe,
current path) or rebase onto upstream's Responses-API Ai.qml (risky, gets
us thoughtSignature + FileView). Defer until we want one of those.
4. SidebarLeft/Right/Overview — dedupe the OSK flee-fix (15 lines triplicated)
into one place after any rebase.
5. Don't rebase dock/osk/TaskbarApps unless a specific breakage forces it.
## Pages still to build (deferred, "in time")
- **Display** — brightness (Brightness service), screen toggle
(`blueline-screen-toggle`), idle/suspend timeouts. hypridle.conf is
user-owned: regenerate whole from template, don't sed.
- **Network** — WiFi via existing Network service; **Cellular** needs a new
`services/Cellular.qml` wrapping `mmcli -J` (data toggle, APN, signal,
operator, SIM PIN). The real new work.
- **Bluetooth** — BluetoothStatus exists, but org.bluez activation fails on
the phone (journal, every boot). Fix the service before building UI on it.
- **App health check** — surfaced as a wanted page; not built.
- **Dock pins/stacks editor** — list editor with rename; consumes the staged
`renameStack`/`stackName` plumbing in TaskbarApps/DockStack.
## Other open items
- Phone cold-boot verification (autostart line flipped, unproven until next boot).
- `souveraine.phone` defaults false; Device page exposes the toggle but
deploy.sh --phone doesn't flip it. Tying it down on phone deploy would
satisfy idempotency doctrine.
- Services rename to be more "souveraine-esq" — deferred.
- Licensing sweep — see ~/Projects/MAKE_PROPER_LICENSES_FOR_ALL_PROJECTS.md.
## Ecosystem — the framework basics (2026-07-13)
Reference studied: `PostMarketOS-Blueline/references/phosh` (full source
checkout). Phosh's shell is built on three primitives we can learn from
without copying its C: `PhoshLayerSurface` (every visible thing subclasses
it), the zwlr_layer_shell_v1 layer it sits on (BACKGROUND/TOP/OVERLAY), and
`zphoc_stacked_layer_surface_v1` — phoc's extension for ordering
layer-surfaces above/below each other *within* a layer (bare wlr can't).
Plus a `PhoshState` bitmask (NONE / MODAL_SYSTEM_PROMPT / BLANKED / LOCKED /
SETTINGS / OVERVIEW) that the shell reads to gate layer visibility —
`use_top_layer = !phosh_shell_get_locked()` is the canonical line. State
drives layer visibility, not the reverse.
Note: `PhoshDockedManager` is NOT "the dock" in our sense — it's *hardware*
docking (phone → external display: disables OSK, stops auto-maximize, flips
`is-phone`). It's the system-level analog of our `souveraine.phone` flag:
the phone-becomes-desktop transition. They rhyme; they aren't the same
feature.
### What we're actually building (three pieces, compose incrementally)
1. **Layer registry + ShellState** — declare each surface's layer and
stack-order explicitly, driven by a state bitmask. GlobalStates is
*almost* this already (it has the state bits) but doesn't own the
layer/stack declarations. Quickshell gives us layer-shell via
`PanelWindow` anchors + exclusive zones — same protocol as Phosh, QML
binding not C. Steal the model, not the code.
2. **Live manifest per surface** — the dock (then each major surface)
projects its current state as a queryable structure: pinned apps,
stacks, positions, visibility, mode. This is a *service*, not UI —
`dock.manifest``{pinned, stacks, hidden, mode}`. The dock already
holds this state internally; the manifest is the projection for
external consumers.
3. **Method abstraction for the agent** — the agent does NOT get a new
toolcall integration. Souveraine is already a fully-fleshed harness
with its own integration state; we use THAT. What lives on the dock
side is a small method surface (`dock.pin(appId)`, `dock.restack(...)`,
`dock.reveal()`) with **state checks baked in** — validate inputs,
refuse mutation when the dock is in a state that forbids it (locked,
mid-drag), return real results. The agent calls a guarded method, not
a footgun; it genuinely can't mess up the dock through the abstraction.
### Where the lessons live — Souveraine School
The *teaching* of these methods — what they are, when to use them, the
lessons — is NOT inline in the dock or the agent. It lives in the
Souveraine School, taught to agent and human in proper time. The dock's
job is to expose the abstraction; the School's job is to teach it. Two
separate concerns; do not conflate.
### First artifact
The **dock manifest** is the smallest concrete first step: the dock is
built, working, and already has internal state (pinned apps, stacks,
reveal/pulse). Projecting it into a queryable manifest + guarded method
surface proves the pattern. The layer registry generalizes it after.
### Reference material on disk
- Phosh source: `~/Projects/PostMarketOS-Blueline/references/phosh` (full
C/meson checkout, read-only reference — do not edit).
- end4-pC fork (pctrade): `~/Projects/end4-pC` — studied for the
panelFamilies + background-widget patterns; not copied.
- Pure Maps (Kirigami/QML nav) — referenced in
`Pixel3Arch/docs/car-and-dock-references.md`; not yet pulled as source.

View file

@ -1,64 +0,0 @@
# Trust boundary matrix
Every session operation classified by caller type, required auth gate, and
current enforcement status. This is the reference for what is gated and what
is not — if an operation is not in this matrix, it is not gated.
## Legend
| Column | Meaning |
| --- | --- |
| Operation | The IPC verb or internal action |
| Caller | Who can invoke it |
| Min tier | Minimum capability tier required |
| Gate | What enforces the tier |
| Status | `enforced`, `partial`, `ungated` |
## Operations
| Operation | Caller | Min tier | Gate | Status |
| --- | --- | --- | --- | --- |
| `session.lock()` | IPC / UI / logind signal | ambient | None (always allowed) | enforced |
| `session.unlock()` | Credential gate only | stepUp | LockContext PIN/PAM | enforced — IPC refuses |
| `session.suspend()` | IPC / UI | ambient | capability probe | enforced |
| `session.hibernate()` | IPC / UI | ambient | capability probe | enforced |
| `session.poweroff()` | IPC / UI | ambient | capability probe + polkit | enforced |
| `session.reboot()` | IPC / UI | ambient | capability probe + polkit | enforced |
| `session.logout()` | IPC / UI | ambient | None | enforced |
| `session.inhibit("idle")` | IPC / internal | ambient | reason required | enforced |
| `session.inhibit("sleep")` | IPC / internal | ambient | reason required | enforced |
| `session.state()` | IPC / agent | ambient | None (read-only projection) | enforced |
| `session.caps()` | IPC / agent | ambient | None (read-only projection) | enforced |
| `StepUpAuth.requestAuth()` | UI / agent | ambient | PAM conversation | enforced |
| `StepUpAuth.isGranted()` | UI / agent | ambient | None (read-only check) | enforced |
| `StepUpAuth.revokeGrant()` | UI / internal | ambient | None | enforced |
| Agent conversation | Souveraine IPC | ambient | Server auth token | enforced |
| Agent send with personal context | Souveraine IPC | personal | !screenLocked && !screenLockSecure | enforced via LockContentPolicy |
| Agent output on lock surface | Lock surface | ambient | LockContentPolicy.allowsOnLock() | enforced |
| In-flight agent response on lock | Lock surface | personal | Ai.qml redacts on screenLocked | enforced |
| Media metadata on lock surface | Lock surface | ambient (if opted) | LockContentPolicy.mediaMetadataAmbient | enforced |
| Media transport controls | Lock surface | ambient | LockContentPolicy.mediaControlsVisible | enforced |
| Lock screen power actions | Lock surface | ambient | Config.lock.security.requirePasswordToPower | enforced |
| Idle state transition | IdleCoordinator | ambient | nativeEnabled config | enforced |
| Sleep/suspend transition | SessionEvents | ambient | delay inhibitor + WlSessionLock.secure | enforced |
| Session audit trail | SessionAudit | ambient | append-only JSONL with hash chain | enforced |
## Not yet gated (gaps)
| Operation | Caller | Required tier | Gap |
| --- | --- | --- | --- |
| Agent delete/push operations | Souveraine IPC | stepUp | StepUpAuth built but minTier metadata not wired |
| Agent physical access | Souveraine IPC | stepUp | StepUpAuth built but minTier metadata not wired |
| Break-glass override | Emergency | scoped grant | Done (StepUpAuth.breakGlass) |
| Boot-time IPC audit | Shell startup | ambient | Done (log in Session.qml) |
## Notes
- The matrix is local-only: all callers are on the same machine, reachable
only over quickshell's IPC socket by the user who owns the session.
- If a network-reachable caller is ever added, every row needs re-evaluation.
- The `personal` tier gate is `!screenLocked && !screenLockSecure` — the
session must not be locked and the compositor must not have secured the
lock surface. This is a live check, not a cached bool.
- The `stepUp` tier requires a recent (TTL-window) StepUpAuth grant for the
relevant action family.