diff --git a/gadget-code/frontend/src/lib/api.ts b/gadget-code/frontend/src/lib/api.ts index 351702f..7a3b120 100644 --- a/gadget-code/frontend/src/lib/api.ts +++ b/gadget-code/frontend/src/lib/api.ts @@ -1,6 +1,6 @@ -const API_BASE = ''; +const API_BASE = ""; -const TOKEN_KEY = 'dtp_auth_token'; +const TOKEN_KEY = "dtp_auth_token"; export interface ApiResponse { success: boolean; @@ -21,21 +21,21 @@ function getToken(): string | null { async function request( method: string, path: string, - body?: Record + body?: Record, ): Promise { const token = getToken(); const headers: Record = { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }; - + if (token) { - headers['Authorization'] = `Bearer ${token}`; + headers["Authorization"] = `Bearer ${token}`; } const options: RequestInit = { method, headers, - credentials: 'include', + credentials: "include", }; if (body) { @@ -44,9 +44,9 @@ async function request( const response = await fetch(`${API_BASE}${path}`, options); const text = await response.text(); - + if (!text) { - throw new Error('Empty response'); + throw new Error("Empty response"); } try { @@ -56,7 +56,7 @@ async function request( } if (!json.success) { - throw new Error(json.message || 'Request failed'); + throw new Error(json.message || "Request failed"); } if (json.token !== undefined && json.user !== undefined) { @@ -69,12 +69,12 @@ async function request( } export const api = { - get: (path: string) => request('GET', path), - post: (path: string, body: Record) => - request('POST', path, body), + get: (path: string) => request("GET", path), + post: (path: string, body?: Record) => + request("POST", path, body), put: (path: string, body: Record) => - request('PUT', path, body), - delete: (path: string) => request('DELETE', path), + request("PUT", path, body), + delete: (path: string) => request("DELETE", path), }; export interface User { @@ -93,19 +93,26 @@ export interface Project { _id: string; createdAt: string; user: string; - status: 'active' | 'inactive' | 'archived'; + status: "active" | "inactive" | "archived"; name: string; slug: string; gitUrl?: string; } export const projectApi = { - getAll: () => api.get('/api/v1/projects'), + getAll: () => api.get("/api/v1/projects"), get: (id: string) => api.get(`/api/v1/projects/${id}`), create: (data: { name: string; slug: string; gitUrl?: string }) => - api.post('/api/v1/projects', data), - update: (id: string, data: Partial<{ name: string; slug: string; gitUrl: string; status: string }>) => - api.put(`/api/v1/projects/${id}`, data), + api.post("/api/v1/projects", data), + update: ( + id: string, + data: Partial<{ + name: string; + slug: string; + gitUrl: string; + status: string; + }>, + ) => api.put(`/api/v1/projects/${id}`, data), delete: (id: string) => api.delete(`/api/v1/projects/${id}`), }; @@ -113,16 +120,20 @@ export interface DroneRegistration { _id: string; hostname: string; workspaceDir: string; - status: 'starting' | 'available' | 'busy' | 'offline'; + status: "starting" | "available" | "busy" | "offline"; user: string; createdAt: string; updatedAt: string; } export const droneApi = { - getAll: () => api.get('/api/v1/drone/registration'), - getForManager: () => api.get('/api/v1/drone/registration/manager'), - terminate: (registrationId: string) => api.post(`/api/v1/drone/registration/${registrationId}/terminate`), + getAll: () => api.get("/api/v1/drone/registration"), + getForManager: () => + api.get("/api/v1/drone/registration/manager"), + terminate: (registrationId: string) => + api.post( + `/api/v1/drone/registration/${registrationId}/terminate`, + ), }; export interface AiModelCapabilities { @@ -153,7 +164,7 @@ export interface AiModel { export interface AiProvider { _id: string; name: string; - apiType: 'ollama' | 'openai'; + apiType: "ollama" | "openai"; baseUrl: string; enabled: boolean; models: AiModel[]; @@ -161,16 +172,16 @@ export interface AiProvider { } export const providerApi = { - getAll: () => api.get('/api/v1/providers'), + getAll: () => api.get("/api/v1/providers"), get: (id: string) => api.get(`/api/v1/providers/${id}`), }; export enum ChatSessionMode { - Plan = 'plan', - Build = 'build', - Test = 'test', - Ship = 'ship', - Develop = 'dev', + Plan = "plan", + Build = "build", + Test = "test", + Ship = "ship", + Develop = "dev", } export interface ChatSessionStats { @@ -217,7 +228,7 @@ export interface ChatTurn { provider: string | AiProvider; llm: string; mode: ChatSessionMode; - status: 'processing' | 'finished' | 'error'; + status: "processing" | "finished" | "error"; prompts: ChatTurnPrompts; thinking?: string; response?: string; @@ -234,7 +245,7 @@ export interface ChatTurn { export const chatSessionApi = { getAll: (projectId?: string) => api.get( - `/api/v1/chat-sessions${projectId ? `?projectId=${projectId}` : ''}` + `/api/v1/chat-sessions${projectId ? `?projectId=${projectId}` : ""}`, ), get: (id: string) => api.get(`/api/v1/chat-sessions/${id}`), create: (data: { @@ -243,9 +254,10 @@ export const chatSessionApi = { selectedModel: string; mode?: ChatSessionMode; name?: string; - }) => api.post('/api/v1/chat-sessions', data), + }) => api.post("/api/v1/chat-sessions", data), update: (id: string, data: Partial) => api.put(`/api/v1/chat-sessions/${id}`, data), delete: (id: string) => api.delete(`/api/v1/chat-sessions/${id}`), - getTurns: (id: string) => api.get(`/api/v1/chat-sessions/${id}/turns`), -}; \ No newline at end of file + getTurns: (id: string) => + api.get(`/api/v1/chat-sessions/${id}/turns`), +}; diff --git a/gadget-code/frontend/src/lib/socket.ts b/gadget-code/frontend/src/lib/socket.ts index 0c12312..5f50de0 100644 --- a/gadget-code/frontend/src/lib/socket.ts +++ b/gadget-code/frontend/src/lib/socket.ts @@ -1,13 +1,22 @@ -import { createContext } from 'react'; -import { io, Socket } from 'socket.io-client'; +import { createContext } from "react"; +import { io, Socket } from "socket.io-client"; -const SOCKET_URL = ''; +const SOCKET_URL = ""; export interface ServerToClientEvents { thinking: (content: string) => void; response: (content: string) => void; - toolCall: (callId: string, name: string, params: string, response: string) => void; - workOrderComplete: (turnId: string, success: boolean, message?: string) => void; + toolCall: ( + callId: string, + name: string, + params: string, + response: string, + ) => void; + workOrderComplete: ( + turnId: string, + success: boolean, + message?: string, + ) => void; } export interface ClientToServerEvents { @@ -16,92 +25,132 @@ export interface ClientToServerEvents { registration: any, project: any, chatSession: any, - cb: (success: boolean, chatSessionId: string) => void + cb: (success: boolean, chatSessionId: string) => void, ) => void; } export interface SocketEvents { - 'agent:thinking': (data: { agentId: string; thinking: string }) => void; - 'agent:response': (data: { agentId: string; chunk: string }) => void; - 'agent:tool-call': (data: { agentId: string; tool: string; args: unknown }) => void; - 'agent:tool-result': (data: { agentId: string; tool: string; result: unknown }) => void; - 'agent:complete': (data: { agentId: string }) => void; - 'log:entry': (data: { level: string; message: string; timestamp: number }) => void; - 'chat:message': (data: { agentId: string; message: string; role: 'user' | 'assistant' | 'system' }) => void; + thinking: (content: string) => void; + response: (content: string) => void; + toolCall: ( + callId: string, + name: string, + params: string, + response: string, + ) => void; + workOrderComplete: ( + turnId: string, + success: boolean, + message?: string, + ) => void; + "agent:thinking": (data: { agentId: string; thinking: string }) => void; + "agent:response": (data: { agentId: string; chunk: string }) => void; + "agent:tool-call": (data: { + agentId: string; + tool: string; + args: unknown; + }) => void; + "agent:tool-result": (data: { + agentId: string; + tool: string; + result: unknown; + }) => void; + "agent:complete": (data: { agentId: string }) => void; + "log:entry": (data: { + level: string; + message: string; + timestamp: number; + }) => void; + "chat:message": (data: { + agentId: string; + message: string; + role: "user" | "assistant" | "system"; + }) => void; connect: () => void; - disconnect: () => void; + disconnect: (reason: string) => void; error: (error: Error) => void; } class SocketClient { - private socket: Socket | null = null; - private eventListeners: Map void>> = new Map(); + private _socket: Socket | null = null; + private eventListeners: Map void>> = + new Map(); private reconnectAttempts = 0; private maxReconnectAttempts = 5; private jwt: string | null = null; get connected(): boolean { - return this.socket?.connected ?? false; + return this._socket?.connected ?? false; } get socket(): Socket | null { - return this.socket; + return this._socket; } connect(token: string): void { - if (this.socket?.connected) { + if (this._socket?.connected) { return; } this.jwt = token; - this.socket = io(SOCKET_URL, { + this._socket = io(SOCKET_URL, { auth: { token: this.jwt, }, - transports: ['websocket', 'polling'], + transports: ["websocket", "polling"], reconnection: true, reconnectionAttempts: this.maxReconnectAttempts, reconnectionDelay: 1000, reconnectionDelayMax: 5000, }); + if (!this.socket) { + return; + } + // Forward server events to our event listeners - this.socket.on('thinking', (content: string) => { - this.emit('thinking', content); + this.socket.on("thinking", (content: string) => { + this.emit("thinking", content); }); - this.socket.on('response', (content: string) => { - this.emit('response', content); + this.socket.on("response", (content: string) => { + this.emit("response", content); }); - this.socket.on('toolCall', (callId: string, name: string, params: string, response: string) => { - this.emit('toolCall', callId, name, params, response); - }); + this.socket.on( + "toolCall", + (callId: string, name: string, params: string, response: string) => { + this.emit("toolCall", callId, name, params, response); + }, + ); - this.socket.on('workOrderComplete', (turnId: string, success: boolean, message?: string) => { - this.emit('workOrderComplete', turnId, success, message); - }); + this.socket.on( + "workOrderComplete", + (turnId: string, success: boolean, message?: string) => { + this.emit("workOrderComplete", turnId, success, message); + }, + ); - this.socket.on('connect', () => { + this._socket.on("connect", () => { this.reconnectAttempts = 0; - this.emit('connect'); + this.emit("connect"); }); - this.socket.on('disconnect', (reason) => { - this.emit('disconnect', reason); + this._socket.on("disconnect", (reason) => { + this.emit("disconnect", reason); }); - this.socket.on('connect_error', (error) => { + this._socket.on("connect_error", (error) => { this.reconnectAttempts++; - this.emit('error', error); + this.emit("error", error); }); } disconnect(): void { - if (this.socket) { - this.socket.disconnect(); - this.socket = null; + if (this._socket) { + this._socket.disconnect(); + this._socket = null; this.jwt = null; } } @@ -110,7 +159,9 @@ class SocketClient { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, new Set()); } - this.eventListeners.get(event)!.add(callback as (...args: unknown[]) => void); + this.eventListeners + .get(event)! + .add(callback as (...args: unknown[]) => void); } off(event: K, callback: SocketEvents[K]): void { @@ -120,35 +171,48 @@ class SocketClient { } } - emit(event: K, ...args: Parameters): void { + emit( + event: K, + ...args: Parameters + ): void { const listeners = this.eventListeners.get(event); if (listeners) { listeners.forEach((callback) => callback(...args)); } } - send(event: K, ...args: Parameters): void { - if (this.socket?.connected) { - this.socket.emit(event, ...args); + send( + event: K, + ...args: Parameters + ): void { + if (this._socket?.connected) { + this._socket.emit(event, ...args); } } - emitServer(event: K, ...args: Parameters): void { - if (this.socket?.connected) { - this.socket.emit(event, ...args); + emitServer( + event: K, + ...args: Parameters + ): void { + if (this._socket?.connected) { + this._socket.emit(event, ...args); } } requestSessionLock( registration: any, project: any, - chatSession: any + chatSession: any, ): Promise { return new Promise((resolve) => { - if (this.socket?.connected) { - this.socket.emit('requestSessionLock', registration, project, chatSession, (success: boolean) => { - resolve(success); - }); + if (this._socket?.connected) { + this._socket.emit( + "requestSessionLock", + registration, + project, + chatSession, + resolve, + ); } else { resolve(false); } @@ -158,4 +222,4 @@ class SocketClient { export const socketClient = new SocketClient(); -export const SocketContext = createContext(null); \ No newline at end of file +export const SocketContext = createContext(null); diff --git a/gadget-code/frontend/src/pages/Home.tsx b/gadget-code/frontend/src/pages/Home.tsx index 088cf56..fca821c 100644 --- a/gadget-code/frontend/src/pages/Home.tsx +++ b/gadget-code/frontend/src/pages/Home.tsx @@ -19,7 +19,7 @@ function SystemReady() { href="/sign-in" className="inline-block px-4 py-2 border border-border-highlight text-text-primary hover:bg-bg-tertiary hover:text-text-primary rounded transition-colors" > - [ Sign In ] + Sign In diff --git a/gadget-code/frontend/src/pages/ProjectManager.tsx b/gadget-code/frontend/src/pages/ProjectManager.tsx index ad172c6..ea640ad 100644 --- a/gadget-code/frontend/src/pages/ProjectManager.tsx +++ b/gadget-code/frontend/src/pages/ProjectManager.tsx @@ -1,35 +1,49 @@ -import { useState, useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import type { User, Project } from '../lib/api'; -import { projectApi, droneApi, chatSessionApi, type DroneRegistration, type ChatSession, type AiProvider, providerApi } from '../lib/api'; -import { socketClient } from '../lib/socket'; +import { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import type { User, Project } from "../lib/api"; +import { + projectApi, + droneApi, + chatSessionApi, + type DroneRegistration, + type ChatSession, + type AiProvider, + providerApi, +} from "../lib/api"; +import { socketClient } from "../lib/socket"; interface ProjectManagerProps { user: User | null; } -function NewProjectForm({ onCancel, onSuccess }: { onCancel: () => void; onSuccess: () => void }) { - const [name, setName] = useState(''); - const [slug, setSlug] = useState(''); - const [gitUrl, setGitUrl] = useState(''); +function NewProjectForm({ + onCancel, + onSuccess, +}: { + onCancel: () => void; + onSuccess: () => void; +}) { + const [name, setName] = useState(""); + const [slug, setSlug] = useState(""); + const [gitUrl, setGitUrl] = useState(""); const [submitting, setSubmitting] = useState(false); - const [error, setError] = useState(''); + const [error, setError] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!name || !slug) { - setError('Name and slug are required'); + setError("Name and slug are required"); return; } setSubmitting(true); - setError(''); + setError(""); try { await projectApi.create({ name, slug, gitUrl: gitUrl || undefined }); onSuccess(); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to create project'); + setError(err instanceof Error ? err.message : "Failed to create project"); } finally { setSubmitting(false); } @@ -40,7 +54,9 @@ function NewProjectForm({ onCancel, onSuccess }: { onCancel: () => void; onSucce

Create New Project

- + void; onSucce />
- + void; onSucce className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none" placeholder="my-project" /> -

Unique identifier for the project directory

+

+ Unique identifier for the project directory +

- + void; onSucce disabled={submitting} className="px-4 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50" > - {submitting ? 'Creating...' : 'Create Project'} + {submitting ? "Creating..." : "Create Project"}
@@ -174,17 +209,25 @@ function ProjectInspector({ project, onDelete }: ProjectInspectorProps) { interface RightSidebarProps { project: Project; + selectedDrone: DroneRegistration | null; + onSelectDrone: (drone: DroneRegistration) => void; onOpenChatSession: (sessionId: string) => void; } -function RightSidebar({ project, onOpenChatSession }: RightSidebarProps) { +function RightSidebar({ + project, + selectedDrone, + onSelectDrone, + onOpenChatSession, +}: RightSidebarProps) { const [drones, setDrones] = useState([]); const [chatSessions, setChatSessions] = useState([]); const [providers, setProviders] = useState([]); - const [selectedDrone, setSelectedDrone] = useState(null); const [showNewChatModal, setShowNewChatModal] = useState(false); const [loading, setLoading] = useState(true); - const [deletingSessions, setDeletingSessions] = useState>(new Set()); + const [deletingSessions, setDeletingSessions] = useState>( + new Set(), + ); useEffect(() => { loadData(); @@ -205,16 +248,12 @@ function RightSidebar({ project, onOpenChatSession }: RightSidebarProps) { const allProviders = await providerApi.getAll(); setProviders(allProviders); } catch (err) { - console.error('Failed to load sidebar data', err); + console.error("Failed to load sidebar data", err); } finally { setLoading(false); } }; - const handleSelectDrone = (drone: DroneRegistration) => { - setSelectedDrone(drone); - }; - const handleCreateChatSession = async (data: { providerId: string; selectedModel: string; @@ -230,32 +269,36 @@ function RightSidebar({ project, onOpenChatSession }: RightSidebarProps) { name: data.name, }); setShowNewChatModal(false); - + // Lock the drone to this session BEFORE navigating if (selectedDrone) { - const success = await socketClient.requestSessionLock(selectedDrone, project, session); + const success = await socketClient.requestSessionLock( + selectedDrone, + project, + session, + ); if (!success) { - console.error('Failed to lock drone session'); + console.error("Failed to lock drone session"); } } - + onOpenChatSession(session._id); await loadData(); // Refresh list } catch (err) { - console.error('Failed to create chat session', err); + console.error("Failed to create chat session", err); } }; const handleDeleteChatSession = async (sessionId: string) => { - if (!confirm('Delete this chat session?')) return; - setDeletingSessions(prev => new Set(prev).add(sessionId)); + if (!confirm("Delete this chat session?")) return; + setDeletingSessions((prev) => new Set(prev).add(sessionId)); try { await chatSessionApi.delete(sessionId); await loadData(); } catch (err) { - console.error('Failed to delete chat session', err); + console.error("Failed to delete chat session", err); } finally { - setDeletingSessions(prev => { + setDeletingSessions((prev) => { const next = new Set(prev); next.delete(sessionId); return next; @@ -267,7 +310,10 @@ function RightSidebar({ project, onOpenChatSession }: RightSidebarProps) { <> @@ -411,17 +477,27 @@ interface NewChatSessionModalProps { providers: AiProvider[]; selectedDrone: DroneRegistration | null; onCancel: () => void; - onCreate: (data: { providerId: string; selectedModel: string; mode: string; name?: string }) => void; + onCreate: (data: { + providerId: string; + selectedModel: string; + mode: string; + name?: string; + }) => void; } -function NewChatSessionModal({ providers, selectedDrone, onCancel, onCreate }: NewChatSessionModalProps) { - const [selectedProviderId, setSelectedProviderId] = useState(''); - const [selectedModel, setSelectedModel] = useState(''); - const [mode, setMode] = useState('build'); - const [name, setName] = useState(''); +function NewChatSessionModal({ + providers, + selectedDrone, + onCancel, + onCreate, +}: NewChatSessionModalProps) { + const [selectedProviderId, setSelectedProviderId] = useState(""); + const [selectedModel, setSelectedModel] = useState(""); + const [mode, setMode] = useState("build"); + const [name, setName] = useState(""); const [creating, setCreating] = useState(false); - const selectedProvider = providers.find(p => p._id === selectedProviderId); + const selectedProvider = providers.find((p) => p._id === selectedProviderId); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -431,14 +507,14 @@ function NewChatSessionModal({ providers, selectedDrone, onCancel, onCreate }: N setCreating(true); try { - await onCreate({ + onCreate({ providerId: selectedProviderId, selectedModel, mode, name: name || undefined, }); } catch (err) { - console.error('Failed to create chat session', err); + console.error("Failed to create chat session", err); } finally { setCreating(false); } @@ -451,13 +527,19 @@ function NewChatSessionModal({ providers, selectedDrone, onCancel, onCreate }: N {selectedDrone && (
Selected Drone
-
{selectedDrone.hostname}
-
{selectedDrone.workspaceDir}
+
+ {selectedDrone.hostname} +
+
+ {selectedDrone.workspaceDir} +
)}
- +
- + setSelectedModel(e.target.value)} @@ -498,20 +584,24 @@ function NewChatSessionModal({ providers, selectedDrone, onCancel, onCreate }: N {selectedProvider.models.map((model) => ( ))} {selectedProvider.models.length === 0 && (

- No models discovered. Run `pnpm cli provider probe {selectedProvider._id}` to discover models. + No models discovered. Run `pnpm cli provider probe{" "} + {selectedProvider._id}` to discover models.

)}
)}
- +