Harden local tool execution
This commit is contained in:
26
PLAN.md
26
PLAN.md
@@ -27,6 +27,13 @@ execution targets one self-contained Mac.
|
|||||||
starting tool set, unlimited tool rounds, queued user guidance between tool
|
starting tool set, unlimited tool rounds, queued user guidance between tool
|
||||||
rounds, session date/time context, periodic tool-contract reminders,
|
rounds, session date/time context, periodic tool-contract reminders,
|
||||||
cooperative Stop, and explicit activity/failure states are implemented.
|
cooperative Stop, and explicit activity/failure states are implemented.
|
||||||
|
- Local tools are hardened for daily use: canonical project boundaries reject
|
||||||
|
parent and symlink escapes, shell commands receive a deliberate environment,
|
||||||
|
risky shell and visible-browser actions share one cancellable Allow once/Deny
|
||||||
|
approval path, and compact tool cards expose bounded parameters, results, and
|
||||||
|
parsing/approval/queue/run/completion lifecycle state without showing DSML.
|
||||||
|
Background jobs and bounded output files stop and clean up with Stop, session
|
||||||
|
switches, and application shutdown.
|
||||||
- Context compaction uses the reference soft and exact token-counted hard
|
- Context compaction uses the reference soft and exact token-counted hard
|
||||||
triggers, private live-model summaries, bounded summary and tool-result
|
triggers, private live-model summaries, bounded summary and tool-result
|
||||||
retries, a recent verbatim tail, running-job observations, and compatible KV
|
retries, a recent verbatim tail, running-job observations, and compatible KV
|
||||||
@@ -38,21 +45,20 @@ execution targets one self-contained Mac.
|
|||||||
- Focused coverage exercises triggers, summary bounds and sanitizing, tail
|
- Focused coverage exercises triggers, summary bounds and sanitizing, tail
|
||||||
selection, queued guidance, checkpoint identity, running jobs, durable
|
selection, queued guidance, checkpoint identity, running jobs, durable
|
||||||
compaction markers, relaunch, and continued tool work after rebuild.
|
compaction markers, relaunch, and continued tool work after rebuild.
|
||||||
- The next baseline gap is tool hardening and safety. SSD streaming,
|
- The next baseline gap is SSD streaming. Speculative decoding, steering, GLM
|
||||||
speculative decoding, steering, GLM 5.2 execution, and DeepSeek V4 Pro
|
5.2 execution, and DeepSeek V4 Pro execution are not implemented in the Rust
|
||||||
execution are not implemented in the Rust executor. Related catalog,
|
executor. Related catalog, validation, and preference plumbing must not be
|
||||||
validation, and preference plumbing must not be treated as runtime support.
|
treated as runtime support.
|
||||||
|
|
||||||
## Delivery order
|
## Delivery order
|
||||||
|
|
||||||
1. **Next:** tool hardening, approvals, and productive tool presentation.
|
1. **Next:** remaining DS4 execution technology, starting with SSD streaming, then
|
||||||
2. Remaining DS4 execution technology, starting with SSD streaming, then
|
|
||||||
speculative decoding and the other Metal/runtime parity work.
|
speculative decoding and the other Metal/runtime parity work.
|
||||||
3. Additional model execution: GLM 5.2 and DeepSeek V4 Pro.
|
2. Additional model execution: GLM 5.2 and DeepSeek V4 Pro.
|
||||||
4. Product completion, exhaustive parity verification, and distribution.
|
3. Product completion, exhaustive parity verification, and distribution.
|
||||||
5. Optional extensions: Dev Brain and A2UI.
|
4. Optional extensions: Dev Brain and A2UI.
|
||||||
|
|
||||||
## 1. Next — tool hardening and safety
|
## 1. Completed — tool hardening and safety
|
||||||
|
|
||||||
Goal: make the existing tool set safe and clear enough for productive daily
|
Goal: make the existing tool set safe and clear enough for productive daily
|
||||||
use without weakening its ability to inspect, edit, build, and test a project.
|
use without weakening its ability to inspect, edit, build, and test a project.
|
||||||
|
|||||||
815
src/agent.rs
815
src/agent.rs
File diff suppressed because it is too large
Load Diff
50
src/app.rs
50
src/app.rs
@@ -103,6 +103,11 @@ pub(crate) struct App {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
active_tools: Option<crate::agent::ActiveTools>,
|
active_tools: Option<crate::agent::ActiveTools>,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(super) tool_cards: Vec<crate::agent::ToolCard>,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(super) pending_tool_approval:
|
||||||
|
Option<(crate::agent::ApprovalPrompt, std::sync::mpsc::Sender<bool>)>,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
active_titling: Option<generation::TitleRequest>,
|
active_titling: Option<generation::TitleRequest>,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
runtime_config: Arc<RwLock<Config>>,
|
runtime_config: Arc<RwLock<Config>>,
|
||||||
@@ -199,6 +204,10 @@ pub(crate) enum Message {
|
|||||||
ComposerChanged(String),
|
ComposerChanged(String),
|
||||||
ToggleReasoning(usize),
|
ToggleReasoning(usize),
|
||||||
OpenLink(markdown::Url),
|
OpenLink(markdown::Url),
|
||||||
|
CopyToolText(String),
|
||||||
|
OpenToolOutput(PathBuf),
|
||||||
|
AllowToolOnce,
|
||||||
|
DenyTool,
|
||||||
SubmitPrompt,
|
SubmitPrompt,
|
||||||
StopGeneration,
|
StopGeneration,
|
||||||
GenerationTick,
|
GenerationTick,
|
||||||
@@ -316,6 +325,10 @@ impl App {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
active_tools: None,
|
active_tools: None,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
tool_cards: Vec::new(),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pending_tool_approval: None,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
active_titling: None,
|
active_titling: None,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
runtime_config,
|
runtime_config,
|
||||||
@@ -415,6 +428,10 @@ impl App {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
active_tools: None,
|
active_tools: None,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
tool_cards: Vec::new(),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pending_tool_approval: None,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
active_titling: None,
|
active_titling: None,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
runtime_config,
|
runtime_config,
|
||||||
@@ -778,6 +795,25 @@ impl App {
|
|||||||
self.error = Some(format!("Could not open the link: {error}"));
|
self.error = Some(format!("Could not open the link: {error}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::CopyToolText(value) => return iced::clipboard::write(value),
|
||||||
|
Message::OpenToolOutput(path) => {
|
||||||
|
if let Err(error) = std::process::Command::new("open").arg(path).spawn() {
|
||||||
|
self.error = Some(format!("Could not open the tool output: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::AllowToolOnce => {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if let Some((_, decision)) = self.pending_tool_approval.take() {
|
||||||
|
let _ = decision.send(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::DenyTool =>
|
||||||
|
{
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if let Some((_, decision)) = self.pending_tool_approval.take() {
|
||||||
|
let _ = decision.send(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::SubmitPrompt => {
|
Message::SubmitPrompt => {
|
||||||
self.start_generation();
|
self.start_generation();
|
||||||
return scroll_chat_to_end();
|
return scroll_chat_to_end();
|
||||||
@@ -794,6 +830,12 @@ impl App {
|
|||||||
active.cancel.store(true, Ordering::Relaxed);
|
active.cancel.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
if let Some((_, decision)) = self.pending_tool_approval.take() {
|
||||||
|
let _ = decision.send(false);
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
self.stop_agent_jobs();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
if let Some(compaction) = &self.active_compaction {
|
if let Some(compaction) = &self.active_compaction {
|
||||||
compaction.active.cancel.store(true, Ordering::Relaxed);
|
compaction.active.cancel.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
@@ -1019,6 +1061,10 @@ impl App {
|
|||||||
if self.selected_session == Some(session_id) {
|
if self.selected_session == Some(session_id) {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
self.stop_agent_jobs();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
self.tool_cards.clear();
|
||||||
let saved_context = self
|
let saved_context = self
|
||||||
.projects
|
.projects
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1298,6 +1344,10 @@ impl Drop for App {
|
|||||||
if let Some(active) = &self.active_tools {
|
if let Some(active) = &self.active_tools {
|
||||||
active.cancel.store(true, Ordering::Relaxed);
|
active.cancel.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if let Some((_, decision)) = self.pending_tool_approval.take() {
|
||||||
|
let _ = decision.send(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,8 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
self.tool_cards.clear();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
if !std::mem::take(&mut self.skip_compaction_once)
|
if !std::mem::take(&mut self.skip_compaction_once)
|
||||||
&& crate::compaction::should_compact(self.context_used, self.context_limit)
|
&& crate::compaction::should_compact(self.context_used, self.context_limit)
|
||||||
{
|
{
|
||||||
@@ -388,11 +390,47 @@ impl App {
|
|||||||
return self.poll_tool_result_check();
|
return self.poll_tool_result_check();
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
if self.active_tools.is_some() {
|
||||||
|
while let Some(event) = self
|
||||||
|
.active_tools
|
||||||
|
.as_ref()
|
||||||
|
.and_then(crate::agent::try_tool_event)
|
||||||
|
{
|
||||||
|
match event {
|
||||||
|
crate::agent::ToolEvent::State {
|
||||||
|
index,
|
||||||
|
state,
|
||||||
|
result,
|
||||||
|
} => {
|
||||||
|
if let Some(card) = self.tool_cards.get_mut(index) {
|
||||||
|
card.state = state;
|
||||||
|
if result.is_some() {
|
||||||
|
card.result = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.activity = Some(format!("Tool {} · {}", index + 1, state.label()));
|
||||||
|
}
|
||||||
|
crate::agent::ToolEvent::Approval {
|
||||||
|
index,
|
||||||
|
prompt,
|
||||||
|
decision,
|
||||||
|
} => {
|
||||||
|
if let Some(card) = self.tool_cards.get_mut(index) {
|
||||||
|
card.state = crate::agent::ToolLifecycle::AwaitingApproval;
|
||||||
|
}
|
||||||
|
self.pending_tool_approval = Some((prompt, decision));
|
||||||
|
self.activity = Some(format!("Tool {} · Awaiting approval", index + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
if let Some(active) = &self.active_tools {
|
if let Some(active) = &self.active_tools {
|
||||||
match crate::agent::try_tool_result(active) {
|
match crate::agent::try_tool_result(active) {
|
||||||
Ok(Some(result)) => {
|
Ok(Some(result)) => {
|
||||||
let cancelled = active.cancel.load(Ordering::Relaxed);
|
let cancelled = active.cancel.load(Ordering::Relaxed);
|
||||||
self.active_tools = None;
|
self.active_tools = None;
|
||||||
|
self.pending_tool_approval = None;
|
||||||
if cancelled {
|
if cancelled {
|
||||||
self.generating = false;
|
self.generating = false;
|
||||||
self.activity = Some("Stopped".into());
|
self.activity = Some("Stopped".into());
|
||||||
@@ -453,6 +491,13 @@ impl App {
|
|||||||
&& !message.user
|
&& !message.user
|
||||||
{
|
{
|
||||||
message.append(reasoning, &content);
|
message.append(reasoning, &content);
|
||||||
|
if !reasoning
|
||||||
|
&& self.tool_cards.is_empty()
|
||||||
|
&& crate::agent::has_tool_markup(&message.content)
|
||||||
|
{
|
||||||
|
self.tool_cards.push(crate::agent::ToolCard::streaming());
|
||||||
|
self.activity = Some("Parsing tool call…".into());
|
||||||
|
}
|
||||||
transcript_changed = true;
|
transcript_changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,6 +546,7 @@ impl App {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.generating = false;
|
self.generating = false;
|
||||||
self.activity = None;
|
self.activity = None;
|
||||||
|
self.tool_cards.clear();
|
||||||
start_queued = !self.queued_inputs.is_empty()
|
start_queued = !self.queued_inputs.is_empty()
|
||||||
|| self.manual_compaction_queued;
|
|| self.manual_compaction_queued;
|
||||||
}
|
}
|
||||||
@@ -628,8 +674,13 @@ impl App {
|
|||||||
self.agent_tools = Some((session_id, Arc::new(Mutex::new(tools))));
|
self.agent_tools = Some((session_id, Arc::new(Mutex::new(tools))));
|
||||||
}
|
}
|
||||||
let tools = Arc::clone(&self.agent_tools.as_ref().unwrap().1);
|
let tools = Arc::clone(&self.agent_tools.as_ref().unwrap().1);
|
||||||
|
self.tool_cards = calls
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(crate::agent::ToolCard::parsing)
|
||||||
|
.collect();
|
||||||
self.active_tools = Some(crate::agent::execute_async(tools, calls));
|
self.active_tools = Some(crate::agent::execute_async(tools, calls));
|
||||||
self.activity = Some("Running tools…".into());
|
self.activity = Some("Parsing tool calls…".into());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,6 +736,7 @@ impl App {
|
|||||||
.collect();
|
.collect();
|
||||||
assistant.reasoning_open = assistant_reasoning;
|
assistant.reasoning_open = assistant_reasoning;
|
||||||
self.conversation.push(assistant);
|
self.conversation.push(assistant);
|
||||||
|
self.tool_cards.clear();
|
||||||
let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
|
let idle_timeout = Duration::from_secs(self.config.idle_timeout_minutes.max(1) as u64 * 60);
|
||||||
self.active_generation = Some(
|
self.active_generation = Some(
|
||||||
self.generation_service
|
self.generation_service
|
||||||
@@ -704,6 +756,20 @@ impl App {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(super) fn stop_agent_jobs(&mut self) {
|
||||||
|
let Some((_, tools)) = &self.agent_tools else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(mut tools) = tools.try_lock() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let failures = tools.stop_all_jobs();
|
||||||
|
if !failures.is_empty() {
|
||||||
|
self.error = Some(failures.join("\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn start_tool_result_check(
|
fn start_tool_result_check(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ impl App {
|
|||||||
if self.database.is_none() {
|
if self.database.is_none() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
self.stop_agent_jobs();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
self.tool_cards.clear();
|
||||||
let title = draft_title(&self.projects, project_id);
|
let title = draft_title(&self.projects, project_id);
|
||||||
self.drafts.entry(project_id).or_insert(title);
|
self.drafts.entry(project_id).or_insert(title);
|
||||||
self.remember_project(project_id);
|
self.remember_project(project_id);
|
||||||
|
|||||||
@@ -69,6 +69,16 @@ impl App {
|
|||||||
|| self.pending_project_path.is_some()
|
|| self.pending_project_path.is_some()
|
||||||
|| self.session_rename.is_some()
|
|| self.session_rename.is_some()
|
||||||
|| self.menu_session().is_some()
|
|| self.menu_session().is_some()
|
||||||
|
|| {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
self.pending_tool_approval.is_some()
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main_view(&self) -> Element<'_, Message> {
|
fn main_view(&self) -> Element<'_, Message> {
|
||||||
@@ -112,6 +122,19 @@ impl App {
|
|||||||
let content: Element<'_, Message> = shell.into();
|
let content: Element<'_, Message> = shell.into();
|
||||||
let mut layers = vec![content];
|
let mut layers = vec![content];
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if let Some((prompt, _)) = &self.pending_tool_approval {
|
||||||
|
layers.push(self.tool_approval_panel(prompt));
|
||||||
|
} else if self.preferences_open {
|
||||||
|
layers.push(self.preferences_panel());
|
||||||
|
} else if let Some(path) = &self.pending_project_path {
|
||||||
|
layers.push(self.project_dialog(path));
|
||||||
|
} else if let Some((_, title)) = &self.session_rename {
|
||||||
|
layers.push(self.rename_dialog(title));
|
||||||
|
} else if let Some(session) = self.menu_session() {
|
||||||
|
layers.push(self.session_menu_panel(session));
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
if self.preferences_open {
|
if self.preferences_open {
|
||||||
layers.push(self.preferences_panel());
|
layers.push(self.preferences_panel());
|
||||||
} else if let Some(path) = &self.pending_project_path {
|
} else if let Some(path) = &self.pending_project_path {
|
||||||
@@ -128,6 +151,39 @@ impl App {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn tool_approval_panel<'a>(
|
||||||
|
&'a self,
|
||||||
|
prompt: &'a crate::agent::ApprovalPrompt,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let dialog = container(
|
||||||
|
column![
|
||||||
|
text(&prompt.title).size(22),
|
||||||
|
text(&prompt.detail).size(13),
|
||||||
|
text("Working directory").size(11).color(muted_text()),
|
||||||
|
text(prompt.working_directory.display().to_string()).size(13),
|
||||||
|
row![
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
action_button("Deny").on_press(Message::DenyTool),
|
||||||
|
action_button("Allow once").on_press(Message::AllowToolOnce),
|
||||||
|
]
|
||||||
|
.spacing(8),
|
||||||
|
]
|
||||||
|
.spacing(12),
|
||||||
|
)
|
||||||
|
.padding(22)
|
||||||
|
.width(560)
|
||||||
|
.style(overview_style);
|
||||||
|
opaque(
|
||||||
|
container(dialog)
|
||||||
|
.center_x(Length::Fill)
|
||||||
|
.center_y(Length::Fill)
|
||||||
|
.style(|_| {
|
||||||
|
container::Style::default().background(Color::from_rgba8(0, 0, 0, 0.68))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn sidebar(&self) -> Element<'_, Message> {
|
fn sidebar(&self) -> Element<'_, Message> {
|
||||||
let preference_content = row![
|
let preference_content = row![
|
||||||
icon(ICON_SETTINGS, 17),
|
icon(ICON_SETTINGS, 17),
|
||||||
|
|||||||
@@ -75,6 +75,17 @@ impl App {
|
|||||||
if message.system {
|
if message.system {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if message.tool
|
||||||
|
&& index > 0
|
||||||
|
&& !crate::agent::stored_tool_cards(
|
||||||
|
self.config.model,
|
||||||
|
&self.conversation[index - 1].content,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let label = if message.user {
|
let label = if message.user {
|
||||||
"You"
|
"You"
|
||||||
} else if message.tool {
|
} else if message.tool {
|
||||||
@@ -135,11 +146,39 @@ impl App {
|
|||||||
text(self.activity.as_deref().unwrap_or("Loading model…")).size(14),
|
text(self.activity.as_deref().unwrap_or("Loading model…")).size(14),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !message.user && !message.tool {
|
if !message.user {
|
||||||
for summary in
|
let stored_result = self
|
||||||
crate::agent::tool_summaries(self.config.model, &message.content)
|
.conversation
|
||||||
|
.get(index + 1)
|
||||||
|
.filter(|message| message.tool)
|
||||||
|
.map(|message| message.content.as_str());
|
||||||
|
let cards = if stored_result.is_none() && {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
body = body.push(text(summary).size(13).color(muted_text()));
|
!self.tool_cards.is_empty()
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
self.tool_cards.clone()
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
crate::agent::stored_tool_cards(
|
||||||
|
self.config.model,
|
||||||
|
&message.content,
|
||||||
|
stored_result,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if !cards.is_empty() {
|
||||||
|
body = body.push(tool_cards(cards));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let user = message.user;
|
let user = message.user;
|
||||||
@@ -295,3 +334,80 @@ impl App {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tool_cards(cards: Vec<crate::agent::ToolCard>) -> Element<'static, Message> {
|
||||||
|
let mut rows = column![].spacing(0);
|
||||||
|
for (index, card) in cards.into_iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
rows = rows.push(horizontal_rule(1));
|
||||||
|
}
|
||||||
|
let parameters = crate::agent::tool_parameters(&card.call);
|
||||||
|
let call = crate::agent::tool_call_text(&card.call);
|
||||||
|
let copy_call = tooltip(
|
||||||
|
action_button(text("Copy call").size(11))
|
||||||
|
.padding([5, 9])
|
||||||
|
.on_press(Message::CopyToolText(call)),
|
||||||
|
container(text("Copy tool name and all arguments").size(11))
|
||||||
|
.padding(8)
|
||||||
|
.style(preference_group_style),
|
||||||
|
tooltip::Position::Top,
|
||||||
|
)
|
||||||
|
.gap(6);
|
||||||
|
let copy_result = action_button(text("Copy result").size(11)).padding([5, 9]);
|
||||||
|
let copy_result = if let Some(result) = &card.result {
|
||||||
|
copy_result.on_press(Message::CopyToolText(result.clone()))
|
||||||
|
} else {
|
||||||
|
copy_result
|
||||||
|
};
|
||||||
|
let copy_result = tooltip(
|
||||||
|
copy_result,
|
||||||
|
container(text("Copy the complete tool result").size(11))
|
||||||
|
.padding(8)
|
||||||
|
.style(preference_group_style),
|
||||||
|
tooltip::Position::Top,
|
||||||
|
)
|
||||||
|
.gap(6);
|
||||||
|
let mut actions = row![copy_call, copy_result]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(Alignment::Center);
|
||||||
|
if let Some(path) = card
|
||||||
|
.result
|
||||||
|
.as_deref()
|
||||||
|
.and_then(crate::agent::tool_output_path)
|
||||||
|
{
|
||||||
|
actions = actions.push(
|
||||||
|
tooltip(
|
||||||
|
action_button(text("Open output").size(11))
|
||||||
|
.padding([5, 9])
|
||||||
|
.on_press(Message::OpenToolOutput(path)),
|
||||||
|
container(text("Open the complete output file").size(11))
|
||||||
|
.padding(8)
|
||||||
|
.style(preference_group_style),
|
||||||
|
tooltip::Position::Top,
|
||||||
|
)
|
||||||
|
.gap(6),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut content = column![
|
||||||
|
row![
|
||||||
|
text(card.call.name).size(13),
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
text(card.state.label()).size(11).color(muted_text()),
|
||||||
|
actions,
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(Alignment::Center),
|
||||||
|
text(parameters).size(12).color(muted_text()),
|
||||||
|
]
|
||||||
|
.spacing(5);
|
||||||
|
if let Some(result) = card.result {
|
||||||
|
let bounded = crate::agent::bounded_tool_text(&result, 1_200);
|
||||||
|
content = content.push(text(bounded).size(12));
|
||||||
|
}
|
||||||
|
rows = rows.push(container(content).padding(10).width(Length::Fill));
|
||||||
|
}
|
||||||
|
container(rows)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.style(preference_group_style)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user