Skip to content
Merged
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
73 changes: 70 additions & 3 deletions apps/staged/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1432,12 +1432,50 @@ fn build_badge_prompt(
prompt
}

fn badge_provider_id(
provider: Option<&str>,
available_ids: &[String],
recent_ids: &[String],
) -> Option<String> {
provider
.map(str::trim)
.filter(|provider| !provider.is_empty())
.map(ToOwned::to_owned)
.or_else(|| session_commands::select_preferred_provider(available_ids, recent_ids))
}

fn find_badge_agent(provider: Option<&str>) -> Option<acp_client::AcpAgent> {
if let Some(provider) = provider
.map(str::trim)
.filter(|provider| !provider.is_empty())
{
let agent = acp_client::find_acp_agent_by_id(provider);
if agent.is_none() {
log::warn!("[repo_badges] selected badge-name provider `{provider}` is unavailable");
}
return agent;
}

let available_ids: Vec<String> = agent::discover_providers()
.into_iter()
.map(|provider| provider.id)
.collect();
let provider = badge_provider_id(
None,
&available_ids,
&session_commands::read_recent_agent_ids(),
)?;

acp_client::find_acp_agent_by_id(&provider)
}

/// Try to generate short names via ACP. Returns a map from "repo" or "repo (subpath)" to short name.
async fn ai_generate_short_names(
existing_badges: &[store::RepoBadge],
new_repos: &[(String, String)],
provider: Option<&str>,
) -> Option<std::collections::HashMap<String, String>> {
let agent = acp_client::find_acp_agent()?;
let agent = find_badge_agent(provider)?;
let prompt = build_badge_prompt(existing_badges, new_repos);
let working_dir = std::env::temp_dir();

Expand Down Expand Up @@ -1481,6 +1519,7 @@ async fn ai_generate_short_names(
async fn ensure_repo_badges(
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
repos: Vec<(String, String)>,
provider: Option<String>,
) -> Result<Vec<store::RepoBadge>, String> {
let store = get_store(&store)?;
let mut result = Vec::new();
Expand Down Expand Up @@ -1511,7 +1550,7 @@ async fn ensure_repo_badges(
}

// Try AI generation for all missing repos at once
let ai_names = ai_generate_short_names(&all_badges, &missing).await;
let ai_names = ai_generate_short_names(&all_badges, &missing, provider.as_deref()).await;

for (github_repo, subpath) in &missing {
let subpath_str = subpath.as_str();
Expand Down Expand Up @@ -2340,10 +2379,14 @@ pub fn run() {

#[cfg(test)]
mod tests {
use super::cleanup_project_branches_best_effort;
use super::{badge_provider_id, cleanup_project_branches_best_effort};
use crate::store::{Branch, BranchType};
use std::collections::HashMap;

fn ids(values: &[&str]) -> Vec<String> {
values.iter().map(|value| value.to_string()).collect()
}

fn remote_branch(
project_id: &str,
id: &str,
Expand All @@ -2355,6 +2398,30 @@ mod tests {
branch
}

#[test]
fn badge_provider_id_uses_explicit_provider() {
assert_eq!(
badge_provider_id(Some("codex"), &ids(&["goose", "claude"]), &ids(&["claude"])),
Some("codex".to_string())
);
}

#[test]
fn badge_provider_id_uses_recent_available_provider() {
assert_eq!(
badge_provider_id(None, &ids(&["goose", "claude"]), &ids(&["codex", "claude"])),
Some("claude".to_string())
);
}

#[test]
fn badge_provider_id_falls_back_to_first_available_provider() {
assert_eq!(
badge_provider_id(None, &ids(&["goose", "claude"]), &ids(&["codex"])),
Some("goose".to_string())
);
}

#[test]
fn delete_project_cleanup_retries_branch_row_deletes_and_re_sweeps_remote_workspaces() {
let branches = vec![
Expand Down
7 changes: 5 additions & 2 deletions apps/staged/src-tauri/src/session_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1837,7 +1837,7 @@ fn available_provider_ids(is_remote: bool) -> Vec<String> {
}
}

fn read_recent_agent_ids() -> Vec<String> {
pub(crate) fn read_recent_agent_ids() -> Vec<String> {
crate::preferences_store_path_buf()
.and_then(|path| std::fs::read_to_string(&path).ok())
.and_then(|contents| serde_json::from_str::<serde_json::Value>(&contents).ok())
Expand All @@ -1848,7 +1848,10 @@ fn read_recent_agent_ids() -> Vec<String> {
.unwrap_or_default()
}

fn select_preferred_provider(available_ids: &[String], recent_ids: &[String]) -> Option<String> {
pub(crate) fn select_preferred_provider(
available_ids: &[String],
recent_ids: &[String],
) -> Option<String> {
for agent_id in recent_ids {
if available_ids.contains(agent_id) {
return Some(agent_id.clone());
Expand Down
5 changes: 3 additions & 2 deletions apps/staged/src/lib/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1300,9 +1300,10 @@ export function getAllRepoBadges(): Promise<import('./types').RepoBadge[]> {
/** Ensure badges exist for the given (githubRepo, subpath) pairs.
* Generates missing badges (fallback names + hues) and returns all requested. */
export function ensureRepoBadges(
repos: [string, string][]
repos: [string, string][],
provider?: string
): Promise<import('./types').RepoBadge[]> {
return invokeCommand('ensure_repo_badges', { repos });
return invokeCommand('ensure_repo_badges', { repos, provider: provider ?? null });
}

/** Update the short name and hue of an existing repo badge. */
Expand Down
5 changes: 4 additions & 1 deletion apps/staged/src/lib/stores/repoBadges.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*/

import { getAllRepoBadges, ensureRepoBadges, updateRepoBadge } from '../commands';
import { agentState } from '../features/agents/agent.svelte';
import { getPreferredAgent } from '../features/settings/preferences.svelte';
import type { RepoBadge } from '../types';

function badgeKey(githubRepo: string, subpath: string): string {
Expand Down Expand Up @@ -76,7 +78,8 @@ class RepoBadgeStore {

try {
const pairs: [string, string][] = missing.map((r) => [r.githubRepo, r.subpath ?? '']);
const newBadges = await ensureRepoBadges(pairs);
const provider = getPreferredAgent(agentState.providers) ?? undefined;
const newBadges = await ensureRepoBadges(pairs, provider);
const next = new Map(this.badges);
for (const badge of newBadges) {
next.set(badgeKey(badge.githubRepo, badge.subpath), badge);
Expand Down