Files
Ely-GPUI-Components/src/agent/permission.rs
T
ZacharyZhang-NY 37e53a5ecb T24f Agent: control and cost
HumanInputRequest asks a question with answers to pick and a line to
write one; CostBreakdown totals a session and splits it by part.
InterruptButton, TokenUsageChart, TraceViewer and EvalResultTable
point to chat::stop_button, a stacked BarChart, TimelineProfiler and
DataTable.

UsageBar shows amounts as its owner writes them. MemoryPanel and
HumanInputRequest share agent::line::send_line; the chapter's bool
callbacks take forms::OnFlag.

DataTable scrolls columns wider than its box sideways, pinned or not,
and leaves a plain wheel to the page; sortable headers are Tab stops.
TimelineProfiler fits its ruler to its lane, its spans are Tab stops,
and a picked span takes accent.
2026-09-26 11:39:24 -04:00

210 lines
6.7 KiB
Rust

use std::{cell::Cell, rc::Rc};
use gpui::{
App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
Window, div, prelude::*,
};
use crate::{
buttons::{Button, ButtonVariant},
chat::CodeBlock,
forms::OnFlag,
overlays::Dialog,
primitives::{Icon, IconName},
theme::{ActiveTheme, IconSize, Radius, TextSize},
};
/// Asks before a tool runs: what it will do and its arguments, then Deny or Run. Escape, the scrim and Deny refuse it. Render it while the call waits.
#[derive(IntoElement)]
pub struct ToolApprovalDialog {
id: ElementId,
tool: SharedString,
arguments: SharedString,
detail: Option<SharedString>,
on_answer: OnFlag,
}
impl ToolApprovalDialog {
/// `arguments` as JSON; `on_answer` gets whether the call may run.
pub fn new(
id: impl Into<ElementId>,
tool: impl Into<SharedString>,
arguments: impl Into<SharedString>,
on_answer: impl Fn(bool, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
tool: tool.into(),
arguments: arguments.into(),
detail: None,
on_answer: Rc::new(on_answer),
}
}
/// A line on what the call will do.
pub fn detail(mut self, text: impl Into<SharedString>) -> Self {
self.detail = Some(text.into());
self
}
}
impl RenderOnce for ToolApprovalDialog {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let approved = Rc::new(Cell::new(false));
let (answer, tool, chosen) = (self.on_answer, self.tool.clone(), approved.clone());
let (deny, run) = ((self.id.clone(), "deny"), (self.id.clone(), "run"));
Dialog::new(
self.id.clone(),
format!("Run {}?", self.tool),
move |window, cx| {
let approved = chosen.get();
log::info!(
"tool approval: {tool} {}",
if approved { "runs" } else { "denied" }
);
answer(approved, window, cx)
},
)
.when_some(self.detail, |dialog, detail| dialog.detail(detail))
.child(CodeBlock::new((self.id.clone(), "arguments"), self.arguments).language("json"))
.action(move |close| {
Button::new(deny, "Deny")
.variant(ButtonVariant::Secondary)
.on_click(move |_, window, cx| close(window, cx))
})
.action(move |close| {
Button::new(run, "Run")
.variant(ButtonVariant::Primary)
.on_click(move |_, window, cx| {
approved.set(true);
close(window, cx)
})
})
}
}
/// An answer to a request for permission.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Permission {
Once,
Always,
Deny,
}
type OnPermission = Rc<dyn Fn(Permission, &mut Window, &mut App)>;
/// Asks in place whether the agent may do something: allow once, always allow, or deny.
#[derive(IntoElement)]
pub struct PermissionPrompt {
id: ElementId,
title: SharedString,
detail: Option<SharedString>,
on_answer: OnPermission,
}
impl PermissionPrompt {
pub fn new(
id: impl Into<ElementId>,
title: impl Into<SharedString>,
on_answer: impl Fn(Permission, &mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
title: title.into(),
detail: None,
on_answer: Rc::new(on_answer),
}
}
/// What the permission covers, such as the command or the folder.
pub fn detail(mut self, text: impl Into<SharedString>) -> Self {
self.detail = Some(text.into());
self
}
}
impl RenderOnce for PermissionPrompt {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let choice = |key: &'static str, label: &'static str, variant, answer: Permission| {
let on_answer = self.on_answer.clone();
Button::new((self.id.clone(), key), label)
.variant(variant)
.on_click(move |_, window, cx| {
log::info!("permission: {answer:?}");
on_answer(answer, window, cx)
})
};
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.p_4()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(colors.border)
.bg(colors.surface)
.text_size(theme.text_size(TextSize::Sm))
.child(
div()
.flex()
.items_center()
.gap_2()
.child(
Icon::new(IconName::Shield)
.size(IconSize::Sm)
.color(colors.fg_muted),
)
.child(
div()
.flex_1()
.min_w_0()
.font_weight(FontWeight::MEDIUM)
.text_color(colors.fg)
.child(self.title.clone()),
),
)
.children(self.detail.clone().map(|detail| {
div().w_full().flex().child(
div()
.flex_1()
.min_w_0()
.px_2()
.py_1p5()
.rounded(theme.radius(Radius::Md))
.bg(colors.sunken)
.font_family(theme.mono_family.clone())
.text_color(colors.fg_muted)
.child(detail),
)
}))
.child(
div()
.flex()
.flex_wrap()
.justify_end()
.gap_2()
.child(choice(
"deny",
"Deny",
ButtonVariant::Ghost,
Permission::Deny,
))
.child(choice(
"always",
"Always allow",
ButtonVariant::Secondary,
Permission::Always,
))
.child(choice(
"once",
"Allow once",
ButtonVariant::Primary,
Permission::Once,
)),
)
}
}