feat(permissions): Phase 4 — settings UI for revoking stored decisions
The user can now see and revoke every per-origin permission decision
the page-host has remembered, via a new "Permissions" section in
Settings (between Extensions and Dark Mode).
IPC surface (`tile-ipc.ts` + `tile-preload.cts`):
* `tile:permissions:list` → `api.permissions.list()`
* `tile:permissions:forget` → `api.permissions.forget(origin, perm)`
* `tile:permissions:forgetAll`→ `api.permissions.forgetAll()`
All three handlers gate on `trustedBuiltin` so only peek-core renderers
(the settings page) can read or mutate stored decisions; tile renderers
can't snoop or tamper with another tile's saved choices via this surface.
Settings UI (`app/settings/settings.js`):
* New `renderPermissionsSettings()` lists each decision as a card
showing the origin (mono font, breakable), a friendly permission
label ("Location", "Camera & microphone", "MIDI devices", "Screen
capture", etc. — kept in sync with permission-policy.ts
PERMISSION_LABELS), the verdict (Allowed/Denied), and a timestamp.
Each row gets a Revoke button; the page-level "Clear all" button
wipes everything after a confirm() prompt.
* The section auto-refreshes on every nav-click — `window._refresh
PermissionsList` is exposed so external mutations (a fresh prompt
landing while settings is open) can trigger a re-fetch. Without
this the initial render is stuck at empty state if the user
visited Permissions before any decisions were stored.
* Empty-state message + Clear-all button are hidden/shown based on
list contents.
Type defs (`tile-api.d.ts`):
* New `TilePermissionDecision`, `TilePermissionsResult`,
`TilePermissions` interfaces; added `permissions: TilePermissions`
to `TileAPI`.
Tests:
* `tests/desktop/permissions-settings.spec.ts` (NEW, 3/3) — drives
the settings UI end-to-end: seed decisions via the same
`feature_settings` table the backend reads from, open settings,
click into Permissions, assert each row renders with origin +
label + verdict + Revoke button. The Revoke test clicks a single
Revoke and asserts both the UI row vanishes AND the backend list
drops to length 0. The Clear-all test seeds three decisions,
auto-accepts the confirm() dialog, asserts every row vanishes and
backend wipes.
* Regression: `tests/desktop/permission-prompt.spec.ts` 3/3 (Phase 2
+ Phase 3 unchanged). Build clean.
Tasks doc updated: Phase 4 marked done, remaining Phase 5 polish
(favicon next to origin, multi-prompt queueing UX) tracked.