workspace mode management; drone status message socket events added
This commit is contained in:
parent
d92d61024a
commit
c5e5d16a51
@ -8,5 +8,8 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"author": "Robert Colbert <rob.colbert@openplatform.us>",
|
||||
"license": "Apache-2.0"
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"slug": "^11.0.1"
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import slug from "slug";
|
||||
import type { User, Project } from "../lib/api";
|
||||
import {
|
||||
projectApi,
|
||||
@ -114,13 +115,125 @@ function NewProjectForm({
|
||||
);
|
||||
}
|
||||
|
||||
interface EditProjectFormProps {
|
||||
project: Project;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
function EditProjectForm({
|
||||
project,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}: EditProjectFormProps) {
|
||||
const [name, setName] = useState(project.name);
|
||||
const [slugValue, setSlugValue] = useState(project.slug);
|
||||
const [gitUrl, setGitUrl] = useState(project.gitUrl || "");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name || !slugValue) {
|
||||
setError("Name and slug are required");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
await projectApi.update(project._id, {
|
||||
name,
|
||||
slug: slug(slugValue),
|
||||
gitUrl: gitUrl || undefined,
|
||||
});
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to update project");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-lg">
|
||||
<h2 className="text-xl font-semibold mb-6">Edit Project</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
Project Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Used for display purposes only
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
Project Slug *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={slugValue}
|
||||
onChange={(e) => setSlugValue(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Unique identifier for the project directory. Changing this will
|
||||
affect the workspace directory name on drones.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-text-secondary mb-1">
|
||||
Git Repository URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={gitUrl}
|
||||
onChange={(e) => setGitUrl(e.target.value)}
|
||||
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="https://github.com/user/repo.git"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="px-4 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{submitting ? "Updating..." : "Save Changes"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 border border-border-default text-text-secondary rounded hover:bg-bg-tertiary transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProjectInspectorProps {
|
||||
project: Project;
|
||||
onDelete: () => void;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
function ProjectInspector({ project, onDelete }: ProjectInspectorProps) {
|
||||
function ProjectInspector({ project, onDelete, onUpdate }: ProjectInspectorProps) {
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (
|
||||
@ -145,6 +258,17 @@ function ProjectInspector({ project, onDelete }: ProjectInspectorProps) {
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="max-w-3xl">
|
||||
<h2 className="text-xl font-semibold mb-6">Project Inspector</h2>
|
||||
{editing ? (
|
||||
<EditProjectForm
|
||||
project={project}
|
||||
onCancel={() => setEditing(false)}
|
||||
onSuccess={() => {
|
||||
setEditing(false);
|
||||
onUpdate();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-bg-secondary border border-border-default rounded">
|
||||
@ -192,7 +316,13 @@ function ProjectInspector({ project, onDelete }: ProjectInspectorProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-border-subtle">
|
||||
<div className="pt-6 border-t border-border-subtle flex gap-3">
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="px-4 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Edit Project
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
@ -202,6 +332,8 @@ function ProjectInspector({ project, onDelete }: ProjectInspectorProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -685,6 +817,10 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
|
||||
navigate("/projects");
|
||||
};
|
||||
|
||||
const handleProjectUpdated = () => {
|
||||
loadProjects();
|
||||
};
|
||||
|
||||
const handleSelectDrone = (drone: DroneRegistration) => {
|
||||
setSelectedDrone(drone);
|
||||
};
|
||||
@ -795,6 +931,7 @@ export default function ProjectManager({ user }: ProjectManagerProps) {
|
||||
<ProjectInspector
|
||||
project={selectedProject}
|
||||
onDelete={handleProjectDeleted}
|
||||
onUpdate={handleProjectUpdated}
|
||||
/>
|
||||
|
||||
{/* Right Sidebar - Drones & Chat Sessions */}
|
||||
|
||||
@ -229,6 +229,12 @@ importers:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5(@types/node@24.0.4)(jsdom@29.0.2)(vite@8.0.10(@types/node@24.0.4)(esbuild@0.25.5)(jiti@2.6.1)(less@4.3.0)(tsx@4.21.0))
|
||||
|
||||
frontend:
|
||||
dependencies:
|
||||
slug:
|
||||
specifier: ^11.0.1
|
||||
version: 11.0.1
|
||||
|
||||
packages:
|
||||
|
||||
'@adobe/css-tools@4.4.4':
|
||||
@ -2714,6 +2720,10 @@ packages:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
slug@11.0.1:
|
||||
resolution: {integrity: sha512-VrM060OM/E7rdLQSnp6JHrzFfJFmqQBp0+TMhZStnEB8PfNliaZ9UWYjTHGHLUFVJorZ8TjVd/aKvIxHWU2O7g==}
|
||||
hasBin: true
|
||||
|
||||
socket.io-adapter@2.5.5:
|
||||
resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==}
|
||||
|
||||
@ -5604,6 +5614,8 @@ snapshots:
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
slug@11.0.1: {}
|
||||
|
||||
socket.io-adapter@2.5.5:
|
||||
dependencies:
|
||||
debug: 4.3.7
|
||||
|
||||
@ -37,7 +37,10 @@ export class CodeSession extends SocketSession {
|
||||
super.register();
|
||||
|
||||
this.socket.on("requestSessionLock", this.onRequestSessionLock.bind(this));
|
||||
this.socket.on("requestWorkspaceMode", this.onRequestWorkspaceMode.bind(this));
|
||||
this.socket.on(
|
||||
"requestWorkspaceMode",
|
||||
this.onRequestWorkspaceMode.bind(this),
|
||||
);
|
||||
this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this));
|
||||
}
|
||||
|
||||
@ -84,7 +87,6 @@ export class CodeSession extends SocketSession {
|
||||
this.project = project;
|
||||
SocketService.registerChatSession(chatSession._id, this);
|
||||
droneSession.setChatSessionId(chatSession._id);
|
||||
droneSession.setCodeSession(this);
|
||||
}
|
||||
cb(success, chatSessionId);
|
||||
},
|
||||
@ -192,4 +194,8 @@ export class CodeSession extends SocketSession {
|
||||
this.workspaceMode = mode;
|
||||
this.socket.emit("workspaceModeChanged", mode);
|
||||
}
|
||||
|
||||
onStatus(content: string): void {
|
||||
this.socket.emit("status", content);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@ export class DroneSession extends SocketSession {
|
||||
chatSessionId: GadgetId | undefined;
|
||||
currentTurnId: GadgetId | undefined;
|
||||
workspaceMode: WorkspaceMode = WorkspaceMode.Idle;
|
||||
codeSession: import("./code-session.js").CodeSession | undefined;
|
||||
|
||||
constructor(socket: GadgetSocket, registration: IDroneRegistration) {
|
||||
super(socket, registration.user as IUser);
|
||||
@ -33,16 +32,41 @@ export class DroneSession extends SocketSession {
|
||||
register() {
|
||||
super.register();
|
||||
|
||||
this.socket.on("status", this.onStatus.bind(this));
|
||||
this.socket.on(
|
||||
"workspaceModeChanged",
|
||||
this.onWorkspaceModeChanged.bind(this),
|
||||
);
|
||||
|
||||
this.socket.on("thinking", this.onThinking.bind(this));
|
||||
this.socket.on("response", this.onResponse.bind(this));
|
||||
this.socket.on("toolCall", this.onToolCall.bind(this));
|
||||
|
||||
this.socket.on("workOrderComplete", this.onWorkOrderComplete.bind(this));
|
||||
|
||||
this.socket.on(
|
||||
"requestCrashRecovery",
|
||||
this.onRequestCrashRecovery.bind(this),
|
||||
);
|
||||
|
||||
this.socket.on("requestTermination", this.onRequestTermination.bind(this));
|
||||
this.socket.on("workspaceModeChanged", this.onWorkspaceModeChanged.bind(this));
|
||||
}
|
||||
|
||||
async onStatus(message: string): Promise<void> {
|
||||
if (!this.chatSessionId) {
|
||||
this.log.warn(
|
||||
"drone status event received but no chat session is active",
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const codeSession = SocketService.getCodeSessionByChatSessionId(
|
||||
this.chatSessionId,
|
||||
);
|
||||
codeSession.socket.emit("status", message);
|
||||
} catch (error) {
|
||||
this.log.error("failed to route status message", { error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,23 +207,22 @@ export class DroneSession extends SocketSession {
|
||||
this.currentTurnId = turnId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the associated code session for routing events back to the IDE.
|
||||
*/
|
||||
setCodeSession(codeSession: import("./code-session.js").CodeSession): void {
|
||||
this.codeSession = codeSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the drone emits a workspace mode change.
|
||||
*/
|
||||
async onWorkspaceModeChanged(mode: WorkspaceMode): Promise<void> {
|
||||
if (!this.chatSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.workspaceMode = mode;
|
||||
this.log.info("workspace mode changed", { mode });
|
||||
|
||||
if (this.codeSession) {
|
||||
this.codeSession.onWorkspaceModeChanged(mode);
|
||||
}
|
||||
const codeSession = SocketService.getCodeSessionByChatSessionId(
|
||||
this.chatSessionId,
|
||||
);
|
||||
|
||||
codeSession.onWorkspaceModeChanged(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
"numeral": "^2.0.6",
|
||||
"ollama": "^0.6.3",
|
||||
"openai": "^6.34.0",
|
||||
"simple-git": "^3.36.0",
|
||||
"socket.io-client": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -226,6 +226,8 @@ class GadgetDrone extends GadgetProcess {
|
||||
chatSession: IChatSession,
|
||||
cb: RequestSessionLockCallback,
|
||||
) {
|
||||
assert(this.socket, "invalid application state");
|
||||
|
||||
/*
|
||||
* Validate gadget-drone registration to ensure correct sync with IDE
|
||||
*/
|
||||
@ -286,8 +288,45 @@ class GadgetDrone extends GadgetProcess {
|
||||
session: chatSession,
|
||||
};
|
||||
this.workspaceMode = WorkspaceMode.Idle;
|
||||
this.socket.emit("status", "session lock granted");
|
||||
|
||||
/*
|
||||
* Add the project to the workspace, lock to it, and deploy it.
|
||||
*/
|
||||
WorkspaceService.addProject({
|
||||
_id: project._id,
|
||||
slug: project.slug,
|
||||
gitUrl: project.gitUrl,
|
||||
});
|
||||
WorkspaceService.updateLockedProject({
|
||||
_id: project._id,
|
||||
slug: project.slug,
|
||||
gitUrl: project.gitUrl,
|
||||
});
|
||||
|
||||
const projectDir = WorkspaceService.getProjectDirectory(project.slug);
|
||||
|
||||
let haveProjectDir = await WorkspaceService.exists(projectDir);
|
||||
if (!haveProjectDir) {
|
||||
this.socket.emit("status", `deploying project [slug=${project.slug}]`);
|
||||
await WorkspaceService.deployProject(project);
|
||||
|
||||
/*
|
||||
* Make sure a project directory got deployed
|
||||
*/
|
||||
haveProjectDir = await WorkspaceService.exists(projectDir);
|
||||
if (!haveProjectDir) {
|
||||
return cb(false, "failed to deploy project directory");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Commit the changes to the workspace.
|
||||
*/
|
||||
await WorkspaceService.writeWorkspaceData();
|
||||
cb(true, chatSession._id);
|
||||
|
||||
this.socket.emit("status", "session lock granted");
|
||||
}
|
||||
|
||||
async onRequestWorkspaceMode(
|
||||
@ -434,17 +473,29 @@ class GadgetDrone extends GadgetProcess {
|
||||
|
||||
cb(true, "work order accepted"); // confirm that drone has the work order
|
||||
|
||||
const workspaceDir =
|
||||
WorkspaceService.workspaceData?.workspaceDir || process.cwd();
|
||||
const projectDir = WorkspaceService.getProjectDirectory(project.slug);
|
||||
process.chdir(projectDir);
|
||||
|
||||
this.isProcessingWorkOrder = true;
|
||||
try {
|
||||
this.socket.emit("status", "processing work order");
|
||||
await AgentService.process(order, this.socket);
|
||||
this.socket.emit("status", "work order processing finished");
|
||||
await WorkspaceService.removeWorkOrderCache();
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.log.error("work order processing failed", {
|
||||
error: err.message,
|
||||
});
|
||||
this.socket.emit(
|
||||
"status",
|
||||
`failed to process work order: ${(error as Error).message}`,
|
||||
);
|
||||
// Leave cache in place for recovery
|
||||
} finally {
|
||||
process.chdir(workspaceDir);
|
||||
this.isProcessingWorkOrder = false;
|
||||
this.log.info("work order processing complete", {
|
||||
isProcessingWorkOrder: this.isProcessingWorkOrder,
|
||||
|
||||
@ -2,11 +2,43 @@
|
||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
import assert from "node:assert";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
|
||||
import { simpleGit, SimpleGit, SimpleGitOptions } from "simple-git";
|
||||
|
||||
import { IChatTurn, IProject } from "@gadget/api";
|
||||
|
||||
import { GadgetService } from "../lib/service.ts";
|
||||
import { IChatTurn } from "@gadget/api";
|
||||
|
||||
export interface WorkspaceChatSession {
|
||||
_id: string; // MongoDB ChatSession._id
|
||||
name: string; // Session name for display
|
||||
lockedAt: string; // ISO 8601 timestamp
|
||||
}
|
||||
|
||||
export interface WorkspaceLockedProject {
|
||||
_id: string; // MongoDB Project._id
|
||||
slug: string; // Project slug (directory name)
|
||||
gitUrl?: string; // Remote git URL
|
||||
lockedAt: string; // ISO 8601 timestamp
|
||||
}
|
||||
|
||||
export interface WorkspaceProject {
|
||||
_id: string;
|
||||
slug: string;
|
||||
gitUrl?: string;
|
||||
clonedAt: string | null;
|
||||
lastSyncAt: string | null;
|
||||
}
|
||||
|
||||
export interface WorkspaceRegistration {
|
||||
_id: string; // MongoDB DroneRegistration._id
|
||||
status: string; // Current drone status
|
||||
registeredAt: string; // ISO 8601 timestamp
|
||||
}
|
||||
|
||||
export interface WorkspaceData {
|
||||
workspaceId: string; // UUID v4, immutable once created
|
||||
@ -15,35 +47,16 @@ export interface WorkspaceData {
|
||||
workspaceDir: string; // Absolute path to workspace directory
|
||||
|
||||
// Active session state (null when idle)
|
||||
chatSession: {
|
||||
_id: string; // MongoDB ChatSession._id
|
||||
name: string; // Session name for display
|
||||
lockedAt: string; // ISO 8601 timestamp
|
||||
} | null;
|
||||
chatSession: WorkspaceChatSession | null;
|
||||
|
||||
// Project currently being worked on (null when idle)
|
||||
lockedProject: {
|
||||
_id: string; // MongoDB Project._id
|
||||
slug: string; // Project slug (directory name)
|
||||
gitUrl: string; // Remote git URL
|
||||
lockedAt: string; // ISO 8601 timestamp
|
||||
} | null;
|
||||
lockedProject: WorkspaceLockedProject | null;
|
||||
|
||||
// All projects cloned into this workspace
|
||||
projects: Array<{
|
||||
_id: string;
|
||||
slug: string;
|
||||
gitUrl: string;
|
||||
clonedAt: string;
|
||||
lastSyncAt: string;
|
||||
}>;
|
||||
projects: Array<WorkspaceProject>;
|
||||
|
||||
// Drone registration (updated each startup)
|
||||
registration: {
|
||||
_id: string; // MongoDB DroneRegistration._id
|
||||
status: string; // Current drone status
|
||||
registeredAt: string; // ISO 8601 timestamp
|
||||
} | null;
|
||||
registration: WorkspaceRegistration | null;
|
||||
}
|
||||
|
||||
export interface WorkOrderCache {
|
||||
@ -57,9 +70,12 @@ export interface WorkOrderCache {
|
||||
class WorkspaceService extends GadgetService {
|
||||
private gadgetDir: string = "";
|
||||
private cacheDir: string = "";
|
||||
|
||||
private workspaceFile: string = "";
|
||||
private _workspaceData: WorkspaceData | null = null;
|
||||
|
||||
private repos = new Map<string, SimpleGit>();
|
||||
|
||||
get name(): string {
|
||||
return "WorkspaceService";
|
||||
}
|
||||
@ -111,7 +127,7 @@ class WorkspaceService extends GadgetService {
|
||||
*/
|
||||
private async loadOrCreateWorkspaceData(workspaceDir: string): Promise<void> {
|
||||
try {
|
||||
if (await this.fileExists(this.workspaceFile)) {
|
||||
if (await this.exists(this.workspaceFile)) {
|
||||
// Load existing workspace
|
||||
const content = await fs.promises.readFile(this.workspaceFile, "utf-8");
|
||||
this._workspaceData = JSON.parse(content) as WorkspaceData;
|
||||
@ -175,34 +191,59 @@ class WorkspaceService extends GadgetService {
|
||||
* Updates the locked project in workspace data.
|
||||
*/
|
||||
updateLockedProject(
|
||||
project: { _id: string; slug: string; gitUrl: string } | null,
|
||||
project: {
|
||||
_id: string; // MongoDB Project._id
|
||||
slug: string; // Project slug (directory name)
|
||||
gitUrl?: string; // Remote git URL
|
||||
} | null,
|
||||
): void {
|
||||
if (!this._workspaceData) return;
|
||||
|
||||
this._workspaceData.lockedProject = project
|
||||
? {
|
||||
...project,
|
||||
lockedAt: new Date().toISOString(),
|
||||
if (!project) {
|
||||
this._workspaceData.lockedProject = null;
|
||||
return;
|
||||
}
|
||||
: null;
|
||||
this._workspaceData.lockedProject = Object.assign(
|
||||
{
|
||||
lockedAt: new Date().toISOString(),
|
||||
},
|
||||
project,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a project to the workspace projects list.
|
||||
*/
|
||||
addProject(project: { _id: string; slug: string; gitUrl: string }): void {
|
||||
addProject(project: { _id: string; slug: string; gitUrl?: string }): void {
|
||||
if (!this._workspaceData) return;
|
||||
|
||||
const existing = this._workspaceData.projects.find(
|
||||
(p) => p.slug === project.slug,
|
||||
);
|
||||
if (!existing) {
|
||||
if (existing) {
|
||||
existing._id = project._id;
|
||||
if (project.gitUrl) {
|
||||
existing.gitUrl = project.gitUrl;
|
||||
} else {
|
||||
delete existing.gitUrl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._workspaceData.projects.push({
|
||||
...project,
|
||||
clonedAt: new Date().toISOString(),
|
||||
lastSyncAt: new Date().toISOString(),
|
||||
clonedAt: null,
|
||||
lastSyncAt: null,
|
||||
});
|
||||
}
|
||||
|
||||
async deployProject(project: IProject): Promise<void> {
|
||||
assert(this.workspaceData, "workspace uninitialized");
|
||||
|
||||
if (project.gitUrl) {
|
||||
const git: SimpleGit = this.getGitRepo(project.slug);
|
||||
git.clone(project.gitUrl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -258,7 +299,7 @@ class WorkspaceService extends GadgetService {
|
||||
async removeWorkOrderCache(): Promise<void> {
|
||||
const cacheFile = path.join(this.cacheDir, "work-order.json");
|
||||
try {
|
||||
if (await this.fileExists(cacheFile)) {
|
||||
if (await this.exists(cacheFile)) {
|
||||
await fs.promises.unlink(cacheFile);
|
||||
this.log.info("work order cache removed");
|
||||
}
|
||||
@ -276,7 +317,7 @@ class WorkspaceService extends GadgetService {
|
||||
async readWorkOrderCache(): Promise<WorkOrderCache | null> {
|
||||
const cacheFile = path.join(this.cacheDir, "work-order.json");
|
||||
try {
|
||||
if (await this.fileExists(cacheFile)) {
|
||||
if (await this.exists(cacheFile)) {
|
||||
const content = await fs.promises.readFile(cacheFile, "utf-8");
|
||||
return JSON.parse(content) as WorkOrderCache;
|
||||
}
|
||||
@ -290,9 +331,9 @@ class WorkspaceService extends GadgetService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file exists.
|
||||
* Checks if a file or directory exists.
|
||||
*/
|
||||
private async fileExists(filePath: string): Promise<boolean> {
|
||||
async exists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.access(filePath);
|
||||
return true;
|
||||
@ -300,6 +341,35 @@ class WorkspaceService extends GadgetService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getProjectDirectory(slug: string): string {
|
||||
assert(this.workspaceData, "workspace uninitialized");
|
||||
return path.join(this.workspaceData?.workspaceDir, slug);
|
||||
}
|
||||
|
||||
getGitRepo(slug: string): SimpleGit {
|
||||
assert(this.workspaceData, "workspace uninitialized");
|
||||
|
||||
/*
|
||||
* If we have a SimpleGit instance for the project slug, return it.
|
||||
*/
|
||||
let git: SimpleGit | undefined = this.repos.get(slug);
|
||||
if (git) {
|
||||
return git;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a new SimpleGit instance rooted in the project's workspace
|
||||
* directory, add it to the repo cache, and return it.
|
||||
*/
|
||||
const options: Partial<SimpleGitOptions> = {
|
||||
baseDir: this.getProjectDirectory(slug),
|
||||
};
|
||||
git = simpleGit(options);
|
||||
this.repos.set(slug, git);
|
||||
|
||||
return git;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkspaceService();
|
||||
|
||||
@ -18,6 +18,8 @@ export type ProcessWorkOrderMessage = (
|
||||
cb: ProcessWorkOrderCallback,
|
||||
) => void;
|
||||
|
||||
export type StatusMessage = (content: string) => void;
|
||||
|
||||
export type ThinkingMessage = (content: string) => void;
|
||||
|
||||
export type ResponseMessage = (content: string) => void;
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
import {
|
||||
StatusMessage,
|
||||
ProcessWorkOrderMessage,
|
||||
ResponseMessage,
|
||||
ThinkingMessage,
|
||||
@ -50,6 +51,7 @@ export interface ClientToServerEvents {
|
||||
* gadget-drone => gadget-code:web
|
||||
*/
|
||||
|
||||
status: StatusMessage;
|
||||
thinking: ThinkingMessage;
|
||||
response: ResponseMessage;
|
||||
toolCall: ToolCallMessage;
|
||||
@ -74,6 +76,7 @@ export interface ServerToClientEvents {
|
||||
* gadget-code:web => gadget-code:ide
|
||||
*/
|
||||
|
||||
status: StatusMessage;
|
||||
thinking: ThinkingMessage;
|
||||
response: ResponseMessage;
|
||||
toolCall: ToolCallMessage;
|
||||
|
||||
@ -287,6 +287,9 @@ importers:
|
||||
openai:
|
||||
specifier: ^6.34.0
|
||||
version: 6.34.0(ws@8.18.3)
|
||||
simple-git:
|
||||
specifier: ^3.36.0
|
||||
version: 3.36.0
|
||||
socket.io-client:
|
||||
specifier: ^4.8.3
|
||||
version: 4.8.3
|
||||
@ -943,6 +946,12 @@ packages:
|
||||
'@kurkle/color@0.3.4':
|
||||
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
|
||||
|
||||
'@kwsites/file-exists@1.1.1':
|
||||
resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
|
||||
|
||||
'@kwsites/promise-deferred@1.1.1':
|
||||
resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==}
|
||||
|
||||
'@mediapipe/tasks-vision@0.10.17':
|
||||
resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==}
|
||||
|
||||
@ -1150,6 +1159,12 @@ packages:
|
||||
'@rolldown/pluginutils@1.0.0-rc.7':
|
||||
resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==}
|
||||
|
||||
'@simple-git/args-pathspec@1.0.3':
|
||||
resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==}
|
||||
|
||||
'@simple-git/argv-parser@1.1.1':
|
||||
resolution: {integrity: sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==}
|
||||
|
||||
'@socket.io/component-emitter@3.1.2':
|
||||
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
||||
|
||||
@ -3171,6 +3186,9 @@ packages:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
simple-git@3.36.0:
|
||||
resolution: {integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==}
|
||||
|
||||
slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
@ -4103,6 +4121,14 @@ snapshots:
|
||||
|
||||
'@kurkle/color@0.3.4': {}
|
||||
|
||||
'@kwsites/file-exists@1.1.1':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@kwsites/promise-deferred@1.1.1': {}
|
||||
|
||||
'@mediapipe/tasks-vision@0.10.17': {}
|
||||
|
||||
'@mongodb-js/saslprep@1.4.9':
|
||||
@ -4265,6 +4291,12 @@ snapshots:
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.7': {}
|
||||
|
||||
'@simple-git/args-pathspec@1.0.3': {}
|
||||
|
||||
'@simple-git/argv-parser@1.1.1':
|
||||
dependencies:
|
||||
'@simple-git/args-pathspec': 1.0.3
|
||||
|
||||
'@socket.io/component-emitter@3.1.2': {}
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
@ -6421,6 +6453,16 @@ snapshots:
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-git@3.36.0:
|
||||
dependencies:
|
||||
'@kwsites/file-exists': 1.1.1
|
||||
'@kwsites/promise-deferred': 1.1.1
|
||||
'@simple-git/args-pathspec': 1.0.3
|
||||
'@simple-git/argv-parser': 1.1.1
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
slug@11.0.1: {}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user