mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 19:04:40 +00:00
Add foundry terminal and process pane
This commit is contained in:
parent
0471214d65
commit
28c4ac22ff
16 changed files with 2412 additions and 36 deletions
|
|
@ -27,6 +27,14 @@ import type {
|
|||
RepoRecord,
|
||||
SwitchResult
|
||||
} from "@openhandoff/shared";
|
||||
import type {
|
||||
ProcessCreateRequest,
|
||||
ProcessInfo,
|
||||
ProcessLogFollowQuery,
|
||||
ProcessLogsResponse,
|
||||
ProcessSignalQuery,
|
||||
} from "sandbox-agent";
|
||||
import { createMockBackendClient } from "./mock/backend-client.js";
|
||||
import { sandboxInstanceKey, workspaceKey } from "./keys.js";
|
||||
|
||||
export type HandoffAction = "push" | "sync" | "merge" | "archive" | "kill";
|
||||
|
|
@ -59,6 +67,8 @@ export interface SandboxSessionEventRecord {
|
|||
payload: unknown;
|
||||
}
|
||||
|
||||
export type SandboxProcessRecord = ProcessInfo;
|
||||
|
||||
interface WorkspaceHandle {
|
||||
addRepo(input: AddRepoInput): Promise<RepoRecord>;
|
||||
listRepos(input: { workspaceId: string }): Promise<RepoRecord[]>;
|
||||
|
|
@ -97,8 +107,15 @@ interface SandboxInstanceHandle {
|
|||
createSession(input: { prompt: string; cwd?: string; agent?: AgentType | "opencode" }): Promise<{ id: string | null; status: "running" | "idle" | "error"; error?: string }>;
|
||||
listSessions(input?: { cursor?: string; limit?: number }): Promise<{ items: SandboxSessionRecord[]; nextCursor?: string }>;
|
||||
listSessionEvents(input: { sessionId: string; cursor?: string; limit?: number }): Promise<{ items: SandboxSessionEventRecord[]; nextCursor?: string }>;
|
||||
createProcess(input: ProcessCreateRequest): Promise<SandboxProcessRecord>;
|
||||
listProcesses(): Promise<{ processes: SandboxProcessRecord[] }>;
|
||||
getProcessLogs(input: { processId: string; query?: ProcessLogFollowQuery }): Promise<ProcessLogsResponse>;
|
||||
stopProcess(input: { processId: string; query?: ProcessSignalQuery }): Promise<SandboxProcessRecord>;
|
||||
killProcess(input: { processId: string; query?: ProcessSignalQuery }): Promise<SandboxProcessRecord>;
|
||||
deleteProcess(input: { processId: string }): Promise<void>;
|
||||
sendPrompt(input: { sessionId: string; prompt: string; notification?: boolean }): Promise<void>;
|
||||
sessionStatus(input: { sessionId: string }): Promise<{ id: string; status: "running" | "idle" | "error" }>;
|
||||
sandboxAgentConnection(): Promise<{ endpoint: string; token?: string }>;
|
||||
providerState(): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }>;
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +131,7 @@ interface RivetClient {
|
|||
export interface BackendClientOptions {
|
||||
endpoint: string;
|
||||
defaultWorkspaceId?: string;
|
||||
mode?: "remote" | "mock";
|
||||
}
|
||||
|
||||
export interface BackendMetadata {
|
||||
|
|
@ -156,6 +174,50 @@ export interface BackendClient {
|
|||
sandboxId: string,
|
||||
input: { sessionId: string; cursor?: string; limit?: number }
|
||||
): Promise<{ items: SandboxSessionEventRecord[]; nextCursor?: string }>;
|
||||
createSandboxProcess(input: {
|
||||
workspaceId: string;
|
||||
providerId: ProviderId;
|
||||
sandboxId: string;
|
||||
request: ProcessCreateRequest;
|
||||
}): Promise<SandboxProcessRecord>;
|
||||
listSandboxProcesses(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string
|
||||
): Promise<{ processes: SandboxProcessRecord[] }>;
|
||||
getSandboxProcessLogs(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
processId: string,
|
||||
query?: ProcessLogFollowQuery
|
||||
): Promise<ProcessLogsResponse>;
|
||||
stopSandboxProcess(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
processId: string,
|
||||
query?: ProcessSignalQuery
|
||||
): Promise<SandboxProcessRecord>;
|
||||
killSandboxProcess(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
processId: string,
|
||||
query?: ProcessSignalQuery
|
||||
): Promise<SandboxProcessRecord>;
|
||||
deleteSandboxProcess(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
processId: string
|
||||
): Promise<void>;
|
||||
subscribeSandboxProcesses(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
listener: () => void
|
||||
): () => void;
|
||||
sendSandboxPrompt(input: {
|
||||
workspaceId: string;
|
||||
providerId: ProviderId;
|
||||
|
|
@ -175,6 +237,11 @@ export interface BackendClient {
|
|||
providerId: ProviderId,
|
||||
sandboxId: string
|
||||
): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }>;
|
||||
getSandboxAgentConnection(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string
|
||||
): Promise<{ endpoint: string; token?: string }>;
|
||||
getWorkbench(workspaceId: string): Promise<HandoffWorkbenchSnapshot>;
|
||||
subscribeWorkbench(workspaceId: string, listener: () => void): () => void;
|
||||
createWorkbenchHandoff(
|
||||
|
|
@ -346,6 +413,10 @@ async function probeMetadataEndpoint(
|
|||
}
|
||||
|
||||
export function createBackendClient(options: BackendClientOptions): BackendClient {
|
||||
if (options.mode === "mock") {
|
||||
return createMockBackendClient(options.defaultWorkspaceId);
|
||||
}
|
||||
|
||||
let clientPromise: Promise<RivetClient> | null = null;
|
||||
const workbenchSubscriptions = new Map<
|
||||
string,
|
||||
|
|
@ -354,6 +425,13 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
disposeConnPromise: Promise<(() => Promise<void>) | null> | null;
|
||||
}
|
||||
>();
|
||||
const sandboxProcessSubscriptions = new Map<
|
||||
string,
|
||||
{
|
||||
listeners: Set<() => void>;
|
||||
disposeConnPromise: Promise<(() => Promise<void>) | null> | null;
|
||||
}
|
||||
>();
|
||||
|
||||
const getClient = async (): Promise<RivetClient> => {
|
||||
if (clientPromise) {
|
||||
|
|
@ -525,6 +603,69 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
};
|
||||
};
|
||||
|
||||
const sandboxProcessSubscriptionKey = (
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
): string => `${workspaceId}:${providerId}:${sandboxId}`;
|
||||
|
||||
const subscribeSandboxProcesses = (
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
listener: () => void,
|
||||
): (() => void) => {
|
||||
const key = sandboxProcessSubscriptionKey(workspaceId, providerId, sandboxId);
|
||||
let entry = sandboxProcessSubscriptions.get(key);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
listeners: new Set(),
|
||||
disposeConnPromise: null,
|
||||
};
|
||||
sandboxProcessSubscriptions.set(key, entry);
|
||||
}
|
||||
|
||||
entry.listeners.add(listener);
|
||||
|
||||
if (!entry.disposeConnPromise) {
|
||||
entry.disposeConnPromise = (async () => {
|
||||
const handle = await sandboxByKey(workspaceId, providerId, sandboxId);
|
||||
const conn = (handle as any).connect();
|
||||
const unsubscribeEvent = conn.on("processesUpdated", () => {
|
||||
const current = sandboxProcessSubscriptions.get(key);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
for (const currentListener of [...current.listeners]) {
|
||||
currentListener();
|
||||
}
|
||||
});
|
||||
const unsubscribeError = conn.onError(() => {});
|
||||
return async () => {
|
||||
unsubscribeEvent();
|
||||
unsubscribeError();
|
||||
await conn.dispose();
|
||||
};
|
||||
})().catch(() => null);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const current = sandboxProcessSubscriptions.get(key);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
current.listeners.delete(listener);
|
||||
if (current.listeners.size > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
sandboxProcessSubscriptions.delete(key);
|
||||
void current.disposeConnPromise?.then(async (disposeConn) => {
|
||||
await disposeConn?.();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
async addRepo(workspaceId: string, remoteUrl: string): Promise<RepoRecord> {
|
||||
return (await workspace(workspaceId)).addRepo({ workspaceId, remoteUrl });
|
||||
|
|
@ -669,6 +810,101 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
);
|
||||
},
|
||||
|
||||
async createSandboxProcess(input: {
|
||||
workspaceId: string;
|
||||
providerId: ProviderId;
|
||||
sandboxId: string;
|
||||
request: ProcessCreateRequest;
|
||||
}): Promise<SandboxProcessRecord> {
|
||||
return await withSandboxHandle(
|
||||
input.workspaceId,
|
||||
input.providerId,
|
||||
input.sandboxId,
|
||||
async (handle) => handle.createProcess(input.request)
|
||||
);
|
||||
},
|
||||
|
||||
async listSandboxProcesses(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string
|
||||
): Promise<{ processes: SandboxProcessRecord[] }> {
|
||||
return await withSandboxHandle(
|
||||
workspaceId,
|
||||
providerId,
|
||||
sandboxId,
|
||||
async (handle) => handle.listProcesses()
|
||||
);
|
||||
},
|
||||
|
||||
async getSandboxProcessLogs(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
processId: string,
|
||||
query?: ProcessLogFollowQuery
|
||||
): Promise<ProcessLogsResponse> {
|
||||
return await withSandboxHandle(
|
||||
workspaceId,
|
||||
providerId,
|
||||
sandboxId,
|
||||
async (handle) => handle.getProcessLogs({ processId, query })
|
||||
);
|
||||
},
|
||||
|
||||
async stopSandboxProcess(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
processId: string,
|
||||
query?: ProcessSignalQuery
|
||||
): Promise<SandboxProcessRecord> {
|
||||
return await withSandboxHandle(
|
||||
workspaceId,
|
||||
providerId,
|
||||
sandboxId,
|
||||
async (handle) => handle.stopProcess({ processId, query })
|
||||
);
|
||||
},
|
||||
|
||||
async killSandboxProcess(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
processId: string,
|
||||
query?: ProcessSignalQuery
|
||||
): Promise<SandboxProcessRecord> {
|
||||
return await withSandboxHandle(
|
||||
workspaceId,
|
||||
providerId,
|
||||
sandboxId,
|
||||
async (handle) => handle.killProcess({ processId, query })
|
||||
);
|
||||
},
|
||||
|
||||
async deleteSandboxProcess(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
processId: string
|
||||
): Promise<void> {
|
||||
await withSandboxHandle(
|
||||
workspaceId,
|
||||
providerId,
|
||||
sandboxId,
|
||||
async (handle) => handle.deleteProcess({ processId })
|
||||
);
|
||||
},
|
||||
|
||||
subscribeSandboxProcesses(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string,
|
||||
listener: () => void
|
||||
): () => void {
|
||||
return subscribeSandboxProcesses(workspaceId, providerId, sandboxId, listener);
|
||||
},
|
||||
|
||||
async sendSandboxPrompt(input: {
|
||||
workspaceId: string;
|
||||
providerId: ProviderId;
|
||||
|
|
@ -717,6 +953,19 @@ export function createBackendClient(options: BackendClientOptions): BackendClien
|
|||
);
|
||||
},
|
||||
|
||||
async getSandboxAgentConnection(
|
||||
workspaceId: string,
|
||||
providerId: ProviderId,
|
||||
sandboxId: string
|
||||
): Promise<{ endpoint: string; token?: string }> {
|
||||
return await withSandboxHandle(
|
||||
workspaceId,
|
||||
providerId,
|
||||
sandboxId,
|
||||
async (handle) => handle.sandboxAgentConnection()
|
||||
);
|
||||
},
|
||||
|
||||
async getWorkbench(workspaceId: string): Promise<HandoffWorkbenchSnapshot> {
|
||||
return (await workspace(workspaceId)).getWorkbench({ workspaceId });
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue