Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
//! Commands to interact with available agents via the public API.
|
||||
|
||||
use crate::ai::agent_sdk::oauth_flow::poll_oauth_until_terminal;
|
||||
use crate::ai::cloud_environments::GithubRepo;
|
||||
use crate::server::server_api::ai::AgentListItem;
|
||||
use crate::server::server_api::ServerApiProvider;
|
||||
use warp_cli::agent::ListAgentConfigsArgs;
|
||||
use warp_graphql::queries::get_oauth_connect_tx_status::OauthConnectTxStatus;
|
||||
use warp_graphql::queries::user_repo_auth_status::UserRepoAuthStatusEnum;
|
||||
use warpui::{platform::TerminationMode, AppContext, ModelContext, SingletonEntity};
|
||||
|
||||
const MAX_LINE_WIDTH: usize = 90;
|
||||
const MAX_AUTH_ATTEMPTS: u32 = 8;
|
||||
|
||||
/// Singleton model that runs async work for agent CLI commands.
|
||||
struct AgentConfigRunner;
|
||||
|
||||
/// List all available agents.
|
||||
pub fn list_agents(ctx: &mut AppContext, args: ListAgentConfigsArgs) -> anyhow::Result<()> {
|
||||
let runner = ctx.add_singleton_model(|_ctx| AgentConfigRunner);
|
||||
runner.update(ctx, |runner, ctx| runner.list(args.repo.clone(), ctx))
|
||||
}
|
||||
|
||||
/// Parse a repo spec string (owner/repo or GitHub URL) into a GithubRepo.
|
||||
fn parse_repo_spec(spec: &str) -> anyhow::Result<GithubRepo> {
|
||||
let spec = spec.trim();
|
||||
|
||||
// Try URL format: https://github.com/owner/repo or https://github.com/owner/repo.git
|
||||
if spec.starts_with("https://github.com/") || spec.starts_with("http://github.com/") {
|
||||
let path = spec
|
||||
.trim_start_matches("https://github.com/")
|
||||
.trim_start_matches("http://github.com/")
|
||||
.trim_end_matches(".git")
|
||||
.trim_end_matches('/');
|
||||
|
||||
let parts: Vec<&str> = path.split('/').collect();
|
||||
if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
|
||||
return Ok(GithubRepo::new(parts[0].to_string(), parts[1].to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// Try slug format: owner/repo
|
||||
let parts: Vec<&str> = spec.split('/').collect();
|
||||
if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
|
||||
return Ok(GithubRepo::new(parts[0].to_string(), parts[1].to_string()));
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"Invalid repo format: '{}'. Expected 'owner/repo' or 'https://github.com/owner/repo'",
|
||||
spec
|
||||
))
|
||||
}
|
||||
|
||||
impl AgentConfigRunner {
|
||||
fn list(&self, repo: Option<String>, ctx: &mut ModelContext<Self>) -> anyhow::Result<()> {
|
||||
// If a repo is specified, check auth first
|
||||
if let Some(ref repo_spec) = repo {
|
||||
let github_repo = parse_repo_spec(repo_spec)?;
|
||||
self.auth_then_list(vec![github_repo], 1, repo, ctx);
|
||||
} else {
|
||||
// No repo specified - just list from environments
|
||||
self.fetch_and_display_agents(repo, ctx);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check GitHub auth for repos, then list agents.
|
||||
fn auth_then_list(
|
||||
&self,
|
||||
repos: Vec<GithubRepo>,
|
||||
attempt: u32,
|
||||
repo_spec: Option<String>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if attempt > MAX_AUTH_ATTEMPTS {
|
||||
ctx.terminate_app(
|
||||
TerminationMode::ForceTerminate,
|
||||
Some(Err(anyhow::anyhow!(
|
||||
"Exceeded maximum number of authorization attempts ({}). Please try again later.",
|
||||
MAX_AUTH_ATTEMPTS
|
||||
))),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let integrations_client = ServerApiProvider::handle(ctx)
|
||||
.as_ref(ctx)
|
||||
.get_integrations_client();
|
||||
|
||||
let repo_tuples: Vec<(String, String)> = repos
|
||||
.iter()
|
||||
.map(|repo| (repo.owner.clone(), repo.repo.clone()))
|
||||
.collect();
|
||||
|
||||
let auth_check_future = async move {
|
||||
integrations_client
|
||||
.check_user_repo_auth_status(repo_tuples)
|
||||
.await
|
||||
};
|
||||
|
||||
ctx.spawn(auth_check_future, move |runner, auth_result, ctx| {
|
||||
match auth_result {
|
||||
Ok(response) => {
|
||||
let mut has_blocking_private_issues = false;
|
||||
|
||||
for status in &response.statuses {
|
||||
match status.status {
|
||||
UserRepoAuthStatusEnum::Success => {}
|
||||
UserRepoAuthStatusEnum::NoInstallationOrAccessForRepo => {
|
||||
if !status.is_public {
|
||||
eprintln!(
|
||||
"Cannot access private repo {}/{}",
|
||||
status.owner, status.repo,
|
||||
);
|
||||
has_blocking_private_issues = true;
|
||||
}
|
||||
// Public repos without auth are fine - no warning needed
|
||||
}
|
||||
UserRepoAuthStatusEnum::UserNotConnectedToGithub => {
|
||||
eprintln!("User not connected to GitHub");
|
||||
has_blocking_private_issues = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !has_blocking_private_issues {
|
||||
// No blocking issues - proceed with listing
|
||||
runner.fetch_and_display_agents(repo_spec, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle OAuth flow if server provides auth_url + tx_id
|
||||
match (response.auth_url, response.tx_id) {
|
||||
(Some(auth_url), Some(tx_id)) => {
|
||||
println!("\nAuthorization required for private repository access.");
|
||||
println!("Opening browser for GitHub authorization: {auth_url}\n");
|
||||
ctx.open_url(&auth_url);
|
||||
|
||||
let integrations_client = ServerApiProvider::handle(ctx)
|
||||
.as_ref(ctx)
|
||||
.get_integrations_client();
|
||||
let tx_id = tx_id.into_inner();
|
||||
let poll_future = poll_oauth_until_terminal(integrations_client, tx_id);
|
||||
|
||||
let next_attempt = attempt + 1;
|
||||
|
||||
ctx.spawn(poll_future, move |runner, poll_result, ctx| {
|
||||
match poll_result {
|
||||
Ok(OauthConnectTxStatus::Completed) => {
|
||||
// OAuth completed, retry
|
||||
runner.auth_then_list(repos, next_attempt, repo_spec, ctx);
|
||||
}
|
||||
Ok(OauthConnectTxStatus::Failed) => {
|
||||
ctx.terminate_app(
|
||||
TerminationMode::ForceTerminate,
|
||||
Some(Err(anyhow::anyhow!(
|
||||
"GitHub authorization failed. Please try again."
|
||||
))),
|
||||
);
|
||||
}
|
||||
Ok(OauthConnectTxStatus::Expired) => {
|
||||
ctx.terminate_app(
|
||||
TerminationMode::ForceTerminate,
|
||||
Some(Err(anyhow::anyhow!(
|
||||
"GitHub authorization expired. Please try again."
|
||||
))),
|
||||
);
|
||||
}
|
||||
Ok(_) => {
|
||||
ctx.terminate_app(
|
||||
TerminationMode::ForceTerminate,
|
||||
Some(Err(anyhow::anyhow!(
|
||||
"Unexpected OAuth status"
|
||||
))),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
ctx.terminate_app(
|
||||
TerminationMode::ForceTerminate,
|
||||
Some(Err(anyhow::anyhow!(
|
||||
"Error polling OAuth status: {err}"
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
(Some(auth_url), None) => {
|
||||
println!("\nAuthorize access here: {auth_url}\n");
|
||||
println!("After authorizing, please re-run this command.");
|
||||
ctx.terminate_app(TerminationMode::ForceTerminate, None);
|
||||
}
|
||||
_ => {
|
||||
ctx.terminate_app(
|
||||
TerminationMode::ForceTerminate,
|
||||
Some(Err(anyhow::anyhow!(
|
||||
"Cannot list agents: authorization required but no auth flow provided"
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
ctx.terminate_app(
|
||||
TerminationMode::ForceTerminate,
|
||||
Some(Err(e.context("Failed to check GitHub auth status"))),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn fetch_and_display_agents(&self, repo: Option<String>, ctx: &mut ModelContext<Self>) {
|
||||
let ai_client = ServerApiProvider::handle(ctx).as_ref(ctx).get_ai_client();
|
||||
|
||||
if repo.is_some() {
|
||||
println!("Fetching agent skills from the specified repository...");
|
||||
} else {
|
||||
println!("Fetching agent skills from your Warp environments...");
|
||||
}
|
||||
|
||||
let list_future = async move { ai_client.list_agents(repo).await };
|
||||
|
||||
ctx.spawn(list_future, |_, result, ctx| match result {
|
||||
Ok(agents) => {
|
||||
Self::print_agents_table(&agents);
|
||||
ctx.terminate_app(TerminationMode::ForceTerminate, None);
|
||||
}
|
||||
Err(err) => {
|
||||
super::report_fatal_error(err, ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Print a list of agents in a card-style format.
|
||||
fn print_agents_table(agents: &[AgentListItem]) {
|
||||
if agents.is_empty() {
|
||||
println!("No agents found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if agents.len() == 1 {
|
||||
println!("\nAgent:");
|
||||
} else {
|
||||
println!("\nAgents ({}):", agents.len());
|
||||
}
|
||||
|
||||
for agent in agents {
|
||||
println!("\n{}", agent.name);
|
||||
|
||||
for variant in &agent.variants {
|
||||
let mut table = super::output::standard_table();
|
||||
|
||||
// ID
|
||||
table.add_row(vec![format!("ID: {}", variant.id)]);
|
||||
|
||||
// Description
|
||||
if !variant.description.is_empty() {
|
||||
let description_cell = super::text_layout::render_labeled_wrapped_field(
|
||||
"Description",
|
||||
&variant.description,
|
||||
MAX_LINE_WIDTH,
|
||||
);
|
||||
table.add_row(vec![description_cell]);
|
||||
}
|
||||
|
||||
// Base prompt (truncated)
|
||||
if !variant.base_prompt.is_empty() {
|
||||
let mut chars = variant.base_prompt.chars();
|
||||
let truncated: String = chars.by_ref().take(100).collect();
|
||||
let truncated_prompt = if chars.next().is_some() {
|
||||
format!("{truncated}...")
|
||||
} else {
|
||||
truncated
|
||||
};
|
||||
let prompt_cell = super::text_layout::render_labeled_wrapped_field(
|
||||
"Base Prompt",
|
||||
&truncated_prompt,
|
||||
MAX_LINE_WIDTH,
|
||||
);
|
||||
table.add_row(vec![prompt_cell]);
|
||||
}
|
||||
|
||||
// Source
|
||||
table.add_row(vec![format!(
|
||||
"Source: {}/{}",
|
||||
variant.source.owner, variant.source.name
|
||||
)]);
|
||||
|
||||
// Environments
|
||||
if !variant.environments.is_empty() {
|
||||
let env_entries: Vec<_> = variant
|
||||
.environments
|
||||
.iter()
|
||||
.map(|e| format!("{} ({})", e.name, e.uid))
|
||||
.collect();
|
||||
table.add_row(vec![format!("Environments: {}", env_entries.join(", "))]);
|
||||
}
|
||||
|
||||
println!("{table}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl warpui::Entity for AgentConfigRunner {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl SingletonEntity for AgentConfigRunner {}
|
||||
Reference in New Issue
Block a user