drone manager created (wip)

This commit is contained in:
Rob Colbert 2026-04-30 09:40:27 -04:00
parent 3904c19bbf
commit c6d0c66563
11 changed files with 637 additions and 12 deletions

View File

@ -9,6 +9,7 @@ import ProjectManager from './pages/ProjectManager';
import SignIn from './pages/SignIn';
import SignUp from './pages/SignUp';
import ChatSessionView from './pages/ChatSessionView';
import DroneManager from './pages/DroneManager';
const TOKEN_KEY = 'dtp_auth_token';
const USER_KEY = 'dtp_user';
@ -117,6 +118,7 @@ export default function App() {
<Route path="/projects" element={<ProjectManager user={user} />} />
<Route path="/projects/new" element={<ProjectManager user={user} />} />
<Route path="/projects/:slug" element={<ProjectManager user={user} />} />
<Route path="/drones" element={<DroneManager user={user} />} />
<Route path="/projects/:projectId/chat-session/:sessionId" element={<ChatSessionView />} />
<Route
path="/sign-in"

View File

@ -121,6 +121,8 @@ export interface DroneRegistration {
export const droneApi = {
getAll: () => api.get<DroneRegistration[]>('/api/v1/drone/registration'),
getForManager: () => api.get<DroneRegistration[]>('/api/v1/drone/registration/manager'),
terminate: (registrationId: string) => api.post<ApiResponse>(`/api/v1/drone/registration/${registrationId}/terminate`),
};
export interface AiModelCapabilities {

View File

@ -0,0 +1,391 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import type { User, DroneRegistration } from '../lib/api';
import { droneApi } from '../lib/api';
interface DroneManagerProps {
user: User | null;
}
interface LogEntry {
id: number;
timestamp: string;
message: string;
}
export default function DroneManager({ user }: DroneManagerProps) {
const navigate = useNavigate();
const [allDrones, setAllDrones] = useState<DroneRegistration[]>([]);
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(null);
const [loading, setLoading] = useState(true);
const [terminating, setTerminating] = useState<string | null>(null);
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
// Placeholder log entries for testing scroll behavior
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
const logContainerRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true);
useEffect(() => {
if (!user) {
navigate('/sign-in');
return;
}
loadDrones();
}, [user]);
// Mock log generator for testing scroll behavior
// TODO: Replace with live log streaming when backend is implemented
useEffect(() => {
if (!selectedDrone) {
setLogEntries([]);
return;
}
// Initialize with some placeholder entries
const initialEntries: LogEntry[] = Array.from({ length: 50 }, (_, i) => ({
id: i,
timestamp: new Date(Date.now() - (50 - i) * 1000).toLocaleTimeString(),
message: `[PLACEHOLDER] Log entry ${i + 1} - This is mock data for testing scroll behavior`,
}));
setLogEntries(initialEntries);
// Add a new entry every second for testing auto-scroll
const interval = setInterval(() => {
setLogEntries(prev => {
const newEntry: LogEntry = {
id: prev.length > 0 ? prev[prev.length - 1].id + 1 : 0,
timestamp: new Date().toLocaleTimeString(),
message: `[PLACEHOLDER] New log entry at ${new Date().toLocaleTimeString()} - Auto-generated for testing`,
};
const updated = [...prev, newEntry];
// Keep only last 200 entries as per spec
if (updated.length > 200) {
return updated.slice(updated.length - 200);
}
return updated;
});
}, 1000);
return () => clearInterval(interval);
}, [selectedDrone]);
// Auto-scroll log to bottom when new entries arrive (if user hasn't scrolled up)
useEffect(() => {
if (autoScroll && logContainerRef.current) {
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
}
}, [logEntries, autoScroll]);
const handleScroll = () => {
if (logContainerRef.current) {
const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current;
// Check if user is near the bottom (within 100px)
const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
setAutoScroll(isNearBottom);
}
};
const loadDrones = async () => {
try {
const data = await droneApi.getForManager();
setAllDrones(data);
} catch (err) {
console.error('Failed to load drones', err);
showToast('Failed to load drones', 'error');
} finally {
setLoading(false);
}
};
const showToast = (message: string, type: 'success' | 'error') => {
setToast({ message, type });
setTimeout(() => setToast(null), 5000);
};
const handleSelectDrone = (drone: DroneRegistration) => {
setSelectedDrone(drone);
setLogEntries([]); // Clear log when switching drones
};
const handleTerminate = async () => {
if (!selectedDrone) return;
// Show confirmation if drone is busy
if (selectedDrone.status === 'busy') {
const confirmed = confirm('Are you sure you want to terminate the drone? Any work or operations currently in progress may be lost.');
if (!confirmed) return;
}
setTerminating(selectedDrone._id);
try {
const result = await droneApi.terminate(selectedDrone._id);
if (result.message) {
showToast(result.message, result.success ? 'success' : 'error');
} else {
showToast('Drone terminated successfully', 'success');
}
// Refresh drone list
await loadDrones();
// Clear selection if drone is now offline
if (selectedDrone.status !== 'offline') {
setSelectedDrone(null);
}
} catch (err) {
console.error('Failed to terminate drone', err);
showToast('Failed to terminate drone', 'error');
} finally {
setTerminating(null);
}
};
const onlineDrones = allDrones.filter(d => d.status === 'available' || d.status === 'busy');
const offlineDrones = allDrones.filter(d => d.status === 'offline' || d.status === 'starting');
if (!user) {
return (
<div className="flex-1 flex items-center justify-center bg-bg-primary">
<p className="text-text-muted">Please sign in to view drones.</p>
</div>
);
}
return (
<div className="flex-1 flex bg-bg-primary overflow-hidden">
{/* Toast Notification */}
{toast && (
<div className={`fixed top-16 right-4 z-50 px-4 py-3 rounded border ${
toast.type === 'success'
? 'bg-green-900/80 border-green-600 text-green-100'
: 'bg-red-900/80 border-red-600 text-red-100'
}`}>
{toast.message}
</div>
)}
{/* Left Panel - Drone Lists */}
<aside className="w-80 border-r border-border-subtle bg-bg-secondary flex flex-col overflow-hidden">
<div className="p-3 border-b border-border-subtle flex-shrink-0">
<h2 className="text-lg font-semibold text-text-primary">Drone Manager</h2>
</div>
{/* Online Drones Section - 50% of list area */}
<div className="flex flex-col min-h-0 flex-1" style={{ minHeight: 0 }}>
<div className="p-2 border-b border-border-subtle flex-shrink-0 bg-bg-tertiary">
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider">
Online Drones ({onlineDrones.length})
</h3>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-2 min-h-0">
{loading ? (
<p className="text-sm text-text-muted p-2">Loading...</p>
) : onlineDrones.length === 0 ? (
<div className="text-text-muted text-sm p-2">
No online drones.
</div>
) : (
onlineDrones.map((drone) => (
<div
key={drone._id}
onClick={() => handleSelectDrone(drone)}
className={`p-3 border rounded cursor-pointer transition-colors ${
selectedDrone?._id === drone._id
? 'border-brand bg-bg-elevated'
: 'border-border-default bg-bg-tertiary hover:bg-bg-elevated'
}`}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${
drone.status === 'available'
? 'bg-green-500'
: 'bg-yellow-500'
}`}
/>
<span className="font-mono text-sm font-medium text-text-primary">
{drone.hostname}
</span>
</div>
<span className={`text-xs px-2 py-0.5 rounded ${
drone.status === 'available'
? 'bg-green-900/50 text-green-400'
: 'bg-yellow-900/50 text-yellow-400'
}`}>
{drone.status}
</span>
</div>
<div className="text-xs text-text-muted truncate">
{drone.workspaceDir}
</div>
</div>
))
)}
</div>
</div>
{/* Offline Drones Section - 50% of list area */}
<div className="flex flex-col min-h-0 flex-1" style={{ minHeight: 0 }}>
<div className="p-2 border-t border-border-subtle flex-shrink-0 bg-bg-tertiary">
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider">
Offline Drones ({offlineDrones.length})
</h3>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-2 min-h-0">
{loading ? (
<p className="text-sm text-text-muted p-2">Loading...</p>
) : offlineDrones.length === 0 ? (
<div className="text-text-muted text-sm p-2">
No offline drones.
</div>
) : (
offlineDrones.map((drone) => (
<div
key={drone._id}
onClick={() => handleSelectDrone(drone)}
className={`p-3 border rounded cursor-pointer transition-colors ${
selectedDrone?._id === drone._id
? 'border-brand bg-bg-elevated'
: 'border-border-default bg-bg-tertiary hover:bg-bg-elevated'
}`}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-gray-500" />
<span className="font-mono text-sm font-medium text-text-primary">
{drone.hostname}
</span>
</div>
<span className="text-xs px-2 py-0.5 rounded bg-gray-900/50 text-gray-400">
{drone.status}
</span>
</div>
<div className="text-xs text-text-muted truncate">
{drone.workspaceDir}
</div>
</div>
))
)}
</div>
</div>
</aside>
{/* Right Panel - Drone Details */}
<main className="flex-1 flex flex-col overflow-hidden bg-bg-primary">
{selectedDrone ? (
<>
{/* Drone Info Card */}
<div className="p-6 border-b border-border-subtle flex-shrink-0">
<div className="max-w-3xl">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-text-primary">Drone Details</h2>
{selectedDrone.status !== 'offline' && (
<button
onClick={handleTerminate}
disabled={terminating === selectedDrone._id}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium"
>
{terminating === selectedDrone._id ? 'Terminating...' : 'Terminate'}
</button>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-bg-secondary border border-border-default rounded">
<div className="text-sm text-text-muted mb-1">Hostname</div>
<div className="font-mono text-text-primary">{selectedDrone.hostname}</div>
</div>
<div className="p-4 bg-bg-secondary border border-border-default rounded">
<div className="text-sm text-text-muted mb-1">Status</div>
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${
selectedDrone.status === 'available'
? 'bg-green-500'
: selectedDrone.status === 'busy'
? 'bg-yellow-500'
: 'bg-gray-500'
}`}
/>
<span className="text-text-primary capitalize">{selectedDrone.status}</span>
</div>
</div>
<div className="p-4 bg-bg-secondary border border-border-default rounded">
<div className="text-sm text-text-muted mb-1">Workspace</div>
<div className="font-mono text-text-primary text-sm truncate">
{selectedDrone.workspaceDir}
</div>
</div>
<div className="p-4 bg-bg-secondary border border-border-default rounded">
<div className="text-sm text-text-muted mb-1">Registered</div>
<div className="text-text-primary">
{new Date(selectedDrone.createdAt).toLocaleString()}
</div>
</div>
</div>
</div>
</div>
{/* Drone Monitor Placeholder */}
<div className="flex-1 overflow-y-auto p-6 min-h-0">
<div className="max-w-3xl mb-6">
<h3 className="text-sm font-semibold text-text-secondary uppercase tracking-wider mb-4">
Drone Monitor
</h3>
<div className="p-8 bg-bg-secondary border border-border-default rounded text-center">
<p className="text-text-muted">
Monitor charts coming soon.
</p>
<p className="text-text-muted text-sm mt-2">
Memory usage, AI operations, and log production metrics will be displayed here.
</p>
</div>
</div>
{/* Log Viewer - Collapsible panel at bottom */}
<div className="flex flex-col min-h-[300px] max-h-[400px] border-t border-border-subtle">
<div className="p-2 bg-bg-secondary border-b border-border-subtle flex-shrink-0">
<h3 className="text-xs font-semibold text-text-secondary uppercase tracking-wider">
Drone Log {selectedDrone.status !== 'offline' && '(Live)'}
</h3>
</div>
<div
ref={logContainerRef}
onScroll={handleScroll}
className="flex-1 overflow-y-auto p-3 bg-bg-primary font-mono text-xs min-h-0"
>
{logEntries.length === 0 ? (
<div className="text-text-muted">
<p>No log entries yet.</p>
<p className="mt-2 text-text-secondary">
{/* TODO: Remove this placeholder comment when live logs are implemented */}
[PLACEHOLDER] Log entries will appear here when the drone is running.
</p>
</div>
) : (
logEntries.map((entry) => (
<div key={entry.id} className="py-1 border-b border-border-subtle last:border-0">
<span className="text-text-muted mr-3">[{entry.timestamp}]</span>
<span className="text-text-secondary">{entry.message}</span>
</div>
))
)}
</div>
</div>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center text-text-muted">
<div className="text-center">
<p className="mb-2">Select a drone to view details</p>
<p className="text-sm">Choose from the online or offline drone lists</p>
</div>
</div>
)}
</main>
</div>
);
}

View File

@ -163,8 +163,18 @@ function DashboardSidebar({ onNavigate, selectedDrone, onSelectDrone }: Dashboar
</div>
<div className="p-3 border-t border-border-subtle">
<div className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
Drones
<div className="flex items-center justify-between text-xs font-semibold text-text-muted uppercase tracking-wider mb-2">
<span>Drones</span>
<Link
to="/drones"
className="text-text-secondary hover:text-text-primary transition-colors"
title="Drone Manager"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</Link>
</div>
{loading ? (
<p className="text-sm text-text-muted px-2">Loading...</p>
@ -172,9 +182,7 @@ function DashboardSidebar({ onNavigate, selectedDrone, onSelectDrone }: Dashboar
<p className="text-sm text-text-muted px-2">No drones available.</p>
) : (
<div className="space-y-1">
{drones
.filter((d) => d.status === 'available' || d.status === 'busy')
.map((drone) => (
{drones.map((drone) => (
<button
key={drone._id}
onClick={() => onSelectDrone(drone)}

View File

@ -191,9 +191,8 @@ function RightSidebar({ project, onOpenChatSession }: RightSidebarProps) {
const loadData = async () => {
setLoading(true);
try {
// Load available drones only (filter out offline)
const allDrones = await droneApi.getAll();
const availableDrones = allDrones.filter(d => d.status !== 'offline');
// Load available drones (backend filters offline)
const availableDrones = await droneApi.getAll();
setDrones(availableDrones);
// Load chat sessions for this project

View File

@ -3,7 +3,9 @@
// All Rights Reserved
import { Request, Response } from "express";
import { Types } from "mongoose";
import { DroneStatus } from "@gadget/api";
import DroneService from "../../../services/drone.ts";
import UserService from "../../../services/user.ts";
@ -48,6 +50,18 @@ export class DroneApiControllerV1 extends DtpController {
this.getRegistrations.bind(this),
);
this.router.get(
"/registration/manager",
requireUser,
this.getManagerRegistrations.bind(this),
);
this.router.post(
"/registration/:registrationId/terminate",
requireUser,
this.postTerminate.bind(this),
);
this.router.delete(
"/registration/:registrationId",
this.deleteRegistration.bind(this),
@ -92,7 +106,10 @@ export class DroneApiControllerV1 extends DtpController {
async getRegistrations(req: Request, res: Response): Promise<void> {
try {
const registrations = await DroneService.getForUser(req.user);
const registrations = await DroneService.getForUser(req.user, [
DroneStatus.Starting,
DroneStatus.Offline,
]);
res.status(200).json({
success: true,
data: registrations,
@ -108,6 +125,46 @@ export class DroneApiControllerV1 extends DtpController {
}
}
async getManagerRegistrations(req: Request, res: Response): Promise<void> {
try {
const registrations = await DroneService.getForManager(req.user);
res.status(200).json({
success: true,
data: registrations,
});
} catch (error) {
this.log.error("failed to present drone manager registrations for User", {
error,
});
res.status((error as Error).statusCode || 500).json({
success: false,
message: (error as Error).message,
});
}
}
async postTerminate(req: Request, res: Response): Promise<void> {
try {
const registrationId = Types.ObjectId.createFromHexString(
req.params.registrationId as string,
);
const result = await DroneService.requestTermination(registrationId);
res.status(200).json({
success: result.success,
message: result.message,
});
} catch (error) {
this.log.error("failed to terminate drone", {
registrationId: req.params.registrationId,
error,
});
res.status((error as Error).statusCode || 500).json({
success: false,
message: (error as Error).message,
});
}
}
async deleteRegistration(_req: Request, res: Response): Promise<void> {
try {
await DroneService.unregister(res.locals.registration);

View File

@ -8,6 +8,7 @@ import { IUser, DroneStatus, IDroneRegistration } from "@gadget/api";
import DroneRegistration from "../models/drone-registration.ts";
import { DtpService } from "../lib/service.ts";
import SocketService from "./socket.ts";
export interface IDroneDefinition {
hostname: string;
@ -105,15 +106,136 @@ class DroneService extends DtpService {
/**
* Request a User's list of registered drones.
* @param user The user for which a list of registered drones is being requested
* @param statusFilter Optional array of statuses to exclude from the results
* @returns The list of drone registrations for the user.
*/
async getForUser(user: IUser): Promise<IDroneRegistration[]> {
const registrations = await DroneRegistration.find({ user: user._id })
async getForUser(
user: IUser,
statusFilter?: DroneStatus[],
): Promise<IDroneRegistration[]> {
const query: Record<string, unknown> = { user: user._id };
if (statusFilter && statusFilter.length > 0) {
query.status = { $nin: statusFilter };
}
const registrations = await DroneRegistration.find(query)
.sort({ hostname: 1, workspaceDir: 1 })
.populate(this.populateDroneRegistration);
return registrations;
}
/**
* Request a User's complete list of drone registrations for the Drone Manager.
* Returns all drones (online and offline) without filtering.
* @param user The user for which a list of drone registrations is being requested
* @returns The complete list of drone registrations for the user.
*/
async getForManager(user: IUser): Promise<IDroneRegistration[]> {
const registrations = await DroneRegistration.find({ user: user._id })
.sort({ status: 1, hostname: 1, workspaceDir: 1 })
.populate(this.populateDroneRegistration);
return registrations;
}
/**
* Request a drone to terminate itself. Sets a 10-second timeout to wait for
* the drone to mark itself as offline. If the timeout expires, forces the
* status to offline.
* @param registrationId The _id of the drone registration to terminate
* @returns Promise that resolves when termination is complete (success or timeout)
*/
async requestTermination(
registrationId: Types.ObjectId,
): Promise<{ success: boolean; message?: string }> {
const registration = await this.getById(registrationId);
if (registration.status === DroneStatus.Offline) {
return { success: false, message: "Drone is already offline" };
}
let droneSession: any;
try {
droneSession = SocketService.getDroneSession(registration);
} catch (error) {
// Drone is not connected - mark as offline immediately
this.log.warn("drone not connected, marking offline", {
registrationId: registrationId.toHexString(),
hostname: registration.hostname,
});
await this.setStatus(registration, DroneStatus.Offline);
return {
success: true,
message: "Drone was not connected. Marked offline.",
};
}
return new Promise((resolve) => {
let resolved = false;
let timeoutHandle: NodeJS.Timeout;
const cleanup = () => {
clearTimeout(timeoutHandle);
resolved = true;
};
timeoutHandle = setTimeout(async () => {
if (resolved) return;
cleanup();
this.log.warn("drone termination timed out, forcing offline", {
registrationId: registrationId.toHexString(),
hostname: registration.hostname,
});
await this.setStatus(registration, DroneStatus.Offline);
resolve({
success: true,
message: "Drone termination timed out. Marked offline.",
});
}, 10000);
droneSession.socket.emit(
"requestTermination",
async (success: boolean) => {
if (resolved) return;
if (!success) {
cleanup();
resolve({
success: false,
message: "Drone rejected termination request",
});
return;
}
this.log.info("drone accepted termination request", {
registrationId: registrationId.toHexString(),
hostname: registration.hostname,
});
const checkInterval = setInterval(async () => {
try {
const updated = await this.getById(registrationId);
if (updated.status === DroneStatus.Offline) {
clearInterval(checkInterval);
cleanup();
resolve({ success: true });
}
} catch (error) {
clearInterval(checkInterval);
cleanup();
resolve({
success: true,
message: "Drone termination timed out. Marked offline.",
});
}
}, 500);
},
);
});
}
/**
* The drone calls this to update it's status in the database.
* @param registration The registration for which a status is being updated.

View File

@ -0,0 +1,14 @@
{
"workspaceId": "9737cffd-ae5b-418f-b181-e30553bec83d",
"createdAt": "2026-04-30T12:37:32.429Z",
"hostname": "mysterymachine",
"workspaceDir": "/home/rob/projects/gadget/gadget-drone",
"chatSession": null,
"lockedProject": null,
"projects": [],
"registration": {
"_id": "69f3533069d00859987b4e56",
"status": "available",
"registeredAt": "2026-04-30T13:03:45.023Z"
}
}

View File

@ -122,13 +122,22 @@ class GadgetDrone extends GadgetProcess {
async stop(): Promise<number> {
this.log.info(`Gadget Drone v${env.pkg.version} shutting down`);
this.isShuttingDown = true;
if (this.socket) {
this.socket.disconnect();
delete this.socket;
}
try {
await PlatformService.unregister();
} catch (error) {
this.log.error("failed to unregister drone from Gadget Code platform", {
error,
});
// fall through
}
await this.stopServices();
return 0;
@ -191,6 +200,10 @@ class GadgetDrone extends GadgetProcess {
this.onRequestWorkspaceMode.bind(this),
);
this.socket.on("processWorkOrder", this.onProcessWorkOrder.bind(this));
this.socket.on(
"requestTermination",
this.onRequestTermination.bind(this),
);
});
}
@ -357,6 +370,16 @@ class GadgetDrone extends GadgetProcess {
chatSessionId: cache.chatSessionId,
});
}
async onRequestTermination(cb: (success: boolean) => void): Promise<void> {
this.log.info("requestTermination received from platform", {
registrationId: this.registration?._id,
});
cb(true);
process.kill(process.pid, "SIGINT");
}
}
(async () => {

View File

@ -47,3 +47,7 @@ export type CrashRecoveryResponseMessage = (data: {
action: "discard" | "retry";
retryDelay?: number;
}) => void;
export type RequestTerminationMessage = (
cb: (success: boolean) => void,
) => void;

View File

@ -10,6 +10,7 @@ import {
WorkOrderCompleteMessage,
RequestCrashRecoveryMessage,
CrashRecoveryResponseMessage,
RequestTerminationMessage,
} from "./drone.ts";
import {
RequestSessionLockMessage,
@ -53,6 +54,7 @@ export interface ClientToServerEvents {
toolCall: ToolCallMessage;
workOrderComplete: WorkOrderCompleteMessage;
requestCrashRecovery: RequestCrashRecoveryMessage;
requestTermination: RequestTerminationMessage;
}
export interface ServerToClientEvents {
@ -64,6 +66,7 @@ export interface ServerToClientEvents {
requestWorkspaceMode: RequestWorkspaceModeMessage;
processWorkOrder: ProcessWorkOrderMessage;
crashRecoveryResponse: CrashRecoveryResponseMessage;
requestTermination: RequestTerminationMessage;
/*
* gadget-code:web => gadget-code:ide