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:
Hibryda 2026-03-19 06:05:58 +01:00
parent 90c7315336
commit f3d2ca78ba
34 changed files with 17467 additions and 0 deletions

View 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()
}
}