JIT single-renderer consolidation: agent-hosted live window#196
Merged
Conversation
… 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>
Owner
Author
|
Subagent code review completed (7 finder angles, verified). Outcome: Fixed on this branch ( 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
plus the consolidation pointer.
identifier and swaps the content view per generation, creating it once.
JITRenderWindow(frame + title) isbaked 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).
-[NSApplication run]instead of a bare CFRunLoop spin, so its window actually receives events.
Guarded by an event-delivery probe test.
agent for
preview_switch/preview_configure/ variants instead ofresurrecting the dead dylib window (same mutation + rollback semantics).
flags, SDK override, and dylib; the reloader runs
previewSetUponce peragent process. Fixes a bug
mainhas today where structural reloads stripthe plugin.
Remaining consolidation (follow-ups, not in this PR)
Verification
event delivery, agent-backed switch/configure routing, setup-wrapped agent
render (pixel-asserts the plugin banner).
command suites 46/46 (pre-setup-commit), all serial.
structural edits; badge survives structural reloads.
🤖 Generated with Claude Code