feat: record agent approvals

This commit is contained in:
Georg Bauer
2026-08-02 09:47:28 +02:00
parent 94ed73e235
commit 118a9d41e0
11 changed files with 154 additions and 19 deletions

View File

@@ -62,6 +62,7 @@ pub(crate) struct ChatMessage {
pub(super) reasoning_open: bool,
pub(super) content: String,
pub(super) model_content: Option<String>,
pub(super) tool_approval_reasons: Vec<Option<String>>,
pub(super) markdown: markdown::Content,
pub(super) transcript: text_editor::Content,
pub(super) a2ui_lines_processed: usize,
@@ -229,6 +230,11 @@ impl From<StoredMessage> for ChatMessage {
reasoning_open: false,
content: message.content,
model_content: message.model_content,
tool_approval_reasons: message
.tool_approval_reasons
.as_deref()
.and_then(|reasons| serde_json::from_str(reasons).ok())
.unwrap_or_default(),
markdown: iced::widget::markdown::Content::new(),
transcript: iced::widget::text_editor::Content::new(),
a2ui_lines_processed: 0,
@@ -789,6 +795,26 @@ impl App {
self.pending_tool_approval = Some((prompt, decision));
self.activity = Some(format!("Tool {} · Awaiting approval", index + 1));
}
crate::agent::ToolEvent::ApprovalReason { index, reason } => {
if let Some(card) = self.tool_cards.get_mut(index) {
card.approval_reason = Some(reason);
}
let reasons = self
.tool_cards
.iter()
.map(|card| card.approval_reason.clone())
.collect::<Vec<_>>();
if let Some(message) = self.conversation.last_mut() {
message.tool_approval_reasons = reasons.clone();
if let Some(database) = &mut self.database
&& let Err(error) = database
.update_message_tool_approval_reasons(message.id, &reasons)
{
self.error =
Some(format!("Could not save tool approval reasons: {error}"));
}
}
}
}
}
}
@@ -1961,6 +1987,7 @@ mod tests {
reasoning_open: false,
content: content.to_owned(),
model_content: None,
tool_approval_reasons: Vec::new(),
markdown: iced::widget::markdown::Content::new(),
transcript: iced::widget::text_editor::Content::new(),
a2ui_lines_processed: 0,
@@ -2156,6 +2183,7 @@ mod tests {
content: "### Core / Setup\n\n| File | Lines |\n|---|---:|\n| `src/app.rs` | **1,750** |\n| `src/engine.rs` | 2,400 |\n\n### Summary\n\nDone."
.to_owned(),
model_content: None,
tool_approval_reasons: Vec::new(),
markdown: iced::widget::markdown::Content::new(),
transcript: iced::widget::text_editor::Content::new(),
a2ui_lines_processed: 0,
@@ -2223,6 +2251,7 @@ mod tests {
reasoning_open: false,
content: format!("message {id}"),
model_content: None,
tool_approval_reasons: Vec::new(),
markdown: iced::widget::markdown::Content::new(),
transcript: iced::widget::text_editor::Content::new(),
a2ui_lines_processed: 0,
@@ -2262,6 +2291,7 @@ mod tests {
reasoning_open: false,
content: format!("message {id}"),
model_content: None,
tool_approval_reasons: Vec::new(),
markdown: iced::widget::markdown::Content::new(),
transcript: iced::widget::text_editor::Content::new(),
a2ui_lines_processed: 0,

View File

@@ -40,6 +40,7 @@ const ICON_MORE: &[u8] = include_bytes!("../../assets/icons/more.svg");
const ICON_PAPERCLIP: &[u8] = include_bytes!("../../assets/icons/paperclip.svg");
const ICON_SEND: &[u8] = include_bytes!("../../assets/icons/send.svg");
const ICON_MODEL: &[u8] = include_bytes!("../../assets/icons/model.svg");
const ICON_ROBOT: &[u8] = include_bytes!("../../assets/icons/robot.svg");
const ICON_SPARK: &[u8] = include_bytes!("../../assets/icons/spark.svg");
const ICON_PIN: &[u8] = include_bytes!("../../assets/icons/pin.svg");
const ICON_SIDEBAR: &[u8] = include_bytes!("../../assets/icons/sidebar.svg");

View File

@@ -81,6 +81,7 @@ impl App {
self.config.model,
&self.conversation[index - 1].content,
None,
&self.conversation[index - 1].tool_approval_reasons,
)
.is_empty()
{
@@ -191,6 +192,7 @@ impl App {
self.config.model,
&message.content,
stored_result,
&message.tool_approval_reasons,
)
};
if !cards.is_empty() {
@@ -540,9 +542,25 @@ fn tool_cards(cards: Vec<crate::agent::ToolCard>) -> Element<'static, Message> {
.gap(6),
);
}
let mut name = row![text(card.call.name).size(13)]
.spacing(5)
.align_y(Alignment::Center);
if let Some(reason) = &card.approval_reason {
name = name.push(
tooltip(
icon(ICON_ROBOT, 13),
container(text(reason.clone()).size(12))
.padding(8)
.max_width(320)
.style(preference_group_style),
tooltip::Position::Top,
)
.gap(5),
);
}
let mut content = column![
row![
text(card.call.name).size(13),
name,
Space::new().width(Length::Fill),
text(card.state.label()).size(11).color(muted_text()),
actions,

View File

@@ -825,6 +825,7 @@ mod tests {
reasoning_open: false,
content: content.to_owned(),
model_content: None,
tool_approval_reasons: Vec::new(),
markdown: markdown::Content::new(),
transcript: text_editor::Content::new(),
a2ui_lines_processed: 0,