Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
388 changes: 388 additions & 0 deletions Changelogs/v2.1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,388 @@
# v2.1.0

The first stable release on the **2.1** line, built up over nine public betas
and three release candidates and consolidated here. If you have been running
the stable channel since v2.0.1, everything below is new to you.

Headline work spans runtime inspection, background indexing, multi-tab
navigation, appearance, and code injection — plus a late pass over what the app
looks like *while* it is working, which removed the flashes and grey blinks that
appeared whenever you clicked from one type to another.

One upgrade step needs your attention: the privileged helper daemon reinstalls
itself on first launch. See **Upgrade notes** at the bottom — this applies to
release-candidate users too, not only to those coming from 2.0.x.

---

## Highlights

- **Multi-tab content navigation** with a window tab bar and per-tab history
- **Generic type specialization** — inspect Swift generics with concrete arguments filled in
- **Background indexing** — the sidebar is ready when you click into it, not seconds later
- **Inspector → Relationships** — see who references a type, and what a type depends on
- **Batch export** interfaces for multiple images in one pass
- **Theme settings** with a data-driven color model and a built-in Xcode preset
- **Jump to Definition** for Swift type references
- **Line numbers** in the content view
- **Injection into strict-seatbelt daemons** via `mach_vm_remap`, and into Apple applications via the same path
- **iOS Simulator runtime installer** built in

---

## What's new

### Runtime inspection

#### Generic Type Specialization
Pick any Swift generic from the sidebar and a new **Specialize…** sheet lets
you fill in its type parameters. The Inspector renders the fully specialized
interface alongside the original unspecialized form.

- Nested generics work — if an argument is itself generic, you can drill in
and fill those parameters in the same sheet.
- Type definitions can come from a different binary than the generic itself,
so you can specialize types in framework *A* with a model from framework
*B* loaded in the same document.
- Invalid combinations are caught up front with a clear error instead of
silently producing garbage.

#### Inspector → Relationships
A new **Relationships** tab sits between *Hierarchy* and *Specialization*. It
lists inbound references (who uses this type) and outbound references (what
this type depends on), drawn across every binary the document has loaded.

#### Jump to Definition for Swift types
Command-click a Swift type reference in the content view to navigate to its
definition, the same way you already could for Objective-C.

#### Enum layout fixed-bit masks
The Swift enum-layout view now surfaces fixed-bit masks for each case, giving
you the exact bit pattern the runtime uses to discriminate cases without
having to read it out of the raw metadata.

---

### Indexing & batch operations

#### Background Indexing
Once a document opens, RuntimeViewer walks the dylib dependency graph and
starts indexing related binaries in the background, so by the time you click
into them they are ready to inspect.

- A new toolbar progress popover shows what is currently indexing, lets you
cancel any individual batch, and keeps a history of recent runs.
- Selecting a binary that is still in the queue immediately bumps it to the
front.
- Background work runs at low priority and never makes the main UI stutter.
- A new **Settings → Indexing** page controls how aggressively background
indexing runs (or turns it off entirely).

#### Always Index list + master switch
The Indexing settings page grew a master switch plus two independently
toggleable sub-modes:

- **Heuristic Discovery** — main-executable BFS at document open and engine
swap, with configurable depth (1–5).
- **Always Index** — a user-editable list where each row pins a single image
(full path or file-name shorthand). Toggle **Follow Dependencies** on a row
to also walk its dependency closure.

The batches popover groups its active and history sections by reason
(*Heuristic Discovery* / *Always Index* / *Manual*) under collapsible headers.

#### Batch Export
Export the interfaces of multiple images in one pass without opening each
document individually.

- New **File → Export Multiple Images…** entry walks you through image
selection, format selection, and destination directory in a single sheet.
- Per-language format choices (Objective-C `.h`, Swift surface, etc.) are
remembered across runs.
- The completion summary uses stat cards to show how many files succeeded,
failed, or were skipped, and lists each failure individually so you can
re-run only what broke.
- Long Swift-mangled names no longer produce APFS `NAME_MAX` errors mid-batch.

