Skip to content

Commit bec8cea

Browse files
committed
feat: add JSON import functionality for CC agents
1 parent ad03503 commit bec8cea

4 files changed

Lines changed: 193 additions & 12 deletions

File tree

src-tauri/src/commands/agents.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,28 @@ pub struct AgentRunWithMetrics {
169169
pub output: Option<String>, // Real-time JSONL content
170170
}
171171

172+
/// Agent export format
173+
#[derive(Debug, Serialize, Deserialize)]
174+
pub struct AgentExport {
175+
pub version: u32,
176+
pub exported_at: String,
177+
pub agent: AgentData,
178+
}
179+
180+
/// Agent data within export
181+
#[derive(Debug, Serialize, Deserialize)]
182+
pub struct AgentData {
183+
pub name: String,
184+
pub icon: String,
185+
pub system_prompt: String,
186+
pub default_task: Option<String>,
187+
pub model: String,
188+
pub sandbox_enabled: bool,
189+
pub enable_file_read: bool,
190+
pub enable_file_write: bool,
191+
pub enable_network: bool,
192+
}
193+
172194
/// Database connection state
173195
pub struct AgentDb(pub Mutex<Connection>);
174196

@@ -1904,3 +1926,91 @@ fn create_command_with_env(program: &str) -> Command {
19041926

19051927
cmd
19061928
}
1929+
1930+
/// Import an agent from JSON data
1931+
#[tauri::command]
1932+
pub async fn import_agent(db: State<'_, AgentDb>, json_data: String) -> Result<Agent, String> {
1933+
// Parse the JSON data
1934+
let export_data: AgentExport = serde_json::from_str(&json_data)
1935+
.map_err(|e| format!("Invalid JSON format: {}", e))?;
1936+
1937+
// Validate version
1938+
if export_data.version != 1 {
1939+
return Err(format!("Unsupported export version: {}. This version of the app only supports version 1.", export_data.version));
1940+
}
1941+
1942+
let agent_data = export_data.agent;
1943+
let conn = db.0.lock().map_err(|e| e.to_string())?;
1944+
1945+
// Check if an agent with the same name already exists
1946+
let existing_count: i64 = conn
1947+
.query_row(
1948+
"SELECT COUNT(*) FROM agents WHERE name = ?1",
1949+
params![agent_data.name],
1950+
|row| row.get(0),
1951+
)
1952+
.map_err(|e| e.to_string())?;
1953+
1954+
// If agent with same name exists, append a suffix
1955+
let final_name = if existing_count > 0 {
1956+
format!("{} (Imported)", agent_data.name)
1957+
} else {
1958+
agent_data.name
1959+
};
1960+
1961+
// Create the agent
1962+
conn.execute(
1963+
"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)",
1964+
params![
1965+
final_name,
1966+
agent_data.icon,
1967+
agent_data.system_prompt,
1968+
agent_data.default_task,
1969+
agent_data.model,
1970+
agent_data.sandbox_enabled,
1971+
agent_data.enable_file_read,
1972+
agent_data.enable_file_write,
1973+
agent_data.enable_network
1974+
],
1975+
)
1976+
.map_err(|e| format!("Failed to create agent: {}", e))?;
1977+
1978+
let id = conn.last_insert_rowid();
1979+
1980+
// Fetch the created agent
1981+
let agent = conn
1982+
.query_row(
1983+
"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",
1984+
params![id],
1985+
|row| {
1986+
Ok(Agent {
1987+
id: Some(row.get(0)?),
1988+
name: row.get(1)?,
1989+
icon: row.get(2)?,
1990+
system_prompt: row.get(3)?,
1991+
default_task: row.get(4)?,
1992+
model: row.get(5)?,
1993+
sandbox_enabled: row.get(6)?,
1994+
enable_file_read: row.get(7)?,
1995+
enable_file_write: row.get(8)?,
1996+
enable_network: row.get(9)?,
1997+
created_at: row.get(10)?,
1998+
updated_at: row.get(11)?,
1999+
})
2000+
},
2001+
)
2002+
.map_err(|e| format!("Failed to fetch created agent: {}", e))?;
2003+
2004+
Ok(agent)
2005+
}
2006+
2007+
/// Import agent from file
2008+
#[tauri::command]
2009+
pub async fn import_agent_from_file(db: State<'_, AgentDb>, file_path: String) -> Result<Agent, String> {
2010+
// Read the file
2011+
let json_data = std::fs::read_to_string(&file_path)
2012+
.map_err(|e| format!("Failed to read file: {}", e))?;
2013+
2014+
// Import the agent
2015+
import_agent(db, json_data).await
2016+
}

