feat: add JSON import functionality for CC agents
This commit is contained in:
@@ -169,6 +169,28 @@ pub struct AgentRunWithMetrics {
|
|||||||
pub output: Option<String>, // Real-time JSONL content
|
pub output: Option<String>, // Real-time JSONL content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Agent export format
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AgentExport {
|
||||||
|
pub version: u32,
|
||||||
|
pub exported_at: String,
|
||||||
|
pub agent: AgentData,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Agent data within export
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct AgentData {
|
||||||
|
pub name: String,
|
||||||
|
pub icon: String,
|
||||||
|
pub system_prompt: String,
|
||||||
|
pub default_task: Option<String>,
|
||||||
|
pub model: String,
|
||||||
|
pub sandbox_enabled: bool,
|
||||||
|
pub enable_file_read: bool,
|
||||||
|
pub enable_file_write: bool,
|
||||||
|
pub enable_network: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Database connection state
|
/// Database connection state
|
||||||
pub struct AgentDb(pub Mutex<Connection>);
|
pub struct AgentDb(pub Mutex<Connection>);
|
||||||
|
|
||||||
@@ -1904,3 +1926,91 @@ fn create_command_with_env(program: &str) -> Command {
|
|||||||
|
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Import an agent from JSON data
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_agent(db: State<'_, AgentDb>, json_data: String) -> Result<Agent, String> {
|
||||||
|
// Parse the JSON data
|
||||||
|
let export_data: AgentExport = serde_json::from_str(&json_data)
|
||||||
|
.map_err(|e| format!("Invalid JSON format: {}", e))?;
|
||||||
|
|
||||||
|
// Validate version
|
||||||
|
if export_data.version != 1 {
|
||||||
|
return Err(format!("Unsupported export version: {}. This version of the app only supports version 1.", export_data.version));
|
||||||
|
}
|
||||||
|
|
||||||
|
let agent_data = export_data.agent;
|
||||||
|
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Check if an agent with the same name already exists
|
||||||
|
let existing_count: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM agents WHERE name = ?1",
|
||||||
|
params![agent_data.name],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// If agent with same name exists, append a suffix
|
||||||
|
let final_name = if existing_count > 0 {
|
||||||
|
format!("{} (Imported)", agent_data.name)
|
||||||
|
} else {
|
||||||
|
agent_data.name
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the agent
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO agents (name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||||
|
params![
|
||||||
|
final_name,
|
||||||
|
agent_data.icon,
|
||||||
|
agent_data.system_prompt,
|
||||||
|
agent_data.default_task,
|
||||||
|
agent_data.model,
|
||||||
|
agent_data.sandbox_enabled,
|
||||||
|
agent_data.enable_file_read,
|
||||||
|
agent_data.enable_file_write,
|
||||||
|
agent_data.enable_network
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to create agent: {}", e))?;
|
||||||
|
|
||||||
|
let id = conn.last_insert_rowid();
|
||||||
|
|
||||||
|
// Fetch the created agent
|
||||||
|
let agent = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT id, name, icon, system_prompt, default_task, model, sandbox_enabled, enable_file_read, enable_file_write, enable_network, created_at, updated_at FROM agents WHERE id = ?1",
|
||||||
|
params![id],
|
||||||
|
|row| {
|
||||||
|
Ok(Agent {
|
||||||
|
id: Some(row.get(0)?),
|
||||||
|
name: row.get(1)?,
|
||||||
|
icon: row.get(2)?,
|
||||||
|
system_prompt: row.get(3)?,
|
||||||
|
default_task: row.get(4)?,
|
||||||
|
model: row.get(5)?,
|
||||||
|
sandbox_enabled: row.get(6)?,
|
||||||
|
enable_file_read: row.get(7)?,
|
||||||
|
enable_file_write: row.get(8)?,
|
||||||
|
enable_network: row.get(9)?,
|
||||||
|
created_at: row.get(10)?,
|
||||||
|
updated_at: row.get(11)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to fetch created agent: {}", e))?;
|
||||||
|
|
||||||
|
Ok(agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import agent from file
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_agent_from_file(db: State<'_, AgentDb>, file_path: String) -> Result<Agent, String> {
|
||||||
|
// Read the file
|
||||||
|
let json_data = std::fs::read_to_string(&file_path)
|
||||||
|
.map_err(|e| format!("Failed to read file: {}", e))?;
|
||||||
|
|
||||||
|
// Import the agent
|
||||||
|
import_agent(db, json_data).await
|
||||||
|
}
|
||||||
|
@@ -26,7 +26,8 @@ use commands::agents::{
|
|||||||
migrate_agent_runs_to_session_ids, list_running_sessions, kill_agent_session,
|
migrate_agent_runs_to_session_ids, list_running_sessions, kill_agent_session,
|
||||||
get_session_status, cleanup_finished_processes, get_session_output,
|
get_session_status, cleanup_finished_processes, get_session_output,
|
||||||
get_live_session_output, stream_session_output, get_claude_binary_path,
|
get_live_session_output, stream_session_output, get_claude_binary_path,
|
||||||
set_claude_binary_path, export_agent, export_agent_to_file, AgentDb
|
set_claude_binary_path, export_agent, export_agent_to_file, import_agent,
|
||||||
|
import_agent_from_file, AgentDb
|
||||||
};
|
};
|
||||||
use commands::sandbox::{
|
use commands::sandbox::{
|
||||||
list_sandbox_profiles, create_sandbox_profile, update_sandbox_profile, delete_sandbox_profile,
|
list_sandbox_profiles, create_sandbox_profile, update_sandbox_profile, delete_sandbox_profile,
|
||||||
@@ -139,6 +140,8 @@ fn main() {
|
|||||||
get_agent,
|
get_agent,
|
||||||
export_agent,
|
export_agent,
|
||||||
export_agent_to_file,
|
export_agent_to_file,
|
||||||
|
import_agent,
|
||||||
|
import_agent_from_file,
|
||||||
execute_agent,
|
execute_agent,
|
||||||
list_agent_runs,
|
list_agent_runs,
|
||||||
get_agent_run,
|
get_agent_run,
|
||||||
|
@@ -16,12 +16,13 @@ import {
|
|||||||
Terminal,
|
Terminal,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
History,
|
History,
|
||||||
Download
|
Download,
|
||||||
|
Upload
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||||
import { api, type Agent, type AgentRunWithMetrics } from "@/lib/api";
|
import { api, type Agent, type AgentRunWithMetrics } from "@/lib/api";
|
||||||
import { save } from "@tauri-apps/plugin-dialog";
|
import { save, open } from "@tauri-apps/plugin-dialog";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Toast, ToastContainer } from "@/components/ui/toast";
|
import { Toast, ToastContainer } from "@/components/ui/toast";
|
||||||
@@ -187,6 +188,34 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImportAgent = async () => {
|
||||||
|
try {
|
||||||
|
// Show native open dialog
|
||||||
|
const filePath = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [{
|
||||||
|
name: 'Claudia Agent',
|
||||||
|
extensions: ['claudia.json', 'json']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
// User cancelled the dialog
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import the agent from the selected file
|
||||||
|
await api.importAgentFromFile(filePath as string);
|
||||||
|
|
||||||
|
setToast({ message: "Agent imported successfully", type: "success" });
|
||||||
|
await loadAgents();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to import agent:", err);
|
||||||
|
const errorMessage = err instanceof Error ? err.message : "Failed to import agent";
|
||||||
|
setToast({ message: errorMessage, type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Pagination calculations
|
// Pagination calculations
|
||||||
const totalPages = Math.ceil(agents.length / AGENTS_PER_PAGE);
|
const totalPages = Math.ceil(agents.length / AGENTS_PER_PAGE);
|
||||||
const startIndex = (currentPage - 1) * AGENTS_PER_PAGE;
|
const startIndex = (currentPage - 1) * AGENTS_PER_PAGE;
|
||||||
@@ -264,6 +293,16 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setView("create")}
|
onClick={() => setView("create")}
|
||||||
size="default"
|
size="default"
|
||||||
@@ -273,6 +312,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
|||||||
Create CC Agent
|
Create CC Agent
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Error display */}
|
{/* Error display */}
|
||||||
@@ -402,7 +442,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
|
|||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
title="Export agent to .claudia.json"
|
title="Export agent to .claudia.json"
|
||||||
>
|
>
|
||||||
<Download className="h-3 w-3" />
|
<Upload className="h-3 w-3" />
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
@@ -747,6 +747,34 @@ export const api = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports an agent from JSON data
|
||||||
|
* @param jsonData - The JSON string containing the agent export
|
||||||
|
* @returns Promise resolving to the imported agent
|
||||||
|
*/
|
||||||
|
async importAgent(jsonData: string): Promise<Agent> {
|
||||||
|
try {
|
||||||
|
return await invoke<Agent>('import_agent', { jsonData });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to import agent:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports an agent from a file
|
||||||
|
* @param filePath - The path to the JSON file
|
||||||
|
* @returns Promise resolving to the imported agent
|
||||||
|
*/
|
||||||
|
async importAgentFromFile(filePath: string): Promise<Agent> {
|
||||||
|
try {
|
||||||
|
return await invoke<Agent>('import_agent_from_file', { filePath });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to import agent from file:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes an agent
|
* Executes an agent
|
||||||
* @param agentId - The agent ID to execute
|
* @param agentId - The agent ID to execute
|
||||||
|
Reference in New Issue
Block a user