#### iOS Simulator Runtime Installer
A new built-in installer downloads and installs missing iOS Simulator runtime
images directly from the app — no more chasing them down through Xcode.

---

### Navigation & windowing

#### Multi-Tab Content Navigation
The main window now hosts multiple content tabs, so several runtime objects
can be open side by side instead of replacing one another in a single pane.

- **Window tab bar** drives content navigation directly from the title bar.
- **Open in New Tab** is available both from content-view type links and from
sidebar rows, with a matching menu icon.
- Each tab keeps its own navigation timeline.
- Tabs are keyed by identity, and **Close Tab / Close Window** with **⌘W**
re-bind against the current key window automatically.

#### Selection History & Navigation Menu
The toolbar drives back / forward navigation off a true selection-history
cursor, the way Xcode and Finder do.

- Picking a sidebar item, drilling into a type, and switching between
Hierarchy / Relationships / Specialization tabs all push onto the same
stack.
- A **Navigation History** menu exposes the full per-tab timeline.
- The buttons enable / disable based on what's actually in the history, so
you never end up clicking back into nothing.
- The active inspector tab is preserved across selections.

#### Smarter Sidebar Expansion
- Single-clicking a non-leaf row now expands it (matching the rest of macOS).
- A new setting caps how deep a double-click expands an entire subtree, so
you can preview a framework without exploding hundreds of children.

#### Sidebar Grouping & Filter Scope
- Runtime objects are grouped into kind sections (classes, protocols,
categories…) instead of a flat list.
- A new **Filter Scope** popover in the sidebar lets you prefilter by kind
and property before typing, so the search corpus stays focused.

#### Quit After Closing Last Window
**Settings → General → Windows** gains a *Quit After Closing Last Window*
toggle. It is off by default, which is the existing behaviour: the app stays
resident with no window open and a Dock icon click brings a window back. Turn
it on to have RuntimeViewer behave like a single-purpose utility that exits
with its last window.

---

### Appearance & editing

#### Theme Settings
A new **Theme** settings panel with a data-driven color model. Pick from
built-in presets (including an Xcode-matching one) or edit colors directly.
Themes drive the content view, inspector, and all syntax-highlighted
surfaces from a single source, and changes apply live.

#### Line Numbers in the Content View
A line-number gutter runs down the left edge of the interface text view,
handy for cross-referencing decompiled or exported output.

#### Content-shaped loading placeholders
Panes that fetch their content asynchronously now stand in a placeholder shaped
like the content it replaces, instead of leaving the previous type's data on
screen until the new one arrives.

The placeholder is deliberately hard to catch sight of. Engine calls that hit a
warm cache return in single-digit milliseconds, so anything drawn the moment
work starts would appear and vanish inside one frame — a flash in its own right.
Work that finishes quickly now shows no placeholder at all, and a placeholder
that does appear stays long enough to read, measured from when it appeared, so
genuinely slow work is never delayed further.

---

### Code injection

#### Injection into strict-seatbelt daemons and Apple applications
System daemons that opt into the strict seatbelt (e.g. `sharingd`, `rapportd`)
can now be injected via a `mach_vm_remap`-based path, which previously refused
to load the runtime dylib. Apple applications, which enforce AMFI library
validation, take the same path — see the corresponding bug fix below.
Everyday injection targets continue to work unchanged; the new path only kicks
in when the standard route is blocked.

---

## Improvements

- Popovers (generation options, MCP status, background-indexing, type
picker) now size themselves to their content instead of defaulting to a
fixed 500×500 box.
- The Specialization type picker no longer blocks the UI when a generic
parameter has a wide constraint (10k+ candidates) — the list is built
off the main thread with a loading indicator, and rows are constructed
lazily as you scroll.
- Sidebar filtering skips the debounce when the query is empty, so
clearing the field feels instantaneous.
- The Mirror menu icon was refreshed and the toolbar no longer shows an
empty overflow chevron on macOS 15 when its items are hidden.

