A comment is a last resort, and these were not one: section banners,
doc headers naming what the declaration below already names, and
notes saying in prose what the next line says in code.
What is left is what the code cannot show -- an ordering that matters,
a platform quirk, why the obvious way fails.
A notebook cell holds a Pharo snippet and what it produced, updated
field by field and synced back when it is re-run. Trace blobs move
to a store of their own, read through one loader.
The method mark's editor keeps its own keys and clicks, so editing
inside an opened body works like editing anywhere else.
Make the REPL prompt a clickable mode toggle between JavaScript
and r2; ":", ":r2" and ":js" switch from the keyboard. Carry the
language on REPLCell and REPLResult, route r2 input through the
session disassembler, and render output as a JSInspectValue tree
for JSON commands or ANSI-parsed styled text otherwise.
The driver seat became fully redundant with the host once session
interaction moved to owner gating: it was always equal to the host and
nothing read it for authorization. Remove it wholesale — the
claim-driver op, driverBySession, localUserIsDriver, Session.driver,
and the REPL "Driving" indicator.
Gate REPL submission on ownership (solo always). Claim hosting through
an acknowledged request and attach only once the server accepts,
instead of optimistically attaching before the claim is even sent.
New update-module-symbols SessionOp carries a batch of
(modulePath, offset, name) entries. persistSymbolicateResults
enqueues one op per symbolicate roundtrip; non-drivers upsert
into module_symbol on receive. Late-joiner snapshots ingest
the whole table.
Identify a process instance by a deterministic identity returned
from the agent (per-OS, with a Process.mainModule + pid fallback)
and stash it on ProcessSession.processInfo. Rekey memory_page
into memory_page_module (module UUID + offset) so cached bytes
survive ASLR, and memory_page_anon (process identity + address)
for JIT / heap regions that only survive within one process
instance. Purge stale anon pages on every fresh attach.
Add updateProcessInfo, updateModuleAnalysis and updateInsight
SessionOps plus a side-channel publish-memory-page op and a
+fetch-memory-page request. The driver write-throughs every
page it fetches; non-drivers pull on miss and persist locally.
Module-analysis rows flow the same way so non-drivers can key
their cache by module UUID. The lab-join snapshot now carries
processInfo and module_analyses too.
Refresh actions on the insight detail view: Reread Bytes drops
the pages overlapping the visible range; Reanalyze Module drops
the module's analysis and pages, re-running r2 on next access.
The macOS detail view now reads the insight live from
insightsBySession via observation; GTK forwards .insightUpdated
into the detail and reschedules a refresh when the resolved
address changes.
Engine: deleteAddressNoteMessage drops the row from the store
and enqueues an address_note_message_remove op. Collab apply
mirrors the existing edit path. AddressNoteChange grows a
messageRemoved case.
GTK: right-click on a user message offers Edit and Delete.
Edit swaps the body label for an inline editor; Save commits
via editUserMessage. Delete confirms first, then removes the
row from the popover and via the engine.
Replace the void did-change callback with a
ChatMessageChange enum so the GTK panel routes each
mutation to a targeted ListBox op. With a 100-message
buffer the prior full rebuild on every send cost ~200 row
constructions for the optimistic+reconcile pair.
sendChat appends the message immediately, awaits the .add
response, then replaces the entry with the server-stamped
version or rolls it back on failure. Wire id ties the
optimistic and canonical entries together so peers see the
same identity. UI restores the draft when the send fails.
Catches around loadInstrumentOnNode, applyInstrumentConfig
and the custom-instrument reloader now set the runtime
status via InstrumentStatus.from(error:kind:) instead of
swallowing the error with print. Status changes are
broadcast both locally (so the host UI refreshes) and to
collaboration peers via a new runtime_status piggy-backed
on the existing add/update-instrument wire payload.
- Store source files as named CustomInstrumentFile rows per def
- Render sidebar with disclosure groups
- Drive editor via Monaco project models with activePath
- Validate Add/Rename/Delete dialogs and submit on Enter
- Fork installed hook packs into editable custom instruments
- Reload instances on def change; clear stale compat reasons live
- Focus editor on selection (native + Monaco)
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:).
Engine owns the widget state cache, applies inbound updates from
the agent, persists session-scoped widgets, and broadcasts to
collaborating peers (action invocations route to the host).
Renderers in both UIs read the cache and the unified update
stream, with a Clear button that propagates to peers.
Agent SDK adds ctx.widget(id) with push/upsertItem/removeItem/
clear, plus a typed onAction handler. create() now takes a
restored snapshot seeded from persisted state and the join
snapshot.
Per-def .d.ts narrows the widget map, restored shape, and action
union to the exact ids declared in the editor.
LLM agent for user-stated RE goals: propose-and-approve over the
existing RPC, evidence-grounded findings, collab via the op-outbox
pattern. Anthropic / OpenAI / local-OAI providers. r2ai used for
explain_function when the plugin is loaded; falls back to the
mission's own provider otherwise.
Engine drives device.enableSpawnGating() per armed-session intent;
matched spawns re-attach via the existing attach path and misses
resume immediately. Engagement intent is in-memory only so launches
don't auto-engage gating for sessions armed in a previous run.
ProcessSession gains armingState and lastArmPattern, mirrored in
the SQLite schema. RuntimeEvent.Source.spawnGating carries the
captured/released outcome with wire encoding.
Lab protocol adds update-arming as a SessionOp variant; armed-only
sessions announce themselves with pid 0 and phase detached so peers
can see them before any capture.
Engine.onUserNotification surfaces gating-enable failures using the
clean Frida.Error message.
Engine subscribes to module/thread deltas and trace updates, drives
the unified ITrace lifecycle, paginates set-trace-data uploads with
lazy server-stat resume, and falls back to a paginated +fetch with
optional first-paint prefix and onProgress reporting. Server-driven
trace-data-progressed broadcasts feed back as cache invalidations.
ProjectStore picks up itrace, project_ui_state and session_ui_state
tables; the schema is created with if-not-exists, no migrations.
SessionOp carries upsertTrace, removeTrace, traceDataProgressed,
and the module/thread updates are now ModuleDelta/ThreadDelta.
When a host enables collaboration on an existing project, replay
each session's REPL cells, instruments, insights, and captures so
peers get a complete snapshot. Fire the sessions fan-out right
after createLab.
Sessions in a lab are now collaborative artifacts. Any owner can
host (own the local Frida attachment) and any owner can drive
(issue REPL evals and instrument commands); a driver's actions
are relayed to the current host. Members watch read-only and see
an "Only lab owners can host sessions" CTA when they try the new
session button.
Re-host (Run on My Device…) lets an owner take over a running
session by attaching to their own equivalent process; the prior
host's Frida attachment detaches and the new host auto-loads the
session's instruments. Driver auto-reverts to host when the
current driver fully disconnects from the lab.
A lab now always has a picture. The client fetches the
signed-in user's GitHub avatar before sending .create
/labs and submits its bytes as the initial picture; if
the avatar fetch fails, lab creation is aborted with an
error rather than producing a pictureless lab.
Drop the remove-picture flow on both sides — the owner
can still upload a replacement, but there's no way to
clear the picture, since the data model now requires
one. With the invariant in place, LabSummary's picture
fields and the welcome-screen lab rows no longer carry
fallback paths to the owner's avatar.
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.
Lab pictures are now uploadable by the owner — pick an image in
the CollaborationPanel (NSOpenPanel on Mac, GtkFileDialog shim
on GTK) and it becomes the lab's avatar on every client and on
the browser /l/<id> page. PNG, JPEG, WebP, and GIF are accepted;
the server enforces a 512 KiB cap. When no picture is set we
fall through to the owner's GitHub avatar, so a lab that nobody
bothers customizing still shows a real face. Clients get
pictures inline in the .join snapshot (bytes appended after the
notebook blob) and live updates via +update on
/labs/<id>/picture; CollaborationSession gains setLabPicture /
removeLabPicture RPCs.
Two other fixes in the same pass:
- Relative timestamps now refresh. Mac wraps each label in a
TimelineView(.periodic(..., by: 60)); GTK runs a single 60 s
Task that rewrites the string on every registered Label. So
"1 min ago" actually advances to "2 min ago" without a
rebuild.
- Closing a document window now stops collaboration. Previously
the session stayed joined until the whole app quit — Mac via
.onDisappear on MainWindowView, GTK via the existing
closeHandler calling engine.collaboration.stop() before
persisting state.
Every lab now has a title. On .create the client mints one from
the owner's local timezone — "<Weekday> <morning|afternoon|
evening|night> <verb>" with verb ∈ {reversing, tracing, hacking,
spelunking, poking, sleuthing, dissecting} — so the first thing
everyone sees is something like "Friday evening spelunking"
instead of an opaque id. Owners rename at any time via a pencil
button in the CollaborationPanel (SwiftUI inline field on Mac,
inline Entry on GTK); the server broadcasts +update on
/labs/<id> so every connected client gets the new title live.
Separately, all visible timestamps switch to a coarse "time ago"
form ("just now", "5 min ago", "2 hr ago", "yesterday", "3 days
ago", "last week"). A shared `LumaCore.RelativeTime.string(from:)`
helper keeps Mac and GTK consistent; the absolute timestamp lives
on as a tooltip for anyone who needs it. Event stream / REPL
cells keep their absolute high-precision times since those are
high-volume and the exact value matters.
Replace the four per-mutation notification types (+add/+update/
+remove/+reorder) with a single +op envelope carrying {op_id,
kind, ...}. Pending mutations persist to a `notebook_outbox`
table on the client and replay on every .joined transition, so
an offline user's work propagates as soon as they're back on the
bus. Projects that never enable collaboration keep a zero-row
outbox — we only touch it once `collaboration_state.lab_id` is
set, and bulk-enqueue existing entries on createLab's success.
Model changes, in the same pass:
- NotebookEntry swaps `author: Author?` for `editors: [Author]`
(creator at index 0, each editor appended as the server applies
their update op). The boolean `isUserNote` is gone; callers now
read `entry.kind == .note` directly.
- Entries carry a `position: Double` — server-authoritative on
add (`max + 1000`), client-computed midpoint on drag. Clients
sort by `(position, id)` so concurrent reorders converge.
- Schema folds the new columns into v1 (no migration — Luma isn't
shipped).
UI:
- Mac: header renders a stack of editor avatars (tooltip = name,
tap opens the GitHub profile). Entry rows are draggable and
accept drops on top/bottom halves to compute the insertion slot.
- GTK: same avatar stack; reordering lives in the right-click
menu (Move Up / Move Down) since SwiftGtk's DragSource binding
isn't verified yet on this branch.
Replace the per-entry send loop with notifyEntriesAdded(_:), which
greedily packs entries into chunks capped at 4 MiB per +add (JSON
payload size plus binary bytes) and emits one message per chunk.
notifyEntryAdded delegates to the plural form so the live-add
path shares the same framing.
Matches the batch-friendly protocol and gives createLab one
round-trip for the initial sync instead of N fire-and-forget
notifications.
Three related fixes so the GTK desktop notifications only fire
when they're actually useful.
- CollaborationSession grows an isSelf(userID:) helper so the
"don't notify myself" check reads cleanly at call sites.
Engine now stamps NotebookEntry.author with the local user on
addNotebookEntry so the check works for entries the user
authored before the server has had a chance to echo one back.
- NotebookChange gains a .snapshot([NotebookEntry]) case that
Engine fires once for the batch delivered on .join. Previously
every entry in the initial snapshot went out as .added, which
would have surfaced as a notification per historical entry
when the user joined a lab. NotebookPane routes it to
rebuildEntries.
- DesktopNotifier holds the main window and skips delivery
whenever that window is the active toplevel, so users looking
at Luma don't get a bubble over the app showing them what
they just watched happen.
The MainWindow / CollaborationPanel callback chains are also
tidied up to go via self.engine once and use the new isSelf
helper, so the optional soup shrinks. Not yet addressed:
macOS's equivalent blind-spot when the app is minimised but
still connected — APNs won't fire because the user looks
"online" to the server. Covering that wants a mirror of this
with UNUserNotificationCenter, next pass.
CollaborationSession now keeps registeredPushPlatforms as a
Set<String> populated from the server's push.registered list
in the welcome envelope, cleared on stop(), and updated
optimistically when the client sends its own
+add push_subscriptions so the UI reacts without a round trip.
LumaGtk's collaboration panel hides the "Enable notifications"
button whenever that set is non-empty: if the user already has
APNs from the Mac app, the browser handoff would be dormant
(APNs wins), and if they already enrolled Web Push from this
or another browser, re-enrolling is redundant.
LumaCore exposes requestPushEnrollmentToken() on
CollaborationSession — a thin request/response wrapper around
.create on /users/<id>/push_enrollment_tokens — and Engine
wraps it with webPushEnrollmentURL() that folds the returned
token plus VAPID public key into
https://luma.frida.re/push-enroll#token=…&vapid=… .
The GTK collaboration panel grows an "Enable notifications"
button next to the Participants heading that mints a ticket
and hands the URL to the system browser via UriLauncher. The
Mac app deliberately skips this — it will register APNs
natively (next pass) rather than farm push out to Safari.
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.
Match the server's rename: CollaborationSession uses labID,
createLab / joinLab, ProjectCollaborationState's roomID field
+ its DB column become labID / lab_id, and UI strings / deep-
link query param all track "lab".
Since the app hasn't launched, the DB column rename is landed
directly in the v1 migration rather than via a new migration —
dev databases need to be wiped.
The server puts the sender under "user" (matching the rest of the
protocol), but ChatMessage.fromJSON was looking for "sender", so
every chat message — including the sender's own echo — failed to
parse and silently dropped.
Replace snapshot-and-rebuild patterns with granular change
events throughout the app.
REPL cells: Engine emits onREPLCellAdded; REPLPane appends
individual cell widgets instead of clearing and rebuilding.
Notebook entries: Add NotebookChange enum with added/updated/
removed/reordered cases. Engine emits from addNotebookEntry,
updateNotebookEntry, deleteNotebookEntry, and collaboration
callbacks. NotebookPane tracks rows by ID and handles each
event incrementally.
Event stream: EventLog emits onEventsAppended with just the
new events from each flush. EventStreamPane appends matching
rows inline. Full rebuild only on filter/search change or
resume from pause.
Device list: Add DeviceChange enum to frida-swift with
appeared/disappeared cases and a changes() stream alongside
existing snapshots(). TargetPicker inserts and removes
individual device rows.
Collaboration: CollaborationSession emits onParticipantJoined,
onParticipantLeft, onChatMessageReceived. CollaborationPanel
appends participant rows and chat bubbles incrementally.
CollaborationSession is now @Observable. Views read its
state directly via workspace.engine.collaboration. The
notebook entry methods live on Engine. The portal cert
is a LumaCore SwiftPM resource via Bundle.module.
Workspace keeps only the auth-flow glue (showing the
sign-in sheet, signOut clearing UI state) and the deep
link join handler.
Move the core business logic for both subsystems:
Package management (CompilerWorkspace):
- PackageOperationQueue: serial async operation queue
- ESMBundleParser: Frida ESM bundle format parsing
- CompilerWorkspace: package install/remove/upgrade,
bundle building via Frida Compiler, workspace lifecycle
- CompilerWorkspacePaths: URL helpers
Collaboration (CollaborationSession):
- Portal bus state machine (connect, create/join room)
- Participant tracking, chat messaging
- Notebook sync protocol (add/update/delete/reorder)
- Uses AsyncEventSource for status change notifications
Both use ProjectStore for persistence instead of SwiftData.