WelcomeModel, CollaborationSession, and AuthFailure all hand-rolled
String(describing:) variants to dodge NSError bridging or the enum-case
form. With Frida.Error now formatting itself, these collapse to plain
localizedDescription / String(describing:).
Several small, related fixes:
- GTK NotebookPane: fix insert-first regression in the .updated
handler. We were anchoring on next.prevSibling *after*
removing the existing row, which collapses to nil when the row
was last and lands the fresh one at the top. Capture prev
before removal instead.
- GTK NotebookPane: drop the per-row "+ Note" and "Delete"
buttons from the header. Both actions live in the right-click
menu already, and the header was getting crowded. Left the
Edit button in place.
- GTK + Mac CollaborationPanel: remove the "Project
collaboration" heading — it's redundant with the "Collaboration"
title at the top of the panel — and drop the "Lab: <id>" row;
the id is already on the invite link.
- GTK CollaborationPanel: widen the invite hint label so it
fills the frame instead of wrapping too early, and relocate
the "Enable notifications" affordance from the participants
row into labSection so it sits near the project identity
rather than the member list.
- Both panels: rename "Disable collaboration" to "Disconnect
from lab". We're just leaving the session — the lab itself,
and the user's membership, stick around.
- Client: handle +update on /users/<id>/push_subscriptions so
the "Enable notifications" button disappears live once the
browser enrollment completes (server now broadcasts on every
subscription change).
- AuthFailure.fromError: decode the `luma-auth:<base64>` marker
the server now emits instead of spelunking for a JSON blob
through quote-escape layers. Survives any amount of
stringification frida-core does on its way across the wire.
- Engine: temporary reorder-path diagnostic print while we
chase the "Move Up stays in place" report.
@Observable's generated storage wraps Result<JSONObject,
AuthFailure> in the pendingRequests dict, and Swift requires
the Failure type to be an Error. Adding the conformance is
zero-cost — AuthFailure has nothing to implement for it.
Rewrite CollaborationSession around the server's new envelope
shape: a single dispatcher that routes (from, type), explicit
request/response correlation via client-chosen ids, and typed
Member objects carrying role + Presence + last_seen_at.
NotebookEntry gains an Author field stamped by the server and
a Kind enum (note | capture); isUserNote is preserved as a
computed alias so the UI doesn't churn. Notebook update hooks
now fire with (UUID, changes) so Engine can merge into the
locally-cached entry without the server having to re-send the
full row.
Protocol key shake-up is snake_case throughout, and UserInfo
now reads "avatar" (matches what the server actually emits —
the previous "avatar_url" lookup was a latent bug). The Mac
and GTK collaboration panels switch from participants to
members.
The portal's authenticator now surfaces a structured
{domain, code, message} envelope in the thrown error when it
rejects a token. Parse that in AuthFailure, and if domain is
"auth" (persistent problem — bad token, missing scope,
unverified email), fire an onAuthRejected callback on the
CollaborationSession. Engine wires that to gitHubAuth.signOut,
clearing the keychain so the user is prompted to re-auth with
the current scope set. Transient github.* failures leave the
session alone.
This also unblocks future scope widening: older tokens will be
rejected with a recognised code and the client will silently
bounce the user through sign-in again.