Read the shared connection activity in the target picker and put the
current stage, with a progress bar that eases toward the next stage
over its expected duration, under the spinner in place of "Querying".
Show the stage on an attaching session's row too, since a session
re-establishing connects the same way.
A drill reveals its new column whole, scrolling through the reader
rather than at a position measured before the column exists, and
the strip's thumb both follows the columns and drives them.
Boot the VM against a bundled image, evaluate from a notebook cell
or a playground snippet, and keep what each evaluation produced.
The image is staged on every build; the framework release is pinned.
Give instruments that have hooks a disclosure chevron and persist
the per-instrument collapsed state in the session UI state. Toggle
the module and thread groups on a full header-row click. Pad the
module view so its tabs clear the top-left edge.
Drop the modules/threads tabs; show both as collapsible sidebar
groups mirroring the hooks UX: a handful of top picks plus a
browse popover for the rest. Default threads to collapsed.
Revert the eager re-analysis of persisted modules on attach; keep
disassembly analysis lazy and add a per-module Analyze action and
status indicator instead.
Share one sidebar-row and one browser-popover component across
hooks, modules, and threads in both the SwiftUI and GTK frontends.
Claim the driver seat when hosting a session, both on a fresh
spawn/attach and on re-host, wiring the previously unused claim-driver
op so the driver tracks the actual host.
Gate every spawn/attach/re-attach control on canTakeHosting: owner,
joined to the lab, and the seat free (not actively hosted by another).
Re-attach is now disabled while offline, since you can't hold the seat
then. Apply across both frontends — sidebar menus and detached
banners.
Replace the auto-derived title with userTitle (nil = auto)
and a parentInsightID link. Display name is userTitle, else
<parent.title>+0x<delta>, else sub_<offset> if at a function
start, else module+offset. Sidebar context menu Rename… and
an LLM rename_insight tool both go through Engine.renameInsight,
which broadcasts via the existing updateInsight op and pushes
the new name into r2 as a flag (plus afn at function starts).
getOrCreateInsight kicks off a background pass that links a
new insight to its enclosing function-start insight, and
adopts siblings inside its region when the new insight is
itself the function start. The same adoption runs when a
fresh ModuleAnalysis newly reveals a function start under an
existing insight.
Adopt Finder-style indentation: child icon starts where its
parent icon ends; spacings are 3/5/6 (leading, chevron-to-icon,
icon-to-label). Same rule cascades to grandchildren.
GTK: drop the 1px border on .luma-session-icon for an inset
shadow so the icon's allocation stays at 24.
Replace the simple warning icon with a button that opens a
popover containing the headline, summary and diagnostic
stack (when present). Tracer hook rows reuse the same
indicator, driven by the per-component status map on the
node's InstrumentRef.
Promote the runtime per-instrument flag from a bare reason
string to an InstrumentStatus enum so later kinds (load,
reload, compile failures) can share the surface. Behaviour
is unchanged: skipIfIncompatible sets .incompatible and the
sidebar still renders a single warning icon.
Sidebar: manual expansion for sessions and custom instruments;
child rows align under parent icon column. Surface tracer hooks
inline under the Tracer item with a Browse-all popover for the
overflow.
Editor: drop static header rows; SaveBar slides down from the
editor top with Save button and unsaved indicator. Shared
between tracer and custom instrument editors.
GTK: pin detached banner to system-orange across focused and
backdrop states. Trim chevron button padding so it honors the
24px column, and tighten row padding so the icon sits 4px from
the row edge.
- 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)
Pickers gray out mismatched descriptors with a tooltip and
disable the Add action. Sidebars badge instruments skipped at
restore time. Custom defs grow a Compatibility… popover with
canonical Frida values (macOS, x86_64, MIPS, …).
Tinted icons disappeared into the accent-filled background
on selected rows. Letting them inherit the primary color
lets the sidebar invert them to white on selection.
EngineRegistry caches one Engine per working-project URL.
MainWindowView.init does a pure lookup, so SwiftUI's
view-tree re-evaluations cannot open extra ProjectStores
that race the live one's BEGIN IMMEDIATE and crash via
try!.
UI-state persistence (collapse flag, sidebar selection,
session detail section, etc.) lives on Engine for GTK to
adopt. SQLite has a 5s busy timeout. Engine open failures
surface as an error view.
TargetPicker is host @State injected via environment.
Global instrument-UI defaults register at app startup;
per-engine instrument and local-notifier hookups live in
Engine extensions.
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.
Hookpack manifest mirrors CustomInstrumentDef: rich features,
TypeScript entry, optional file-based icon. Folder name is the
pack id.
HookPackConfig holds FeatureState per feature, producing the
same agent-JSON shape as custom instruments. Hookpack sources
compile through the shared TypeScript pipeline.
Import sits next to "New Custom Instrument..."; Export on the
custom-instrument context menu. Both UIs.
Sidebar and phone session list both rendered from
node.processIcons when the session was attached and
session.iconPNGData otherwise, with the iconPNGData branch
in the sidebar carrying an extra accent-color overlay. That
fork made agent-created sessions look subtly off until the
node arrived. Drop the live branch, render only from
session.iconPNGData, and replace the inline icon-stash
copies with a new ProcessSession.adoptIcon helper that
performAttach keeps current.
A new title column carries a short label generated in
parallel with mission start. Each LLMProviderDescriptor
declares a summarizationModelID so the pass runs on the
mission's own backend (Haiku for Anthropic and Claude
Code, gpt-5-mini for OpenAI, llama3.2:3b locally). The
instruction sits in the user message to dodge Claude
Code's prompt layering and asks for two to four punchy
words. Sidebar shows title or goal; header keeps the
full goal under the title.
Right-click on a mission row exposes a Delete Mission
action behind a confirmation dialog so it follows the
same pattern as session, instrument, and trace deletion.
MCPServer.start tries a preferred port (persisted via the
TokenStore) before falling back to .any, so the URL stays
stable across launches. SidebarMissionRow swaps the Text
maxWidth frame for a trailing Spacer — matches the existing
session-row pattern and survives sidebar widening.
NewMissionSheet drops the API-key prompt for providers that
don't need one. Action Queue header anchors top instead of
centring. Sidebar mission row truncates long goals.
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.
Target picker grows a Wait for Launch mode with a popover-anchored
suggestions list. Sidebar context menu adds Arm/Disarm with a regex
prompt; armed sessions get a scope badge.
Detail banner picks among armed, idle, and detached states. The
armed banner offers Resume when gating is paused or failed and
Disarm otherwise.
EventStreamView renders spawn-gating events; raw payloads unwrap to
the inner message string instead of String(describing:) on the case
wrapper. REPL inactive message reflects armed/paused/detached.
Sessions now have a top-level detail pane (Summary / Modules /
Threads) with master-detail panes for module symbols and thread
register snapshots. Sidebar gains a session row, and the ITrace
sidebar entries adopt the unified naming.
ITrace detail decodes the streaming trace data with first-paint
prefix, onProgress, and a Stop control restricted to thread
origins; the timeline paginates past 200 instructions.
The document is now a .luma directory wrapper containing
db.sqlite alongside a traces/ folder. Workspace owns the
ProjectUIState (selection, event-stream layout, collaboration
visibility) and a per-session SessionUIState (detail section,
last selected module/thread). MainWindowView reads/writes these
through Bindings instead of local @State.
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.
The monogram placeholder wasn't really session-specific — the same
hue-from-seed + initials logic fits every target that has an
optional bundle icon. Rename `SessionPlaceholder` → `IconPlaceholder`
(data in LumaCore) and the two view wrappers to `IconPlaceholderView`
so the scope reads correctly, then wire the target picker's app and
process rows to fall back on it when no Frida-provided icon is
available. Seeds are app.identifier for applications and proc.name
for processes so repeat targets keep consistent colours.
Sessions that lack a bundle icon all rendered the same generic
glyph, making rows hard to tell apart at a glance. Derive a stable
hue and monogram from the device ID and process name in LumaCore,
and pair each frontend with a native renderer — SwiftUI paints the
gradient + initials view, GTK draws the equivalent through Cairo
so the same session ends up the same colour across platforms.
Both frontends showed stale sidebars after an install that
didn't come from the Manage Packages dialog — SwiftUI had a
dead @State placeholder that nobody assigned to, and GTK only
re-rendered when the search dialog invoked its own completion
callback. Installing from the tracer's inline Install button
skipped both paths entirely.
Add observeInstalledPackages to ProjectStore, wire the engine
to mirror it into Engine.installedPackages (an @Observable
array), and publish an onInstalledPackagesChanged callback.
SwiftUI reads the observable directly; the GTK window
subscribes to the callback and calls its existing
renderPackages helper.
The sidebar fetched instruments, insights, and ITrace captures
directly from the SQLite store inside its body, so SwiftUI's
observation tracking never registered any dependency on them.
Adds, deletes, and updates went through the store and the
LumaGtk-side onSessionListChanged callback, but the SwiftUI host
had no way to know it should re-render. Items only appeared when
some unrelated observable change happened to trigger a body
re-evaluation.
Add ValueObservation-backed observers to ProjectStore for all
three collections, mirroring the existing observeSessions /
observeNotebookEntries pattern. Engine subscribes in start() and
exposes instrumentsBySession / insightsBySession / capturesBySession
as @Observable maps. SidebarView reads from these instead of
hitting the store directly, so changes propagate the moment GRDB
notices a row insert or delete.
LumaGtk's incremental sidebar continues to drive itself from
onSessionListChanged callbacks unchanged.
Workspace is now a thin host adapter. Engine owns sessions and
notebook entries (via store observation), GitHub auth + OAuth
device flow, hook-pack discovery, address annotations and address
actions, the disassembler cache, and all instrument lifecycle.
EventLog is @Observable so SwiftUI tracks it directly; the host
no longer mirrors event arrays or version counters.
R2 (live disassembly + trace inspection) lives entirely in
LumaCore as concrete Disassembler / TraceDisassembler classes
returning portable StyledText spans. ProcessNodeViewModel and
InstrumentRuntime are gone; views read engine.processNodes /
node.instruments / engine.descriptor(for:) directly.
Reorganize Luma/Models/ — instrument UI dispatch lives under
Luma/Instruments/, Monaco editor glue under Luma/Editor/, and the
remaining UI types sit at the top of Luma/.
Engine and ProcessNode are @Observable. Workspace
observes engine.processNodes via withObservationTracking
to sync VMs reactively. Views call workspace.engine.*
directly for spawn, attach, resume, instrument lifecycle,
insights, and tracer hooks. Engine instrument methods
take InstrumentInstance directly instead of split params.
Engine now handles the full instrument lifecycle (add,
remove, enable, config, load via RPC, dispose), tracer
instruction hook creation, session reestablishment,
and address-to-hook resolution. InstrumentDescriptor
is the UI-agnostic instrument metadata type.
Workspace is reduced to a thin SwiftUI shell: template
view factories, address decoration UI, ProcessNode VM
wrapping, and @Published state for observation.