From a9dfb90128b5dd4baecddf15daadd0061604d326 Mon Sep 17 00:00:00 2001 From: Joeseph Grey Date: Sat, 14 Mar 2026 20:36:25 -0600 Subject: [PATCH 1/2] ci: add GitHub Actions workflow for cargo check, clippy, fmt --- .github/workflows/ci.yml | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e06780c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: cargo check + clippy + fmt + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libgtk-3-dev + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Cache cargo registry and target + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-tauri/target + key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: cargo fmt + working-directory: src-tauri + run: cargo fmt --check + + - name: cargo check + working-directory: src-tauri + run: cargo check + + - name: cargo clippy + working-directory: src-tauri + run: cargo clippy -- -D warnings From 0f7ee73635f4db2e561145609769302f0ea84cdd Mon Sep 17 00:00:00 2001 From: Joeseph Grey Date: Sat, 14 Mar 2026 20:41:36 -0600 Subject: [PATCH 2/2] style: run cargo fmt on existing code --- src-tauri/src/config.rs | 3 +- src-tauri/src/main.rs | 143 +++++++++++++++++++--------------- src-tauri/src/orchestrator.rs | 77 ++++++++++++------ src-tauri/src/provider.rs | 131 +++++++++++++++++++++---------- 4 files changed, 226 insertions(+), 128 deletions(-) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 179d032..92cf032 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -76,8 +76,7 @@ impl AppConfig { pub fn save(&self) -> Result<(), String> { let path = config_path(); if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(|e| format!("failed to create config dir: {e}"))?; + fs::create_dir_all(parent).map_err(|e| format!("failed to create config dir: {e}"))?; } let json = serde_json::to_string_pretty(self) .map_err(|e| format!("failed to serialize config: {e}"))?; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 20a49c1..aa8d870 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -348,8 +348,12 @@ fn list_team_configs() -> Vec { let mut result = vec![]; for team in list_teams(&tdir) { let path = tdir.join(&team).join("config.json"); - let Ok(raw) = std::fs::read_to_string(&path) else { continue }; - let Ok(val) = serde_json::from_str::(&raw) else { continue }; + let Ok(raw) = std::fs::read_to_string(&path) else { + continue; + }; + let Ok(val) = serde_json::from_str::(&raw) else { + continue; + }; let name = val["name"].as_str().unwrap_or(&team).to_string(); let description = val["description"].as_str().unwrap_or("").to_string(); let members = val["members"] @@ -364,7 +368,11 @@ fn list_team_configs() -> Vec { .collect() }) .unwrap_or_default(); - result.push(TeamConfig { name, description, members }); + result.push(TeamConfig { + name, + description, + members, + }); } result } @@ -375,8 +383,7 @@ fn delete_team(team: String) -> Result<(), String> { if !path.exists() { return Err(format!("team '{team}' not found")); } - std::fs::remove_dir_all(&path) - .map_err(|e| format!("failed to delete team '{team}': {e}")) + std::fs::remove_dir_all(&path).map_err(|e| format!("failed to delete team '{team}': {e}")) } #[tauri::command] @@ -390,10 +397,7 @@ fn get_config(state: State<'_, Arc>>) -> AppConfig { } #[tauri::command] -fn save_config( - state: State<'_, Arc>>, - config: AppConfig, -) -> Result<(), String> { +fn save_config(state: State<'_, Arc>>, config: AppConfig) -> Result<(), String> { config.save()?; state.lock().unwrap().config = config; Ok(()) @@ -437,29 +441,50 @@ fn create_debate( // Archive existing inbox messages for this team so the new debate // starts clean while old logs are preserved for reference. - let team_dir = std::path::PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string())) - .join(".claude") - .join("teams") - .join(&team); + let team_dir = + std::path::PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string())) + .join(".claude") + .join("teams") + .join(&team); let inbox_dir = team_dir.join("inboxes"); if inbox_dir.exists() { let has_json = std::fs::read_dir(&inbox_dir) .ok() - .map(|mut e| e.any(|f| f.ok().map(|f| f.path().extension().map(|x| x == "json").unwrap_or(false)).unwrap_or(false))) + .map(|mut e| { + e.any(|f| { + f.ok() + .map(|f| f.path().extension().map(|x| x == "json").unwrap_or(false)) + .unwrap_or(false) + }) + }) .unwrap_or(false); if has_json { // Name the archive folder after the debate topic (slugified), // with a numeric suffix if that folder already exists. - let topic_raw = config.topics.first().map(String::as_str).unwrap_or("debate"); + let topic_raw = config + .topics + .first() + .map(String::as_str) + .unwrap_or("debate"); let slug: String = topic_raw .chars() - .map(|c| if c.is_alphanumeric() || c == '-' { c.to_ascii_lowercase() } else { '-' }) + .map(|c| { + if c.is_alphanumeric() || c == '-' { + c.to_ascii_lowercase() + } else { + '-' + } + }) .collect::() .split('-') .filter(|s| !s.is_empty()) .collect::>() .join("-"); - let slug = if slug.is_empty() { "debate".to_string() } else { slug }; + let slug = if slug.is_empty() { + "debate".to_string() + } else { + slug + }; let archive_base = team_dir.join("archive"); let mut archive_dir = archive_base.join(&slug); let mut n = 2u32; @@ -509,10 +534,7 @@ fn start_debate_cmd( } #[tauri::command] -fn stop_debate( - state: State<'_, Arc>>, - team: String, -) -> Result<(), String> { +fn stop_debate(state: State<'_, Arc>>, team: String) -> Result<(), String> { let st = state.lock().unwrap(); let ds = st .debates @@ -524,10 +546,7 @@ fn stop_debate( } #[tauri::command] -fn pause_debate( - state: State<'_, Arc>>, - team: String, -) -> Result<(), String> { +fn pause_debate(state: State<'_, Arc>>, team: String) -> Result<(), String> { let st = state.lock().unwrap(); let ds = st .debates @@ -603,19 +622,16 @@ fn get_debate_status( } #[tauri::command] -fn enhance_topic( - state: State<'_, Arc>>, - text: String, -) -> Result { +fn enhance_topic(state: State<'_, Arc>>, text: String) -> Result { // CC first — no key needed and it's the primary use case for Agora. // Falls back through API-key providers in order. const PRIORITY: &[(&str, &str)] = &[ ("claude-code", "haiku"), - ("anthropic", "claude-haiku-4-5-20251001"), - ("groq", "llama-3.3-70b-versatile"), - ("openai", "gpt-4o-mini"), - ("gemini", "gemini-2.0-flash"), - ("openrouter", "meta-llama/llama-3.3-70b-instruct:free"), + ("anthropic", "claude-haiku-4-5-20251001"), + ("groq", "llama-3.3-70b-versatile"), + ("openai", "gpt-4o-mini"), + ("gemini", "gemini-2.0-flash"), + ("openrouter", "meta-llama/llama-3.3-70b-instruct:free"), ]; let config = { state.lock().unwrap().config.clone() }; @@ -623,7 +639,8 @@ fn enhance_topic( // Use user-configured provider if set, otherwise auto-select from priority list. let (provider_name, model): (String, String) = if !config.enhance_provider.is_empty() { let model = if config.enhance_model.is_empty() { - PRIORITY.iter() + PRIORITY + .iter() .find(|(p, _)| *p == config.enhance_provider.as_str()) .map(|(_, m)| m.to_string()) .unwrap_or_else(|| "haiku".to_string()) @@ -713,24 +730,24 @@ fn main() { .manage(shared_state) .manage(shared_hashes) .invoke_handler(tauri::generate_handler![ - get_teams, - delete_team, - list_team_configs, - get_messages, - get_config, - save_config, - list_models, - list_role_presets, - list_debate_presets, - create_debate, - start_debate_cmd, - stop_debate, - pause_debate, - restart_debate, - get_debate_status, - enhance_topic, - show_main_and_close_splash, - ]) + get_teams, + delete_team, + list_team_configs, + get_messages, + get_config, + save_config, + list_models, + list_role_presets, + list_debate_presets, + create_debate, + start_debate_cmd, + stop_debate, + pause_debate, + restart_debate, + get_debate_status, + enhance_topic, + show_main_and_close_splash, + ]) .setup(move |app| { let handle: AppHandle = app.handle().clone(); let state = state_for_setup.clone(); @@ -740,14 +757,18 @@ fn main() { if let Some(main) = app.get_webview_window("main") { let _ = main.hide(); } - let _ = tauri::WebviewWindowBuilder::new(app, "splash", tauri::WebviewUrl::App("splash.html".into())) - .title("") - .inner_size(464.0, 688.0) - .decorations(false) - .resizable(false) - .center() - .always_on_top(true) - .build(); + let _ = tauri::WebviewWindowBuilder::new( + app, + "splash", + tauri::WebviewUrl::App("splash.html".into()), + ) + .title("") + .inner_size(464.0, 688.0) + .decorations(false) + .resizable(false) + .center() + .always_on_top(true) + .build(); // Initial scan on the calling thread (fast) { diff --git a/src-tauri/src/orchestrator.rs b/src-tauri/src/orchestrator.rs index 375fe66..02d6887 100644 --- a/src-tauri/src/orchestrator.rs +++ b/src-tauri/src/orchestrator.rs @@ -120,7 +120,12 @@ fn now_ms() -> u64 { // --------------------------------------------------------------------------- const AUTHORITY_ROLES: &[&str] = &[ - "moderator", "synthesizer", "arbiter", "mediator", "judge", "facilitator", + "moderator", + "synthesizer", + "arbiter", + "mediator", + "judge", + "facilitator", ]; fn is_authority_role(role: &str) -> bool { @@ -239,13 +244,20 @@ fn normalize_context(context: Vec) -> Vec { fn build_context(state: &DebateState, agent: &AgentConfig) -> Vec { // Count how many times this agent has spoken so far (0-indexed turn count) - let my_turn_count = state.messages.iter().filter(|m| m.from == agent.name).count(); + let my_turn_count = state + .messages + .iter() + .filter(|m| m.from == agent.name) + .count(); let hidden = hidden_debate_instructions(agent, &state.config.agents, my_turn_count); // Embed topic + hidden protocol in system prompt let system_content = if let Some(topic) = state.config.topics.get(state.current_topic_idx) { - format!("{}\n\ncurrent debate topic: {topic}\n\n{hidden}", agent.system_prompt) + format!( + "{}\n\ncurrent debate topic: {topic}\n\n{hidden}", + agent.system_prompt + ) } else { format!("{}\n\n{hidden}", agent.system_prompt) }; @@ -345,7 +357,13 @@ fn team_inbox_dir(team: &str) -> PathBuf { fn safe_filename(name: &str) -> String { name.chars() - .map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '_' }) + .map(|c| { + if c.is_alphanumeric() || c == '-' || c == '_' { + c + } else { + '_' + } + }) .collect() } @@ -441,9 +459,9 @@ pub fn start_debate( let state = debate_state.lock().unwrap(); match &state.status { - DebateStatus::Stopped - | DebateStatus::Converged - | DebateStatus::Error(_) => break, + DebateStatus::Stopped | DebateStatus::Converged | DebateStatus::Error(_) => { + break + } DebateStatus::Paused => { drop(state); std::thread::sleep(std::time::Duration::from_millis(500)); @@ -491,10 +509,13 @@ pub fn start_debate( }; // Signal to frontend that this agent is waiting for a response - let _ = handle.emit("debate-thinking", DebateThinkingEvent { - team: team_name.clone(), - agent: agent_config.name.clone(), - }); + let _ = handle.emit( + "debate-thinking", + DebateThinkingEvent { + team: team_name.clone(), + agent: agent_config.name.clone(), + }, + ); let response = 'call: { let mut last_err = None; @@ -503,11 +524,14 @@ pub fn start_debate( let agent_name = agent_config.name.clone(); let team_ref = team_name.clone(); let mut on_chunk = |chunk: &str| { - let _ = handle_ref.emit("debate-message-chunk", DebateChunkEvent { - team: team_ref.clone(), - agent: agent_name.clone(), - chunk: chunk.to_string(), - }); + let _ = handle_ref.emit( + "debate-message-chunk", + DebateChunkEvent { + team: team_ref.clone(), + agent: agent_name.clone(), + chunk: chunk.to_string(), + }, + ); }; match provider.chat_streaming(&context, &agent_config.model, &mut on_chunk) { Ok(text) => break 'call text, @@ -574,15 +598,18 @@ pub fn start_debate( } // Emit complete event so frontend can finalise the streaming bubble - let _ = handle.emit("debate-message-complete", DebateMessageCompleteEvent { - team: msg.team.clone(), - agent: msg.from.clone(), - from: msg.from.clone(), - to: msg.to.clone(), - content: msg.content.clone(), - timestamp: msg.timestamp, - role: msg.role.clone(), - }); + let _ = handle.emit( + "debate-message-complete", + DebateMessageCompleteEvent { + team: msg.team.clone(), + agent: msg.from.clone(), + from: msg.from.clone(), + to: msg.to.clone(), + content: msg.content.clone(), + timestamp: msg.timestamp, + role: msg.role.clone(), + }, + ); // Persist to disk — file watcher will skip due to pre-inserted hash persist_message(&msg); diff --git a/src-tauri/src/provider.rs b/src-tauri/src/provider.rs index 5e392ce..fb10c13 100644 --- a/src-tauri/src/provider.rs +++ b/src-tauri/src/provider.rs @@ -1,6 +1,6 @@ +use serde::{Deserialize, Serialize}; use std::io::BufRead; use std::time::Duration; -use serde::{Deserialize, Serialize}; // --------------------------------------------------------------------------- // Shared types @@ -192,8 +192,9 @@ impl Provider for OpenAiCompatible { .text() .map_err(|e| ProviderError::Other(format!("failed to read response body: {e}")))?; - let parsed: OpenAiResponse = serde_json::from_str(&body) - .map_err(|e| ProviderError::Other(format!("failed to parse response: {e} | body: {body}")))?; + let parsed: OpenAiResponse = serde_json::from_str(&body).map_err(|e| { + ProviderError::Other(format!("failed to parse response: {e} | body: {body}")) + })?; parsed .choices @@ -297,7 +298,9 @@ impl Provider for OpenAiCompatible { } if accumulated.is_empty() { - return Err(ProviderError::Other("stream ended with no content".to_string())); + return Err(ProviderError::Other( + "stream ended with no content".to_string(), + )); } Ok(accumulated) } @@ -466,14 +469,24 @@ impl Provider for AnthropicClient { if !resp.status().is_success() { // Coding plan fallback if self.base_url.contains("minimax") { - return Ok(vec![ - ModelInfo { id: "MiniMax-M2.5".to_string(), provider: "minimax-coding".to_string() }, - ]); + return Ok(vec![ModelInfo { + id: "MiniMax-M2.5".to_string(), + provider: "minimax-coding".to_string(), + }]); } return Ok(vec![ - ModelInfo { id: "claude-opus-4-6".to_string(), provider: "anthropic".to_string() }, - ModelInfo { id: "claude-sonnet-4-6".to_string(), provider: "anthropic".to_string() }, - ModelInfo { id: "claude-haiku-4-5-20251001".to_string(), provider: "anthropic".to_string() }, + ModelInfo { + id: "claude-opus-4-6".to_string(), + provider: "anthropic".to_string(), + }, + ModelInfo { + id: "claude-sonnet-4-6".to_string(), + provider: "anthropic".to_string(), + }, + ModelInfo { + id: "claude-haiku-4-5-20251001".to_string(), + provider: "anthropic".to_string(), + }, ]); } @@ -573,7 +586,9 @@ impl Provider for AnthropicClient { } if accumulated.is_empty() { - return Err(ProviderError::Other("stream ended with no content".to_string())); + return Err(ProviderError::Other( + "stream ended with no content".to_string(), + )); } Ok(accumulated) } @@ -597,10 +612,7 @@ impl Provider for ClaudeCodeProvider { .map(|m| m.content.as_str()) .unwrap_or(""); - let conv: Vec<&ChatMessage> = messages - .iter() - .filter(|m| m.role != "system") - .collect(); + let conv: Vec<&ChatMessage> = messages.iter().filter(|m| m.role != "system").collect(); if conv.is_empty() { return Err(ProviderError::Other("no messages to send".to_string())); @@ -613,7 +625,11 @@ impl Provider for ClaudeCodeProvider { } else { conv.iter() .map(|m| { - let label = if m.role == "assistant" { "you" } else { "other" }; + let label = if m.role == "assistant" { + "you" + } else { + "other" + }; format!("[{label}]: {}", m.content) }) .collect::>() @@ -633,11 +649,16 @@ impl Provider for ClaudeCodeProvider { let mut cmd = std::process::Command::new(claude_bin); cmd.args([ - "-p", &prompt, - "--model", model, - "--output-format", "json", - "--max-turns", "1", - "--tools", "", + "-p", + &prompt, + "--model", + model, + "--output-format", + "json", + "--max-turns", + "1", + "--tools", + "", "--no-session-persistence", "--disable-slash-commands", ]); @@ -665,8 +686,12 @@ impl Provider for ClaudeCodeProvider { Ok(Ok(o)) => o, Ok(Err(e)) => return Err(ProviderError::Network(format!("claude CLI error: {e}"))), Err(_) => { - unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL); } - return Err(ProviderError::Other("claude CLI timed out after 120s".to_string())); + unsafe { + libc::kill(pid as libc::pid_t, libc::SIGKILL); + } + return Err(ProviderError::Other( + "claude CLI timed out after 120s".to_string(), + )); } }; @@ -697,10 +722,7 @@ impl Provider for ClaudeCodeProvider { .map(|m| m.content.as_str()) .unwrap_or(""); - let conv: Vec<&ChatMessage> = messages - .iter() - .filter(|m| m.role != "system") - .collect(); + let conv: Vec<&ChatMessage> = messages.iter().filter(|m| m.role != "system").collect(); if conv.is_empty() { return Err(ProviderError::Other("no messages to send".to_string())); @@ -711,7 +733,11 @@ impl Provider for ClaudeCodeProvider { } else { conv.iter() .map(|m| { - let label = if m.role == "assistant" { "you" } else { "other" }; + let label = if m.role == "assistant" { + "you" + } else { + "other" + }; format!("[{label}]: {}", m.content) }) .collect::>() @@ -730,13 +756,18 @@ impl Provider for ClaudeCodeProvider { let mut cmd = std::process::Command::new(claude_bin); cmd.args([ - "-p", &prompt, - "--model", model, - "--output-format", "stream-json", + "-p", + &prompt, + "--model", + model, + "--output-format", + "stream-json", "--verbose", "--include-partial-messages", - "--max-turns", "1", - "--tools", "", + "--max-turns", + "1", + "--tools", + "", "--no-session-persistence", "--disable-slash-commands", ]); @@ -753,14 +784,18 @@ impl Provider for ClaudeCodeProvider { .map_err(|e| ProviderError::Network(format!("failed to run claude CLI: {e}")))?; let pid = child.id(); - let stdout = child.stdout.take() + let stdout = child + .stdout + .take() .ok_or_else(|| ProviderError::Other("failed to capture stdout".to_string()))?; // Spawn a kill timer — 120s max. let kill_pid = pid; std::thread::spawn(move || { std::thread::sleep(Duration::from_secs(120)); - unsafe { libc::kill(kill_pid as libc::pid_t, libc::SIGKILL); } + unsafe { + libc::kill(kill_pid as libc::pid_t, libc::SIGKILL); + } }); let reader = std::io::BufReader::new(stdout); @@ -807,15 +842,30 @@ impl Provider for ClaudeCodeProvider { let _ = child.wait(); final_result - .or_else(|| if accumulated.is_empty() { None } else { Some(accumulated) }) + .or_else(|| { + if accumulated.is_empty() { + None + } else { + Some(accumulated) + } + }) .ok_or_else(|| ProviderError::Other("no output from claude CLI stream".to_string())) } fn list_models(&self) -> Result, ProviderError> { Ok(vec![ - ModelInfo { id: "haiku".to_string(), provider: "claude-code".to_string() }, - ModelInfo { id: "sonnet".to_string(), provider: "claude-code".to_string() }, - ModelInfo { id: "opus".to_string(), provider: "claude-code".to_string() }, + ModelInfo { + id: "haiku".to_string(), + provider: "claude-code".to_string(), + }, + ModelInfo { + id: "sonnet".to_string(), + provider: "claude-code".to_string(), + }, + ModelInfo { + id: "opus".to_string(), + provider: "claude-code".to_string(), + }, ]) } } @@ -833,7 +883,8 @@ pub fn build_provider(name: &str, api_key: &str) -> Option> { api_key, "https://api.minimax.io/anthropic", ))), - "openai" | "openrouter" | "groq" | "opencode" | "deepseek" | "moonshot" | "minimax" | "zai" | "zai-coding" | "gemini" => { + "openai" | "openrouter" | "groq" | "opencode" | "deepseek" | "moonshot" | "minimax" + | "zai" | "zai-coding" | "gemini" => { OpenAiCompatible::for_provider(name, api_key).map(|p| Box::new(p) as Box) } _ => None,