---

## Bug fixes

### Objective-C protocols reappear for UIKitCore, Foundation, and friends

Opening UIKitCore showed an empty protocol list. Foundation showed no
`NSCoding`, `NSCopying`, or `NSFastEnumeration`. Classes, categories, and C
structs were listed normally, so the image itself parsed fine — only protocols
went missing, and they went missing entirely rather than partially.

The cause was a protocol-ownership heuristic. Because the compiler emits a full
`protocol_t` into every image that saw a `@protocol` declaration at compile
time, `__objc_protolist` carries plenty of protocols an image merely imported.
The heuristic dropped a protocol whenever some other carrier of it sat in the
image's transitive dependency closure — which relied on an image never
appearing in its own closure. dyld's upward dependencies
(`LC_LOAD_UPWARD_DYLIB`) exist precisely to express cycles, so
UIKitCore ↔ PrintKitUI/ShareSheet and Foundation ↔ CoreFoundation each put an
image inside its own closure. Every protocol it carried was then classified as
imported, and the listing came back empty.

Nothing in the Mach-O format records a defining image for a protocol, so
attribution can only ever be a heuristic — and this one failed silently, with
no error to hint that anything had been dropped. The filter is gone;
`__objc_protolist` is now listed verbatim. Some imported protocols reappear as
noise you can see and judge for yourself, which is the better failure mode: a
protocol that is silently absent is not.

### Code injection picks its path from the whole rule

Attaching to Music failed silently — nothing loaded on the target side, and no
error was reported. Two causes stacked. Routing between the two injection paths
consulted only the target's sandbox, which catches strict-seatbelt daemons but
says nothing about AMFI library validation; and Music, like every Apple
application, enforces library validation, which refuses a developer-signed
payload regardless of what the sandbox allows. The `dlopen` it was routed onto
could never have loaded anything, while the injector's shellcode reported
success anyway.

The decision now lives in the helper daemon, which consults both the seatbelt
predicate and the code-signing flag, so Apple applications are routed onto the
`mach_vm_remap` path instead of one that cannot work. The investigation — the
`csops` status words separating the two target classes, the controlled
experiment isolating library validation from the payload's signature — is
written up in `Documentations/ResolvedIssues/`.

### The Inspector no longer blanks when you switch types

Clicking from one runtime object to another emptied the Inspector's list and
refilled it once the query returned. Relationships unions its results across
every indexed image, so that gap was long enough to read as a flash rather than
a redraw.

The Inspector had been reusing its view controllers, but it still built a fresh
ViewModel per object and rebound it, which tore down the table's data binding
and installed a fresh one starting from an empty list. The tab ViewModels are
now bound once and handed the new object instead. Re-entering the same object —
closing a tab, moving the history cursor — short-circuits entirely, so it
neither refetches nor flashes a placeholder over content that is already
correct.

This also fixed a real defect hiding behind the visual one: the old load had no
cancellation, so on rapid selection an earlier query could return late and
overwrite a newer object's rows.

### Sidebar selection no longer blinks grey

Two separate causes, both of which repainted the sidebar's selected row in the
inactive grey for a frame or two:

- **Clicking a related type stole keyboard focus.** A row highlight is drawn
unemphasized whenever its table is not the first responder, so the Inspector
taking focus greyed out the sidebar row the user was actually looking at — for
focus the app handed straight back some 50ms later. The Relationships and
Specializations lists exist only to be clicked through, so they now refuse
first responder and paint no highlight of their own. Clicking, selecting and
navigating are unaffected; only arrow-key traversal inside those two lists is
gone, and every arrow press there navigated away anyway.
- **Jumping to a type selected the row before scrolling to it.** An outline view
materializes a row only as it scrolls into view and applies the emphasized
selection style after that, so the target row was drawn once in grey and
flipped to the accent colour on the next frame. Scrolling first — and forcing
the layout the scroll would otherwise defer — puts both in the same frame.

