T24a: Agent, tool calls

- ToolCallCard: a step mark, the tool in mono, what it touched and how long
  it took; a press opens its arguments and result as code, an error in red
- ToolCallGroup folds several calls under one line
- ToolApprovalDialog over overlays::Dialog: Deny or Run, one answer; Escape,
  the scrim and the close deny
- PermissionPrompt: deny, always allow or allow once, in place
- chat::StepState gains Failed; chat::step_mark draws a step's mark for
  search steps and tool calls alike
- typography::format::took reads ms, seconds or minutes
- gallery: an Agent page with calls in each state, a group, the dialog and
  the prompt
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 08:00:21 -04:00
1 parent 85b01098a1
commit 1db0a0db44
13 files changed
+817 -33

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
mod tools;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 24,
slug: "agent",
title: "Agent",
summary: "An assistant at work: the tools it calls and what it asks before it acts.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::DownAt("agent-call", 60.0, 18.0),
Step::UpAt("agent-call", 60.0, 18.0),
Step::Wait(300),
Step::Shot("tool-call"),
Step::Click("agent-approve-open"),
Step::Wait(300),
Step::Shot("approval"),
Step::Key("escape"),
Step::Wait(200),
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(tools::calls(window, cx))
.child(tools::approval(window, cx))
.into_any_element()
}
+131
View File
@@ -0,0 +1,131 @@
use std::time::Duration;
use ely_gpui_component::{
agent::{Permission, PermissionPrompt, ToolApprovalDialog, ToolCallCard, ToolCallGroup},
buttons::{Button, ButtonVariant},
chat::StepState,
theme::{ActiveTheme, TextSize},
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{keep, section, set},
};
const READ: &str = r#"{ "path": "src/theme/palette.rs" }"#;
const PALETTE: &str = "pub const ACCENT_LIFT: f32 = 0.18;\npub const SURFACE_LIFT: f32 = 0.04;";
const SEARCH: &str = "{\n \"query\": \"oklch lift contrast\",\n \"limit\": 5\n}";
const TESTS: &str = r#"{ "command": "cargo test --lib theme" }"#;
const WRITE: &str =
"{\n \"path\": \"src/theme/lift.rs\",\n \"content\": \"pub fn lift(color, by) -> Hsla\"\n}";
pub fn calls(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
section(
"ToolCallCard / ToolCallGroup",
"Each call an assistant makes: a mark for where it stands, the tool, what it touched and how long it took; a press opens its arguments and what came back. Several calls fold under one line.",
cx,
)
.child(
div()
.w(px(560.))
.flex()
.flex_col()
.gap_2()
.child(probe(
"agent-call",
div().w(px(560.)).child(
ToolCallCard::new("agent-read", "read_file", StepState::Done)
.summary("src/theme/palette.rs")
.took(Duration::from_millis(340))
.arguments(READ)
.result(PALETTE),
),
))
.child(
ToolCallCard::new("agent-search", "web_search", StepState::Working)
.summary("oklch lift contrast")
.arguments(SEARCH),
)
.child(
ToolCallCard::new("agent-tests", "run_tests", StepState::Failed)
.summary("cargo test --lib theme")
.took(Duration::from_millis(12_400))
.arguments(TESTS)
.result("2 tests failed: lift_keeps_hue, dark_lifts_further"),
)
.child(
ToolCallCard::new("agent-write", "write_file", StepState::Waiting)
.summary("src/theme/lift.rs")
.arguments(WRITE),
)
.child(
ToolCallGroup::new("agent-group", "Read 3 files")
.child(ToolCallCard::new("agent-group-1", "read_file", StepState::Done).summary("src/theme/mod.rs").took(Duration::from_millis(120)))
.child(ToolCallCard::new("agent-group-2", "read_file", StepState::Done).summary("src/theme/tokens.rs").took(Duration::from_millis(95)))
.child(ToolCallCard::new("agent-group-3", "read_file", StepState::Done).summary("src/motion/curve.rs").took(Duration::from_millis(80))),
),
)
}
pub fn approval(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let asking = keep("agent-asking", || false, window, cx);
let heard = keep("agent-heard", || None::<SharedString>, window, cx);
let allowed = keep("agent-allowed", || None::<Permission>, window, cx);
let (open, now_heard, now_allowed) =
(*asking.read(cx), heard.read(cx).clone(), *allowed.read(cx));
let (opener, closer) = (asking.clone(), asking);
let theme = cx.theme();
let note = |text: String| {
div()
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg_muted)
.child(text)
};
section(
"ToolApprovalDialog / PermissionPrompt",
"Asking first: a dialog before one call runs, with its arguments, where Escape denies; and a prompt in place that allows once, always, or not at all.",
cx,
)
.child(
div()
.flex()
.items_center()
.gap_3()
.child(probe(
"agent-approve-open",
Button::new("agent-approve", "Review the call…")
.variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| set(&opener, true, cx)),
))
.children(now_heard.map(|text| note(text.to_string()))),
)
.child(
div()
.w(px(560.))
.flex()
.flex_col()
.gap_2()
.child(
PermissionPrompt::new("agent-permission", "Let the assistant run commands in this folder?", move |answer, _, cx| {
set(&allowed, Some(answer), cx)
})
.detail("cargo test --lib theme"),
)
.children(now_allowed.map(|answer| {
note(match answer {
Permission::Once => "Allowed once.".to_string(),
Permission::Always => "Always allowed in this folder.".to_string(),
Permission::Deny => "Denied.".to_string(),
})
})),
)
.children(open.then(|| {
ToolApprovalDialog::new("agent-approval", "write_file", WRITE, move |approved, _, cx| {
let verdict = if approved { "Ran write_file." } else { "Denied write_file." };
set(&heard, Some(verdict.into()), cx);
set(&closer, false, cx)
})
.detail("Writes a new file in src/theme.")
}))
}
+2
View File
@@ -1,3 +1,4 @@
mod agent;
mod buttons;
mod charts;
mod chat;
@@ -61,6 +62,7 @@ pub const ALL: &[Page] = &[
documents::PAGE,
collab::PAGE,
chat::PAGE,
agent::PAGE,
theme::PAGE,
];