From c3be35006fa1b6417afc5a312fcec4d51aab4bea Mon Sep 17 00:00:00 2001 From: Pranit Date: Wed, 21 Oct 2026 21:57:46 -0400 Subject: [PATCH] Keep approval answers when unrelated redraws are pending - Pressing 1 or 2 on a file approval now applies the answer while fx is redrawing something else, such as a new session title, an MCP server's status, or a subagent's progress. Before, the answer was ignored until that redraw painted. - An approval whose request or change hasn't painted yet still ignores 1 and 2 until it has. --- src/ui/footer/approval_ui.zig | 21 +++++++++++++++++++++ src/ui/render_request.zig | 29 +++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/ui/footer/approval_ui.zig b/src/ui/footer/approval_ui.zig index 166741cc6..660cdf560 100644 --- a/src/ui/footer/approval_ui.zig +++ b/src/ui/footer/approval_ui.zig @@ -2194,6 +2194,27 @@ test "file approval affirmative readiness requires settled committed geometry" { try std.testing.expect(fileApprovalAffirmativeReady(&shell, prompt.projection().?)); } +test "inline file approval stays answerable through footer work but not a pending approval change" { + const alloc = std.testing.allocator; + var prompt = ApprovalPrompt{}; + defer prompt.deinit(alloc); + const preview = diff_mod.FileChangePreview{ .path = "note.txt", .lines = &.{.{ .op = .addition, .new_line = 1, .text = "after" }}, .additions = 1, .deletions = 0, .truncated = false }; + try std.testing.expect(try prompt.syncRequest(alloc, .{ .id = 41, .label = "ignored", .file = .{ .kind = .edit, .intent = .mutation, .preview = preview, .scope = .workspace_files } })); + var shell = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 96, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24 } }; + defer shell.deinit(alloc); + shell.footer_viewport.has_frame = true; + shell.footer_viewport.geometry = .{ .top = 1, .top_divider = 1, .input_base = 2, .bottom_divider = fileApprovalDesiredRows(preview), .hint = fileApprovalDesiredRows(preview) }; + try std.testing.expect(fileApprovalAffirmativeReady(&shell, prompt.projection().?)); + + // A status update, such as a new session title, repaints the footer. + shell.render_requests.request(.footer); + try std.testing.expect(fileApprovalAffirmativeReady(&shell, prompt.projection().?)); + + // A changed request or review asks for a modal frame and must paint first. + shell.render_requests.request(.modal); + try std.testing.expect(!fileApprovalAffirmativeReady(&shell, prompt.projection().?)); +} + test "approval panel renders request context and numbered choices" { var prompt = ApprovalPrompt{}; defer prompt.deinit(std.testing.allocator); diff --git a/src/ui/render_request.zig b/src/ui/render_request.zig index a4644c257..339ca7bcf 100644 --- a/src/ui/render_request.zig +++ b/src/ui/render_request.zig @@ -360,11 +360,13 @@ pub const RenderRequestState = struct { } }; +/// Pending work that can change what a file approval shows. Every change to an +/// approval's request, review, choice, or scroll position requests `.modal`, so +/// footer, transcript, and subagent panel frames only repaint around the +/// approval and must not hold back the user's answer. fn reasonsAffectApproval(reasons: ReasonSet) bool { - var relevant = reasons; - relevant.remove(.animation); - relevant.remove(.notification); - return relevant.count() > 0; + return reasons.contains(.modal) or reasons.contains(.resize) or + reasons.contains(.external_damage) or reasons.contains(.first_frame); } test "compaction revisions request footer repaint without restarting animation" { @@ -624,6 +626,25 @@ test "approval settlement ignores animation and notification presentation work" try std.testing.expect(!state.settledForApproval()); } +test "approval settlement ignores footer, transcript, and subagent panel work" { + var state = RenderRequestState{}; + for ([_]Reason{ .footer, .transcript, .subagent_panel }) |reason| { + state.request(reason); + try std.testing.expect(state.settledForApproval()); + state.clearReason(reason); + } + for ([_]Reason{ .modal, .resize, .external_damage, .first_frame }) |reason| { + state.request(reason); + try std.testing.expect(!state.settledForApproval()); + state.clearReason(reason); + } + + // Unrelated work queued with an approval change still waits for the change. + state.request(.footer); + state.request(.modal); + try std.testing.expect(!state.settledForApproval()); +} + test "animation phase and deadline publish only after a committed attempt" { var state = RenderRequestState{ .animation_visible = true,