diff --git a/gadget-code/docs/ui-design-guide.md b/gadget-code/docs/ui-design-guide.md index 1f4b9f2..1a07b89 100644 --- a/gadget-code/docs/ui-design-guide.md +++ b/gadget-code/docs/ui-design-guide.md @@ -29,6 +29,7 @@ The IDE uses the following color palette (CSS custom properties): ``` Font stack: + - Primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif - Code/Retro: Courier New, Courier, monospace @@ -80,14 +81,14 @@ Implementation: `frontend/src/components/StatusBar.tsx` Between the header and status bars is the Content Area. It uses React Router for URL-based navigation: -| Route | View | -|-------|------| -| `/` | Home (authenticated dashboard or unauthenticated) | -| `/projects` | Project Manager (list view) | -| `/projects/:slug` | Project Manager (project selected) | -| `/projects/new` | New project form | -| `/sign-in` | Sign in page | -| `/sign-out` | Signs out and redirects to `/` | +| Route | View | +| ----------------- | ------------------------------------------------- | +| `/` | Home (authenticated dashboard or unauthenticated) | +| `/projects` | Project Manager (list view) | +| `/projects/:slug` | Project Manager (project selected) | +| `/projects/new` | New project form | +| `/sign-in` | Sign in page | +| `/sign-out` | Signs out and redirects to `/` | ## Unauthenticated Home View @@ -132,6 +133,7 @@ Select a project or chat session from the sidebar to get started. ``` Components: + 1. Clock - local time in AM/PM format, current date 2. Projects list - links to `/projects/:slug`, [+]/link navigates to `/projects` 3. Drones list - status indicator (green=available, yellow=busy, gray=offline) @@ -174,6 +176,7 @@ When a project is selected: ``` Features: + - Left sidebar: Project list with [+ New Project] button - Project Inspector: Shows name, slug, gitUrl, status, createdAt - Delete: Confirmation before deletion @@ -205,12 +208,13 @@ Triggered by [New Project] button or `/projects/new` route: All API requests include the JWT in the Authorization header: ```typescript -headers['Authorization'] = `Bearer ${token}`; +headers["Authorization"] = `Bearer ${token}`; ``` ### Backend Session Restoration The backend restores user sessions from: + 1. `Authorization: Bearer ` header (JWT) 2. Express session (fallback) @@ -221,6 +225,7 @@ The `requireUser()` middleware ensures endpoints are authenticated. ### Password Field Handling Password credentials are NEVER exposed: + - Mongoose `select: false` on password fields in models - Population uses `select: "-passwordSalt -password"` to exclude from queries - Frontend User interface only includes: `_id`, `email`, `displayName`, `flags` @@ -233,16 +238,17 @@ The Chat Session View presents: Work Area | Session Status ----------------------------------------------|--------------- Chat Messages | Chat: name - | ID: ... - | Model: ... -------------------------------------------|--------------- -[Prompt input ][Expand][Send] | TC | FO | SA + | ID: ... + | Model: ... +----------------------------------------------|--------------- +[Prompt input ][Expand][Send]| TC | FO | SA ----------------------------------------------|--------------- Log | Files | ``` Implemented components: + - Chat Messages (stubbed) - Prompt Input (stubbed) - Session Status sidebar (stubbed) @@ -285,4 +291,4 @@ pnpm dev:frontend # Frontend on https://localhost:5174 pnpm build # Build all (backend -> dist/, frontend -> frontend/dist/) pnpm test # Unit tests npx playwright test # E2E tests -``` \ No newline at end of file +``` diff --git a/gadget-code/frontend/src/lib/api.ts b/gadget-code/frontend/src/lib/api.ts index 329b1b5..9064bc5 100644 --- a/gadget-code/frontend/src/lib/api.ts +++ b/gadget-code/frontend/src/lib/api.ts @@ -107,4 +107,18 @@ export const projectApi = { 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}`), +}; + +export interface DroneRegistration { + _id: string; + hostname: string; + workspaceDir: string; + status: 'starting' | 'available' | 'busy' | 'offline'; + user: string; + createdAt: string; + updatedAt: string; +} + +export const droneApi = { + getAll: () => api.get('/api/v1/drone/registration'), }; \ No newline at end of file diff --git a/gadget-code/frontend/src/pages/Home.tsx b/gadget-code/frontend/src/pages/Home.tsx index 62e4abd..9f78637 100644 --- a/gadget-code/frontend/src/pages/Home.tsx +++ b/gadget-code/frontend/src/pages/Home.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import type { User, Project } from '../lib/api'; -import { projectApi } from '../lib/api'; +import type { User, Project, DroneRegistration } from '../lib/api'; +import { projectApi, droneApi } from '../lib/api'; import Clock from '../components/Clock'; const ansiColors = [ @@ -51,23 +51,79 @@ function AnsiLogo() { interface DashboardSidebarProps { onNavigate: (view: string, data?: unknown) => void; + selectedDrone: DroneRegistration | null; + onSelectDrone: (drone: DroneRegistration | null) => void; } -function DashboardSidebar({ onNavigate }: DashboardSidebarProps) { +function DroneInspector({ drone, onClose }: { drone: DroneRegistration; onClose: () => void }) { + return ( +
+
+
+

