This chapter marks a pivotal moment for Kanbots. We’re moving beyond a static Kanban board and injecting intelligence by integrating our first AI agent. You’ll learn how to connect an AI model like Claude Code or a modern OpenAI equivalent (e.g., GPT-4o) to a Kanban card. This enables the agent to perform specific tasks, such as generating code, within its dedicated git worktree. By the end of this milestone, your Kanbots application will be able to dispatch a task to an AI agent, have that agent generate content (like a simple code file), and observe the results directly within the isolated worktree associated with your Kanban card. This lays the foundation for powerful, automated development workflows.
Project Overview: Bringing AI to Your Kanban Board
The goal of this chapter is to empower individual Kanban cards with AI capabilities. Imagine a card titled “Implement User Login.” Instead of manually writing the initial boilerplate, you could prompt an AI agent directly from that card. The agent would then operate in its isolated environment, generate the code, and even commit it, ready for review. This significantly accelerates the initial development phase and allows developers to focus on higher-level logic.
Why This Matters
Integrating AI agents directly into your development workflow, especially in a local-first desktop application like Kanbots, offers several key advantages:
- Accelerated Prototyping: Quickly generate boilerplate, test cases, or initial code structures, reducing manual setup time.
- Contextual AI Assistance: Agents operate within the specific context of a Kanban card and its associated worktree, ensuring outputs are highly relevant.
- Isolated Experimentation: Git worktrees provide a safe sandbox for AI-generated code. Changes are isolated, easy to review, and can be discarded without affecting the main codebase.
- Privacy and Control: Running AI orchestration locally gives you more control over your data and interactions compared to purely cloud-based solutions.
By the end of this chapter, you will have a Kanbots application where a user can select an AI model, provide a prompt to a Kanban card, and witness the AI agent generate code directly into the card’s dedicated git worktree.
Tech Stack Deep Dive
For this chapter, we’re building on our existing Tauri (Rust + Svelte) foundation and introducing new components:
- Rust (
reqwest,dotenv,tokio): The backend orchestrator will usereqwestfor making HTTP requests to external AI APIs,dotenvfor securely loading API keys, andtokiofor asynchronous operations. - AI Agent APIs (Claude Code or OpenAI): We’ll integrate with either Anthropic’s Claude API (using the latest Opus model) or OpenAI’s API (using GPT-4o as a capable code-generation model, superseding older Codex models). These services provide the actual intelligence.
- Git: Our existing
git_worktreemodule is crucial, as it provides the isolated environment for AI agents to operate within. - Svelte: The frontend will provide the UI elements to trigger AI tasks and display their status.
Milestones and Build Plan
To achieve our goal of integrating a functional AI agent, we’ll follow these steps:
- Secure API Key Management: Implement a robust way to load API keys without hardcoding them.
- Rust AI Client Module: Create a dedicated Rust module to handle communication with external AI APIs, abstracting away the HTTP details.
- Rust Agent Orchestration: Modify the main Rust backend to receive AI task requests, prepare the git worktree, invoke the AI client, and write the generated output to a file within the worktree.
- Svelte UI Integration: Add interactive elements to the Kanban card for triggering AI tasks, providing prompts, selecting models, and displaying agent status.
Architecture: Orchestrating the AI Interaction
Integrating an AI agent requires careful orchestration between the frontend, the Rust backend, and the external AI API. The core idea is that a user action on a Kanban card triggers a backend process. This process then prepares a specific environment (a git worktree), invokes the AI, and writes the AI’s output back into that environment.
Agent Interaction Data Flow
Here’s the high-level flow we’ll implement:
- Svelte UI (Frontend): A user initiates an AI task from a Kanban card (e.g., clicks “Generate Code”). This sends a request to the Rust backend via Tauri’s IPC mechanism, passing the card’s ID, the worktree path, the task prompt, and the chosen AI model.
- Rust Backend (Orchestrator):
- Receives the request from the frontend.
- Initializes or switches to the specific git worktree associated with the card.
- Communicates with the external AI API (Claude or OpenAI) using the provided prompt and API key.
- Receives the AI’s generated text response.
- Writes the AI-generated content into a new file within the active git worktree.
- Optionally, performs
git addandgit committo stage and save the AI’s changes, providing a clear record. - Sends a success or failure message back to the Svelte UI.
- AI API (External Service): Processes the prompt, applies its intelligence, and returns a generated text response.
- Git Worktree (Local Environment): Serves as the isolated workspace where the AI agent performs its modifications. This ensures that agent changes are contained and don’t conflict with other development efforts.
API Key Management Strategy
Accessing AI services like Claude or OpenAI requires an API key. For security, these keys must never be hardcoded directly into your application’s source code. We will leverage environment variables for development and local testing. This is a standard and relatively secure practice for local environments. For production desktop applications, more robust OS-level secret management (e.g., system keychains, credential managers) would be the next step, but it’s beyond the scope of this initial integration.
AI Client Abstraction
To maintain a clean and extensible Rust backend, we’ll encapsulate the logic for interacting with AI APIs within a dedicated ai_client module. This module will handle HTTP requests, API key authentication, and response parsing, offering a simplified interface to the main application logic. This abstraction makes it easier to swap out AI providers or add new models in the future.
Step-by-Step Implementation
Let’s build out the components required for our first AI agent integration.
1. Securely Manage AI API Keys with dotenv
First, ensure your AI API keys are handled securely. We’ll use the dotenv crate to load environment variables from a .env file, preventing them from being committed to source control.
Create a .env file in the root of your Tauri project (where Cargo.toml is located) if you don’t have one.
# .env
CLAUDE_API_KEY=sk-YOUR_CLAUDE_API_KEY_HERE
OPENAI_API_KEY=sk-YOUR_OPENAI_API_KEY_HEREImportant: Replace sk-YOUR_CLAUDE_API_KEY_HERE and sk-YOUR_OPENAI_API_KEY_HERE with your actual API keys. You only need to set the key for the model you intend to use.
Next, add .env to your .gitignore file to prevent accidental commitment:
# .gitignore
# ... other entries ...
.envNow, open Cargo.toml and add the necessary dependencies for HTTP requests and environment variable loading:
# Cargo.toml
[dependencies]
tauri = { version = "2.0.0-beta.16", features = ["shell", "process"] } # Checked 2026-05-24
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.38.0", features = ["full"] } # Checked 2026-05-24
reqwest = { version = "0.12.5", features = ["json"] } # Checked 2026-05-24
dotenv = "0.15.0" # Checked 2026-05-24Finally, modify your src/main.rs to load these environment variables at application startup:
// src/main.rs
// ... existing imports ...
use dotenv::dotenv; // Add this line
use std::env; // Add this line
// ... existing tauri::command macros and mod declarations ...
// We'll add the actual implementation for this command later.
// For now, ensure it's declared for the main function to pick up.
#[tauri::command]
async fn run_ai_agent(card_id: String, worktree_path: String, prompt: String, model_name: String) -> Result<String, String> {
// Placeholder for now, full implementation below
println!("AI agent invoked for card: {} in worktree: {} with prompt: {} using model: {}", card_id, worktree_path, prompt, model_name);
Ok(format!("AI agent task for card {} started (placeholder).", card_id))
}
fn main() {
dotenv().ok(); // Load environment variables from .env file. `.ok()` prevents crashing if file not found.
// ... rest of your main function ...
tauri::Builder::new()
.invoke_handler(tauri::generate_handler![
// ... existing commands ...
run_ai_agent // Add our new command here
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}This setup ensures that your API keys are loaded securely during development and runtime.
2. Rust Backend - AI Client Module (src/ai_client.rs)
Create a new file src/ai_client.rs. This module will encapsulate the logic for interacting with different AI providers.
// src/ai_client.rs
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::env;
use std::collections::HashMap;
/// Enum to represent different AI models Kanbots can use.
/// We provide Claude and OpenAI (as a modern equivalent to Codex).
#[derive(Debug)]
pub enum AiModel {
Claude,
OpenAI,
}
impl AiModel {
pub fn name(&self) -> &str {
match self {
AiModel::Claude => "Claude",
AiModel::OpenAI => "OpenAI (GPT-4o)",
}
}
}
// --- Claude API Structures (Anthropic Messages API) ---
// See: https://docs.anthropic.com/claude/reference/messages_post
#[derive(Serialize, Debug)]
struct ClaudeMessage {
role: String,
content: String,
}
#[derive(Serialize, Debug)]
struct ClaudeRequest {
model: String,
max_tokens: u32,
messages: Vec<ClaudeMessage>,
#[serde(skip_serializing_if = "Option::is_none")] // Optional, for example, system prompt
system: Option<String>,
}
#[derive(Deserialize, Debug)]
struct ClaudeResponse {
content: Vec<ClaudeContent>,
#[serde(flatten)] // Capture other fields like 'id', 'model', 'usage', etc.
_other: HashMap<String, serde_json::Value>,
}
#[derive(Deserialize, Debug)]
struct ClaudeContent {
#[serde(rename = "type")]
content_type: String,
text: String,
}
// --- OpenAI API Structures (Chat Completions API) ---
// See: https://platform.openai.com/docs/api-reference/chat/create
#[derive(Serialize, Debug)]
struct OpenAIRequest {
model: String,
messages: Vec<OpenAIMessage>,
max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")] // Optional, for example, temperature
temperature: Option<f32>,
}
#[derive(Serialize, Debug, Deserialize)] // Deserialize for response parsing too
struct OpenAIMessage {
role: String,
content: String,
}
#[derive(Deserialize, Debug)]
struct OpenAIResponse {
choices: Vec<OpenAIChoice>,
#[serde(flatten)]
_other: HashMap<String, serde_json::Value>,
}
#[derive(Deserialize, Debug)]
struct OpenAIChoice {
message: OpenAIMessage,
#[serde(flatten)]
_other: HashMap<String, serde_json::Value>,
}
/// Client for interacting with various AI models.
pub struct AiClient {
client: Client,
claude_api_key: Option<String>,
openai_api_key: Option<String>,
}
impl AiClient {
/// Creates a new `AiClient` by loading API keys from environment variables.
pub fn new() -> Self {
let claude_api_key = env::var("CLAUDE_API_KEY").ok();
let openai_api_key = env::var("OPENAI_API_KEY").ok();
if claude_api_key.is_none() && openai_api_key.is_none() {
eprintln!("⚠️ Warning: No AI API keys found. Please set CLAUDE_API_KEY or OPENAI_API_KEY environment variable.");
}
AiClient {
client: Client::new(),
claude_api_key,
openai_api_key,
}
}
/// Generates code using the specified AI model.
/// Returns the generated text or a descriptive error.
pub async fn generate_code(&self, model_type: AiModel, prompt: &str) -> Result<String, String> {
match model_type {
AiModel::Claude => self.generate_with_claude(prompt).await,
AiModel::OpenAI => self.generate_with_openai(prompt).await,
}
}
/// Internal method to interact with the Claude API.
async fn generate_with_claude(&self, prompt: &str) -> Result<String, String> {
let api_key = self.claude_api_key.as_ref()
.ok_or_else(|| "Claude API key (CLAUDE_API_KEY) not set in environment.".to_string())?;
let url = "https://api.anthropic.com/v1/messages";
// Using Claude 3 Opus as of 2026-05-24, known for strong reasoning and code capabilities.
let model = "claude-3-opus-20240229";
let request_body = ClaudeRequest {
model: model.to_string(),
max_tokens: 1024, // Limit output length to prevent excessive costs/response times
messages: vec![
ClaudeMessage {
role: "user".to_string(),
content: prompt.to_string(),
},
],
system: Some("You are a helpful and precise code generation assistant. Only output code or explanations relevant to the user's request.".to_string()),
};
let response = self.client.post(url)
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01") // Required Anthropic-Version header
.header("content-type", "application/json")
.json(&request_body)
.send()
.await
.map_err(|e| format!("Failed to send request to Claude API: {}", e))?;
let status = response.status();
let response_text = response.text().await.map_err(|e| format!("Failed to read Claude API response text: {}", e))?;
if status.is_success() {
let claude_response: ClaudeResponse = serde_json::from_str(&response_text)
.map_err(|e| format!("Failed to parse Claude API response: {}. Raw response: {}", e, response_text))?;
if let Some(content_block) = claude_response.content.into_iter().find(|c| c.content_type == "text") {
Ok(content_block.text)
} else {
Err(format!("Claude API response missing expected text content. Raw response: {}", response_text))
}
} else {
Err(format!("Claude API error: Status {}. Response: {}", status, response_text))
}
}
/// Internal method to interact with the OpenAI API.
async fn generate_with_openai(&self, prompt: &str) -> Result<String, String> {
let api_key = self.openai_api_key.as_ref()
.ok_or_else(|| "OpenAI API key (OPENAI_API_KEY) not set in environment.".to_string())?;
let url = "https://api.openai.com/v1/chat/completions";
// Using gpt-4o as a modern, highly capable model for code generation,
// effectively superseding older Codex models. Checked 2026-05-24.
let model = "gpt-4o";
let request_body = OpenAIRequest {
model: model.to_string(),
messages: vec![
OpenAIMessage {
role: "user".to_string(),
content: prompt.to_string(),
},
],
max_tokens: 1024, // Limit output length
temperature: Some(0.7), // Controls randomness: lower for more deterministic, higher for more creative
};
let response = self.client.post(url)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.json(&request_body)
.send()
.await
.map_err(|e| format!("Failed to send request to OpenAI API: {}", e))?;
let status = response.status();
let response_text = response.text().await.map_err(|e| format!("Failed to read OpenAI API response text: {}", e))?;
if status.is_success() {
let openai_response: OpenAIResponse = serde_json::from_str(&response_text)
.map_err(|e| format!("Failed to parse OpenAI API response: {}. Raw response: {}", e, response_text))?;
if let Some(choice) = openai_response.choices.into_iter().next() {
Ok(choice.message.content)
} else {
Err(format!("OpenAI API response missing choice content. Raw response: {}", response_text))
}
} else {
Err(format!("OpenAI API error: Status {}. Response: {}", status, response_text))
}
}
}Explanation of src/ai_client.rs:
AiModelEnum: This enum allows us to easily select which AI model to use, abstracting the underlying API calls.- Request/Response Structs: These
structdefinitions, marked with#[derive(Serialize, Deserialize)], mirror the JSON payloads expected by the Claude and OpenAI APIs. This ensures type-safe communication.- Claude: Interacts with Anthropic’s
https://api.anthropic.com/v1/messagesendpoint. It requires specific headers likex-api-keyandanthropic-version. We’re usingclaude-3-opus-20240229, the latest powerful model as of 2026-05-24. - OpenAI (Codex equivalent): Interacts with OpenAI’s
https://api.openai.com/v1/chat/completionsendpoint. It uses anAuthorization: Bearer <API_KEY>header. We’ve chosengpt-4oas a modern, highly capable model for code generation, as the original Codex models are largely superseded by newer OpenAI models like GPT-3.5 and GPT-4 series.
- Claude: Interacts with Anthropic’s
AiClientStruct: This holds thereqwest::Clientinstance (for making HTTP requests) and the loaded API keys.new(): The constructor attempts to load API keys from environment variables. If none are found, it prints a warning, but allows the client to be created, enabling graceful degradation if a specific model isn’t configured.generate_code(): This public method serves as the entry point for AI code generation. It dispatches the request to the appropriate internal method (generate_with_claudeorgenerate_with_openai) based on theAiModelenum.generate_with_claude()/generate_with_openai(): These private methods handle the specifics of constructing HTTP requests, setting headers, sending JSON payloads, and parsing the responses for each API. They include robust error handling for network issues, API-specific error responses, and JSON deserialization failures. Amax_tokenslimit is set to manage response length and potential costs.
3. Rust Backend - Agent Orchestration Logic (src/main.rs)
Now, let’s connect the AiClient with our git worktree management. We’ll modify the run_ai_agent command in src/main.rs. We’ll also need to import our new ai_client module and the git_worktree module (which you should have from the previous chapter).
First, ensure src/main.rs includes the mod declarations for both modules:
// src/main.rs
mod git_worktree; // From previous chapter (Chapter 2)
mod ai_client; // Our new module
// ... other uses ...
use git_worktree::{GitWorktree, WorktreeError}; // Ensure WorktreeError is imported if used
use ai_client::{AiClient, AiModel};
// ... existing tauri::command macros ...
#[tauri::command]
async fn run_ai_agent(card_id: String, worktree_path: String, prompt: String, model_name: String) -> Result<String, String> {
println!("Invoking AI agent for card: {} in worktree: {} with prompt: {} using model: {}", card_id, worktree_path, prompt, model_name);
let ai_client = AiClient::new();
let model_type = match model_name.as_str() {
"Claude" => AiModel::Claude,
"OpenAI" => AiModel::OpenAI,
_ => return Err(format!("Unsupported AI model selected: {}. Please choose 'Claude' or 'OpenAI'.", model_name)),
};
// 1. Ensure the worktree exists and switch to it
// This provides the isolated environment for the AI agent.
let worktree_manager = GitWorktree::new(&worktree_path)
.map_err(|e| format!("Failed to initialize GitWorktree for {}: {}", worktree_path, e))?;
worktree_manager.switch_to_worktree()
.map_err(|e| format!("Failed to switch to worktree {}: {}", worktree_path, e))?;
// 2. Call the AI client to generate code
let generated_content = ai_client.generate_code(model_type, &prompt).await?;
// 3. Define a filename for the generated content.
// In a more advanced system, the AI might suggest a filename or the user could specify one.
// For now, we'll use a descriptive name based on the card ID.
let file_name = format!("{}_generated_task.md", card_id); // Using .md for general content, could be .py, .js, etc.
let file_path = std::path::PathBuf::from(&worktree_path).join(&file_name);
// 4. Write the generated content to the file within the active worktree
tokio::fs::write(&file_path, generated_content.as_bytes())
.await
.map_err(|e| format!("Failed to write generated content to file {}: {}", file_path.display(), e))?;
// 5. Stage and commit the changes
// This creates a clear, auditable record of the AI agent's contribution.
worktree_manager.add(&file_name)
.map_err(|e| format!("Failed to stage file {}: {}", file_name, e))?;
let commit_message = format!("feat(ai): Kanbots AI agent generated content for card {}", card_id);
worktree_manager.commit(&commit_message)
.map_err(|e| format!("Failed to commit changes for card {}: {}", card_id, e))?;
Ok(format!("AI agent successfully generated content to {} and committed changes.", file_path.display()))
}
// ... main function remains the same, ensure run_ai_agent is in generate_handler! ...Explanation of run_ai_agent command:
- Arguments: It now accepts
card_id,worktree_path,prompt, andmodel_namefrom the frontend. AiClientInitialization: AnAiClientinstance is created, which automatically attempts to load API keys.- Model Selection: The
model_namestring from the frontend is matched to ourAiModelenum. - Worktree Preparation: It uses
GitWorktree::newto manage the worktree atworktree_pathandswitch_to_worktreeto ensure the AI operates in the correct, isolated environment. - AI Invocation:
ai_client.generate_codeis called with the selected model and the user’s prompt.await?handles potential errors, propagating them up. - File Creation: A descriptive filename (
{card_id}_generated_task.md) is constructed, and the AI’s response is written to this file within the active worktree usingtokio::fs::write. - Git Commit: The generated file is staged (
worktree_manager.add) and committed (worktree_manager.commit) with a clear commit message. This makes the AI’s contribution a traceable part of the version history. - Result: A success message or an error is returned to the frontend.
4. Svelte Frontend - UI Integration (src/lib/components/KanbanCard.svelte)
Now, let’s add the UI elements to a Kanban card to trigger this AI agent. First, ensure your Card interface (or type) in Svelte includes a worktreePath, as we need this to tell the Rust backend where the agent should operate. This was likely added in Chapter 2 or 3.
// src/lib/types/kanban.ts (example, verify path in your project)
export interface Card {
id: string;
title: string;
description: string;
columnId: string;
worktreePath?: string; // This property is crucial for agent operations
// ... other properties you might have
}Now, modify src/lib/components/KanbanCard.svelte to add a button, prompt input, model selection, and display agent status.
<!-- src/lib/components/KanbanCard.svelte -->
<script lang="ts">
import type { Card } from '$lib/types/kanban'; // Adjust path if needed based on your project structure
import { invoke } from '@tauri-apps/api/core';
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
export let card: Card;
const dispatch = createEventDispatcher();
let showEdit = false;
let newTitle = card.title;
let newDescription = card.description;
let agentStatus: string = '';
let isLoadingAgent = false;
let aiPrompt = 'Generate a simple "hello world" Python script, including a main function and a docstring.';
let selectedModel = 'Claude'; // Default to Claude, allow user to pick
function startEditing() {
showEdit = true;
}
function saveCard() {
dispatch('updateCard', {
id: card.id,
title: newTitle,
description: newDescription,
});
showEdit = false;
}
function deleteCard() {
dispatch('deleteCard', card.id);
}
async function runAgent() {
if (!card.worktreePath) {
agentStatus = 'Error: No worktree path defined for this card. Please assign a worktree first.';
return;
}
if (!aiPrompt.trim()) {
agentStatus = 'Error: Please enter a prompt for the AI agent.';
return;
}
isLoadingAgent = true;
agentStatus = `Running ${selectedModel} agent... This may take a moment.`;
try {
const result: string = await invoke('run_ai_agent', {
cardId: card.id,
worktreePath: card.worktreePath,
prompt: aiPrompt,
modelName: selectedModel,
});
agentStatus = result; // Display the success message from Rust
console.log('AI Agent Result:', result);
} catch (e: any) {
agentStatus = `Agent Error: ${e}`;
console.error('AI Agent Error:', e);
} finally {
isLoadingAgent = false;
}
}
</script>
<div
class="bg-white p-4 rounded-lg shadow-md mb-3 border border-gray-200 hover:border-blue-400 cursor-grab"
draggable="true"
on:dragstart={(e) => {
e.dataTransfer?.setData('text/plain', card.id);
e.dataTransfer?.effectAllowed = 'move';
}}
>
{#if showEdit}
<input
type="text"
bind:value={newTitle}
class="w-full p-2 mb-2 border rounded-md"
on:blur={saveCard}
on:keydown={(e) => e.key === 'Enter' && saveCard()}
/>
<textarea
bind:value={newDescription}
class="w-full p-2 mb-2 border rounded-md"
on:blur={saveCard}
></textarea>
<div class="flex justify-end space-x-2">
<button on:click={saveCard} class="px-3 py-1 bg-green-500 text-white rounded-md text-sm">Save</button>
<button on:click={deleteCard} class="px-3 py-1 bg-red-500 text-white rounded-md text-sm">Delete</button>
</div>
{:else}
<h3 class="font-semibold text-lg mb-1">{card.title}</h3>
<p class="text-gray-600 text-sm mb-2">{card.description}</p>
<p class="text-gray-500 text-xs mb-2">Worktree: {card.worktreePath || 'N/A'}</p>
<div class="mt-4 border-t pt-3">
<h4 class="font-medium text-md mb-2">AI Agent Task</h4>
<textarea
bind:value={aiPrompt}
rows="3"
class="w-full p-2 mb-2 text-sm border rounded-md bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter AI prompt here..."
></textarea>
<select bind:value={selectedModel} class="w-full p-2 mb-2 text-sm border rounded-md bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="Claude">Claude (Anthropic)</option>
<option value="OpenAI">OpenAI (GPT-4o)</option>
</select>
<button
on:click={runAgent}
disabled={isLoadingAgent || !card.worktreePath || !aiPrompt.trim()}
class="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm font-semibold"
>
{#if isLoadingAgent}
Running Agent...
{:else}
Run AI Agent
{/if}
</button>
{#if agentStatus}
<p class="mt-2 text-xs {agentStatus.startsWith('Error:') ? 'text-red-600' : 'text-gray-700'}" transition:fade>{agentStatus}</p>
{/if}
</div>
<div class="flex justify-end mt-3 space-x-2">
<button on:click={startEditing} class="px-3 py-1 bg-gray-200 text-gray-800 rounded-md text-sm hover:bg-gray-300">Edit</button>
<button on:click={deleteCard} class="px-3 py-1 bg-red-500 text-white rounded-md text-sm hover:bg-red-600">Delete</button>
</div>
{/if}
</div>
<style>
/* Add any specific styles for KanbanCard here */
</style>Explanation of KanbanCard.svelte changes:
runAgent()function: This asynchronous function is triggered by the “Run AI Agent” button.- It includes checks for
card.worktreePathand a non-emptyaiPromptto provide immediate user feedback. isLoadingAgentis set totrueto disable the button and show a loading state, preventing multiple concurrent requests from the same card.- It calls the Tauri backend command
run_ai_agentusinginvoke, passing the card’s ID, its associatedworktreePath, theaiPromptfrom the textarea, and theselectedModel. - It updates
agentStatuswith success messages from the Rust backend or any errors encountered.
- It includes checks for
- UI Elements:
- A
textareais provided for the user to input the AI prompt. - A
selectdropdown allows choosing between “Claude (Anthropic)” and “OpenAI (GPT-4o)”. - A “Run AI Agent” button is dynamically enabled/disabled based on
isLoadingAgent, the presence of aworktreePath, and a validaiPrompt. - A paragraph displays the
agentStatus, providing real-time feedback to the user. The text color changes to red for errors.
- A
transition:fade: A simple Svelte transition is used on theagentStatusparagraph for a smoother user experience.
Testing & Verification
Now that we’ve integrated the AI agent, let’s verify it works as expected.
Start the Kanbots application:
cargo tauri devObserve the terminal output for any Rust compilation errors or warnings from
AiClient::new()regarding missing API keys.Ensure API Keys are Set:
- Verify that your
.envfile exists in the project root. - Confirm that
CLAUDE_API_KEYorOPENAI_API_KEY(depending on which model you want to test) is correctly set with your actual API key. - Crucial: If you modified
.env, you must restartcargo tauri devfor the changes to take effect.
- Verify that your
Create a New Kanban Card: Add a new card to any column in your Kanbots application.
Initialize a Worktree for the Card:
- From a previous chapter (Chapter 2 or 3), you should have functionality to associate a git worktree with a card. Use that to create a new worktree for your new card.
- For example, if your base repository is
~/kanbots-repo, you might create a worktree at~/kanbots-repo/worktrees/card-123-feature. Note the exact path.
Enter a Prompt: In the “AI Agent Task” section of the card, enter a clear, concise prompt. For example:
- “Write a Python function
is_prime(n)that checks if a number is prime. Include a docstring and example usage.” - “Generate an HTML boilerplate with a title ‘Kanbots Generated Page’ and a simple h1 tag.”
- “Write a Python function
Select AI Model: Choose either “Claude (Anthropic)” or “OpenAI (GPT-4o)” from the dropdown, depending on which API key you’ve set up.
Run AI Agent: Click the “Run AI Agent” button.
- Observe the
agentStatusmessage updating in the Svelte UI. It should first show “Running Agent…” then a success or error message. - Check the terminal where
cargo tauri devis running for Rust backendprintln!messages and any potential errors.
- Observe the
Verify Output:
- Once the agent reports success in the UI, open your terminal or file explorer.
- Navigate to the worktree directory associated with your card (e.g.,
~/kanbots-repo/worktrees/card-123-feature). - You should find a new file there, named something like
your-card-id_generated_task.md(or.py,.htmlif you adjust the extension in Rust). - Open this file. It should contain the code or text generated by the AI based on your prompt.
- To verify the
git commit, navigate to your base repository (~/kanbots-repo) and rungit log --oneline --graph --all. You should see a new commit from the AI agent in its worktree branch.
Quick Debugging Checks:
- Tauri Terminal Output: The terminal running
cargo tauri devis your primary Rust backend log. Look forprintln!messages fromrun_ai_agentand anyeprintln!warnings fromAiClient::new()or errors fromreqwestorserde_json. - Browser Developer Console (Svelte): Press F12 in the Kanbots app window and check the “Console” tab for any frontend errors related to
invokecalls or Svelte component rendering. - Environment Variables: If you encounter “API key not set” errors, carefully re-check your
.envfile’s spelling, location, and ensure you restartedcargo tauri dev. - Network Requests (Rust): If AI API calls fail, the
ai_client.rsmodule’s error messages will be crucial. These typically indicate network issues, invalid API keys, or API-specific errors (e.g., rate limits). - File System Permissions: Ensure the Kanbots application has write permissions to the worktree directories.
Production Considerations
While our current implementation is functional, deploying a desktop application with AI integration requires attention to several production-grade concerns:
- API Key Security: For a truly production-ready desktop app, relying solely on
.envfiles is insufficient. Consider using OS-level secret management (e.g.,keytarfor Node.js/Electron, or native Rust crates that interface with system keychains likekeyring-rs) to store API keys encrypted. This protects keys even if the.envfile is accidentally exposed. - Rate Limiting and Cost Management: AI APIs are metered and can be expensive. Implement:
- User Warnings: Clearly inform users about potential costs associated with AI agent usage.
- Client-side Guards: Basic rate limiting in the frontend to prevent accidental rapid-fire requests.
- Backend Throttling: More robust rate limiting in the Rust backend to manage calls to the external APIs and provide clear feedback (
429 Too Many Requests). - Usage Tracking: Log AI API call counts and token usage to help users monitor their spending.
- Robust Error Handling & User Feedback: The current
Result<String, String>is simple. For production, define a custom errorenumin Rust for more structured error types (e.g.,AiApiError::InvalidKey,AiApiError::RateLimitExceeded,WorktreeError::NotFound). This allows the frontend to display highly specific, actionable error messages to the user. - Asynchronous Operations & Responsiveness: AI API calls can introduce noticeable latency (~2-10 seconds, depending on the model and prompt complexity). Ensure the UI remains responsive using clear loading indicators, and that long-running operations in Rust are truly non-blocking, which
async/awaitandtokiohelp facilitate. - Comprehensive Logging: Implement detailed logging in the Rust backend for all AI interactions: prompts sent, full responses received, token counts, and all errors. This is invaluable for debugging, auditing agent behavior, and understanding usage patterns.
- Agent Persona and Context Management: As agents become more complex, managing their “persona” (e.g., “Developer,” “Reviewer”) and providing precise context will be critical to getting reliable outputs. This will be explored in later chapters.
Common Issues & Solutions
“API key not set” or authentication errors:
- Issue: The
CLAUDE_API_KEYorOPENAI_API_KEYenvironment variable is not correctly loaded or is missing. - Solution:
- Verify the
.envfile exists in the project root of your Tauri project. - Ensure the variable name is exactly
CLAUDE_API_KEYorOPENAI_API_KEY. - Confirm
dotenv().ok()is called at the very beginning of yourmainfunction insrc/main.rs. - Crucial: Always restart
cargo tauri devafter making changes to.envorCargo.toml. - Double-check the API key itself for typos, leading/trailing spaces, or expiration.
- For OpenAI, ensure your key starts with
sk-and is not an organization ID. - For Claude, ensure your key starts with
sk-ant-and is not an organization ID.
- Verify the
- Issue: The
invoke('run_ai_agent', ...)fails with “command not found”:- Issue: The Tauri backend isn’t correctly registering the
run_ai_agentcommand. - Solution: Ensure
run_ai_agentis explicitly included in thetauri::generate_handler![]macro within yourmain.rs. Recompile and restart the app. Check for any Rust compiler errors related to missing#[tauri::command]attributes.
- Issue: The Tauri backend isn’t correctly registering the
AI agent generates unexpected, incomplete, or empty output:
- Issue: The prompt might be too vague, the AI model is hallucinating, the
max_tokenslimit was hit, or the API call failed silently (though ourai_clienttries to catch this). - Solution:
- Check the Rust backend’s console output for any errors from
ai_client(e.g., JSON parsing errors, API specific errors). - Try a simpler, more direct prompt. Experiment with prompt engineering.
- Temporarily increase
max_tokensinai_client.rsto see if the output was truncated. - If possible, inspect the raw
response_textin theai_client.rscode during debugging to see exactly what the API returned. This can reveal subtle API-side issues.
- Check the Rust backend’s console output for any errors from
- Issue: The prompt might be too vague, the AI model is hallucinating, the
File not found in worktree after agent runs:
- Issue: The agent successfully ran, but the file was written to the wrong location, or a file system error prevented writing.
- Solution:
- Double-check the
file_pathconstruction inrun_ai_agentto ensure it’s pointing to the correct worktree and filename. Usefile_path.display()to print the full path in Rust logs. - Verify that the
worktree_pathpassed from the frontend is correct and corresponds to an actual, initialized git worktree. - Check for file system permission errors in the Rust logs. The application needs write access to the worktree directory.
- Double-check the
Summary & Next Step
You’ve just enabled your Kanbots application to leverage the power of external AI agents! You’ve successfully:
- Implemented secure API key management using environment variables and the
dotenvcrate. - Created a robust Rust
AiClientmodule to abstract and handle communication with Claude and OpenAI APIs. - Extended the Rust backend to orchestrate AI agent tasks, including switching to specific git worktrees, invoking the AI, writing generated content to files, and committing changes to version control.
- Updated the Svelte frontend to allow users to trigger AI tasks from Kanban cards, provide prompts, select models, and view real-time status.
This milestone transforms Kanbots from a simple task manager into a nascent intelligent development assistant, capable of generating code and committing it directly into isolated work environments.
Next, in Chapter 5: Multi-Agent Orchestration, we’ll build upon this foundation to enable multiple AI agents to work together on a single card, orchestrating more complex, persona-based development workflows.
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.