From c550169c8360ac79094f6c518e3317f5406f4524 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 2 Nov 2026 15:56:21 -0500 Subject: [PATCH] Add quick review button. --- assets/icons/icons.gresource.xml | 1 + .../actions/chat-message-new-symbolic.svg | 4 ++ src/Pane.zig | 11 ++++ src/View.zig | 50 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 assets/icons/scalable/actions/chat-message-new-symbolic.svg diff --git a/assets/icons/icons.gresource.xml b/assets/icons/icons.gresource.xml index f54bcf4..0814aed 100644 --- a/assets/icons/icons.gresource.xml +++ b/assets/icons/icons.gresource.xml @@ -7,6 +7,7 @@ --> + scalable/actions/chat-message-new-symbolic.svg scalable/actions/document-edit-symbolic.svg scalable/actions/edit-undo-symbolic.svg scalable/actions/emblem-system-symbolic.svg diff --git a/assets/icons/scalable/actions/chat-message-new-symbolic.svg b/assets/icons/scalable/actions/chat-message-new-symbolic.svg new file mode 100644 index 0000000..00c17b9 --- /dev/null +++ b/assets/icons/scalable/actions/chat-message-new-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Pane.zig b/src/Pane.zig index a42e923..380c3b3 100644 --- a/src/Pane.zig +++ b/src/Pane.zig @@ -506,6 +506,13 @@ fn buildHeader(self: *Pane) void { _ = gtk.Button.signals.clicked.connect(review_button, *Pane, &onReviewClicked, self, .{}); header.append(review_button.as(gtk.Widget)); + const agent_review = gtk.Button.newFromIconName("chat-message-new-symbolic"); + agent_review.as(gtk.Widget).addCssClass("flat"); + agent_review.as(gtk.Widget).addCssClass("playpen-pane-button"); + agent_review.as(gtk.Widget).setTooltipText("Have Claude review this tab's changes in the review pane"); + _ = gtk.Button.signals.clicked.connect(agent_review, *Pane, &onAgentReviewClicked, self, .{}); + header.append(agent_review.as(gtk.Widget)); + // Last before close, so the destructive button stays on the end where it // is expected and the zoom toggle sits with the other view controls. self.zoom.as(gtk.Widget).addCssClass("flat"); @@ -765,6 +772,10 @@ fn onReviewClicked(_: *gtk.Button, self: *Pane) callconv(.c) void { self.view.requestReview(); } +fn onAgentReviewClicked(_: *gtk.Button, self: *Pane) callconv(.c) void { + self.view.startAgentReview(self); +} + fn onZoomClicked(_: *gtk.Button, self: *Pane) callconv(.c) void { self.view.toggleZoom(self); } diff --git a/src/View.zig b/src/View.zig index 15e2eae..b6d6631 100644 --- a/src/View.zig +++ b/src/View.zig @@ -355,6 +355,56 @@ pub fn requestReview(self: *View) void { self.on_review(self.ctx); } +/// What the pane header's agent-review button types into the new shell. +const agent_review_command = "claude '/pr-review using /leave-review'"; + +/// Open a Claude Code session beside `from`, in the directory `from` is working +/// in, and have it review the tab's diff into the review pane. +/// +/// `/leave-review` posts to this tab's review pane and stops if there isn't +/// one, so a missing pane is opened first rather than leaving the session to +/// fail on arrival. `from` is focused before that, so the review is bound to +/// the same directory the session starts in. +pub fn startAgentReview(self: *View, from: *Pane) void { + var buf: [std.fs.max_path_bytes]u8 = undefined; + const cwd = self.paneDirectory(from, &buf); + + if (self.reviewPane() == null) { + self.setFocused(from); + self.requestReview(); + } + + self.addPane(.{ .terminal = .{ + .cwd = cwd, + .command = agent_review_command, + } }) catch |err| { + std.log.err("failed to open the agent review: {s}", .{@errorName(err)}); + }; +} + +/// The directory `pane` is working in, copied into `buf`, or empty to leave it +/// to the shell's default. +/// +/// A terminal answers for itself, a review pane with the repository it is bound +/// to; a web pane has no directory, so it borrows the first terminal's in the +/// tab. +fn paneDirectory(self: *View, pane: *Pane, buf: []u8) []const u8 { + switch (pane.content) { + .terminal => |t| if (t.session.cwd(buf)) |dir| return dir, + .review => if (self.review_spec.repo.len > 0) { + const n = @min(buf.len, self.review_spec.repo.len); + @memcpy(buf[0..n], self.review_spec.repo[0..n]); + return buf[0..n]; + }, + .web => {}, + } + for (self.panes.items) |other| { + const t = other.terminal() orelse continue; + if (t.session.cwd(buf)) |dir| return dir; + } + return ""; +} + pub fn closePane(self: *View, pane: *Pane) void { if (self.closing) return;