Files
galaxy/app/src/ai/agent_management/details_action_buttons.rs
T
2026-08-12 17:36:21 -05:00

311 lines
11 KiB
Rust

//! Action buttons row for conversation details panel.
use galaxy_core::ui::theme::AnsiColorIdentifier;
use galaxyui::elements::{ChildView, CrossAxisAlignment, Empty, Flex, ParentElement};
use galaxyui::{AppContext, Element, Entity, TypedActionView, View, ViewContext, ViewHandle};
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::agent_conversations_model::{AgentConversationEntryId, AgentRunDisplayStatus};
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ui_components::icons::Icon;
use crate::view_components::action_button::{ActionButton, ButtonSize, SecondaryTheme};
use crate::view_components::copyable_text_field::COPY_FEEDBACK_DURATION;
use crate::workspace::WorkspaceAction;
const BUTTON_SPACING: f32 = 4.;
/// Per-button config for the action buttons row.
/// Each field controls one button independently.
#[derive(Debug, Clone, Default)]
pub struct ActionButtonsConfig {
pub open_action: Option<WorkspaceAction>,
pub cancel_task_id: Option<AmbientAgentTaskId>,
pub fork_conversation_id: Option<AIConversationId>,
/// Shows an info button for viewing more details.
/// Only used in management view hover toolbelt.
pub view_details_item_id: Option<AgentConversationEntryId>,
/// Conversation link URL (either to the transcript or live session) for copy link button.
pub copy_link_url: Option<String>,
pub delete_conversation_id: Option<AIConversationId>,
}
impl ActionButtonsConfig {
/// Returns true if no buttons will be rendered.
pub fn is_empty(&self) -> bool {
self.open_action.is_none()
&& self.cancel_task_id.is_none()
&& self.fork_conversation_id.is_none()
&& self.view_details_item_id.is_none()
&& self.copy_link_url.is_none()
&& self.delete_conversation_id.is_none()
}
/// Create config for a task.
/// - `display_status`: used to determine if cancel button should show.
/// - `open_action`: pass `Some(action)` to show open button, `None` to hide
/// - `copy_link_url`: conversation link URL, or `None` to hide
pub fn for_task(
task_id: AmbientAgentTaskId,
display_status: &AgentRunDisplayStatus,
open_action: Option<WorkspaceAction>,
copy_link_url: Option<String>,
) -> Self {
Self {
open_action,
cancel_task_id: if display_status.is_cancellable() {
Some(task_id)
} else {
None
},
fork_conversation_id: None,
view_details_item_id: None,
copy_link_url,
delete_conversation_id: None,
}
}
/// Create config for a conversation.
/// - `open_action`: pass `Some(action)` to show open button, `None` to hide
/// - `copy_link_url`: conversation link URL, or `None` to hide
pub fn for_conversation(
conversation_id: AIConversationId,
open_action: Option<WorkspaceAction>,
copy_link_url: Option<String>,
) -> Self {
Self {
open_action,
cancel_task_id: None,
fork_conversation_id: Some(conversation_id),
view_details_item_id: None,
copy_link_url,
delete_conversation_id: Some(conversation_id),
}
}
}
/// Events emitted by the action buttons.
#[derive(Debug, Clone)]
pub enum AgentDetailsButtonEvent {
Open,
CancelTask { task_id: AmbientAgentTaskId },
ForkConversation { conversation_id: AIConversationId },
ViewDetails { item_id: AgentConversationEntryId },
CopyLink { link: String },
DeleteConversation { conversation_id: AIConversationId },
}
/// Actions dispatched by button clicks (internal).
#[derive(Debug, Clone)]
pub enum AgentDetailsAction {
Open,
CancelTask,
ForkConversation,
ViewDetails,
CopyLink,
DeleteConversation,
}
/// Reusable action buttons row for details panel.
pub struct ConversationActionButtonsRow {
config: ActionButtonsConfig,
open_button: ViewHandle<ActionButton>,
cancel_task_button: ViewHandle<ActionButton>,
fork_conversation_button: ViewHandle<ActionButton>,
view_details_button: ViewHandle<ActionButton>,
copy_link_button: ViewHandle<ActionButton>,
delete_conversation_button: ViewHandle<ActionButton>,
}
impl ConversationActionButtonsRow {
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
let open_button = ctx.add_typed_action_view(|_| {
Self::make_action_button(
Icon::LinkExternal,
"Open conversation",
None,
AgentDetailsAction::Open,
)
});
let cancel_task_button = ctx.add_typed_action_view(|_| {
Self::make_action_button(
Icon::StopFilled,
"Cancel task",
Some(AnsiColorIdentifier::Red),
AgentDetailsAction::CancelTask,
)
});
let fork_conversation_button = ctx.add_typed_action_view(|_| {
Self::make_action_button(
Icon::ArrowSplit,
"Fork conversation",
None,
AgentDetailsAction::ForkConversation,
)
});
let view_details_button = ctx.add_typed_action_view(|_| {
Self::make_action_button(
Icon::Info,
"View details",
None,
AgentDetailsAction::ViewDetails,
)
});
let copy_link_button = ctx.add_typed_action_view(|_| {
Self::make_action_button(
Icon::Link,
"Copy link to run",
None,
AgentDetailsAction::CopyLink,
)
});
let delete_conversation_button = ctx.add_typed_action_view(|_| {
Self::make_action_button(
Icon::Trash,
"Delete conversation",
Some(AnsiColorIdentifier::Red),
AgentDetailsAction::DeleteConversation,
)
});
Self {
config: ActionButtonsConfig::default(),
open_button,
cancel_task_button,
fork_conversation_button,
view_details_button,
copy_link_button,
delete_conversation_button,
}
}
/// Set the config and rerender.
pub fn set_config(&mut self, config: ActionButtonsConfig, ctx: &mut ViewContext<Self>) {
self.config = config;
ctx.notify();
}
/// Returns true if no buttons will be rendered.
pub fn is_empty(&self) -> bool {
self.config.is_empty()
}
fn make_action_button(
icon: Icon,
tooltip: &str,
icon_color: Option<AnsiColorIdentifier>,
action: AgentDetailsAction,
) -> ActionButton {
let mut button = ActionButton::new("", SecondaryTheme)
.with_icon(icon)
.with_size(ButtonSize::Small)
.with_tooltip(tooltip)
.on_click(move |ctx| {
ctx.dispatch_typed_action(action.clone());
});
if let Some(color) = icon_color {
button = button.with_icon_ansi_color(color);
}
button
}
}
impl Entity for ConversationActionButtonsRow {
type Event = AgentDetailsButtonEvent;
}
impl View for ConversationActionButtonsRow {
fn ui_name() -> &'static str {
"ConversationActionButtonsRow"
}
fn render(&self, _app: &AppContext) -> Box<dyn Element> {
if self.config.is_empty() {
return Empty::new().finish();
}
let mut row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_spacing(BUTTON_SPACING);
if self.config.copy_link_url.is_some() {
row.add_child(ChildView::new(&self.copy_link_button).finish());
}
if self.config.open_action.is_some() {
row.add_child(ChildView::new(&self.open_button).finish());
}
if self.config.cancel_task_id.is_some() {
row.add_child(ChildView::new(&self.cancel_task_button).finish());
}
if self.config.fork_conversation_id.is_some() && !cfg!(target_family = "wasm") {
row.add_child(ChildView::new(&self.fork_conversation_button).finish());
}
if self.config.view_details_item_id.is_some() {
row.add_child(ChildView::new(&self.view_details_button).finish());
}
if self.config.delete_conversation_id.is_some() && !cfg!(target_family = "wasm") {
row.add_child(ChildView::new(&self.delete_conversation_button).finish());
}
row.finish()
}
}
impl TypedActionView for ConversationActionButtonsRow {
type Action = AgentDetailsAction;
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
match action {
AgentDetailsAction::Open => {
if self.config.open_action.is_some() {
ctx.emit(AgentDetailsButtonEvent::Open);
}
}
AgentDetailsAction::CancelTask => {
if let Some(task_id) = self.config.cancel_task_id {
ctx.emit(AgentDetailsButtonEvent::CancelTask { task_id });
}
}
AgentDetailsAction::ForkConversation => {
if let Some(conversation_id) = self.config.fork_conversation_id {
ctx.emit(AgentDetailsButtonEvent::ForkConversation { conversation_id });
}
}
AgentDetailsAction::ViewDetails => {
if let Some(item_id) = &self.config.view_details_item_id {
ctx.emit(AgentDetailsButtonEvent::ViewDetails { item_id: *item_id });
}
}
AgentDetailsAction::CopyLink => {
if let Some(link) = &self.config.copy_link_url {
ctx.emit(AgentDetailsButtonEvent::CopyLink { link: link.clone() });
self.copy_link_button.update(ctx, |button, ctx| {
button.set_icon(Some(Icon::Check), ctx);
});
let duration = COPY_FEEDBACK_DURATION;
ctx.spawn(
async move {
galaxyui::r#async::Timer::after(duration).await;
},
|me, _, ctx| {
me.copy_link_button.update(ctx, |button, ctx| {
button.set_icon(Some(Icon::Link), ctx);
});
},
);
}
}
AgentDetailsAction::DeleteConversation => {
if let Some(conversation_id) = self.config.delete_conversation_id {
ctx.emit(AgentDetailsButtonEvent::DeleteConversation { conversation_id });
}
}
}
}
}