feat: add Dioxus and GPUI UI prototypes for framework comparison
Dioxus (ui-dioxus/): 2,169 lines, WebView mode (same wry as Tauri), Catppuccin theme, 12 components, agor-core integration, compiles clean. Evolution path — keeps xterm.js, gradual migration from Tauri. GPUI (ui-gpui/): 2,490 lines, GPU-accelerated rendering, alacritty_terminal for native terminal, 17 files, Catppuccin palette, demo data. Revolution path — pure Rust UI, 120fps target, no WebView. Both are standalone (not in workspace), share agor-core backend. Created for side-by-side comparison to inform framework decision.
This commit is contained in:
parent
90c7315336
commit
f3d2ca78ba
34 changed files with 17467 additions and 0 deletions
155
ui-dioxus/src/components/agent_pane.rs
Normal file
155
ui-dioxus/src/components/agent_pane.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/// 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<AgentSession>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue