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:
1 parent
85b01098a1
commit
1db0a0db44
13 files changed
+817
-33
No files matched your search
@@ -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()
|
||||
}
|
||||
@@ -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.")
|
||||
}))
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user