diff --git a/archon-ui-main/src/App.tsx b/archon-ui-main/src/App.tsx
index 2a0cdc22..f81dbec2 100644
--- a/archon-ui-main/src/App.tsx
+++ b/archon-ui-main/src/App.tsx
@@ -6,6 +6,7 @@ import { KnowledgeBasePage } from './pages/KnowledgeBasePage';
import { SettingsPage } from './pages/SettingsPage';
import { MCPPage } from './pages/MCPPage';
import { OnboardingPage } from './pages/OnboardingPage';
+import CodingAgents from './pages/CodingAgents';
import { MainLayout } from './components/layout/MainLayout';
import { ThemeProvider } from './contexts/ThemeContext';
import { ToastProvider } from './contexts/ToastContext';
@@ -49,6 +50,7 @@ const AppRoutes = () => {
} />
} />
} />
+ } />
} />
{projectsEnabled ? (
<>
diff --git a/archon-ui-main/src/components/layout/Navigation.tsx b/archon-ui-main/src/components/layout/Navigation.tsx
index e2f1e806..27b14e37 100644
--- a/archon-ui-main/src/components/layout/Navigation.tsx
+++ b/archon-ui-main/src/components/layout/Navigation.tsx
@@ -1,4 +1,4 @@
-import { BookOpen, Settings } from "lucide-react";
+import { BookOpen, Settings, Code2 } from "lucide-react";
import type React from "react";
import { Link, useLocation } from "react-router-dom";
// TEMPORARY: Use old SettingsContext until settings are migrated
@@ -34,6 +34,12 @@ export function Navigation({ className }: NavigationProps) {
label: "Knowledge Base",
enabled: true,
},
+ {
+ path: "/coding-agents",
+ icon: ,
+ label: "Coding Agents",
+ enabled: true,
+ },
{
path: "/mcp",
icon: (
diff --git a/archon-ui-main/src/features/coding-agents/components/ChatInterface.tsx b/archon-ui-main/src/features/coding-agents/components/ChatInterface.tsx
new file mode 100644
index 00000000..e21e99de
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/components/ChatInterface.tsx
@@ -0,0 +1,95 @@
+import { useEffect } from "react";
+import { useCodingAgentChat, useCodingAgentSession } from "../hooks";
+import { MessageList } from "./MessageList";
+import { MessageInput } from "./MessageInput";
+import { SessionStatus } from "./SessionStatus";
+
+interface ChatInterfaceProps {
+ workingDirectory: string;
+}
+
+export function ChatInterface({ workingDirectory }: ChatInterfaceProps) {
+ const {
+ currentSessionId,
+ session,
+ isSessionLoading,
+ createSession,
+ cancelSession,
+ isCreatingSession,
+ isCancellingSession,
+ } = useCodingAgentSession();
+
+ const {
+ messages,
+ isProcessing,
+ sendMessage,
+ clearMessages,
+ } = useCodingAgentChat(currentSessionId);
+
+ // Create session when working directory is set
+ useEffect(() => {
+ if (workingDirectory && !currentSessionId && !isCreatingSession) {
+ createSession({
+ workingDirectory,
+ mcpServers: [] // Explicitly pass empty array for now
+ });
+ }
+ }, [workingDirectory, currentSessionId, isCreatingSession, createSession]);
+
+ // Clear messages when session changes
+ useEffect(() => {
+ if (currentSessionId) {
+ clearMessages();
+ }
+ }, [currentSessionId, clearMessages]);
+
+ const handleCancel = () => {
+ if (currentSessionId && !isCancellingSession) {
+ cancelSession();
+ }
+ };
+
+ const isDisabled = !currentSessionId ||
+ session?.status === "error" ||
+ session?.status === "cancelled" ||
+ isSessionLoading ||
+ isCreatingSession;
+
+ return (
+
+ {/* Header with session status */}
+
+
Coding Agent Chat
+
+
+
+ {/* Messages area */}
+
+
+ {/* Input area */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/components/FolderSelector.tsx b/archon-ui-main/src/features/coding-agents/components/FolderSelector.tsx
new file mode 100644
index 00000000..eb87dc32
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/components/FolderSelector.tsx
@@ -0,0 +1,105 @@
+import { Folder, FolderOpen } from "lucide-react";
+import { useState, useRef } from "react";
+import { Button } from "../../ui/primitives/button";
+import { Input } from "../../ui/primitives/input";
+
+interface FolderSelectorProps {
+ value: string;
+ onChange: (path: string) => void;
+ disabled?: boolean;
+ placeholder?: string;
+ label?: string;
+}
+
+export function FolderSelector({
+ value,
+ onChange,
+ disabled = false,
+ placeholder = "Enter full absolute path (e.g., /Users/name/Projects/myproject)",
+ label = "Working Directory",
+}: FolderSelectorProps) {
+ const [isSelecting, setIsSelecting] = useState(false);
+ const inputRef = useRef(null);
+
+ // Handle folder selection via file input
+ const handleFolderSelect = () => {
+ // Create a hidden input element for folder selection
+ const input = document.createElement("input");
+ input.type = "file";
+ input.webkitdirectory = true;
+ input.directory = true;
+
+ input.onchange = (e: any) => {
+ const files = e.target.files;
+ if (files && files.length > 0) {
+ // Extract the folder path from the first file
+ const path = files[0].webkitRelativePath || files[0].path || "";
+ const folderPath = path.split("/")[0];
+
+ // Browser security prevents getting the full path
+ // Only the folder name is available, which isn't enough
+ // Show a message to the user
+ const folderName = folderPath || "selected folder";
+ alert(`Browser security prevents accessing the full path.\n\nPlease manually enter the complete path to "${folderName}" in the input field.\n\nExample: /Users/username/Projects/${folderName}`);
+ // Don't update with just the folder name as it won't work
+ // onChange(folderPath || value);
+ }
+ setIsSelecting(false);
+ };
+
+ input.oncancel = () => {
+ setIsSelecting(false);
+ };
+
+ setIsSelecting(true);
+ input.click();
+ };
+
+ return (
+
+ {label && (
+
+ )}
+
+
+
+ onChange(e.target.value)}
+ disabled={disabled || isSelecting}
+ placeholder={placeholder}
+ className="pl-10 bg-gray-900/50 border-gray-700 text-gray-100 placeholder:text-gray-500"
+ />
+
+
+
+ {value && (
+
+ Selected: {value}
+
+ )}
+
+ );
+}
+
+// Add type declarations for webkitdirectory
+declare module "react" {
+ interface InputHTMLAttributes extends AriaAttributes, DOMAttributes {
+ webkitdirectory?: boolean;
+ directory?: boolean;
+ }
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/components/MessageInput.tsx b/archon-ui-main/src/features/coding-agents/components/MessageInput.tsx
new file mode 100644
index 00000000..ff325cdc
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/components/MessageInput.tsx
@@ -0,0 +1,96 @@
+import { Send, Square } from "lucide-react";
+import { useState, useRef, useEffect, KeyboardEvent } from "react";
+import { Button } from "../../ui/primitives/button";
+
+interface MessageInputProps {
+ onSend: (message: string) => void;
+ onCancel?: () => void;
+ disabled?: boolean;
+ isProcessing?: boolean;
+ placeholder?: string;
+}
+
+export function MessageInput({
+ onSend,
+ onCancel,
+ disabled = false,
+ isProcessing = false,
+ placeholder = "Type a message...",
+}: MessageInputProps) {
+ const [message, setMessage] = useState("");
+ const textareaRef = useRef(null);
+
+ // Auto-resize textarea
+ useEffect(() => {
+ const textarea = textareaRef.current;
+ if (textarea) {
+ textarea.style.height = "auto";
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
+ }
+ }, [message]);
+
+ const handleSend = () => {
+ const trimmedMessage = message.trim();
+ if (trimmedMessage && !disabled && !isProcessing) {
+ onSend(trimmedMessage);
+ setMessage("");
+ }
+ };
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ };
+
+ return (
+
+
+
+
+ {isProcessing ? (
+
+ ) : (
+
+ )}
+
+
+
+ Press Enter to send,{" "}
+ Shift+Enter for new line
+
+
+ );
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/components/MessageList.tsx b/archon-ui-main/src/features/coding-agents/components/MessageList.tsx
new file mode 100644
index 00000000..39388f92
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/components/MessageList.tsx
@@ -0,0 +1,124 @@
+import { Bot, User, AlertCircle, CheckCircle, Clock } from "lucide-react";
+import { useEffect, useRef } from "react";
+import type { ChatMessage } from "../types";
+
+interface MessageListProps {
+ messages: ChatMessage[];
+ isProcessing?: boolean;
+}
+
+export function MessageList({ messages, isProcessing = false }: MessageListProps) {
+ const messagesEndRef = useRef(null);
+
+ // Auto-scroll to bottom when new messages arrive
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages]);
+
+ const getStatusIcon = (status?: ChatMessage["status"]) => {
+ switch (status) {
+ case "sending":
+ return ;
+ case "sent":
+ return ;
+ case "error":
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ const formatTimestamp = (timestamp: string) => {
+ const date = new Date(timestamp);
+ return date.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ });
+ };
+
+ return (
+
+ {messages.length === 0 && !isProcessing && (
+
+
+
Start a conversation with the coding agent
+
Select a working directory and send a message to begin
+
+ )}
+
+ {messages.map((message) => (
+
+ {message.role === "assistant" && (
+
+ )}
+
+
+
+
+ {message.content}
+
+ {message.stopReason && (
+
+ Status: {message.stopReason}
+
+ )}
+
+
+
+ {formatTimestamp(message.timestamp)}
+
+ {message.status && getStatusIcon(message.status)}
+
+
+
+ {message.role === "user" && (
+
+ )}
+
+ ))}
+
+ {isProcessing && (
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/components/SessionStatus.tsx b/archon-ui-main/src/features/coding-agents/components/SessionStatus.tsx
new file mode 100644
index 00000000..32fc2e19
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/components/SessionStatus.tsx
@@ -0,0 +1,69 @@
+import { Activity, AlertCircle, CheckCircle, Clock, XCircle } from "lucide-react";
+import type { SessionStatus as SessionStatusType } from "../types";
+
+interface SessionStatusProps {
+ status: SessionStatusType | undefined;
+ sessionId?: string | null;
+ className?: string;
+}
+
+export function SessionStatus({ status, sessionId, className = "" }: SessionStatusProps) {
+ const getStatusConfig = (status?: SessionStatusType) => {
+ switch (status) {
+ case "creating":
+ return {
+ icon: ,
+ text: "Creating session...",
+ color: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30",
+ };
+ case "active":
+ return {
+ icon: ,
+ text: "Session active",
+ color: "text-green-400 bg-green-400/10 border-green-400/30",
+ };
+ case "processing":
+ return {
+ icon: ,
+ text: "Processing...",
+ color: "text-cyan-400 bg-cyan-400/10 border-cyan-400/30",
+ };
+ case "cancelled":
+ return {
+ icon: ,
+ text: "Session cancelled",
+ color: "text-gray-400 bg-gray-400/10 border-gray-400/30",
+ };
+ case "error":
+ return {
+ icon: ,
+ text: "Session error",
+ color: "text-red-400 bg-red-400/10 border-red-400/30",
+ };
+ default:
+ return {
+ icon: ,
+ text: "No active session",
+ color: "text-gray-500 bg-gray-500/10 border-gray-500/30",
+ };
+ }
+ };
+
+ const config = getStatusConfig(status);
+
+ return (
+
+
+ {config.icon}
+ {config.text}
+
+ {sessionId && status === "active" && (
+
+ ID: {sessionId.slice(0, 8)}...
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/components/index.ts b/archon-ui-main/src/features/coding-agents/components/index.ts
new file mode 100644
index 00000000..6eacf3eb
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/components/index.ts
@@ -0,0 +1,5 @@
+export { ChatInterface } from "./ChatInterface";
+export { FolderSelector } from "./FolderSelector";
+export { MessageList } from "./MessageList";
+export { MessageInput } from "./MessageInput";
+export { SessionStatus } from "./SessionStatus";
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/hooks/index.ts b/archon-ui-main/src/features/coding-agents/hooks/index.ts
new file mode 100644
index 00000000..2db54ddf
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from "./useCodingAgentSession";
+export * from "./useCodingAgentChat";
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/hooks/useCodingAgentChat.ts b/archon-ui-main/src/features/coding-agents/hooks/useCodingAgentChat.ts
new file mode 100644
index 00000000..31296174
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/hooks/useCodingAgentChat.ts
@@ -0,0 +1,152 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useState, useCallback, useRef } from "react";
+import { useToast } from "../../ui/hooks/useToast";
+import { acpService } from "../services";
+import type { ChatMessage, PromptInput, PromptResponse } from "../types";
+import { codingAgentKeys } from "./useCodingAgentSession";
+
+/**
+ * Hook to manage chat interactions with the coding agent
+ */
+export function useCodingAgentChat(sessionId: string | null) {
+ const queryClient = useQueryClient();
+ const { showToast } = useToast();
+ const [messages, setMessages] = useState([]);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const messageIdCounter = useRef(0);
+
+ // Generate unique message ID
+ const generateMessageId = useCallback(() => {
+ messageIdCounter.current += 1;
+ return `msg-${Date.now()}-${messageIdCounter.current}`;
+ }, []);
+
+ // Add a message to the chat
+ const addMessage = useCallback((
+ role: ChatMessage["role"],
+ content: string,
+ stopReason?: string,
+ ) => {
+ const newMessage: ChatMessage = {
+ id: generateMessageId(),
+ role,
+ content,
+ timestamp: new Date().toISOString(),
+ status: role === "user" ? "sent" : undefined,
+ stopReason,
+ };
+
+ setMessages((prev) => [...prev, newMessage]);
+ return newMessage;
+ }, [generateMessageId]);
+
+ // Update message status
+ const updateMessageStatus = useCallback((
+ messageId: string,
+ status: ChatMessage["status"],
+ ) => {
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === messageId ? { ...msg, status } : msg,
+ ),
+ );
+ }, []);
+
+ // Send prompt mutation
+ const sendPromptMutation = useMutation({
+ mutationFn: async (prompt: PromptInput) => {
+ if (!sessionId) throw new Error("No active session");
+
+ const response = await acpService.sendPrompt(sessionId, { prompt });
+ return response.data;
+ },
+ onMutate: async (prompt) => {
+ setIsProcessing(true);
+
+ // Add user message
+ const content = typeof prompt === "string"
+ ? prompt
+ : prompt.map(block => block.text || block.title || "").join("\n");
+
+ const userMessage = addMessage("user", content);
+ updateMessageStatus(userMessage.id, "sending");
+
+ return { userMessageId: userMessage.id };
+ },
+ onSuccess: (data: PromptResponse, _variables, context) => {
+ // Update user message status
+ if (context?.userMessageId) {
+ updateMessageStatus(context.userMessageId, "sent");
+ }
+
+ // Add assistant response
+ const responseContent = data.stopReason === "error"
+ ? "An error occurred while processing your request."
+ : "Task completed successfully.";
+
+ addMessage("assistant", responseContent, data.stopReason);
+
+ // Invalidate session status to get updated state
+ queryClient.invalidateQueries({
+ queryKey: codingAgentKeys.sessionStatus(sessionId!),
+ });
+ },
+ onError: (error, _variables, context) => {
+ // Update user message status
+ if (context?.userMessageId) {
+ updateMessageStatus(context.userMessageId, "error");
+ }
+
+ const message = error instanceof Error ? error.message : "Failed to send prompt";
+
+ // Check if it's a session-related error
+ if (message.includes("404") || message.includes("Session not found")) {
+ addMessage("assistant", "Session expired. Please refresh to create a new session.");
+ showToast("Session expired. Creating new session...", "info");
+ } else if (message.includes("Session did not end in result")) {
+ addMessage("assistant", "The AI agent failed to process your request. This might be due to a configuration issue with the ACP backend.");
+ showToast("AI processing error. Check ACP backend configuration.", "error");
+ } else {
+ addMessage("assistant", `Error: ${message}`);
+ showToast(message, "error");
+ }
+ },
+ onSettled: () => {
+ setIsProcessing(false);
+ },
+ });
+
+ // Clear chat history
+ const clearMessages = useCallback(() => {
+ setMessages([]);
+ messageIdCounter.current = 0;
+ }, []);
+
+ // Send a text prompt
+ const sendMessage = useCallback((text: string) => {
+ if (!text.trim()) return;
+ // Convert plain text to content block format expected by backend
+ const contentBlocks = [
+ {
+ type: "text",
+ text: text,
+ },
+ ];
+ sendPromptMutation.mutate(contentBlocks);
+ }, [sendPromptMutation]);
+
+ return {
+ // State
+ messages,
+ isProcessing: isProcessing || sendPromptMutation.isPending,
+
+ // Actions
+ sendMessage,
+ sendPrompt: sendPromptMutation.mutate,
+ clearMessages,
+
+ // Utilities
+ addMessage,
+ updateMessageStatus,
+ };
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/hooks/useCodingAgentSession.ts b/archon-ui-main/src/features/coding-agents/hooks/useCodingAgentSession.ts
new file mode 100644
index 00000000..31b741aa
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/hooks/useCodingAgentSession.ts
@@ -0,0 +1,148 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
+import { useToast } from "../../ui/hooks/useToast";
+import { acpService } from "../services";
+import type {
+ CreateSessionRequest,
+ InitializeResponse,
+ LoadSessionRequest,
+ Session,
+} from "../types";
+
+// Query keys factory
+export const codingAgentKeys = {
+ all: ["coding-agent"] as const,
+ initialize: () => [...codingAgentKeys.all, "initialize"] as const,
+ sessions: () => [...codingAgentKeys.all, "sessions"] as const,
+ session: (id: string) => [...codingAgentKeys.sessions(), id] as const,
+ sessionStatus: (id: string) => [...codingAgentKeys.session(id), "status"] as const,
+};
+
+/**
+ * Hook to initialize the ACP connection
+ */
+export function useInitializeCodingAgent() {
+ return useQuery({
+ queryKey: codingAgentKeys.initialize(),
+ queryFn: async () => {
+ const response = await acpService.initialize();
+ return response.data;
+ },
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ retry: 3,
+ });
+}
+
+/**
+ * Hook to manage coding agent sessions
+ */
+export function useCodingAgentSession() {
+ const queryClient = useQueryClient();
+ const { showToast } = useToast();
+ const [currentSessionId, setCurrentSessionId] = useState(null);
+
+ // Query for current session
+ const sessionQuery = useQuery({
+ queryKey: currentSessionId ? codingAgentKeys.session(currentSessionId) : ["no-session"],
+ queryFn: async () => {
+ if (!currentSessionId) throw new Error("No session ID");
+ try {
+ const response = await acpService.getSession(currentSessionId);
+ return response.data;
+ } catch (error) {
+ // If session not found (404), clear the invalid session ID
+ if (error instanceof Error && error.message.includes("404")) {
+ setCurrentSessionId(null);
+ showToast("Session expired. Please create a new session.", "info");
+ }
+ throw error;
+ }
+ },
+ enabled: !!currentSessionId,
+ retry: (failureCount, error) => {
+ // Don't retry on 404 errors (session doesn't exist)
+ if (error instanceof Error && error.message.includes("404")) {
+ return false;
+ }
+ // Retry other errors up to 3 times
+ return failureCount < 3;
+ },
+ refetchInterval: (data) => {
+ // Poll more frequently when session is processing
+ if (data?.status === "processing" || data?.status === "creating") {
+ return 1000; // 1 second
+ }
+ return false; // Don't poll otherwise
+ },
+ });
+
+ // Mutation to create a new session
+ const createSessionMutation = useMutation({
+ mutationFn: async (request: CreateSessionRequest) => {
+ const response = await acpService.createSession(request);
+ return response.data;
+ },
+ onSuccess: (data) => {
+ setCurrentSessionId(data.sessionId);
+ showToast("Session created successfully", "success");
+ // Invalidate session queries
+ queryClient.invalidateQueries({ queryKey: codingAgentKeys.sessions() });
+ },
+ onError: (error) => {
+ const message = error instanceof Error ? error.message : "Failed to create session";
+ showToast(message, "error");
+ },
+ });
+
+ // Mutation to load an existing session
+ const loadSessionMutation = useMutation({
+ mutationFn: async (request: LoadSessionRequest) => {
+ await acpService.loadSession(request);
+ return request.sessionId;
+ },
+ onSuccess: (sessionId) => {
+ setCurrentSessionId(sessionId);
+ showToast("Session loaded successfully", "success");
+ queryClient.invalidateQueries({ queryKey: codingAgentKeys.session(sessionId) });
+ },
+ onError: (error) => {
+ const message = error instanceof Error ? error.message : "Failed to load session";
+ showToast(message, "error");
+ },
+ });
+
+ // Mutation to cancel the current session
+ const cancelSessionMutation = useMutation({
+ mutationFn: async () => {
+ if (!currentSessionId) throw new Error("No active session");
+ await acpService.cancelSession(currentSessionId);
+ return currentSessionId;
+ },
+ onSuccess: (sessionId) => {
+ showToast("Session cancelled", "info");
+ queryClient.invalidateQueries({ queryKey: codingAgentKeys.session(sessionId) });
+ },
+ onError: (error) => {
+ const message = error instanceof Error ? error.message : "Failed to cancel session";
+ showToast(message, "error");
+ },
+ });
+
+ return {
+ // State
+ currentSessionId,
+ session: sessionQuery.data,
+ isSessionLoading: sessionQuery.isLoading,
+ sessionError: sessionQuery.error,
+
+ // Actions
+ createSession: createSessionMutation.mutate,
+ loadSession: loadSessionMutation.mutate,
+ cancelSession: cancelSessionMutation.mutate,
+
+ // Loading states
+ isCreatingSession: createSessionMutation.isPending,
+ isLoadingSession: loadSessionMutation.isPending,
+ isCancellingSession: cancelSessionMutation.isPending,
+ };
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/index.ts b/archon-ui-main/src/features/coding-agents/index.ts
new file mode 100644
index 00000000..b0a8b436
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/index.ts
@@ -0,0 +1,20 @@
+// Main exports for coding-agents feature
+export { CodingAgentsView } from "./views/CodingAgentsView";
+export { CodingAgentsPage } from "./views/CodingAgentsPage";
+
+// Export hooks if needed by other features
+export {
+ useInitializeCodingAgent,
+ useCodingAgentSession,
+ useCodingAgentChat
+} from "./hooks";
+
+// Export types if needed by other features
+export type {
+ Session,
+ SessionStatus,
+ ChatMessage,
+ InitializeResponse,
+ AgentCapabilitiesResponse,
+ PromptCapabilitiesResponse
+} from "./types";
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/services/acpService.ts b/archon-ui-main/src/features/coding-agents/services/acpService.ts
new file mode 100644
index 00000000..33ef5123
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/services/acpService.ts
@@ -0,0 +1,143 @@
+/**
+ * Agent Client Protocol (ACP) Service
+ * Handles all communication with the ACP backend running on localhost:3001
+ */
+
+import type {
+ CreateSessionApiResponse,
+ CreateSessionRequest,
+ EmptyApiResponse,
+ InitializeApiResponse,
+ LoadSessionRequest,
+ PromptApiResponse,
+ PromptRequest,
+ SessionApiResponse,
+} from "../types";
+
+// Direct connection to ACP backend (CORS is configured to allow all origins)
+const ACP_BASE_URL = "http://localhost:3001";
+
+class AcpService {
+ private baseUrl: string;
+
+ constructor(baseUrl: string = ACP_BASE_URL) {
+ this.baseUrl = baseUrl;
+ }
+
+ private async request(
+ path: string,
+ options: RequestInit = {},
+ ): Promise {
+ const url = `${this.baseUrl}${path}`;
+
+ const response = await fetch(url, {
+ ...options,
+ headers: {
+ "Content-Type": "application/json",
+ ...options.headers,
+ },
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+
+ // Try to parse error as JSON for better error messages
+ let errorMessage = `${response.status} ${response.statusText}`;
+ try {
+ const errorJson = JSON.parse(errorText);
+ if (errorJson.error) {
+ errorMessage = errorJson.error;
+ } else if (errorJson.message) {
+ errorMessage = errorJson.message;
+ }
+ } catch {
+ // If not JSON, use the text as-is
+ if (errorText) {
+ errorMessage = errorText;
+ }
+ }
+
+ throw new Error(errorMessage);
+ }
+
+ return response.json();
+ }
+
+ /**
+ * Initialize the ACP connection
+ */
+ async initialize(): Promise {
+ return this.request("/api/initialize", {
+ method: "POST",
+ });
+ }
+
+ /**
+ * Create a new session
+ */
+ async createSession(
+ request: CreateSessionRequest,
+ ): Promise {
+ return this.request("/api/sessions", {
+ method: "POST",
+ body: JSON.stringify(request),
+ });
+ }
+
+ /**
+ * Load an existing session
+ */
+ async loadSession(request: LoadSessionRequest): Promise {
+ return this.request("/api/sessions/load", {
+ method: "POST",
+ body: JSON.stringify(request),
+ });
+ }
+
+ /**
+ * Get session details
+ */
+ async getSession(sessionId: string): Promise {
+ return this.request(`/api/sessions/${sessionId}`);
+ }
+
+ /**
+ * Get session status
+ */
+ async getSessionStatus(sessionId: string): Promise {
+ return this.request(
+ `/api/sessions/${sessionId}/status`,
+ );
+ }
+
+ /**
+ * Send a prompt to a session
+ */
+ async sendPrompt(
+ sessionId: string,
+ request: PromptRequest,
+ ): Promise {
+ console.log("[ACP] Sending prompt request:", JSON.stringify(request, null, 2));
+ return this.request(
+ `/api/sessions/${sessionId}/prompt`,
+ {
+ method: "POST",
+ body: JSON.stringify(request),
+ },
+ );
+ }
+
+ /**
+ * Cancel a session
+ */
+ async cancelSession(sessionId: string): Promise {
+ return this.request(
+ `/api/sessions/${sessionId}/cancel`,
+ {
+ method: "POST",
+ },
+ );
+ }
+}
+
+export const acpService = new AcpService();
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/services/index.ts b/archon-ui-main/src/features/coding-agents/services/index.ts
new file mode 100644
index 00000000..c6409bd4
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/services/index.ts
@@ -0,0 +1 @@
+export { acpService } from "./acpService";
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/types/CodingAgent.ts b/archon-ui-main/src/features/coding-agents/types/CodingAgent.ts
new file mode 100644
index 00000000..16a2f454
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/types/CodingAgent.ts
@@ -0,0 +1,60 @@
+// Agent capabilities and initialization types
+
+export interface AgentCapabilitiesResponse {
+ loadSession?: boolean | null;
+ promptCapabilities: PromptCapabilitiesResponse;
+}
+
+export interface PromptCapabilitiesResponse {
+ image: boolean;
+ embeddedContext: boolean;
+ audio?: boolean | null;
+}
+
+export interface ClientCapabilitiesResponse {
+ fs: FsCapabilitiesResponse;
+}
+
+export interface FsCapabilitiesResponse {
+ readTextFile: boolean;
+ writeTextFile: boolean;
+}
+
+export interface InitializeResponse {
+ protocolVersion: number;
+ capabilities: AgentCapabilitiesResponse;
+ clientCapabilities?: ClientCapabilitiesResponse | null;
+}
+
+// Content and prompt types
+export interface ContentBlock {
+ type: string;
+ text?: string | null;
+ data?: string | null;
+ description?: string | null;
+ mimeType?: string | null;
+ name?: string | null;
+ resource?: any | null;
+ size?: number | null;
+ title?: string | null;
+ uri?: string | null;
+}
+
+export type PromptInput = string | ContentBlock[];
+
+export interface PromptResponse {
+ stopReason: string;
+}
+
+// MCP Server configuration
+export interface EnvVariable {
+ name: string;
+ value: string;
+}
+
+export interface McpServer {
+ name: string;
+ command: string;
+ args: string[];
+ env: EnvVariable[];
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/types/Session.ts b/archon-ui-main/src/features/coding-agents/types/Session.ts
new file mode 100644
index 00000000..cecc1566
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/types/Session.ts
@@ -0,0 +1,58 @@
+import type { McpServer, PromptInput, PromptResponse } from "./CodingAgent";
+
+// Session status enum
+export type SessionStatus = "creating" | "active" | "processing" | "cancelled" | "error";
+
+// Session management types
+export interface Session {
+ id: string;
+ working_directory: string;
+ mcp_servers: McpServer[];
+ created_at: string;
+ status: SessionStatus;
+}
+
+export interface CreateSessionRequest {
+ workingDirectory: string;
+ mcpServers?: McpServer[];
+}
+
+export interface CreateSessionResponse {
+ sessionId: string;
+}
+
+export interface LoadSessionRequest {
+ sessionId: string;
+ workingDirectory: string;
+ mcpServers?: McpServer[];
+}
+
+// Prompt request/response
+export interface PromptRequest {
+ prompt: PromptInput;
+}
+
+// API Response wrappers
+export interface ApiResponse {
+ data: T;
+}
+
+export interface EmptyApiResponse {
+ data?: null;
+}
+
+// Specific API response types
+export interface InitializeApiResponse extends ApiResponse {}
+export interface CreateSessionApiResponse extends ApiResponse {}
+export interface SessionApiResponse extends ApiResponse {}
+export interface PromptApiResponse extends ApiResponse {}
+
+// Message types for chat interface
+export interface ChatMessage {
+ id: string;
+ role: "user" | "assistant";
+ content: string;
+ timestamp: string;
+ status?: "sending" | "sent" | "error";
+ stopReason?: string;
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/types/index.ts b/archon-ui-main/src/features/coding-agents/types/index.ts
new file mode 100644
index 00000000..d1ba0882
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/types/index.ts
@@ -0,0 +1,2 @@
+export * from "./CodingAgent";
+export * from "./Session";
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/views/CodingAgentsPage.tsx b/archon-ui-main/src/features/coding-agents/views/CodingAgentsPage.tsx
new file mode 100644
index 00000000..b7617233
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/views/CodingAgentsPage.tsx
@@ -0,0 +1,30 @@
+import { ErrorBoundary } from "react-error-boundary";
+import { CodingAgentsView } from "./CodingAgentsView";
+import { AlertCircle } from "lucide-react";
+import { Button } from "../../ui/primitives/button";
+
+function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
+ return (
+
+
+
+
Something went wrong
+
{error.message}
+
+
+
+ );
+}
+
+export function CodingAgentsPage() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/features/coding-agents/views/CodingAgentsView.tsx b/archon-ui-main/src/features/coding-agents/views/CodingAgentsView.tsx
new file mode 100644
index 00000000..45e9fe2c
--- /dev/null
+++ b/archon-ui-main/src/features/coding-agents/views/CodingAgentsView.tsx
@@ -0,0 +1,165 @@
+import { useState } from "react";
+import { Code2, Terminal } from "lucide-react";
+import { useInitializeCodingAgent } from "../hooks";
+import { ChatInterface } from "../components/ChatInterface";
+import { FolderSelector } from "../components/FolderSelector";
+import { Button } from "../../ui/primitives/button";
+
+export function CodingAgentsView() {
+ const [workingDirectory, setWorkingDirectory] = useState("");
+ const [isSessionStarted, setIsSessionStarted] = useState(false);
+
+ // Initialize the coding agent connection
+ const { data: initData, isLoading: isInitializing, error: initError } = useInitializeCodingAgent();
+
+ const handleStartSession = () => {
+ // Validate that it's an absolute path
+ if (workingDirectory && workingDirectory.startsWith('/')) {
+ setIsSessionStarted(true);
+ } else {
+ alert('Please enter a full absolute path starting with "/"\n\nExample: /Users/username/Projects/myproject');
+ }
+ };
+
+ const handleReset = () => {
+ setIsSessionStarted(false);
+ setWorkingDirectory("");
+ };
+
+ if (isInitializing) {
+ return (
+
+
+
+
Initializing coding agent...
+
+
+ );
+ }
+
+ if (initError) {
+ return (
+
+
+
+
Failed to initialize coding agent
+
+ {initError instanceof Error ? initError.message : "Unknown error"}
+
+
+ Make sure the ACP backend is running on localhost:3001
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
Coding Agent
+
+ AI-powered coding assistant for your projects
+
+
+
+
+ {isSessionStarted && (
+
+ )}
+
+
+ {/* Protocol info */}
+ {initData && (
+
+
+
+
+ Protocol v{initData.protocolVersion}
+
+ •
+
+ Image support: {initData.capabilities.promptCapabilities.image ? "✓" : "✗"}
+
+ •
+
+ Context: {initData.capabilities.promptCapabilities.embeddedContext ? "✓" : "✗"}
+
+ {initData.capabilities.promptCapabilities.audio && (
+ <>
+ •
+ Audio: ✓
+ >
+ )}
+
+ {initData.clientCapabilities && (
+
+
+ File read: {initData.clientCapabilities.fs.readTextFile ? "✓" : "✗"}
+
+
+ File write: {initData.clientCapabilities.fs.writeTextFile ? "✓" : "✗"}
+
+
+ )}
+
+
+ )}
+
+ {/* Main content */}
+
+ {!isSessionStarted ? (
+
+
+
+
+
+ Select a Working Directory
+
+
+ Choose the folder where the coding agent will work on your project
+
+
+
+
+
+
+
+
+
+
+
The agent will have access to read and modify files in this directory
+
Make sure to choose the correct project folder
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/archon-ui-main/src/pages/CodingAgents.tsx b/archon-ui-main/src/pages/CodingAgents.tsx
new file mode 100644
index 00000000..7acad906
--- /dev/null
+++ b/archon-ui-main/src/pages/CodingAgents.tsx
@@ -0,0 +1,5 @@
+import { CodingAgentsPage } from "../features/coding-agents";
+
+export default function CodingAgents() {
+ return ;
+}
\ No newline at end of file
diff --git a/archon-ui-main/vite.config.ts b/archon-ui-main/vite.config.ts
index 52c2be86..0fd4fc09 100644
--- a/archon-ui-main/vite.config.ts
+++ b/archon-ui-main/vite.config.ts
@@ -305,6 +305,22 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
console.log('🔄 [VITE PROXY] Forwarding:', req.method, req.url, 'to', `http://${host}:${port}${req.url}`);
});
}
+ },
+ // Proxy for ACP backend on localhost:3001
+ '/acp': {
+ target: 'http://localhost:3001',
+ changeOrigin: true,
+ secure: false,
+ rewrite: (path) => path.replace(/^\/acp/, ''),
+ configure: (proxy, options) => {
+ proxy.on('error', (err, req, res) => {
+ console.log('🚨 [ACP PROXY ERROR]:', err.message);
+ console.log('🚨 [ACP PROXY ERROR] Request:', req.url);
+ });
+ proxy.on('proxyReq', (proxyReq, req, res) => {
+ console.log('🔄 [ACP PROXY] Forwarding:', req.method, req.url, 'to localhost:3001');
+ });
+ }
}
},
},