Skip to content

JIT single-renderer consolidation: agent-hosted live window#196

Merged
obj-p merged 8 commits into
mainfrom
jit-single-renderer
Jun 10, 2026
Merged

JIT single-renderer consolidation: agent-hosted live window#196
obj-p merged 8 commits into
mainfrom
jit-single-renderer

Conversation

@obj-p

@obj-p obj-p commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Problem

After Phase 3, a session had two renderers: the daemon's dylib window and the
JIT agent's off-screen PNG render. The seams between them produced a frozen
visible window after the first structural edit, stale-snapshot bugs, and a
setup plugin that silently vanished on every structural reload.

Changes (in commit order)

  1. Docs: record the agent-render fixes and merge-bar results from JIT: hot-reload non-leaf files via single-module incremental split #194,
    plus the consolidation pointer.
  2. Persistent agent window: the render entry looks its window up by
    identifier and swaps the content view per generation, creating it once.
  3. On-screen handoff: an optional JITRenderWindow (frame + title) is
    baked into the bridge. On a visible session the daemon bakes its window's
    frame into the structural reload and orders its dylib window out after the
    first successful agent render. The agent window is the interactive surface
    from then on. Spec applies only at creation, so drags/resizes survive leaf
    edits (respawn handover tracked in JIT agent window: frame handover across respawns (drags/resizes lost on non-leaf edits) #195).
  4. Interactivity: the agent main thread runs -[NSApplication run]
    instead of a bare CFRunLoop spin, so its window actually receives events.
    Guarded by an event-delivery probe test.
  5. Switch/configure routing: agent-backed sessions re-render through the
    agent for preview_switch / preview_configure / variants instead of
    resurrecting the dead dylib window (same mutation + rollback semantics).
  6. Setup plugin through the agent: the JIT compile carries the setup wrap,
    flags, SDK override, and dylib; the reloader runs previewSetUp once per
    agent process. Fixes a bug main has today where structural reloads strip
    the plugin.

Remaining consolidation (follow-ups, not in this PR)

Verification

  • New tests: window reuse across generations, window-spec application, AppKit
    event delivery, agent-backed switch/configure routing, setup-wrapped agent
    render (pixel-asserts the plugin banner).
  • Suites: JIT 55/55, Core/MacOS/Engine units 321/321, MCP macOS 7/7, CLI
    command suites 46/46 (pre-setup-commit), all serial.
  • Manual: live SPM session with interactive agent window across literal and
    structural edits; badge survives structural reloads.

🤖 Generated with Claude Code

obj-p and others added 7 commits June 9, 2026 20:27
… pointer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First step of single-renderer consolidation. The render entry created
a fresh off-screen NSWindow per call and ordered it out afterward, so
the agent had no live surface to keep between edits. The entry now
looks the window up by identifier in the agent's window list and swaps
its content view, creating it only on the first render. AppKit state
is process-wide, so the window survives JITDylib generations; later
steps point session start, switch, and configure at this window and
retire the daemon's dylib window for JIT builds.

bridgeReusesPreviewWindowAcrossGenerations renders two real bridge
builds through one agent and probes from a third generation that
exactly one identified window exists, with each render's pixels
reflecting its own generation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second step of single-renderer consolidation. An optional
JITRenderWindow (frame + title) is baked into the render entry: when
present the agent creates its persistent window titled and on screen
at the daemon window's frame instead of borderless off-screen, with
sizingOptions cleared so SwiftUI's ideal size cannot resize it away
from the requested frame. The spec applies only at window creation, so
a user's drag or resize survives leaf edits. On a visible session the
daemon bakes its window's frame into the structural reload and orders
its own dylib window out after the first successful agent render — the
agent window is the interactive surface from then on. Headless
sessions and tests pass no spec and keep the off-screen behavior.

bridgeAppliesWindowSpecOnCreation drives a real bridge build through
the agent and probes title, content size, and titled style from a
second generation. Origin is not asserted: AppKit constrains titled
windows onto a screen, which is the desired behavior for real frames.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The agent's main thread spun CFRunLoopRunInMode, which drains the main
dispatch queue and draws but never dequeues window-server events, so
the agent's on-screen window painted without responding to clicks.
The tail of main now starts -[NSApplication run] via the ObjC runtime
(AppKit is already dlopen'd at startup), with the CF spin kept as the
no-AppKit fallback. run_on_main's dispatch_sync target still drains
under the event loop.

agentDispatchesAppKitEvents guards it: a local monitor plus a posted
applicationDefined event only observes delivery when the event loop is
actually pumping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sessions

Third step of single-renderer consolidation. Once a session hands its
window to the agent, preview_switch / preview_configure / variants'
setTraits went down the dylib path: recompile, loadPreview into the
daemon's ordered-out window, agentImagePaths cleared — resurrecting the
dead window as a second, stale surface and flipping snapshots away
from the agent.

MacOSPreviewHandle now detects the agent-backed state (reloader
present + agent snapshot recorded) and re-renders through the agent
instead: PreviewSession gains JIT counterparts of switchPreview /
reconfigure / setTraits (same mutation and rollback semantics,
compiled via compileObjectForJIT), and PreviewHost splits its reload
into agentWindowSpec(for:) + jitRender(sessionID:build:) so the handle
can compose them. Non-agent sessions and non-JIT builds keep the dylib
path unchanged.

Covered by agentBackedSwitchAndConfigureRouteThroughReloader: switch
and reconfigure on an agent-backed handle render through the reloader,
keep the agent PNG authoritative, reject an invalid index without a
render, and switch cleanly afterward.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every structural reload silently dropped the setup plugin: the agent
render compiled without the setup wrap, never ran previewSetUp, and
never loaded the setup dylib, so plugin-injected UI and state vanished
the moment a session went agent-backed (on main too).

compileObjectForJIT now passes setupModule/setupType into the bridge
(wrap + previewSetUp entry generated), appends the setup compiler
flags, inherits the setup SDK override (the issue #170 guard, added to
compileObject and compileModuleIncremental), prepends the setup dylib
to the build's dylibPaths, and reports the setup entry on
JITRenderBuild. JITStructuralReloader runs that entry once per agent
process — re-run after a respawn, skipped per generation and on
literal re-renders. PreviewStartHandler forwards the setup dylib path
into the session.

splitRendersSetupWrappedPreviewInAgent renders the SPM example's
Summary preview with the real ToDoPreviewSetup plugin through the
reloader and asserts the plugin banner's pixels appear in the agent
PNG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From the PR #196 code review:

- Setup validation is one predicate (BridgeGenerator.isUsableSetup),
  so a build can no longer advertise a previewSetUp entry the
  generated source omitted (non-identifier module/type names made
  every reload fail symbol-not-found).
- The stable module compiles under the same SDK as the editable
  overlay (emitStableModule gains overrideSDK), and the issue-#170
  stale-SDK guard now covers compileObject and
  compileModuleIncremental via a shared Compiler.resolveSDK.
- Generated string literals escape control characters as well as
  quotes/backslashes (a newline-bearing window title or path no
  longer breaks the bridge compile), applied to title and both
  baked paths.
- The agent only runs -[NSApplication run] when an Aqua session is
  reachable (CGSessionCopyCurrentDictionary), keeping the CFRunLoop
  fallback live for SSH/launchd contexts.
- compileObjectForJIT generates the bridge exactly once (the
  non-leaf path no longer builds a discarded twin, removing the
  literals-vs-compiled-source divergence risk and a duplicate
  source transform per edit).
- MacOSPreviewHandle routes all mutating reloads through one
  reload(jit:dylib:) seam, and PreviewSession's dylib/JIT twins
  share withPreviewIndex/applyReconfigure so rollback and merge
  semantics cannot diverge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@obj-p

obj-p commented Jun 10, 2026

Copy link
Copy Markdown
Owner Author

Subagent code review completed (7 finder angles, verified). Outcome:

Fixed on this branch (88d29ff): shared setup validation (isUsableSetup), stable-module SDK consistency + #170 guard in all compile paths, control-character escaping in generated literals, Aqua-session guard before [NSApp run], single bridge generation per edit, one routing seam in the handle, shared mutation helpers.

Deferred with tracking issues: #195 (frame handover across respawns), #197 (multi-session: shared agent window, zombie window on stop, per-process setup flag — to be designed into the start-through-agent phase).

Refuted: render RPCs hanging during live resize (tracking mode is a common run-loop mode, the main queue drains).

🤖 Generated with Claude Code

@obj-p obj-p merged commit 2f8bcc0 into main Jun 10, 2026
@obj-p obj-p deleted the jit-single-renderer branch June 10, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant