Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"))?;
Expand Down
143 changes: 82 additions & 61 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,12 @@ fn list_team_configs() -> Vec<TeamConfig> {
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::<serde_json::Value>(&raw) else { continue };
let Ok(raw) = std::fs::read_to_string(&path) else {
continue;
};
let Ok(val) = serde_json::from_str::<serde_json::Value>(&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"]
Expand All @@ -364,7 +368,11 @@ fn list_team_configs() -> Vec<TeamConfig> {
.collect()
})
.unwrap_or_default();
result.push(TeamConfig { name, description, members });
result.push(TeamConfig {
name,
description,
members,
});
}
result
}
Expand All @@ -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]
Expand All @@ -390,10 +397,7 @@ fn get_config(state: State<'_, Arc<Mutex<AppState>>>) -> AppConfig {
}

#[tauri::command]
fn save_config(
state: State<'_, Arc<Mutex<AppState>>>,
config: AppConfig,
) -> Result<(), String> {
fn save_config(state: State<'_, Arc<Mutex<AppState>>>, config: AppConfig) -> Result<(), String> {
config.save()?;
state.lock().unwrap().config = config;
Ok(())
Expand Down Expand Up @@ -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::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.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;
Expand Down Expand Up @@ -509,10 +534,7 @@ fn start_debate_cmd(
}

#[tauri::command]
fn stop_debate(
state: State<'_, Arc<Mutex<AppState>>>,
team: String,
) -> Result<(), String> {
fn stop_debate(state: State<'_, Arc<Mutex<AppState>>>, team: String) -> Result<(), String> {
let st = state.lock().unwrap();
let ds = st
.debates
Expand All @@ -524,10 +546,7 @@ fn stop_debate(
}

#[tauri::command]
fn pause_debate(
state: State<'_, Arc<Mutex<AppState>>>,
team: String,
) -> Result<(), String> {
fn pause_debate(state: State<'_, Arc<Mutex<AppState>>>, team: String) -> Result<(), String> {
let st = state.lock().unwrap();
let ds = st
.debates
Expand Down Expand Up @@ -603,27 +622,25 @@ fn get_debate_status(
}

#[tauri::command]
fn enhance_topic(
state: State<'_, Arc<Mutex<AppState>>>,
text: String,
) -> Result<String, String> {
fn enhance_topic(state: State<'_, Arc<Mutex<AppState>>>, text: String) -> Result<String, String> {
// 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() };

// 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())
Expand Down Expand Up @@ -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();
Expand All @@ -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)
{
Expand Down
Loading
Loading