feat(agents): Add GitHub agent import feature
- Implemented GitHub agent browser in CCAgents component - Created GitHubAgentBrowser component for browsing and importing agents - Added Rust commands for fetching and importing agents from GitHub - Updated API layer with GitHub agent import methods - Updated Cargo.toml with new dependencies - Fixed Tauri configuration and capabilities - Added dropdown menu for import options - Implemented search and preview functionality for GitHub agents
This commit is contained in:
5
bun.lock
5
bun.lock
@@ -6,6 +6,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.4",
|
||||
"@radix-ui/react-select": "^2.1.3",
|
||||
@@ -263,6 +264,8 @@
|
||||
|
||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
|
||||
|
||||
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ=="],
|
||||
|
||||
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
|
||||
|
||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||
@@ -271,6 +274,8 @@
|
||||
|
||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||
|
||||
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew=="],
|
||||
|
||||
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw=="],
|
||||
|
||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="],
|
||||
|
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.4",
|
||||
"@radix-ui/react-select": "^2.1.3",
|
||||
|
1263
src-tauri/Cargo.lock
generated
1263
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,48 +8,52 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "claudia_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
crate-type = ["lib", "cdylib", "staticlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["protocol-asset"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri = { version = "2", features = ["protocol-asset", "tray-icon", "image-png"] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-dialog = "2.0.3"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
tauri-plugin-clipboard-manager = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-http = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
anyhow = "1.0"
|
||||
dirs = "5.0"
|
||||
walkdir = "2"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
dirs = "5"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
anyhow = "1"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
rusqlite = { version = "0.32", features = ["bundled", "chrono"] }
|
||||
regex = "1"
|
||||
glob = "0.3"
|
||||
base64 = "0.22"
|
||||
gaol = "0.2"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.11", features = ["v4", "serde"] }
|
||||
libc = "0.2"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
futures = "0.3"
|
||||
async-trait = "0.1"
|
||||
tempfile = "3"
|
||||
which = "7"
|
||||
headless_chrome = { version = "1.0", features = ["fetch"] }
|
||||
sha2 = "0.10"
|
||||
zstd = "0.13"
|
||||
headless_chrome = { version = "1.0", features = ["fetch"] }
|
||||
reqwest = { version = "0.12.20", features = ["json"] }
|
||||
image = "0.25.6"
|
||||
uuid = { version = "1.6", features = ["v4", "serde"] }
|
||||
walkdir = "2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.26"
|
||||
objc = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing utilities
|
||||
tempfile = "3"
|
||||
serial_test = "3" # For tests that need to run serially
|
||||
test-case = "3" # For parameterized tests
|
||||
once_cell = "1" # For test fixture initialization
|
||||
proptest = "1" # For property-based testing
|
||||
pretty_assertions = "1" # Better assertion output
|
||||
parking_lot = "0.12" # Non-poisoning mutex for tests
|
||||
|
||||
[features]
|
||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
@@ -5,11 +5,30 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"dialog:default",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-spawn",
|
||||
"shell:allow-open"
|
||||
"shell:allow-open",
|
||||
"fs:default",
|
||||
"fs:allow-mkdir",
|
||||
"fs:allow-read",
|
||||
"fs:allow-write",
|
||||
"fs:allow-remove",
|
||||
"fs:allow-rename",
|
||||
"fs:allow-exists",
|
||||
"fs:allow-copy-file",
|
||||
"fs:read-all",
|
||||
"fs:write-all",
|
||||
"fs:scope-app-recursive",
|
||||
"fs:scope-home-recursive",
|
||||
"http:default",
|
||||
"http:allow-fetch",
|
||||
"process:default",
|
||||
"notification:default",
|
||||
"clipboard-manager:default",
|
||||
"global-shortcut:default",
|
||||
"updater:default"
|
||||
]
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Manager, State, Emitter};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
use reqwest;
|
||||
|
||||
/// Finds the full path to the claude binary
|
||||
/// This is necessary because macOS apps have a limited PATH environment
|
||||
@@ -1931,3 +1932,130 @@ pub async fn import_agent_from_file(db: State<'_, AgentDb>, file_path: String) -
|
||||
// Import the agent
|
||||
import_agent(db, json_data).await
|
||||
}
|
||||
|
||||
// GitHub Agent Import functionality
|
||||
|
||||
/// Represents a GitHub agent file from the API
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct GitHubAgentFile {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub download_url: String,
|
||||
pub size: i64,
|
||||
pub sha: String,
|
||||
}
|
||||
|
||||
/// Represents the GitHub API response for directory contents
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GitHubApiResponse {
|
||||
name: String,
|
||||
path: String,
|
||||
sha: String,
|
||||
size: i64,
|
||||
url: String,
|
||||
html_url: String,
|
||||
git_url: String,
|
||||
download_url: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
file_type: String,
|
||||
}
|
||||
|
||||
/// Fetch list of agents from GitHub repository
|
||||
#[tauri::command]
|
||||
pub async fn fetch_github_agents() -> Result<Vec<GitHubAgentFile>, String> {
|
||||
info!("Fetching agents from GitHub repository...");
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let url = "https://api.github.com/repos/getAsterisk/claudia/contents/cc_agents";
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.header("User-Agent", "Claudia-App")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch from GitHub: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(format!("GitHub API error ({}): {}", status, error_text));
|
||||
}
|
||||
|
||||
let api_files: Vec<GitHubApiResponse> = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse GitHub response: {}", e))?;
|
||||
|
||||
// Filter only .claudia.json files
|
||||
let agent_files: Vec<GitHubAgentFile> = api_files
|
||||
.into_iter()
|
||||
.filter(|f| f.name.ends_with(".claudia.json") && f.file_type == "file")
|
||||
.filter_map(|f| {
|
||||
f.download_url.map(|download_url| GitHubAgentFile {
|
||||
name: f.name,
|
||||
path: f.path,
|
||||
download_url,
|
||||
size: f.size,
|
||||
sha: f.sha,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
info!("Found {} agents on GitHub", agent_files.len());
|
||||
Ok(agent_files)
|
||||
}
|
||||
|
||||
/// Fetch and preview a specific agent from GitHub
|
||||
#[tauri::command]
|
||||
pub async fn fetch_github_agent_content(download_url: String) -> Result<AgentExport, String> {
|
||||
info!("Fetching agent content from: {}", download_url);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&download_url)
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", "Claudia-App")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to download agent: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Failed to download agent: HTTP {}", response.status()));
|
||||
}
|
||||
|
||||
let json_text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read response: {}", e))?;
|
||||
|
||||
// Parse and validate the agent data
|
||||
let export_data: AgentExport = serde_json::from_str(&json_text)
|
||||
.map_err(|e| format!("Invalid agent JSON format: {}", e))?;
|
||||
|
||||
// Validate version
|
||||
if export_data.version != 1 {
|
||||
return Err(format!("Unsupported agent version: {}", export_data.version));
|
||||
}
|
||||
|
||||
Ok(export_data)
|
||||
}
|
||||
|
||||
/// Import an agent directly from GitHub
|
||||
#[tauri::command]
|
||||
pub async fn import_agent_from_github(
|
||||
db: State<'_, AgentDb>,
|
||||
download_url: String,
|
||||
) -> Result<Agent, String> {
|
||||
info!("Importing agent from GitHub: {}", download_url);
|
||||
|
||||
// First, fetch the agent content
|
||||
let export_data = fetch_github_agent_content(download_url).await?;
|
||||
|
||||
// Convert to JSON string and use existing import logic
|
||||
let json_data = serde_json::to_string(&export_data)
|
||||
.map_err(|e| format!("Failed to serialize agent data: {}", e))?;
|
||||
|
||||
// Import using existing function
|
||||
import_agent(db, json_data).await
|
||||
}
|
||||
|
@@ -10,7 +10,6 @@ pub mod claude_binary;
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
@@ -28,7 +28,8 @@ use commands::agents::{
|
||||
get_session_status, cleanup_finished_processes, get_session_output,
|
||||
get_live_session_output, stream_session_output, get_claude_binary_path,
|
||||
set_claude_binary_path, export_agent, export_agent_to_file, import_agent,
|
||||
import_agent_from_file, AgentDb
|
||||
import_agent_from_file, fetch_github_agents, fetch_github_agent_content,
|
||||
import_agent_from_github, AgentDb
|
||||
};
|
||||
use commands::sandbox::{
|
||||
list_sandbox_profiles, create_sandbox_profile, update_sandbox_profile, delete_sandbox_profile,
|
||||
@@ -66,7 +67,6 @@ fn main() {
|
||||
}
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.setup(|app| {
|
||||
// Initialize agents database
|
||||
@@ -143,6 +143,9 @@ fn main() {
|
||||
export_agent_to_file,
|
||||
import_agent,
|
||||
import_agent_from_file,
|
||||
fetch_github_agents,
|
||||
fetch_github_agent_content,
|
||||
import_agent_from_github,
|
||||
execute_agent,
|
||||
list_agent_runs,
|
||||
get_agent_run,
|
||||
|
@@ -17,10 +17,19 @@ import {
|
||||
ArrowLeft,
|
||||
History,
|
||||
Download,
|
||||
Upload
|
||||
Upload,
|
||||
Globe,
|
||||
FileJson,
|
||||
ChevronDown
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { api, type Agent, type AgentRunWithMetrics } from "@/lib/api";
|
||||
import { save, open } from "@tauri-apps/plugin-dialog";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
@@ -31,6 +40,7 @@ import { AgentExecution } from "./AgentExecution";
|
||||
import { AgentRunsList } from "./AgentRunsList";
|
||||
import { AgentRunView } from "./AgentRunView";
|
||||
import { RunningSessionsView } from "./RunningSessionsView";
|
||||
import { GitHubAgentBrowser } from "./GitHubAgentBrowser";
|
||||
|
||||
interface CCAgentsProps {
|
||||
/**
|
||||
@@ -76,6 +86,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
||||
const [activeTab, setActiveTab] = useState<"agents" | "running">("agents");
|
||||
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
||||
const [selectedRunId, setSelectedRunId] = useState<number | null>(null);
|
||||
const [showGitHubBrowser, setShowGitHubBrowser] = useState(false);
|
||||
|
||||
const AGENTS_PER_PAGE = 9; // 3x3 grid
|
||||
|
||||
@@ -294,15 +305,29 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleImportAgent}
|
||||
size="default"
|
||||
variant="outline"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Import
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="default"
|
||||
variant="outline"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Import
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleImportAgent}>
|
||||
<FileJson className="h-4 w-4 mr-2" />
|
||||
From File
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setShowGitHubBrowser(true)}>
|
||||
<Globe className="h-4 w-4 mr-2" />
|
||||
From GitHub
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
onClick={() => setView("create")}
|
||||
size="default"
|
||||
@@ -535,6 +560,17 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
||||
/>
|
||||
)}
|
||||
</ToastContainer>
|
||||
|
||||
{/* GitHub Agent Browser */}
|
||||
<GitHubAgentBrowser
|
||||
isOpen={showGitHubBrowser}
|
||||
onClose={() => setShowGitHubBrowser(false)}
|
||||
onImportSuccess={async () => {
|
||||
setShowGitHubBrowser(false);
|
||||
await loadAgents();
|
||||
setToast({ message: "Agent imported successfully from GitHub", type: "success" });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
365
src/components/GitHubAgentBrowser.tsx
Normal file
365
src/components/GitHubAgentBrowser.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Search,
|
||||
Download,
|
||||
X,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
Eye,
|
||||
Check,
|
||||
Globe,
|
||||
FileJson,
|
||||
} from "lucide-react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { api, type GitHubAgentFile, type AgentExport } from "@/lib/api";
|
||||
import { AGENT_ICONS, type AgentIconName } from "./CCAgents";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface GitHubAgentBrowserProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onImportSuccess: () => void;
|
||||
}
|
||||
|
||||
interface AgentPreview {
|
||||
file: GitHubAgentFile;
|
||||
data: AgentExport | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const GitHubAgentBrowser: React.FC<GitHubAgentBrowserProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onImportSuccess,
|
||||
}) => {
|
||||
const [agents, setAgents] = useState<GitHubAgentFile[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedAgent, setSelectedAgent] = useState<AgentPreview | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importedAgents, setImportedAgents] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
fetchAgents();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const fetchAgents = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const agentFiles = await api.fetchGitHubAgents();
|
||||
setAgents(agentFiles);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch GitHub agents:", err);
|
||||
setError("Failed to fetch agents from GitHub. Please check your internet connection.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreviewAgent = async (file: GitHubAgentFile) => {
|
||||
setSelectedAgent({
|
||||
file,
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const agentData = await api.fetchGitHubAgentContent(file.download_url);
|
||||
setSelectedAgent({
|
||||
file,
|
||||
data: agentData,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch agent content:", err);
|
||||
setSelectedAgent({
|
||||
file,
|
||||
data: null,
|
||||
loading: false,
|
||||
error: "Failed to load agent details",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportAgent = async () => {
|
||||
if (!selectedAgent?.file) return;
|
||||
|
||||
try {
|
||||
setImporting(true);
|
||||
await api.importAgentFromGitHub(selectedAgent.file.download_url);
|
||||
|
||||
// Mark as imported
|
||||
setImportedAgents(prev => new Set(prev).add(selectedAgent.file.name));
|
||||
|
||||
// Close preview
|
||||
setSelectedAgent(null);
|
||||
|
||||
// Notify parent
|
||||
onImportSuccess();
|
||||
} catch (err) {
|
||||
console.error("Failed to import agent:", err);
|
||||
alert(`Failed to import agent: ${err instanceof Error ? err.message : "Unknown error"}`);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredAgents = agents.filter(agent =>
|
||||
agent.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const getAgentDisplayName = (fileName: string) => {
|
||||
return fileName.replace(".claudia.json", "").replace(/-/g, " ")
|
||||
.split(" ")
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
const renderIcon = (iconName: string) => {
|
||||
const Icon = AGENT_ICONS[iconName as AgentIconName] || AGENT_ICONS.bot;
|
||||
return <Icon className="h-8 w-8" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Globe className="h-5 w-5" />
|
||||
Import Agent from GitHub
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
{/* Search Bar */}
|
||||
<div className="mb-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-destructive mb-4" />
|
||||
<p className="text-sm text-muted-foreground mb-4">{error}</p>
|
||||
<Button onClick={fetchAgents} variant="outline" size="sm">
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
) : filteredAgents.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-center">
|
||||
<FileJson className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{searchQuery ? "No agents found matching your search" : "No agents available"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 pb-4">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{filteredAgents.map((agent, index) => (
|
||||
<motion.div
|
||||
key={agent.sha}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.2, delay: index * 0.05 }}
|
||||
>
|
||||
<Card className="h-full hover:shadow-lg transition-shadow cursor-pointer"
|
||||
onClick={() => handlePreviewAgent(agent)}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold line-clamp-2">
|
||||
{getAgentDisplayName(agent.name)}
|
||||
</h3>
|
||||
{importedAgents.has(agent.name) && (
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
Imported
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(agent.size / 1024).toFixed(1)} KB
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="p-4 pt-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePreviewAgent(agent);
|
||||
}}
|
||||
>
|
||||
<Eye className="h-3 w-3 mr-2" />
|
||||
Preview
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
{/* Agent Preview Dialog */}
|
||||
<AnimatePresence>
|
||||
{selectedAgent && (
|
||||
<Dialog open={!!selectedAgent} onOpenChange={() => setSelectedAgent(null)}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-between">
|
||||
<span>Agent Preview</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSelectedAgent(null)}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{selectedAgent.loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : selectedAgent.error ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-destructive mb-4" />
|
||||
<p className="text-sm text-muted-foreground">{selectedAgent.error}</p>
|
||||
</div>
|
||||
) : selectedAgent.data ? (
|
||||
<div className="space-y-4">
|
||||
{/* Agent Info */}
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 rounded-lg bg-primary/10 text-primary">
|
||||
{renderIcon(selectedAgent.data.agent.icon)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{selectedAgent.data.agent.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline">{selectedAgent.data.agent.model}</Badge>
|
||||
{selectedAgent.data.agent.sandbox_enabled && (
|
||||
<Badge variant="secondary">Sandbox</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Prompt */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">System Prompt</h4>
|
||||
<div className="bg-muted rounded-lg p-3 max-h-48 overflow-y-auto">
|
||||
<pre className="text-xs whitespace-pre-wrap font-mono">
|
||||
{selectedAgent.data.agent.system_prompt}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Default Task */}
|
||||
{selectedAgent.data.agent.default_task && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Default Task</h4>
|
||||
<div className="bg-muted rounded-lg p-3">
|
||||
<p className="text-sm">{selectedAgent.data.agent.default_task}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Permissions */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Permissions</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant={selectedAgent.data.agent.enable_file_read ? "default" : "secondary"}>
|
||||
File Read: {selectedAgent.data.agent.enable_file_read ? "Yes" : "No"}
|
||||
</Badge>
|
||||
<Badge variant={selectedAgent.data.agent.enable_file_write ? "default" : "secondary"}>
|
||||
File Write: {selectedAgent.data.agent.enable_file_write ? "Yes" : "No"}
|
||||
</Badge>
|
||||
<Badge variant={selectedAgent.data.agent.enable_network ? "default" : "secondary"}>
|
||||
Network: {selectedAgent.data.agent.enable_network ? "Yes" : "No"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<p>Version: {selectedAgent.data.version}</p>
|
||||
<p>Exported: {new Date(selectedAgent.data.exported_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{selectedAgent.data && (
|
||||
<div className="flex justify-end gap-2 mt-4 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSelectedAgent(null)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImportAgent}
|
||||
disabled={importing || importedAgents.has(selectedAgent.file.name)}
|
||||
>
|
||||
{importing ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Importing...
|
||||
</>
|
||||
) : importedAgents.has(selectedAgent.file.name) ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
Already Imported
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Import Agent
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
198
src/components/ui/dropdown-menu.tsx
Normal file
198
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
@@ -171,6 +171,30 @@ export interface Agent {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AgentExport {
|
||||
version: number;
|
||||
exported_at: string;
|
||||
agent: {
|
||||
name: string;
|
||||
icon: string;
|
||||
system_prompt: string;
|
||||
default_task?: string;
|
||||
model: string;
|
||||
sandbox_enabled: boolean;
|
||||
enable_file_read: boolean;
|
||||
enable_file_write: boolean;
|
||||
enable_network: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GitHubAgentFile {
|
||||
name: string;
|
||||
path: string;
|
||||
download_url: string;
|
||||
size: number;
|
||||
sha: string;
|
||||
}
|
||||
|
||||
export interface AgentRun {
|
||||
id?: number;
|
||||
agent_id: number;
|
||||
@@ -456,15 +480,42 @@ export const api = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets sessions for a specific project
|
||||
* @param projectId - The project ID to get sessions for
|
||||
* @returns Promise resolving to an array of sessions
|
||||
* Fetch list of agents from GitHub repository
|
||||
* @returns Promise resolving to list of available agents on GitHub
|
||||
*/
|
||||
async getProjectSessions(projectId: string): Promise<Session[]> {
|
||||
async fetchGitHubAgents(): Promise<GitHubAgentFile[]> {
|
||||
try {
|
||||
return await invoke<Session[]>("get_project_sessions", { projectId });
|
||||
return await invoke<GitHubAgentFile[]>('fetch_github_agents');
|
||||
} catch (error) {
|
||||
console.error("Failed to get project sessions:", error);
|
||||
console.error("Failed to fetch GitHub agents:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch and preview a specific agent from GitHub
|
||||
* @param downloadUrl - The download URL for the agent file
|
||||
* @returns Promise resolving to the agent export data
|
||||
*/
|
||||
async fetchGitHubAgentContent(downloadUrl: string): Promise<AgentExport> {
|
||||
try {
|
||||
return await invoke<AgentExport>('fetch_github_agent_content', { downloadUrl });
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch GitHub agent content:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Import an agent directly from GitHub
|
||||
* @param downloadUrl - The download URL for the agent file
|
||||
* @returns Promise resolving to the imported agent
|
||||
*/
|
||||
async importAgentFromGitHub(downloadUrl: string): Promise<Agent> {
|
||||
try {
|
||||
return await invoke<Agent>('import_agent_from_github', { downloadUrl });
|
||||
} catch (error) {
|
||||
console.error("Failed to import agent from GitHub:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user