/// AgentPane — message list + prompt input for a single agent session. /// /// Mirrors the Svelte app's AgentPane.svelte: sans-serif font, tool call /// pairing with collapsible details, status strip, prompt bar. use dioxus::prelude::*; use crate::state::{AgentMessage, AgentSession, AgentStatus, MessageRole}; #[component] pub fn AgentPane( session: Signal, project_name: String, ) -> Element { let mut prompt_text = use_signal(|| String::new()); let status = session.read().status; let is_running = status == AgentStatus::Running; let mut do_submit = move || { let text = prompt_text.read().clone(); if text.trim().is_empty() || is_running { return; } // Add user message to the session let msg = AgentMessage { id: uuid::Uuid::new_v4().to_string(), role: MessageRole::User, content: text.clone(), tool_name: None, tool_output: None, }; session.write().messages.push(msg); session.write().status = AgentStatus::Running; prompt_text.set(String::new()); // In a real implementation, this would call: // backend.query_agent(&session_id, &text, &cwd) // For the prototype, we simulate a response after a brief delay. }; let on_click = move |_: MouseEvent| { do_submit(); }; let on_keydown = move |e: KeyboardEvent| { if e.key() == Key::Enter && !e.modifiers().shift() { do_submit(); } }; rsx! { div { class: "agent-pane", // Message list div { class: "message-list", for msg in session.read().messages.iter() { MessageBubble { key: "{msg.id}", message: msg.clone(), } } // Running indicator if is_running { div { class: "message assistant", style: "opacity: 0.6; font-style: italic;", div { class: "message-role", "Claude" } div { class: "message-text", "Thinking..." } } } } // Status strip div { class: "agent-status", span { class: "status-badge {status.css_class()}", "{status.label()}" } span { "Session: {truncate_id(&session.read().session_id)}" } span { "Model: {session.read().model}" } if session.read().cost_usd > 0.0 { span { class: "status-cost", "${session.read().cost_usd:.4}" } } } // Prompt bar div { class: "prompt-bar", input { class: "prompt-input", r#type: "text", placeholder: "Ask Claude...", value: "{prompt_text}", oninput: move |e| prompt_text.set(e.value()), onkeydown: on_keydown, disabled: is_running, } button { class: "prompt-send", onclick: on_click, disabled: is_running || prompt_text.read().trim().is_empty(), "Send" } } } } } /// A single message bubble with role label and optional tool details. #[component] fn MessageBubble(message: AgentMessage) -> Element { let role_class = message.role.css_class(); let has_tool_output = message.tool_output.is_some(); rsx! { div { class: "message {role_class}", div { class: "message-role", "{message.role.label()}" } if message.role == MessageRole::Tool { // Tool call: show name and collapsible output div { class: "message-text", if let Some(ref tool_name) = message.tool_name { span { style: "color: var(--ctp-teal); font-weight: 600;", "[{tool_name}] " } } "{message.content}" } if has_tool_output { details { class: "tool-details", summary { "Show output" } div { class: "tool-output", "{message.tool_output.as_deref().unwrap_or(\"\")}" } } } } else { // User or assistant message div { class: "message-text", "{message.content}" } } } } } /// Truncate a UUID to first 8 chars for display. fn truncate_id(id: &str) -> String { if id.len() > 8 { format!("{}...", &id[..8]) } else { id.to_string() } }