HSplitView sizes itself to its panes' preferred widths rather than the
window, so at narrow widths the panel ran past the window's edge and
was clipped. Hold the panel's width ourselves and resize it through a
handle, keeping it within what the detail pane leaves.
Make NavigationSplitView the window's root so its sidebar tints its
slice of the titlebar natively, the way Xcode's does. Move the side
panel into the detail column and the event stream to a bottom
safeAreaInset with an opaque background, so the stream spans the full
width and pushes the content up whether or not the sidebar is
showing. Replace CollapsibleVSplitView with a SwiftUI resize handle
and collapse bar.
The Liquid Glass toolbar backdrop frosted the top of the detail
column's content and drew a divider across it. Hide it so the
detail stays flush to the top.
Add a machine abstraction to LumaCore, with QEMU as its first
backend: templates mirroring frida-core's test guests, a QMP client,
and a display that maps QEMU's surface over D-Bus rather than
copying frames.
Leave readiness to the user: drive the guest, mark it ready, and come
back to that snapshot later. Keep the machine in the project, so
booting it again resumes where it was marked. Hand the stub to
addBareboneDevice, so it joins the picker beside the other devices.
Show the display while booting, and afterwards in a Machines panel,
which the side panel now models as an enum to share its slot.
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.
Frontends built the session record, set its phase and surfaced it,
briefly showing the detached banner at the idle phase before attach
began. Move construction, phase and surfacing into prepareSpawnSession
and prepareAttachSession so the session appears already attaching.
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.
Mark the document edited on every working-copy commit so
AppKit autosave-in-place writes the file. Drop
ProjectAutosaver and its onDisappear flush, which lost edits
made after reopening a saved document.
Use .frame(maxWidth: .infinity, maxHeight: .infinity) instead of
asymmetric .padding(.top, 4)/.padding(.bottom, 11). Chevron and
tint both fill the 32pt pane.
Wrap CursorOverrideRegion in canImport(AppKit) and provide a no-op
View stub for iOS, which has no AppKit. The call site in
SaveBarOverlay now compiles on both platforms.
Add an explicit empty deinit to CollapsibleVSplitView.Coordinator
to work around an EarlyPerfInliner crash in swift-frontend on
Xcode 26.4 Release builds.
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)
Engine.shutdown() cancels eventLogTask, deviceChangeWatcher,
deviceEventTasks; stops MCP servers; nils observations;
detaches process nodes via node.stop(); stops collaboration.
Idempotent via a one-shot flag.
EngineRegistry.release(workingProjectURL:) awaits any pending
start task, removes the engine from the cache, and awaits
shutdown. Reopening the same URL gets a fresh engine.
MainWindowView and PhoneMainView .onDisappear call release.
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.
Length-prefixed JSON log at events.log next to db.sqlite,
configurable byte cap with rolling compaction. Replays on
Engine.start.
toLogJSON/fromLogJSON carry the .raw payload and sessionID
that wire JSON drops. EventLog flushes a pendingPersist
queue so the visible-window trim can't drop events.
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.
Engine.attach and Engine.spawnAndAttach now throw on
failure and return the updated ProcessSession on success,
so the mission tools can use a straight try await /
catch instead of refetching the row to read lastError.
list_sessions reports phase as a readable string and
includes last_error when set.
Sidebar capped at 320 wide, window minimum lifted to 900,
and the Action Queue / Findings counts get layoutPriority
plus a non-zero Spacer minLength so the right-edge label
isn't first to clip when the user squeezes the window.
Earlier attempts mixed fixedSize, frame(maxWidth: .infinity),
and a horizontal ScrollView in the header, plus min/max
clamps on the navigation sidebar. The combination starved
the inner HSplitView so a freshly created mission rendered
as just the splitter handle until streaming began. Drop the
exotic modifiers, restore the standard sidebar ideal width,
and let labels truncate individually.
Bound the navigation sidebar between 180 and 360 wide so a
drag can't push detail off-screen, clamp the goal text to
two lines, and tuck the metadata labels into a horizontal
scroll view so a long provider/model identifier no longer
shoves the Cancel button out of view.
performAttach now adopts process.icons.last when the
session has none yet, so sessions started by the agent
get the same icon treatment as those started from the
target picker. Icon-to-PNG conversion lives as an Icon
extension in LumaCore so host code uses it via
icon.pngData and the duplicate helper is gone.
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.
Select the session row right after the record is saved, before
awaiting attach. The session detail view now opens immediately
and shows the spawning/attaching status, instead of jumping to
the REPL only after attach completes.
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.
Replace the auto-reopen-last-document flow with a welcome
screen that lets the user start a new project, open one
from disk, or — after signing in with GitHub — pick from
a list of labs they're a member of. The list is fetched
via a new transient portal connection that issues
.list /labs and disconnects.
Token persistence on macOS now goes through a file under
~/Library/Application Support/re.frida.Luma/tokens/ to
avoid the per-launch keychain prompt that dev-signed
builds suffer; iOS keeps using the keychain. GitHubAuth
caches the load Task so concurrent waiters share one
keychain access, and now flips state to .authenticated
when a persisted token is loaded so the welcome view
shows the labs section right away. requestToken awaits
the load before falling through to a fresh sign-in.
Engine accepts an injected GitHubAuth so the welcome
window and any subsequently-opened document share one
auth instance — no second sign-in prompt when crossing
from welcome to a document.
SwiftUI's DocumentGroup only autosaves when the FileDocument
value mutates, but Luma's state lives in an external SQLite file.
Wire a GRDB TransactionObserver to post a didCommit notification
and bump a revision counter on LumaProject whenever any write
commits; autosavesInPlace then writes the file on close without a
prompt. Have the saved FileWrapper go through VACUUM INTO so WAL
contents land in the snapshot too.
Move the working-copy location out of /tmp and under
LumaAppPaths.untitledDirectory/.working/ so it survives launches
on iOS (where /tmp is reset between sessions). Write the working
URL lazily and back untitled documents with LumaDocumentLoader so
they get stable "Untitled N.luma" names that the auto-reopen
path can key off.
Drop the deferred DispatchQueue reopen that reliably flashed an
NSOpenPanel before the previous document slid in. Reopen inside
applicationDidFinishLaunching via synchronous makeDocument +
addDocument + showWindows so DocumentGroup sees a document
present before it decides whether to show its launch panel, and
bypass macOS's secure state restoration (which was triggering
entitlement crashes via NSOpenPanel on relaunch). Track
restorationPath via .onChange with initial so Save-As updates
LumaAppState.lastDocumentPath immediately instead of stranding
the old path.
While here, move the CodeShare sheet sibling out from underneath
the Add Instrument sheet on macOS so the "Browse CodeShare" flow
presents sequentially (dismiss add, then open browser) to match
iOS behavior.
Restore iOS compilation by gating AppKit-only code in
StyledTextSwiftUI, ITraceTimeline, CollaborationPanel, and
MainWindowView. Stub ITraceCFGView behind #if canImport(AppKit)
since the Metal/NSView renderer is not ported yet. Add a
UIApplicationDelegateAdaptor mirroring the macOS delegate for
luma:// URL handling and APNs. Fix the rpath for iOS's flat bundle
via SDK-conditional LD_RUNPATH_SEARCH_PATHS overrides.
Build a new phone-first shell (PhoneMainView /
PhoneSessionsListView / PhoneSessionView) that replaces
MainWindowView on iOS. Right-edge drawers for events and
collaboration with glow indicators, a session-switcher popover, and
a segmented REPL / Instruments / Insights / Captures layout. The
Sessions list and Session screen each render a custom compact
header instead of the system nav bar so they sit flush under the
DocumentGroup chrome. Notebook pushes from the Sessions root.
Extract SessionContent so the detach banner lives at the parent of
any session-bound surface instead of each detail view rendering its
own. Make HexView responsive: hide the offset column and use
smaller hex and ASCII fonts in compact width, with selection hit
testing aware of the hidden column. Move the REPL cell timestamp
to share the request line so the result row gets the full cell
width, and trim the REPL horizontal padding in compact width.
Replace Color.cyan with a light/dark adaptive Color.jsTypeLabel so
Object{N} labels stay readable on a light background, and
ellipsize the JS inspector disclosure header to one line.
A batch of small UI fixes across both frontends:
- GTK header bar: drop the row of text buttons ("New Session…",
"Add Instrument…", "Install Package…", "Collaboration") and
replace with symbolic icon buttons pinned to the right, GNOME
HIG-style. Icons are list-add, applications-engineering,
media-playback-start (unchanged), folder-download, and
system-users. Labels survive as tooltips.
- Both CollaborationPanels: shorten "Share this link so others
can open a new project and join this notebook." to "Share this
link to invite others to this notebook."
- GTK CollaborationPanel: single-line ellipsized lab title
(was wrapping to three lines to match picture height);
consistent "Members"/"Chat" section labels instead of all-caps
"PARTICIPANTS"/"CHAT".
- Mac CollaborationPanel: lab title wraps over multiple lines
instead of ellipsizing; panel width is now draggable via
HSplitView (min 260, ideal 300, max 520); lab picture uses
fixed-size frame+clip so large-dimension uploads render
crisply at 48 pt; uploads downscale to ≤512 px JPEG before
sending.
- GTK CollaborationPanel: matching lab-picture downscale via a
new CLuma shim (`luma_image_normalize`) that uses GdkPixbuf
to decode→scale→JPEG-encode.
- Mac notebook editor: explicit Save/Cancel buttons on the
entry editor (mirrors the GTK row). A freshly-inserted blank
note cancels itself away instead of leaving a stub row.
- Mac notebook drag: capture row height via GeometryReader so
the above/below midpoint is actually computed, which means
drops above the first row finally work.
- GTK notebook row: drop the redundant "Edit" button from the
header — double-click and the right-click menu both already
enter edit mode.
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.
LumaProject was a ReferenceFileDocument, which forced it to also
be an ObservableObject — even though it had no @Published
properties, never published anything via objectWillChange, and
handed its store off to Workspace (the actual ObservableObject
the views observe). The mismatch with Swift 6 strict concurrency
required nonisolated final class, nonisolated(unsafe) on a
publisher we never use, and a nonisolated UTType extension just
to satisfy ReferenceFileDocument's nonisolated protocol surface.
FileDocument is a value type, has no ObservableObject baggage,
and matches what LumaProject actually does: own a temporary
on-disk SQLite file and serialize it in/out. The struct is now
33 lines and contains zero concurrency annotations. The store
is constructed inside MainWindowView from the document's
temporaryDBURL, where it can live in @StateObject Workspace
exactly like before.
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.
Each .luma document is a package (directory bundle)
containing project.sqlite. LumaProject implements
ReferenceFileDocument to integrate with SwiftUI's
DocumentGroup:
- New documents create a fresh SQLite database
- Existing documents read the database from the package
- Snapshots serialize the database for save operations
Workspace takes a ProjectStore from the document rather
than creating its own. Each document window gets an
independent store/workspace pair.
Update every view and Workspace extension to use LumaCore
types and ProjectStore instead of SwiftData:
- SidebarView: fetch instruments/insights/captures from store
- DetailView: resolve sessions via store
- REPLView: load cells from store by sessionID
- NotebookView: use LumaCore.NotebookEntry value types
- Workspace+Packages: delegate to CompilerWorkspace, use
ESMBundleParser.parse(), persist via store
- Workspace+Collaboration: fetch/save collaboration state
and notebook entries through store
- All remaining views qualified with LumaCore types
The Luma app should now compile without SwiftData.
Workspace creates a ProjectStore at Application Support
path and uses it for remote device loading, notebook
entries, and session persistence.
performAttachToProcess, spawnAndAttach, and attachToProcess
all work with value-type ProcessSession structs, saving
through the store after each mutation.
MainWindowView creates sessions as LumaCore structs and
saves them through workspace.store before attaching.