### The tab strip no longer flashes when the tab set changes

Changing the inspected kind rebuilds which Inspector tabs exist — selecting a
protocol after a class drops the Hierarchy tab. The strip used to remove every
tab and add the new set back, which swapped the visible view several times
inside one runloop pass. Tabs that survive the change now keep their view
throughout, and only tabs that genuinely appear or disappear cost anything.

### Clicking the Dock icon reopens a window

AppKit's built-in reopen handling only creates a window when it sees no visible
window at all, and it counts every window the app owns. With Settings or a
Sparkle update alert still on screen, clicking the Dock icon after closing every
document window therefore appeared to do nothing. RuntimeViewer now decides on
document windows alone, restoring a minimized or ordered-out one before falling
back to a new untitled document.

### Attached daemons show their real icon

Attaching to a daemon such as `launchservicesd` left the source menu showing an
App Store placeholder symbol. The icon cache is populated only from Launch
Services registered GUI applications, so a daemon never landed in it and every
lookup fell through to the placeholder. Daemons now fall back to the generic
executable icon — the same one the attach picker already shows for them.

### A second instance no longer reports a phantom MCP server

Binding the MCP bridge port fails whenever another RuntimeViewer instance
already holds it. That failure used to stop at a log line: the losing instance
still wrote the port file, still reported itself as running, and kept its fully
allocated network transport — 28 event loops, 56 threads, roughly 21 MiB —
resident for the lifetime of the process. It now tears the transport down,
truthfully reports itself stopped, and leaves the winning instance's port file
alone. A regression test covers the contention scenario end to end.

### Smaller fixes

- **Bonjour crash on batched receives.** Inspecting an iOS / visionOS /
Apple TV target over Bonjour, or pushing a batch-export through a
Bonjour connection, no longer crashes when a single network read
carries many small frames.
- **Attached-app and Mac Catalyst inspection.** The Inspector no longer
fails with *"An unknown error occurred"* when the runtime engine is
reached through a proxy hop; loading progress now streams across the
hop instead of getting stuck at *"Preparing…"*.
- **Nested sidebar specializations** keep their identity across sidebar
rebuilds instead of collapsing or re-shuffling.
- **Text view re-entry** rebuilds a fresh content view when returning
from another scene, fixing a case where the pane appeared blank.
- **Export module metadata resolution** fixed (#72).
- **The Xcode theme preset** gained a light-mode selection color.
- **The Specialization type picker popover** no longer animates out of place.
- Numerous smaller stability fixes across the sidebar, inspector, and
document lifecycle.

---

## Build & Tooling

- Both package manifests agree on the same `swift-helper-service` floor, so the
two halves of the app can no longer resolve different helper protocols.
- The Core package pins every remote dependency it parses Mach-O with, so a
fresh checkout reproduces a known-good set instead of resolving freely.
- The release process, the documentation index, and the branching model are
written down under `Documentations/`.
- GitHub Releases are titled with the bare version tag.

---

## Platform requirements

- **Main app**: macOS 15+
- **MCP features**: macOS 15+
- **iOS variant**: iOS 18+

---

## Upgrade notes

- **Helper Service reinstalls itself** — the privileged helper daemon moves to
protocol version `1.5.0`. On first launch after upgrade you will be prompted
for admin credentials once and then asked to restart the app.

This applies **even if you were running `v2.1.0-RC.3`**. The daemon-side
change that routes Apple applications onto the `mach_vm_remap` injection path
shipped in RC.3, but the protocol version did not move with it, so an already
installed helper was never replaced and RC.3 users have been running the old
daemon. Bumping it here is what actually delivers that fix.

- **Beta and RC channel users** — this build supersedes `v2.1.0-RC.3`; you can
switch back to the stable channel with no reinstall of the app itself.

- **From v2.0.x** — no manual migration steps. Bookmarks, settings, and
connections carry over as-is.
Loading