src-tauri/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ use commands::agents::{
2626
migrate_agent_runs_to_session_ids, list_running_sessions, kill_agent_session,
2727
get_session_status, cleanup_finished_processes, get_session_output,
2828
get_live_session_output, stream_session_output, get_claude_binary_path,
29-
set_claude_binary_path, export_agent, export_agent_to_file, AgentDb
29+
set_claude_binary_path, export_agent, export_agent_to_file, import_agent,
30+
import_agent_from_file, AgentDb
3031
};
3132
use commands::sandbox::{
3233
list_sandbox_profiles, create_sandbox_profile, update_sandbox_profile, delete_sandbox_profile,
@@ -139,6 +140,8 @@ fn main() {
139140
get_agent,
140141
export_agent,
141142
export_agent_to_file,
143+
import_agent,
144+
import_agent_from_file,
142145
execute_agent,
143146
list_agent_runs,
144147
get_agent_run,

src/components/CCAgents.tsx

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import {
1616
Terminal,
1717
ArrowLeft,
1818
History,
19-
Download
19+
Download,
20+
Upload
2021
} from "lucide-react";
2122
import { Button } from "@/components/ui/button";
2223
import { Card, CardContent, CardFooter } from "@/components/ui/card";
2324
import { api, type Agent, type AgentRunWithMetrics } from "@/lib/api";
24-
import { save } from "@tauri-apps/plugin-dialog";
25+
import { save, open } from "@tauri-apps/plugin-dialog";
2526
import { invoke } from "@tauri-apps/api/core";
2627
import { cn } from "@/lib/utils";
2728
import { Toast, ToastContainer } from "@/components/ui/toast";
@@ -187,6 +188,34 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
187188
}
188189
};
189190

191+
const handleImportAgent = async () => {
192+
try {
193+
// Show native open dialog
194+
const filePath = await open({
195+
multiple: false,
196+
filters: [{
197+
name: 'Claudia Agent',
198+
extensions: ['claudia.json', 'json']
199+
}]
200+
});
201+
202+
if (!filePath) {
203+
// User cancelled the dialog
204+
return;
205+
}
206+
207+
// Import the agent from the selected file
208+
await api.importAgentFromFile(filePath as string);
209+
210+
setToast({ message: "Agent imported successfully", type: "success" });
211+
await loadAgents();
212+
} catch (err) {
213+
console.error("Failed to import agent:", err);
214+
const errorMessage = err instanceof Error ? err.message : "Failed to import agent";
215+
setToast({ message: errorMessage, type: "error" });
216+
}
217+
};
218+
190219
// Pagination calculations
191220
const totalPages = Math.ceil(agents.length / AGENTS_PER_PAGE);
192221
const startIndex = (currentPage - 1) * AGENTS_PER_PAGE;
@@ -264,14 +293,25 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
264293
</p>
265294
</div>
266295
</div>
267-
<Button
268-
onClick={() => setView("create")}
269-
size="default"
270-
className="flex items-center gap-2"
271-
>
272-
<Plus className="h-4 w-4" />
273-
Create CC Agent
274-
</Button>
296+
<div className="flex items-center gap-2">
297+
<Button
298+
onClick={handleImportAgent}
299+
size="default"
300+
variant="outline"
301+
className="flex items-center gap-2"
302+
>
303+
<Download className="h-4 w-4" />
304+
Import
305+
</Button>
306+
<Button
307+
onClick={() => setView("create")}
308+
size="default"
309+
className="flex items-center gap-2"
310+
>
311+
<Plus className="h-4 w-4" />
312+
Create CC Agent
313+
</Button>
314+
</div>
275315
</div>
276316
</motion.div>
277317

@@ -402,7 +442,7 @@ export const CCAgents: React.FC<CCAgentsProps> = ({ onBack, className }) => {
402442
className="flex items-center gap-1"
403443
title="Export agent to .claudia.json"
404444
>
405-
<Download className="h-3 w-3" />
445+
<Upload className="h-3 w-3" />
406446
Export
407447
</Button>
408448
<Button

src/lib/api.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,34 @@ export const api = {
747747
}
748748
},
749749

750+
/**
751+
* Imports an agent from JSON data
752+
* @param jsonData - The JSON string containing the agent export
753+
* @returns Promise resolving to the imported agent
754+
*/
755+
async importAgent(jsonData: string): Promise<Agent> {
756+
try {
757+
return await invoke<Agent>('import_agent', { jsonData });
758+
} catch (error) {
759+
console.error("Failed to import agent:", error);
760+
throw error;
761+
}
762+
},
763+
764+
/**
765+
* Imports an agent from a file
766+
* @param filePath - The path to the JSON file
767+
* @returns Promise resolving to the imported agent
768+
*/
769+
async importAgentFromFile(filePath: string): Promise<Agent> {
770+
try {
771+
return await invoke<Agent>('import_agent_from_file', { filePath });
772+
} catch (error) {
773+
console.error("Failed to import agent from file:", error);
774+
throw error;
775+
}
776+
},
777+
750778
/**
751779
* Executes an agent
752780
* @param agentId - The agent ID to execute

0 commit comments

Comments
 (0)