Drone Inspector

+ +
+
+
+
Hostname
+
{drone.hostname}
+
+
+
Workspace
+
{drone.workspaceDir}
+
+
+
Status
+
+ + {drone.status} +
+
+
+
Registered
+
+ {new Date(drone.createdAt).toLocaleString()} +
+
+
+
+
+ ); +} + +function DashboardSidebar({ onNavigate, selectedDrone, onSelectDrone }: DashboardSidebarProps) { const navigate = useNavigate(); const [projects, setProjects] = useState([]); + const [drones, setDrones] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { - loadProjects(); + loadData(); }, []); - const loadProjects = async () => { + const loadData = async () => { try { - const data = await projectApi.getAll(); - setProjects(data); + const [projectsData, dronesData] = await Promise.all([ + projectApi.getAll(), + droneApi.getAll(), + ]); + setProjects(projectsData); + setDrones(dronesData); } catch (err) { - console.error('Failed to load projects', err); + console.error('Failed to load dashboard data', err); } finally { setLoading(false); } @@ -110,9 +166,51 @@ function DashboardSidebar({ onNavigate }: DashboardSidebarProps) {
Drones
-
+ {loading ? (

Loading...

-
+ ) : drones.length === 0 ? ( +

No drones available.

+ ) : ( +
+ {drones + .filter((d) => d.status === 'available' || d.status === 'busy') + .map((drone) => ( + + ))} +
+ )}
@@ -133,6 +231,7 @@ interface HomeProps { export default function Home({ user }: HomeProps) { const navigate = useNavigate(); + const [selectedDrone, setSelectedDrone] = useState(null); if (!user) { return ( @@ -144,33 +243,41 @@ export default function Home({ user }: HomeProps) { return (
-
-
-

- Welcome, {user.displayName}! -

-

- Your dashboard is under construction. -

-

- Select a project or chat session from the sidebar to get started. -

-
- - Open Project Manager - + {selectedDrone ? ( + setSelectedDrone(null)} /> + ) : ( +
+
+

+ Welcome, {user.displayName}! +

+

+ Your dashboard is under construction. +

+

+ Select a project or chat session from the sidebar to get started. +

+
+ + Open Project Manager + +
-
+ )} - { - if (view === 'project' && typeof navigate === 'function') { - navigate('/projects'); - } - }} /> + { + if (view === 'project' && typeof navigate === 'function') { + navigate('/projects'); + } + }} + selectedDrone={selectedDrone} + onSelectDrone={setSelectedDrone} + />
); } \ No newline at end of file diff --git a/gadget-code/frontend/src/pages/ProjectManager.tsx b/gadget-code/frontend/src/pages/ProjectManager.tsx index 560b241..c895077 100644 --- a/gadget-code/frontend/src/pages/ProjectManager.tsx +++ b/gadget-code/frontend/src/pages/ProjectManager.tsx @@ -229,27 +229,41 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
-
- Projects ({projects.length}) -
- {projects.length === 0 ? ( -

No projects yet.

- ) : ( -
- {projects.map((project) => ( - - ))} + {showNewForm ? ( +
+

Creating new project...

+
+ ) : ( + <> +
+ Projects ({projects.length}) +
+ {projects.length === 0 ? ( +

No projects yet.

+ ) : ( +
+ {projects.map((project) => ( + + ))} +
+ )} + )}
diff --git a/gadget-code/src/controllers/api/v1/drone.ts b/gadget-code/src/controllers/api/v1/drone.ts index f55a398..c44c2da 100644 --- a/gadget-code/src/controllers/api/v1/drone.ts +++ b/gadget-code/src/controllers/api/v1/drone.ts @@ -26,7 +26,9 @@ export class DroneApiControllerV1 extends DtpController { } async start(): Promise { + const requireUser = this.requireUser(); const multer = this.createMulter(this.slug, {}); + this.router.param("registrationId", populateDroneRegistrationById(this)); this.router.post( @@ -40,6 +42,12 @@ export class DroneApiControllerV1 extends DtpController { this.putRegistrationStatus.bind(this), ); + this.router.get( + "/registration", + requireUser, + this.getRegistrations.bind(this), + ); + this.router.delete( "/registration/:registrationId", this.deleteRegistration.bind(this), @@ -82,6 +90,24 @@ export class DroneApiControllerV1 extends DtpController { } } + async getRegistrations(req: Request, res: Response): Promise { + try { + const registrations = await DroneService.getForUser(req.user); + res.status(200).json({ + success: true, + data: registrations, + }); + } catch (error) { + this.log.error("failed to present drone registrations for User", { + error, + }); + res.status((error as Error).statusCode || 500).json({ + success: false, + message: (error as Error).message, + }); + } + } + async deleteRegistration(_req: Request, res: Response): Promise { try { await DroneService.unregister(res.locals.registration); diff --git a/gadget-code/src/controllers/api/v1/project.ts b/gadget-code/src/controllers/api/v1/project.ts index d40c7b1..f0e332a 100644 --- a/gadget-code/src/controllers/api/v1/project.ts +++ b/gadget-code/src/controllers/api/v1/project.ts @@ -191,4 +191,4 @@ export class ProjectApiControllerV1 extends DtpController { } } -export default ProjectApiControllerV1; \ No newline at end of file +export default ProjectApiControllerV1; diff --git a/gadget-code/src/models/drone-registration.ts b/gadget-code/src/models/drone-registration.ts index 8d0008e..3b54504 100644 --- a/gadget-code/src/models/drone-registration.ts +++ b/gadget-code/src/models/drone-registration.ts @@ -17,6 +17,7 @@ export const DroneRegistrationSchema = new Schema({ default: DroneStatus.Starting, required: true, }, + chatSessionId: { type: String, required: false }, currentJobId: { type: String, required: false }, }); diff --git a/gadget-code/src/services/drone.ts b/gadget-code/src/services/drone.ts index 7a80330..8f94157 100644 --- a/gadget-code/src/services/drone.ts +++ b/gadget-code/src/services/drone.ts @@ -4,13 +4,19 @@ import { PopulateOptions, Types } from "mongoose"; -import { IUser, DroneStatus, IDroneRegistration } from "@gadget/api"; +import { + IUser, + DroneStatus, + IDroneRegistration, + IChatSession, +} from "@gadget/api"; import DroneRegistration from "@/models/drone-registration.js"; import { DtpService } from "../lib/service.js"; export interface IDroneDefinition { hostname: string; + workspaceDir: string; } class DroneService extends DtpService { @@ -42,6 +48,13 @@ class DroneService extends DtpService { this.log.info("service stopped"); } + /** + * The drone calls this service to register itself into the platform as + * belonging to a specific User. Users can only ever access their own drones. + * @param user The user for which a drone is being registered + * @param definition The definition of the drone being registered + * @returns the new drone registration + */ async register( user: IUser, definition: IDroneDefinition, @@ -53,6 +66,7 @@ class DroneService extends DtpService { registration.updatedAt = NOW; registration.user = user._id; registration.hostname = definition.hostname; + registration.workspaceDir = definition.workspaceDir; registration.status = DroneStatus.Starting; this.log.info("registering drone", { hostname: registration.hostname }); @@ -61,6 +75,13 @@ class DroneService extends DtpService { return registration.populate(this.populateDroneRegistration); } + /** + * The drone calls this service to unregister from the platform when shutting + * down. This will set the status to offline and remove any associated + * tasks. + * @param registration The drone registration being unregistered (offline) + * @returns the updated drone registration + */ async unregister( registration: IDroneRegistration, ): Promise { @@ -68,6 +89,12 @@ class DroneService extends DtpService { return this.setStatus(registration, DroneStatus.Offline); } + /** + * Retrieve a drone registration by _id. Throws if the registration doesn't + * exist. + * @param registrationId The _id of the drone registration to be fetched + * @returns the drone registration document + */ async getById(registrationId: Types.ObjectId): Promise { const registration = await DroneRegistration.findById( registrationId, @@ -80,6 +107,11 @@ class DroneService extends DtpService { return registration; } + /** + * Request a User's list of registered drones. + * @param user The user for which a list of registered drones is being requested + * @returns The list of drone registrations for the user. + */ async getForUser(user: IUser): Promise { const registrations = await DroneRegistration.find({ user: user._id }) .sort({ hostname: 1, workspaceDir: 1 }) @@ -87,6 +119,12 @@ class DroneService extends DtpService { return registrations; } + /** + * The drone calls this to update it's status in the database. + * @param registration The registration for which a status is being updated. + * @param status The new status of the drone. + * @returns the updated drone registration + */ async setStatus( registration: IDroneRegistration, status: DroneStatus, @@ -108,6 +146,31 @@ class DroneService extends DtpService { } return newRegistration; } + + async requestChatSessionLock( + registration: IDroneRegistration, + session: IChatSession, + ): Promise { + /* + * TODO: Send socket message to drone requesting session lock + * If drone acknowledges lock, update the registration with the chatSessionId. + * If the drone denies the lock, throw a descriptive error. + */ + + // Update the registration with the chatSessionId + const updatedRegistration = await DroneRegistration.findOneAndUpdate( + { _id: registration._id }, + { $set: { chatSessionId: session._id } }, + { new: true, populate: this.populateDroneRegistration }, + ); + if (!updatedRegistration) { + const error = new Error("drone registration has been removed"); + error.statusCode = 404; + throw error; + } + + return updatedRegistration; + } } export default new DroneService(); diff --git a/gadget-code/src/web-app.ts b/gadget-code/src/web-app.ts index 52e49ba..424260f 100644 --- a/gadget-code/src/web-app.ts +++ b/gadget-code/src/web-app.ts @@ -58,6 +58,11 @@ import { User } from "./models/user.js"; import { SocketSessionType } from "./lib/socket-session.js"; import { CodeSession } from "./lib/code-session.js"; import { DroneSession } from "./lib/drone-session.js"; +import { + ClientToServerEvents, + ServerToClientEvents, + SocketData, +} from "@gadget/api"; class DtpWebAppServer implements DtpComponent { private log: DtpLog; @@ -272,7 +277,12 @@ class DtpWebAppServer implements DtpComponent { /* * Create Socket.io server */ - this.io = new SocketIOServer(this.server, { + this.io = new SocketIOServer< + ClientToServerEvents, + ServerToClientEvents, + never, + SocketData + >(this.server, { cors: { origin: "*", methods: ["GET", "POST"], diff --git a/gadget-code/tests/e2e/drones.test.ts b/gadget-code/tests/e2e/drones.test.ts new file mode 100644 index 0000000..f084f2e --- /dev/null +++ b/gadget-code/tests/e2e/drones.test.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test'; + +test('Home page should display user drones from API', async ({ page }) => { + await page.goto('https://code-dev.g4dge7.com:5174/sign-in'); + await page.waitForLoadState('networkidle'); + await page.fill('#email', 'rob@digitaltelepresence.com'); + await page.fill('#password', 'ionfrali'); + await page.click('button[type="submit"]'); + await page.waitForTimeout(3000); + + await page.goto('https://code-dev.g4dge7.com:5174/'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(2000); + + const sidebar = page.locator('aside').first(); + const sidebarText = await sidebar.textContent(); + + expect(sidebarText).toContain('Drones'); + expect(sidebarText).toContain('mysterymachine'); +}); + +test('DroneInspector shows drone details', async ({ page }) => { + await page.goto('https://code-dev.g4dge7.com:5174/sign-in'); + await page.waitForLoadState('networkidle'); + await page.fill('#email', 'rob@digitaltelepresence.com'); + await page.fill('#password', 'ionfrali'); + await page.click('button[type="submit"]'); + await page.waitForTimeout(3000); + + await page.goto('https://code-dev.g4dge7.com:5174/'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(2000); + + const sidebar = page.locator('aside').first(); + + const droneButton = sidebar.locator('button').filter({ hasText: 'mysterymachine' }).first(); + await droneButton.click(); + await page.waitForTimeout(500); + + const inspector = page.locator('text=Drone Inspector'); + await expect(inspector).toBeVisible(); + + const backButton = page.locator('text=← Back to Dashboard'); + await expect(backButton).toBeVisible(); + + const hostname = page.locator('text=Hostname'); + await expect(hostname).toBeVisible(); + + const workspace = page.locator('text=Workspace'); + await expect(workspace).toBeVisible(); + + await backButton.click(); + await page.waitForTimeout(500); + + await expect(inspector).not.toBeVisible(); +}); + +test('Drones API returns available and busy drones', async ({ page }) => { + await page.goto('https://code-dev.g4dge7.com:5174/sign-in'); + await page.waitForLoadState('networkidle'); + await page.fill('#email', 'rob@digitaltelepresence.com'); + await page.fill('#password', 'ionfrali'); + await page.click('button[type="submit"]'); + await page.waitForTimeout(2000); + + const token = await page.evaluate(() => localStorage.getItem('dtp_auth_token')); + expect(token).toBeDefined(); + + const dronesResponse = await page.request.get('https://code-dev.g4dge7.com:5174/api/v1/drone/registration', { + headers: { 'Authorization': `Bearer ${token}` }, + }); + + expect(dronesResponse.status()).toBe(200); + const data = await dronesResponse.json() as { success: boolean; data: Array<{ status: string }> }; + expect(data.success).toBe(true); + expect(data.data).toBeDefined(); + + const availableOrBusy = data.data.filter(d => d.status === 'available' || d.status === 'busy'); + expect(availableOrBusy.length).toBeGreaterThan(0); +}); \ No newline at end of file diff --git a/gadget-drone/package.json b/gadget-drone/package.json index 5c31691..4ed90bc 100644 --- a/gadget-drone/package.json +++ b/gadget-drone/package.json @@ -22,6 +22,7 @@ "dependencies": { "@gadget/ai": "workspace:*", "@gadget/api": "workspace:*", + "@inquirer/prompts": "^8.4.2", "ansicolor": "^2.0.3", "dayjs": "^1.11.20", "dotenv": "^17.4.2", diff --git a/gadget-drone/src/gadget-drone.ts b/gadget-drone/src/gadget-drone.ts index d8317dc..699efe5 100644 --- a/gadget-drone/src/gadget-drone.ts +++ b/gadget-drone/src/gadget-drone.ts @@ -6,6 +6,7 @@ import env from "./config/env.ts"; import assert from "node:assert"; import { io, ManagerOptions, SocketOptions, Socket } from "socket.io-client"; +import { input as inqInput, password as inqPassword } from "@inquirer/prompts"; import AgentService, { IAgentWorkOrder } from "./services/agent.ts"; import AiService from "./services/ai.ts"; @@ -15,10 +16,18 @@ import PlatformService, { } from "./services/platform.ts"; import { GadgetProcess } from "./lib/process.ts"; +import { ClientToServerEvents, ServerToClientEvents } from "@gadget/api"; + +interface UserCredentials { + email: string; + password: string; +} + +type ClientSocket = Socket; class GadgetDrone extends GadgetProcess { private registration: PlatformRegistration | undefined; - private socket: Socket | undefined; + private socket: ClientSocket | undefined; private isShuttingDown: boolean = false; get name(): string { @@ -45,9 +54,13 @@ class GadgetDrone extends GadgetProcess { * Register this Drone with the Gadget Code web services platform. */ - const email = "rob@digitaltelepresence.com"; - const password = "ionfrali"; - this.registration = await PlatformService.register(email, password); + const credentials = await this.getUserCredentials(); + const workspaceDir = process.cwd(); + this.registration = await PlatformService.register( + credentials.email, + credentials.password, + workspaceDir, + ); this.log.info("registered with platform", { registration: this.registration, }); @@ -159,6 +172,13 @@ class GadgetDrone extends GadgetProcess { process.exit(exitCode); }); } + + async getUserCredentials(): Promise { + return { + email: await inqInput({ message: "📧 Enter Drone Email: " }), + password: await inqPassword({ message: "🔑 Enter Password: " }), + }; + } } (async () => { diff --git a/gadget-drone/src/services/platform.ts b/gadget-drone/src/services/platform.ts index acaadd7..8e5527c 100644 --- a/gadget-drone/src/services/platform.ts +++ b/gadget-drone/src/services/platform.ts @@ -55,12 +55,14 @@ class PlatformService extends GadgetService { async register( email: string, password: string, + workspaceDir: string, ): Promise { const url = this.getApiUrl("/drone/registration"); const body = JSON.stringify({ email, password, hostname: os.hostname(), + workspaceDir, }); const response = await fetch(url, { method: "POST", diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 03b4e4e..d39f088 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -2,13 +2,22 @@ // Copyright (C) 2026 Robert Colbert // All Rights Reserved -export * from "./interfaces/ai-provider.ts"; +/* + * Data Model Interfaces + */ +export * from "./interfaces/ai-provider.ts"; export * from "./interfaces/user.ts"; export * from "./interfaces/project.ts"; - export * from "./interfaces/drone-registration.ts"; export * from "./interfaces/drone-monitor.ts"; - export * from "./interfaces/chat-session.ts"; export * from "./interfaces/chat-turn.ts"; + +/* + * Socket.IO Interfaces + */ + +export * from "./messages/ide.ts"; +export * from "./messages/drone.ts"; +export * from "./messages/socket.ts"; diff --git a/packages/api/src/interfaces/drone-registration.ts b/packages/api/src/interfaces/drone-registration.ts index 41cf513..ab1a223 100644 --- a/packages/api/src/interfaces/drone-registration.ts +++ b/packages/api/src/interfaces/drone-registration.ts @@ -20,5 +20,6 @@ export interface IDroneRegistration extends Document { hostname: string; workspaceDir: string; status: DroneStatus; + chatSessionId?: string; currentJobId?: string; } diff --git a/packages/api/src/messages/drone.ts b/packages/api/src/messages/drone.ts new file mode 100644 index 0000000..5c0dd72 --- /dev/null +++ b/packages/api/src/messages/drone.ts @@ -0,0 +1,7 @@ +export type ThinkingMessage = (content: string) => void; +export type ResponseMessage = (content: string) => void; +export type ToolCallMessage = ( + name: string, + params: string, + response: string, +) => void; diff --git a/packages/api/src/messages/ide.ts b/packages/api/src/messages/ide.ts new file mode 100644 index 0000000..26b7381 --- /dev/null +++ b/packages/api/src/messages/ide.ts @@ -0,0 +1,9 @@ +import { IChatSession } from "../interfaces/chat-session.ts"; +import { IProject } from "../interfaces/project.ts"; + +export type RequestSessionLockMessage = ( + project: IProject, + chatSession: IChatSession, + cb: (success: boolean, chatSessionId: string) => void, +) => void; +export type SubmitPromptMessage = (prompt: string) => void; diff --git a/packages/api/src/messages/socket.ts b/packages/api/src/messages/socket.ts new file mode 100644 index 0000000..90a3018 --- /dev/null +++ b/packages/api/src/messages/socket.ts @@ -0,0 +1,42 @@ +// src/messages/gadget-code.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +import { ResponseMessage, ThinkingMessage, ToolCallMessage } from "./drone.ts"; +import { RequestSessionLockMessage, SubmitPromptMessage } from "./ide.ts"; + +export interface ServerToClientEvents { + /* + * GadgetCode => IDE + */ + + thinking: ThinkingMessage; + response: ResponseMessage; + toolCall: ToolCallMessage; + + /* + * Gadget Code => Drone + */ + + requestSessionLock: RequestSessionLockMessage; + submitPrompt: SubmitPromptMessage; +} + +export interface ClientToServerEvents { + /* + * IDE => Gadget Code + */ + + requestSessionLock: RequestSessionLockMessage; + submitPrompt: SubmitPromptMessage; + + /* + * Drone => Gadget Code + */ + + thinking: ThinkingMessage; + response: ResponseMessage; + toolCall: ToolCallMessage; +} + +export interface SocketData {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5cc45e6..b1e4da3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -251,6 +251,9 @@ importers: '@gadget/api': specifier: workspace:* version: link:../packages/api + '@inquirer/prompts': + specifier: ^8.4.2 + version: 8.4.2(@types/node@25.6.0) ansicolor: specifier: ^2.0.3 version: 2.0.3 @@ -750,6 +753,140 @@ packages: resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==} engines: {node: '>=6'} + '@inquirer/ansi@2.0.5': + resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/checkbox@5.1.4': + resolution: {integrity: sha512-w6KF8ZYRvqHhROkOTHXYC3qIV/KYEu5o12oLqQySvch61vrYtRxNSHTONSdJqWiFJPlCUQAHT5OgOIyuTr+MHQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@6.0.12': + resolution: {integrity: sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.1.9': + resolution: {integrity: sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@5.1.1': + resolution: {integrity: sha512-6y11LgmNpmn5D2aB5FgnCfBUBK8ZstwLCalyJmORcJZ/WrhOjm16mu6eSqIx8DnErxDqSLr+Jkp+GP8/Nwd5tA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@5.0.13': + resolution: {integrity: sha512-dF2zvrFo9LshkcB23/O1il13kBkBltWIXzut1evfbuBLXMiGIuC45c+ZQ0uukjCDsvI8OWqun4FRYMnzFCQa3g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@3.0.0': + resolution: {integrity: sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.5': + resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + + '@inquirer/input@5.0.12': + resolution: {integrity: sha512-uiMFBl4LqFzJClh80Q3f9hbOFJ6kgkDWI4LjAeBuyO6EanVVMF69AgOvpi1qdqjDSjDN6578B6nky9ceEpI+1Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@4.0.12': + resolution: {integrity: sha512-/vrwhEf7Xsuh+YlHF4IjSy3g1cyrQuPaSiHIxCEbLu8qnfvrcvJyCkoktOOF+xV9gSb77/G0n3h04RbMDW2sIg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@5.0.12': + resolution: {integrity: sha512-CBh7YHju623lxJRcAOo498ZUwIuMy63bqW/vVq0tQAZVv+lkWlHkP9ealYE1utWSisEShY5VMdzIXRmyEODzcQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@8.4.2': + resolution: {integrity: sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@5.2.8': + resolution: {integrity: sha512-Su7FQvp5buZmCymN3PPoYv31ZQQX4ve2j02k7piGgKAWgE+AQRB5YoYVveGXcl3TZ9ldgRMSxj56YfDFmmaqLg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@4.1.8': + resolution: {integrity: sha512-fGiHKGD6DyPIYUWxoXnQTeXeyYqSOUrasDMABBmMHUalH/LxkuzY0xVRtimXAt1sUeeyYkVuKQx1bebMuN11Kw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@5.1.4': + resolution: {integrity: sha512-2kWcGKPMLAXAWRp1AH1SLsQmX+j0QjeljyXMUji9WMZC8nRDO0b7qquIGr6143E7KMLt3VAIGNXzwa/6PXQs4Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@4.0.5': + resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@ioredis/commands@1.5.1': resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} @@ -1416,6 +1553,9 @@ packages: character-parser@2.2.0: resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chart.js@4.5.1: resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} engines: {pnpm: '>=8'} @@ -1424,6 +1564,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1758,6 +1902,15 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + fast-xml-builder@1.1.5: resolution: {integrity: sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==} @@ -2348,6 +2501,10 @@ packages: resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} engines: {node: '>= 10.16.0'} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + mylas@2.1.14: resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==} engines: {node: '>=16.0.0'} @@ -2797,6 +2954,10 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -3485,6 +3646,125 @@ snapshots: '@fortawesome/fontawesome-free@6.7.2': {} + '@inquirer/ansi@2.0.5': {} + + '@inquirer/checkbox@5.1.4(@types/node@25.6.0)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.9(@types/node@25.6.0) + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/confirm@6.0.12(@types/node@25.6.0)': + dependencies: + '@inquirer/core': 11.1.9(@types/node@25.6.0) + '@inquirer/type': 4.0.5(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/core@11.1.9(@types/node@25.6.0)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.6.0) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/editor@5.1.1(@types/node@25.6.0)': + dependencies: + '@inquirer/core': 11.1.9(@types/node@25.6.0) + '@inquirer/external-editor': 3.0.0(@types/node@25.6.0) + '@inquirer/type': 4.0.5(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/expand@5.0.13(@types/node@25.6.0)': + dependencies: + '@inquirer/core': 11.1.9(@types/node@25.6.0) + '@inquirer/type': 4.0.5(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/external-editor@3.0.0(@types/node@25.6.0)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/figures@2.0.5': {} + + '@inquirer/input@5.0.12(@types/node@25.6.0)': + dependencies: + '@inquirer/core': 11.1.9(@types/node@25.6.0) + '@inquirer/type': 4.0.5(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/number@4.0.12(@types/node@25.6.0)': + dependencies: + '@inquirer/core': 11.1.9(@types/node@25.6.0) + '@inquirer/type': 4.0.5(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/password@5.0.12(@types/node@25.6.0)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.9(@types/node@25.6.0) + '@inquirer/type': 4.0.5(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/prompts@8.4.2(@types/node@25.6.0)': + dependencies: + '@inquirer/checkbox': 5.1.4(@types/node@25.6.0) + '@inquirer/confirm': 6.0.12(@types/node@25.6.0) + '@inquirer/editor': 5.1.1(@types/node@25.6.0) + '@inquirer/expand': 5.0.13(@types/node@25.6.0) + '@inquirer/input': 5.0.12(@types/node@25.6.0) + '@inquirer/number': 4.0.12(@types/node@25.6.0) + '@inquirer/password': 5.0.12(@types/node@25.6.0) + '@inquirer/rawlist': 5.2.8(@types/node@25.6.0) + '@inquirer/search': 4.1.8(@types/node@25.6.0) + '@inquirer/select': 5.1.4(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/rawlist@5.2.8(@types/node@25.6.0)': + dependencies: + '@inquirer/core': 11.1.9(@types/node@25.6.0) + '@inquirer/type': 4.0.5(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/search@4.1.8(@types/node@25.6.0)': + dependencies: + '@inquirer/core': 11.1.9(@types/node@25.6.0) + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/select@5.1.4(@types/node@25.6.0)': + dependencies: + '@inquirer/ansi': 2.0.5 + '@inquirer/core': 11.1.9(@types/node@25.6.0) + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/type@4.0.5(@types/node@25.6.0)': + optionalDependencies: + '@types/node': 25.6.0 + '@ioredis/commands@1.5.1': {} '@jridgewell/gen-mapping@0.3.13': @@ -4132,6 +4412,8 @@ snapshots: dependencies: is-regex: 1.2.1 + chardet@2.1.1: {} + chart.js@4.5.1: dependencies: '@kurkle/color': 0.3.4 @@ -4148,6 +4430,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + cli-width@4.1.0: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -4536,6 +4820,16 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.0: + dependencies: + fast-string-width: 3.0.2 + fast-xml-builder@1.1.5: dependencies: path-expression-matcher: 1.5.0 @@ -5138,6 +5432,8 @@ snapshots: concat-stream: 2.0.0 type-is: 1.6.18 + mute-stream@3.0.0: {} + mylas@2.1.14: {} nanoid@3.3.11: {} @@ -5628,6 +5924,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@4.1.0: {} + slash@3.0.0: {} slug@11.0.1: {}