Rework GUI backend on Zig/MerJS and fix native regressions - #84
Rework GUI backend on Zig/MerJS and fix native regressions#84pranavp311 wants to merge 67 commits into
Conversation
|
Updated this PR with Codegraff GUI fixes in b269fad (pushed to backend-zig-merjs-regression-fixes):
Validated in Codegraff GUI:
Note: native shell bridge implementation for window.close is committed separately to the MerJS PR (#100), not this Codegraff PR. |
4a68c43 to
d0c065d
Compare
Forward per-message text deltas (message-delta) and request finished/cancelled lifecycle events from the GUI backend (runtime.zig) through SSE to the frontend. Wire the new listeners in useSessionBootstrap and add appendMessageDelta / markRequestFinished handlers to the session store so streaming text and request completion update the UI without a full snapshot round-trip.
…answer panel buildChatThreadItems now emits each contiguous run of tool activity as its own request_work segment in place, so a turn reads [user][work][text][work][text] instead of hoisting all tool work above all text. The last work segment of a scope is flagged isFinalSegment (carries turn timing); the last assistant message of a turn is flagged isFinalAnswer and renders as a distinct answer panel (accent rail + tint + "Answer" eyebrow). Reasoning/thinking blocks use a subtler muted italic tone. Also: per-segment activity keys now include the segment index (fixes a duplicate-key collision across segments in one scope), ChatThread memoizes its items and passes activeRequestIds through, ActivityResultCard gains a tone accent strip, the enter-submit lock is shortened, and the QA mock send_prompt returns a showcase interleaved transcript for visual verification.
.graff/ holds ephemeral harness subagent reports; plans/ and shots/ are local handoff docs and visual-QA screenshots. The already-tracked plans/2026-06-15-gui-port-review-handoff.md stays tracked.
- FinalAnswerMessage: premium minimal card on semantic tokens (hairline accent gradient, faint wash, inset ring, elevation-sm) tracking every preset x light/dark; dropped chunky border box/accent rail/eyebrow. - FollowupComposer: redesigned to match the prompt-input card shell (max-w-3xl, same border/bg/padding). Highlightable options rendered above a labeled 'Notes' box; Enter submits selected option(s) + notes, Shift+Enter newline. Theme-consistent font weights. - QA mock: followup-with-options scenario + respond_followup mock case for visual verification.
This reverts commit c2185ad.
d0c065d to
1727d91
Compare
|
@codegraff-bot You're checked out on this PR's branch. Run |
|
@codegraff-bot please review this PR. Focus on: the MerJS local-path dependency in gui/build.zig.zon (fresh-checkout buildability), the Zig backend runtime + MerJS route table, the new PTY-backed terminal lifecycle, and any native GUI regressions in the reworked shell. |
|
On it — I picked up:
Running on your Codegraff account — I'll post my review here when it's done. |
|
I now have a complete picture. Let me compile the final review. PR Review: GUI Backend Rework (Tauri → Zig + MerJS + PTY Terminal)Branch: 1. CRITICAL — Build/Correctness BlockersC1.
|
| Severity | Count | Key Items |
|---|---|---|
| CRITICAL | 2 | C1: broken merjs dep (can't build), C2: shutdown UAF (crash on exit) |
| MAJOR | 4 | M1: silent SSE disconnect, M2: 63 silent data drops, M3: arena leak, M4: zombie procs |
| MINOR | 5 | Global singleton, fallback env, route dispatch, SSE size, instance guard |
Recommendation: Block merge on C1 (buildability) and C2 (memory safety). Address M1–M4 before any user-facing release. The codebase shows strong architectural intent and the PTY/SSE foundations are good — these are the kinds of issues expected in a large backend rewrite that just need a focused cleanup pass.
🤖 codegraff-bot · ⚡ fusion → mimo-v2.5-pro
💸 $0.351 billed to your Codegraff account · 2680k in (2005k cached) + 57.9k out · 147 calls
|
@codegraff-bot fix the C2 shutdown use-after-free: detached terminal reader threads (terminalReaderMain) and graff turn threads (graffTurnThread) call thread.detach() and then touch rt internals (rt.mutex, rt.terminals, rt.emitSseEvent, rt.allocator) after Shell.run() returns and Runtime.deinit() frees them. Store the std.Thread handles in TerminalSessionState and the graff session state, set closing=true + SIGHUP before deinit, and join every spawned thread during cleanup instead of detaching, so no thread outlives the Runtime. Scope this to the shutdown/thread-lifecycle path only. |
|
On it — I picked up:
Running on your Codegraff account — I'll post my review here when it's done. |
|
I prepared a fix for:
Not pushing — this PR is from a fork, so I can't push to its branch. Patch to apply: diff --git a/gui/src-backend/runtime.zig b/gui/src-backend/runtime.zig
index 6a8fb80..73a67d3 100644
--- a/gui/src-backend/runtime.zig
+++ b/gui/src-backend/runtime.zig
@@ -123,6 +123,7 @@ const TerminalSessionState = struct {
io_mutex: std.Io.Mutex = .init,
closing: std.atomic.Value(bool) = .init(false),
exited: std.atomic.Value(bool) = .init(false),
+ reader_thread: ?std.Thread = null,
};
const GraffSession = struct {
@@ -162,6 +163,8 @@ pub const Runtime = struct {
arena_state: std.heap.ArenaAllocator,
arena: std.mem.Allocator,
mutex: std.Io.Mutex = .init,
+ closing: std.atomic.Value(bool) = .init(false),
+ pending_threads: std.ArrayList(std.Thread) = .empty,
version: std.atomic.Value(u64) = .init(1),
workspaces: std.ArrayList(Workspace) = .empty,
saved_workspaces: std.ArrayList(SavedWorkspace) = .empty,
@@ -208,11 +211,33 @@ pub const Runtime = struct {
}
pub fn deinit(self: *Runtime) void {
- var it = self.terminals.valueIterator();
- while (it.next()) |session| closeTerminalSession(session.*);
+ // Phase 1: signal shutdown so threads bail out of hot loops.
+ self.closing.store(true, .release);
+
+ // Phase 2: close every terminal (sets closing=true, sends SIGHUP,
+ // closes the PTY master fd so terminalReaderMain unblocks) and kill
+ // every graff child (so graffTurnThread sees EOF on stdout).
+ {
+ var it = self.terminals.valueIterator();
+ while (it.next()) |session| closeTerminalSession(session.*);
+ }
+ {
+ var graff_it = self.graff_sessions.valueIterator();
+ while (graff_it.next()) |session| self.closeGraffSession(session.*);
+ }
+
+ // Phase 3: join every thread we ever spawned. Threads that already
+ // exited return immediately from join(); blocked threads are unblocked
+ // by the close/kill above. The Runtime is still fully alive at this
+ // point, so any final `self.mutex` / `self.terminals` / `self.arena`
+ // access inside the threads is safe.
+ for (self.pending_threads.items) |thread| {
+ thread.join();
+ }
+ self.pending_threads.deinit(self.allocator);
+
+ // Phase 4: tear down data structures — no thread can touch them now.
self.terminals.deinit();
- var graff_it = self.graff_sessions.valueIterator();
- while (graff_it.next()) |session| self.closeGraffSession(session.*);
self.graff_sessions.deinit();
self.active_children.deinit();
var cache_it = self.runtime_status_cache.iterator();
@@ -578,7 +603,9 @@ pub const Runtime = struct {
self.mutex.unlock(mer_runtime.io);
return self.jsonResponse(req, self.snapshotJson(req.allocator) catch return oom());
};
- thread.detach();
+ self.mutex.lockUncancelable(mer_runtime.io);
+ self.pending_threads.append(self.allocator, thread) catch {};
+ self.mutex.unlock(mer_runtime.io);
return self.jsonResponse(req, self.snapshotJson(req.allocator) catch return oom());
}
@@ -1486,7 +1513,10 @@ pub const Runtime = struct {
closeTerminalSession(session);
return mer.Response.init(.internal_server_error, .json, "{\"error\":\"failed to start terminal reader\"}");
};
- thread.detach();
+ session.reader_thread = thread;
+ self.mutex.lockUncancelable(mer_runtime.io);
+ self.pending_threads.append(self.allocator, thread) catch {};
+ self.mutex.unlock(mer_runtime.io);
return terminalSessionJson(req.allocator, terminal_id, session.instance_id, workspace, shell, cols, rows, "", false, 0);
}
@@ -1704,6 +1734,7 @@ pub const Runtime = struct {
}
fn ensureGraffSession(self: *Runtime, conversation_id: []const u8, session_name: []const u8, workspace: []const u8, agent_id: []const u8, plan_mode: bool, provider: ?[]const u8, model: ?[]const u8) !*GraffSession {
+ if (self.closing.load(.acquire)) return error.RuntimeShuttingDown;
const io = mer_runtime.io;
const desired_yolo = !plan_mode and std.mem.eql(u8, agent_id, "forge");
self.mutex.lockUncancelable(io);🤖 codegraff-bot · ⚡ fusion → mimo-v2.5-pro |
Summary
This PR continues the GUI/backend migration work by replacing the old Tauri/Rust backend path with the Zig + MerJS native shell backend and fixes regressions found while dogfooding the native GUI.
Important
Merge/release blocker: MerJS is not yet available as a normal dependency.
The GUI native shell currently depends on a local MerJS checkout via
gui/build.zig.zon:That means this PR builds on the author machine because a sibling
merjscheckout exists, but a fresh Codegraff checkout will not build the native GUI unless the reviewer also has the same local MerJS checkout.Before this PR can be considered merge/release-ready, we should do one of:
gui/build.zig.zonto a pinned.url+.hashdependency.Until one of the above happens, treat this PR as dependent on the unmerged MerJS work.
Backend + native shell rework
gui/build.zigandgui/native/main.zig.gui/src-backend/runtime.zig) and MerJS route table (gui/routes.zig,gui/api/stub.zig).gui/src-tauribackend from the GUI runtime path.graff update.GUI/session regressions fixed
Terminal work
openptypieces locally undergui/src-backend/ptyso Codegraff does not depend on an external Zig package for PTY support.openpty:terminal-outputevents;TIOCSWINSZ;claude/codex.Harness invocation fixes
graff --jsonprotocol.--resume <session>CLI args from GUI-launchedgraffchildren.set_modelrequest shape:{ "type": "set_model", "name": "provider/model" }.Verification
Ran:
Manual / visual checks:
graff --jsonwithout the old--resumefailure.claude/codexstyle TUI output renders correctly after terminal sizing/raw output fixes.