mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-24 02:39:17 -05:00
feat: implement Agent Client Protocol (ACP) frontend POC
Add complete coding agents feature with: - Full vertical slice architecture in /features/coding-agents - TanStack Query for all data fetching and session management - Direct connection to ACP backend on localhost:3001 - Session creation with working directory selection - Chat interface for sending prompts and viewing responses - Automatic session recreation on backend restart (404 detection) - Proper content block format for prompts - Path validation requiring absolute paths - Real-time session status display - Protocol capabilities display Key components: - CodingAgentsView: Main view with directory selection - ChatInterface: Message handling and display - FolderSelector: Directory input with validation - acpService: Direct HTTP connection to ACP backend - useCodingAgentSession: Session lifecycle management - useCodingAgentChat: Message state and prompt handling 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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 = () => {
|
||||
<Route path="/" element={<KnowledgeBasePage />} />
|
||||
<Route path="/onboarding" element={<OnboardingPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/coding-agents" element={<CodingAgents />} />
|
||||
<Route path="/mcp" element={<MCPPage />} />
|
||||
{projectsEnabled ? (
|
||||
<>
|
||||
|
||||
@@ -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: <Code2 className="h-5 w-5" />,
|
||||
label: "Coding Agents",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
path: "/mcp",
|
||||
icon: (
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col h-full bg-gray-900/50 rounded-lg border border-gray-700 overflow-hidden">
|
||||
{/* Header with session status */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700 bg-gray-900/30">
|
||||
<h3 className="text-lg font-semibold text-gray-100">Coding Agent Chat</h3>
|
||||
<SessionStatus
|
||||
status={session?.status}
|
||||
sessionId={currentSessionId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Messages area */}
|
||||
<MessageList
|
||||
messages={messages}
|
||||
isProcessing={isProcessing}
|
||||
/>
|
||||
|
||||
{/* Input area */}
|
||||
<MessageInput
|
||||
onSend={sendMessage}
|
||||
onCancel={handleCancel}
|
||||
disabled={isDisabled}
|
||||
isProcessing={isProcessing}
|
||||
placeholder={
|
||||
!workingDirectory
|
||||
? "Select a working directory first..."
|
||||
: isCreatingSession
|
||||
? "Creating session..."
|
||||
: session?.status === "error"
|
||||
? "Session error - please refresh"
|
||||
: session?.status === "cancelled"
|
||||
? "Session cancelled - please refresh"
|
||||
: "Ask the coding agent to help with your code..."
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLInputElement>(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 (
|
||||
<div className="space-y-2">
|
||||
{label && (
|
||||
<label className="text-sm font-medium text-gray-300">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Folder className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleFolderSelect}
|
||||
disabled={disabled || isSelecting}
|
||||
className="bg-gray-900/50 border-gray-700 hover:bg-gray-800/50"
|
||||
title="Browse for folder"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{value && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Selected: <span className="text-gray-400 font-mono">{value}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Add type declarations for webkitdirectory
|
||||
declare module "react" {
|
||||
interface InputHTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
|
||||
webkitdirectory?: boolean;
|
||||
directory?: boolean;
|
||||
}
|
||||
}
|
||||
@@ -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<HTMLTextAreaElement>(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<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-t border-gray-700 bg-gray-900/50 p-4">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled || isProcessing}
|
||||
placeholder={placeholder}
|
||||
className="w-full min-h-[44px] max-h-[200px] px-4 py-2.5 pr-12 bg-gray-800/50 border border-gray-700 rounded-lg text-gray-100 placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 resize-none"
|
||||
rows={1}
|
||||
/>
|
||||
<div className="absolute bottom-2 right-2 text-xs text-gray-500">
|
||||
{message.length > 0 && `${message.length} chars`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isProcessing ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
onClick={onCancel}
|
||||
className="h-[44px] w-[44px]"
|
||||
title="Cancel operation"
|
||||
>
|
||||
<Square className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSend}
|
||||
disabled={!message.trim() || disabled}
|
||||
className="h-[44px] px-4 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Send message (Enter)"
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
Press <kbd className="px-1.5 py-0.5 bg-gray-800 border border-gray-700 rounded">Enter</kbd> to send,{" "}
|
||||
<kbd className="px-1.5 py-0.5 bg-gray-800 border border-gray-700 rounded">Shift+Enter</kbd> for new line
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLDivElement>(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 <Clock className="h-3 w-3 animate-pulse" />;
|
||||
case "sent":
|
||||
return <CheckCircle className="h-3 w-3" />;
|
||||
case "error":
|
||||
return <AlertCircle className="h-3 w-3 text-red-400" />;
|
||||
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 (
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.length === 0 && !isProcessing && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||
<Bot className="h-12 w-12 mb-4 opacity-50" />
|
||||
<p className="text-sm">Start a conversation with the coding agent</p>
|
||||
<p className="text-xs mt-2">Select a working directory and send a message to begin</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex gap-3 ${
|
||||
message.role === "user" ? "justify-end" : "justify-start"
|
||||
}`}
|
||||
>
|
||||
{message.role === "assistant" && (
|
||||
<div className="flex-shrink-0">
|
||||
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center">
|
||||
<Bot className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`max-w-[70%] ${
|
||||
message.role === "user" ? "order-1" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-lg px-4 py-2 ${
|
||||
message.role === "user"
|
||||
? "bg-blue-600/20 border border-blue-500/30 text-gray-100"
|
||||
: "bg-gray-800/50 border border-gray-700 text-gray-100"
|
||||
}`}
|
||||
>
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">
|
||||
{message.content}
|
||||
</pre>
|
||||
{message.stopReason && (
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Status: {message.stopReason}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1 px-1">
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatTimestamp(message.timestamp)}
|
||||
</span>
|
||||
{message.status && getStatusIcon(message.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message.role === "user" && (
|
||||
<div className="flex-shrink-0 order-2">
|
||||
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-purple-500 to-pink-600 flex items-center justify-center">
|
||||
<User className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isProcessing && (
|
||||
<div className="flex gap-3 justify-start">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center">
|
||||
<Bot className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-lg px-4 py-2">
|
||||
<div className="flex gap-1">
|
||||
<span className="h-2 w-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: "0ms" }} />
|
||||
<span className="h-2 w-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: "150ms" }} />
|
||||
<span className="h-2 w-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: "300ms" }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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: <Clock className="h-4 w-4 animate-pulse" />,
|
||||
text: "Creating session...",
|
||||
color: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30",
|
||||
};
|
||||
case "active":
|
||||
return {
|
||||
icon: <CheckCircle className="h-4 w-4" />,
|
||||
text: "Session active",
|
||||
color: "text-green-400 bg-green-400/10 border-green-400/30",
|
||||
};
|
||||
case "processing":
|
||||
return {
|
||||
icon: <Activity className="h-4 w-4 animate-pulse" />,
|
||||
text: "Processing...",
|
||||
color: "text-cyan-400 bg-cyan-400/10 border-cyan-400/30",
|
||||
};
|
||||
case "cancelled":
|
||||
return {
|
||||
icon: <XCircle className="h-4 w-4" />,
|
||||
text: "Session cancelled",
|
||||
color: "text-gray-400 bg-gray-400/10 border-gray-400/30",
|
||||
};
|
||||
case "error":
|
||||
return {
|
||||
icon: <AlertCircle className="h-4 w-4" />,
|
||||
text: "Session error",
|
||||
color: "text-red-400 bg-red-400/10 border-red-400/30",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: <Activity className="h-4 w-4 opacity-50" />,
|
||||
text: "No active session",
|
||||
color: "text-gray-500 bg-gray-500/10 border-gray-500/30",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getStatusConfig(status);
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-2 ${className}`}>
|
||||
<div
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-full border ${config.color}`}
|
||||
>
|
||||
{config.icon}
|
||||
<span className="text-sm font-medium">{config.text}</span>
|
||||
</div>
|
||||
{sessionId && status === "active" && (
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
ID: {sessionId.slice(0, 8)}...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { ChatInterface } from "./ChatInterface";
|
||||
export { FolderSelector } from "./FolderSelector";
|
||||
export { MessageList } from "./MessageList";
|
||||
export { MessageInput } from "./MessageInput";
|
||||
export { SessionStatus } from "./SessionStatus";
|
||||
2
archon-ui-main/src/features/coding-agents/hooks/index.ts
Normal file
2
archon-ui-main/src/features/coding-agents/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./useCodingAgentSession";
|
||||
export * from "./useCodingAgentChat";
|
||||
@@ -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<ChatMessage[]>([]);
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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<InitializeResponse>({
|
||||
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<string | null>(null);
|
||||
|
||||
// Query for current session
|
||||
const sessionQuery = useQuery<Session>({
|
||||
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,
|
||||
};
|
||||
}
|
||||
20
archon-ui-main/src/features/coding-agents/index.ts
Normal file
20
archon-ui-main/src/features/coding-agents/index.ts
Normal file
@@ -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";
|
||||
143
archon-ui-main/src/features/coding-agents/services/acpService.ts
Normal file
143
archon-ui-main/src/features/coding-agents/services/acpService.ts
Normal file
@@ -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<T>(
|
||||
path: string,
|
||||
options: RequestInit = {},
|
||||
): Promise<T> {
|
||||
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<InitializeApiResponse> {
|
||||
return this.request<InitializeApiResponse>("/api/initialize", {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session
|
||||
*/
|
||||
async createSession(
|
||||
request: CreateSessionRequest,
|
||||
): Promise<CreateSessionApiResponse> {
|
||||
return this.request<CreateSessionApiResponse>("/api/sessions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an existing session
|
||||
*/
|
||||
async loadSession(request: LoadSessionRequest): Promise<EmptyApiResponse> {
|
||||
return this.request<EmptyApiResponse>("/api/sessions/load", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session details
|
||||
*/
|
||||
async getSession(sessionId: string): Promise<SessionApiResponse> {
|
||||
return this.request<SessionApiResponse>(`/api/sessions/${sessionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session status
|
||||
*/
|
||||
async getSessionStatus(sessionId: string): Promise<SessionApiResponse> {
|
||||
return this.request<SessionApiResponse>(
|
||||
`/api/sessions/${sessionId}/status`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a prompt to a session
|
||||
*/
|
||||
async sendPrompt(
|
||||
sessionId: string,
|
||||
request: PromptRequest,
|
||||
): Promise<PromptApiResponse> {
|
||||
console.log("[ACP] Sending prompt request:", JSON.stringify(request, null, 2));
|
||||
return this.request<PromptApiResponse>(
|
||||
`/api/sessions/${sessionId}/prompt`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a session
|
||||
*/
|
||||
async cancelSession(sessionId: string): Promise<EmptyApiResponse> {
|
||||
return this.request<EmptyApiResponse>(
|
||||
`/api/sessions/${sessionId}/cancel`,
|
||||
{
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const acpService = new AcpService();
|
||||
@@ -0,0 +1 @@
|
||||
export { acpService } from "./acpService";
|
||||
@@ -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[];
|
||||
}
|
||||
58
archon-ui-main/src/features/coding-agents/types/Session.ts
Normal file
58
archon-ui-main/src/features/coding-agents/types/Session.ts
Normal file
@@ -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<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface EmptyApiResponse {
|
||||
data?: null;
|
||||
}
|
||||
|
||||
// Specific API response types
|
||||
export interface InitializeApiResponse extends ApiResponse<import("./CodingAgent").InitializeResponse> {}
|
||||
export interface CreateSessionApiResponse extends ApiResponse<CreateSessionResponse> {}
|
||||
export interface SessionApiResponse extends ApiResponse<Session> {}
|
||||
export interface PromptApiResponse extends ApiResponse<PromptResponse> {}
|
||||
|
||||
// Message types for chat interface
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: string;
|
||||
status?: "sending" | "sent" | "error";
|
||||
stopReason?: string;
|
||||
}
|
||||
2
archon-ui-main/src/features/coding-agents/types/index.ts
Normal file
2
archon-ui-main/src/features/coding-agents/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./CodingAgent";
|
||||
export * from "./Session";
|
||||
@@ -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 (
|
||||
<div className="flex items-center justify-center h-full p-6">
|
||||
<div className="text-center space-y-4 max-w-md">
|
||||
<AlertCircle className="h-12 w-12 mx-auto text-red-500" />
|
||||
<h2 className="text-xl font-semibold text-gray-100">Something went wrong</h2>
|
||||
<p className="text-sm text-gray-400">{error.message}</p>
|
||||
<Button
|
||||
onClick={resetErrorBoundary}
|
||||
className="bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500"
|
||||
>
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CodingAgentsPage() {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<CodingAgentsView />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-4">
|
||||
<Terminal className="h-12 w-12 mx-auto text-cyan-500 animate-pulse" />
|
||||
<p className="text-gray-400">Initializing coding agent...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (initError) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-4 max-w-md">
|
||||
<Terminal className="h-12 w-12 mx-auto text-red-500" />
|
||||
<p className="text-gray-300">Failed to initialize coding agent</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{initError instanceof Error ? initError.message : "Unknown error"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
Make sure the ACP backend is running on localhost:3001
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-gradient-to-br from-cyan-500 to-blue-600 rounded-lg">
|
||||
<Code2 className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-100">Coding Agent</h1>
|
||||
<p className="text-sm text-gray-400">
|
||||
AI-powered coding assistant for your projects
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSessionStarted && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReset}
|
||||
className="bg-gray-900/50 border-gray-700 hover:bg-gray-800/50"
|
||||
>
|
||||
Change Directory
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Protocol info */}
|
||||
{initData && (
|
||||
<div className="bg-gray-900/30 border border-gray-700 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-gray-400">
|
||||
Protocol v{initData.protocolVersion}
|
||||
</span>
|
||||
<span className="text-gray-500">•</span>
|
||||
<span className="text-gray-400">
|
||||
Image support: {initData.capabilities.promptCapabilities.image ? "✓" : "✗"}
|
||||
</span>
|
||||
<span className="text-gray-500">•</span>
|
||||
<span className="text-gray-400">
|
||||
Context: {initData.capabilities.promptCapabilities.embeddedContext ? "✓" : "✗"}
|
||||
</span>
|
||||
{initData.capabilities.promptCapabilities.audio && (
|
||||
<>
|
||||
<span className="text-gray-500">•</span>
|
||||
<span className="text-gray-400">Audio: ✓</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{initData.clientCapabilities && (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-gray-400">
|
||||
File read: {initData.clientCapabilities.fs.readTextFile ? "✓" : "✗"}
|
||||
</span>
|
||||
<span className="text-gray-400">
|
||||
File write: {initData.clientCapabilities.fs.writeTextFile ? "✓" : "✗"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{!isSessionStarted ? (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="max-w-md w-full space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<Terminal className="h-16 w-16 mx-auto text-cyan-500 opacity-50" />
|
||||
<h2 className="text-xl font-semibold text-gray-100">
|
||||
Select a Working Directory
|
||||
</h2>
|
||||
<p className="text-sm text-gray-400">
|
||||
Choose the folder where the coding agent will work on your project
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6 space-y-4">
|
||||
<FolderSelector
|
||||
value={workingDirectory}
|
||||
onChange={setWorkingDirectory}
|
||||
label="Target Directory"
|
||||
placeholder="/path/to/your/project"
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleStartSession}
|
||||
disabled={!workingDirectory || !workingDirectory.startsWith('/')}
|
||||
className="w-full bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Start Coding Session
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500 text-center space-y-1">
|
||||
<p>The agent will have access to read and modify files in this directory</p>
|
||||
<p>Make sure to choose the correct project folder</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ChatInterface workingDirectory={workingDirectory} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
archon-ui-main/src/pages/CodingAgents.tsx
Normal file
5
archon-ui-main/src/pages/CodingAgents.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CodingAgentsPage } from "../features/coding-agents";
|
||||
|
||||
export default function CodingAgents() {
|
||||
return <CodingAgentsPage />;
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user