T24c: Agent, changes

- FileChangeCard: a proposed change to one file, its path and counts,
  Reject and Accept in a slot that keeps presses off the header; opened,
  the diff through a headless git::DiffViewer with its folds opening
- MultiFileDiffReview: every file at once, the total counts, how many are
  reviewed, Reject all and Accept all; verdicts as collab::Decision
- git::DiffViewer gains headless, leaving the file's header to its owner
- CommandExecutionCard is terminal::CommandBlock
- gallery: three proposed files and a command's block
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 08:37:45 -04:00
1 parent 859a9051d8
commit 0a44a2d905
9 files changed
+591 -26

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
use std::{cell::Cell, rc::Rc};
use gpui::{
Context, Entity, IntoElement, KeyBinding, Modifiers, ParentElement, Pixels, Render, Styled,
TestAppContext, VisualTestContext, Window, canvas, div, point, px,
};
use super::{press, settle};
use crate::{
agent::{ChangeState, FileChange, FileChangeCard, MultiFileDiffReview},
collab::Decision,
forms,
primitives::FocusNext,
theme::Theme,
};
fn change(path: &str) -> FileChange {
FileChange {
path: path.to_string().into(),
old: "let lift = 0.12;\n".into(),
new: "let lift = 0.18;\n".into(),
state: ChangeState::Proposed,
}
}
fn setup(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
forms::bind_keys(cx);
cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]);
});
}
/// One change card in a 400px column, its verdicts and the column's height.
struct Card {
verdicts: Vec<bool>,
height: Rc<Cell<Pixels>>,
}
impl Render for Card {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (view, height) = (cx.entity(), self.height.clone());
div()
.relative()
.w(px(400.0))
.child(
FileChangeCard::new("card", change("src/theme/lift.rs")).on_decide(
move |accepted, _, cx| view.update(cx, |card, _| card.verdicts.push(accepted)),
),
)
.child(
canvas(
move |bounds, _, _| height.set(bounds.size.height),
|_, _, _, _| {},
)
.absolute()
.top_0()
.left_0()
.size_full(),
)
}
}
#[gpui::test]
fn a_press_on_accept_decides_and_leaves_the_card_shut(cx: &mut TestAppContext) {
setup(cx);
let height = Rc::new(Cell::new(Pixels::ZERO));
let seen = height.clone();
let (view, cx) = cx.add_window_view(|_, _| Card {
verdicts: Vec::new(),
height: seen,
});
settle(cx);
let shut = height.get();
let accept = point(px(400.0 - 16.0), shut / 2.0);
cx.simulate_mouse_move(accept, None, Modifiers::none());
cx.simulate_click(accept, Modifiers::none());
settle(cx);
assert_eq!(view.read_with(cx, |card, _| card.verdicts.clone()), [true]);
assert_eq!(height.get(), shut, "the press stays with the button");
let chevron = point(px(14.0), shut / 2.0);
cx.simulate_mouse_move(chevron, None, Modifiers::none());
cx.simulate_click(chevron, Modifiers::none());
settle(cx);
assert!(height.get() > shut, "the header opens the diff");
}
/// Three proposed changes under review, and the decisions made.
struct Review {
decisions: Vec<Decision>,
}
impl Render for Review {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
div().w(px(560.0)).child(
MultiFileDiffReview::new("review", ["a.rs", "b.rs", "c.rs"].map(change)).on_decide(
move |decision, _, cx| view.update(cx, |review, _| review.decisions.push(decision)),
),
)
}
}
fn review(cx: &mut TestAppContext) -> (Entity<Review>, &mut VisualTestContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|_, _| Review {
decisions: Vec::new(),
});
cx.update(|window, _| window.activate_window());
settle(cx);
(view, cx)
}
#[gpui::test]
fn accept_all_decides_every_file(cx: &mut TestAppContext) {
let (view, cx) = review(cx);
cx.update(|window, _| window.focus_next());
cx.update(|window, _| window.focus_next());
press("enter", cx);
assert_eq!(
view.read_with(cx, |review, _| review.decisions.clone()),
[Decision::AcceptAll]
);
}
#[gpui::test]
fn a_cards_verdict_names_its_file(cx: &mut TestAppContext) {
let (view, cx) = review(cx);
for _ in 0..8 {
cx.update(|window, _| window.focus_next());
}
press("enter", cx);
assert_eq!(
view.read_with(cx, |review, _| review.decisions.clone()),
[Decision::Accept(1)]
);
}
+158
View File
@@ -0,0 +1,158 @@
use gpui::{
Context, Entity, IntoElement, KeyBinding, KeyUpEvent, Keystroke, ParentElement, Render, Styled,
TestAppContext, VisualTestContext, Window, div,
};
mod changes;
use super::{AgentProgress, Permission, PermissionPrompt, ToolApprovalDialog};
use crate::{forms, primitives::FocusNext, theme::Theme};
/// An approval dialog while open, and the answers it gave.
struct Asking {
open: bool,
answers: Vec<bool>,
}
impl Render for Asking {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
div().size_full().children(self.open.then(|| {
ToolApprovalDialog::new(
"approve",
"write_file",
r#"{ "path": "lift.rs" }"#,
move |approved, _, cx| {
view.update(cx, |asking, cx| {
asking.answers.push(approved);
asking.open = false;
cx.notify();
})
},
)
}))
}
}
pub(super) fn settle(cx: &mut VisualTestContext) {
cx.run_until_parked();
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
/// Presses and releases `key`, a frame apart.
pub(super) fn press(key: &str, cx: &mut VisualTestContext) {
cx.simulate_keystrokes(key);
settle(cx);
cx.simulate_event(KeyUpEvent {
keystroke: Keystroke::parse(key).expect("a key"),
});
settle(cx);
}
fn asking(cx: &mut TestAppContext) -> (Entity<Asking>, &mut VisualTestContext) {
cx.update(|cx| {
Theme::init(cx);
forms::bind_keys(cx);
cx.bind_keys([KeyBinding::new("tab", FocusNext, None)]);
});
let (view, cx) = cx.add_window_view(|_, _| Asking {
open: true,
answers: Vec::new(),
});
cx.update(|window, _| window.activate_window());
settle(cx);
(view, cx)
}
#[gpui::test]
fn escape_denies_the_call(cx: &mut TestAppContext) {
let (view, cx) = asking(cx);
press("escape", cx);
assert_eq!(
view.read_with(cx, |asking, _| asking.answers.clone()),
[false]
);
}
#[gpui::test]
fn run_approves_the_call_once(cx: &mut TestAppContext) {
let (view, cx) = asking(cx);
press("tab", cx);
press("tab", cx);
press("tab", cx);
press("enter", cx);
assert_eq!(
view.read_with(cx, |asking, _| asking.answers.clone()),
[true]
);
}
/// A permission prompt and the answers it gave.
struct Prompting {
answers: Vec<Permission>,
}
impl Render for Prompting {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
div().w_full().child(
PermissionPrompt::new("permission", "Run commands here?", move |answer, _, cx| {
view.update(cx, |prompting, _| prompting.answers.push(answer))
})
.detail("cargo test"),
)
}
}
#[gpui::test]
fn the_prompt_walks_deny_always_then_once(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
forms::bind_keys(cx);
});
let (view, cx) = cx.add_window_view(|_, _| Prompting {
answers: Vec::new(),
});
cx.update(|window, _| window.activate_window());
settle(cx);
for _ in 0..3 {
cx.update(|window, _| window.focus_next());
}
press("enter", cx);
assert_eq!(
view.read_with(cx, |prompting, _| prompting.answers.clone()),
[Permission::Once]
);
}
#[test]
#[should_panic(expected = "progress of 6 in 5")]
fn progress_past_its_total_fails_loud() {
let _ = AgentProgress::new("progress", "Writing", 6, 5);
}
/// A progress bar for a task of one step, `done` or not.
struct Single {
done: usize,
}
impl Render for Single {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.w_full()
.child(AgentProgress::new("single", "The one step", self.done, 1))
}
}
#[gpui::test]
fn a_task_of_one_step_draws_before_and_after(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (view, cx) = cx.add_window_view(|_, _| Single { done: 0 });
settle(cx);
view.update(cx, |single, cx| {
single.done = 1;
cx.notify();
});
settle(cx);
}