Add foundry terminal and process pane

This commit is contained in:
Nathan Flurry 2026-03-10 23:55:43 -07:00
parent 0471214d65
commit 28c4ac22ff
16 changed files with 2412 additions and 36 deletions

View file

@ -15,7 +15,8 @@
},
"dependencies": {
"@openhandoff/shared": "workspace:*",
"rivetkit": "2.1.6"
"rivetkit": "2.1.6",
"sandbox-agent": "workspace:*"
},
"devDependencies": {
"tsup": "^8.5.0"

View file

@ -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 });
},

View file

@ -0,0 +1,497 @@
import type {
AddRepoInput,
CreateHandoffInput,
HandoffRecord,
HandoffSummary,
HandoffWorkbenchChangeModelInput,
HandoffWorkbenchCreateHandoffInput,
HandoffWorkbenchCreateHandoffResponse,
HandoffWorkbenchDiffInput,
HandoffWorkbenchRenameInput,
HandoffWorkbenchRenameSessionInput,
HandoffWorkbenchSelectInput,
HandoffWorkbenchSetSessionUnreadInput,
HandoffWorkbenchSendMessageInput,
HandoffWorkbenchSnapshot,
HandoffWorkbenchTabInput,
HandoffWorkbenchUpdateDraftInput,
HistoryEvent,
HistoryQueryInput,
ProviderId,
RepoOverview,
RepoRecord,
RepoStackActionInput,
RepoStackActionResult,
SwitchResult,
} from "@openhandoff/shared";
import type {
ProcessCreateRequest,
ProcessLogFollowQuery,
ProcessLogsResponse,
ProcessSignalQuery,
} from "sandbox-agent";
import type {
BackendClient,
SandboxProcessRecord,
SandboxSessionEventRecord,
SandboxSessionRecord,
} from "../backend-client.js";
import { getSharedMockWorkbenchClient } from "./workbench-client.js";
interface MockProcessRecord extends SandboxProcessRecord {
logText: string;
}
function notSupported(name: string): never {
throw new Error(`${name} is not supported by the mock backend client.`);
}
function encodeBase64Utf8(value: string): string {
if (typeof Buffer !== "undefined") {
return Buffer.from(value, "utf8").toString("base64");
}
return globalThis.btoa(unescape(encodeURIComponent(value)));
}
function nowMs(): number {
return Date.now();
}
function mockRepoRemote(label: string): string {
return `https://example.test/${label}.git`;
}
function mockCwd(repoLabel: string, handoffId: string): string {
return `/mock/${repoLabel.replace(/\//g, "-")}/${handoffId}`;
}
function toHandoffStatus(status: HandoffRecord["status"], archived: boolean): HandoffRecord["status"] {
if (archived) {
return "archived";
}
return status;
}
export function createMockBackendClient(defaultWorkspaceId = "default"): BackendClient {
const workbench = getSharedMockWorkbenchClient();
const listenersBySandboxId = new Map<string, Set<() => void>>();
const processesBySandboxId = new Map<string, MockProcessRecord[]>();
let nextPid = 4000;
let nextProcessId = 1;
const requireHandoff = (handoffId: string) => {
const handoff = workbench.getSnapshot().handoffs.find((candidate) => candidate.id === handoffId);
if (!handoff) {
throw new Error(`Unknown mock handoff ${handoffId}`);
}
return handoff;
};
const ensureProcessList = (sandboxId: string): MockProcessRecord[] => {
const existing = processesBySandboxId.get(sandboxId);
if (existing) {
return existing;
}
const created: MockProcessRecord[] = [];
processesBySandboxId.set(sandboxId, created);
return created;
};
const notifySandbox = (sandboxId: string): void => {
const listeners = listenersBySandboxId.get(sandboxId);
if (!listeners) {
return;
}
for (const listener of [...listeners]) {
listener();
}
};
const buildHandoffRecord = (handoffId: string): HandoffRecord => {
const handoff = requireHandoff(handoffId);
const cwd = mockCwd(handoff.repoName, handoff.id);
const archived = handoff.status === "archived";
return {
workspaceId: defaultWorkspaceId,
repoId: handoff.repoId,
repoRemote: mockRepoRemote(handoff.repoName),
handoffId: handoff.id,
branchName: handoff.branch,
title: handoff.title,
task: handoff.title,
providerId: "local",
status: toHandoffStatus(archived ? "archived" : "running", archived),
statusMessage: archived ? "archived" : "mock sandbox ready",
activeSandboxId: handoff.id,
activeSessionId: handoff.tabs[0]?.sessionId ?? null,
sandboxes: [
{
sandboxId: handoff.id,
providerId: "local",
sandboxActorId: "mock-sandbox",
switchTarget: `mock://${handoff.id}`,
cwd,
createdAt: handoff.updatedAtMs,
updatedAt: handoff.updatedAtMs,
},
],
agentType: handoff.tabs[0]?.agent === "Codex" ? "codex" : "claude",
prSubmitted: Boolean(handoff.pullRequest),
diffStat: handoff.fileChanges.length > 0 ? `+${handoff.fileChanges.length}/-${handoff.fileChanges.length}` : "+0/-0",
prUrl: handoff.pullRequest ? `https://example.test/pr/${handoff.pullRequest.number}` : null,
prAuthor: handoff.pullRequest ? "mock" : null,
ciStatus: null,
reviewStatus: null,
reviewer: null,
conflictsWithMain: "0",
hasUnpushed: handoff.fileChanges.length > 0 ? "1" : "0",
parentBranch: null,
createdAt: handoff.updatedAtMs,
updatedAt: handoff.updatedAtMs,
};
};
const cloneProcess = (process: MockProcessRecord): MockProcessRecord => ({ ...process });
const createProcessRecord = (
sandboxId: string,
cwd: string,
request: ProcessCreateRequest,
): MockProcessRecord => {
const processId = `proc_${nextProcessId++}`;
const createdAtMs = nowMs();
const args = request.args ?? [];
const interactive = request.interactive ?? false;
const tty = request.tty ?? false;
const statusLine = interactive && tty
? "Mock terminal session created.\nInteractive transport is unavailable in mock mode.\n"
: "Mock process created.\n";
const commandLine = `$ ${[request.command, ...args].join(" ").trim()}\n`;
return {
id: processId,
command: request.command,
args,
createdAtMs,
cwd: request.cwd ?? cwd,
exitCode: null,
exitedAtMs: null,
interactive,
pid: nextPid++,
status: "running",
tty,
logText: `${statusLine}${commandLine}`,
};
};
return {
async addRepo(_workspaceId: string, _remoteUrl: string): Promise<RepoRecord> {
notSupported("addRepo");
},
async listRepos(_workspaceId: string): Promise<RepoRecord[]> {
return workbench.getSnapshot().repos.map((repo) => ({
workspaceId: defaultWorkspaceId,
repoId: repo.id,
remoteUrl: mockRepoRemote(repo.label),
createdAt: nowMs(),
updatedAt: nowMs(),
}));
},
async createHandoff(_input: CreateHandoffInput): Promise<HandoffRecord> {
notSupported("createHandoff");
},
async listHandoffs(_workspaceId: string, repoId?: string): Promise<HandoffSummary[]> {
return workbench
.getSnapshot()
.handoffs
.filter((handoff) => !repoId || handoff.repoId === repoId)
.map((handoff) => ({
workspaceId: defaultWorkspaceId,
repoId: handoff.repoId,
handoffId: handoff.id,
branchName: handoff.branch,
title: handoff.title,
status: handoff.status === "archived" ? "archived" : "running",
updatedAt: handoff.updatedAtMs,
}));
},
async getRepoOverview(_workspaceId: string, _repoId: string): Promise<RepoOverview> {
notSupported("getRepoOverview");
},
async runRepoStackAction(_input: RepoStackActionInput): Promise<RepoStackActionResult> {
notSupported("runRepoStackAction");
},
async getHandoff(_workspaceId: string, handoffId: string): Promise<HandoffRecord> {
return buildHandoffRecord(handoffId);
},
async listHistory(_input: HistoryQueryInput): Promise<HistoryEvent[]> {
return [];
},
async switchHandoff(_workspaceId: string, handoffId: string): Promise<SwitchResult> {
return {
workspaceId: defaultWorkspaceId,
handoffId,
providerId: "local",
switchTarget: `mock://${handoffId}`,
};
},
async attachHandoff(_workspaceId: string, handoffId: string): Promise<{ target: string; sessionId: string | null }> {
return {
target: `mock://${handoffId}`,
sessionId: requireHandoff(handoffId).tabs[0]?.sessionId ?? null,
};
},
async runAction(_workspaceId: string, _handoffId: string): Promise<void> {
notSupported("runAction");
},
async createSandboxSession(): Promise<{ id: string; status: "running" | "idle" | "error" }> {
notSupported("createSandboxSession");
},
async listSandboxSessions(): Promise<{ items: SandboxSessionRecord[]; nextCursor?: string }> {
return { items: [] };
},
async listSandboxSessionEvents(): Promise<{ items: SandboxSessionEventRecord[]; nextCursor?: string }> {
return { items: [] };
},
async createSandboxProcess(input: {
workspaceId: string;
providerId: ProviderId;
sandboxId: string;
request: ProcessCreateRequest;
}): Promise<SandboxProcessRecord> {
const handoff = requireHandoff(input.sandboxId);
const processes = ensureProcessList(input.sandboxId);
const created = createProcessRecord(input.sandboxId, mockCwd(handoff.repoName, handoff.id), input.request);
processes.unshift(created);
notifySandbox(input.sandboxId);
return cloneProcess(created);
},
async listSandboxProcesses(
_workspaceId: string,
_providerId: ProviderId,
sandboxId: string,
): Promise<{ processes: SandboxProcessRecord[] }> {
return {
processes: ensureProcessList(sandboxId).map((process) => cloneProcess(process)),
};
},
async getSandboxProcessLogs(
_workspaceId: string,
_providerId: ProviderId,
sandboxId: string,
processId: string,
query?: ProcessLogFollowQuery,
): Promise<ProcessLogsResponse> {
const process = ensureProcessList(sandboxId).find((candidate) => candidate.id === processId);
if (!process) {
throw new Error(`Unknown mock process ${processId}`);
}
return {
processId,
stream: query?.stream ?? (process.tty ? "pty" : "combined"),
entries: process.logText
? [
{
data: encodeBase64Utf8(process.logText),
encoding: "base64",
sequence: 1,
stream: query?.stream ?? (process.tty ? "pty" : "combined"),
timestampMs: process.createdAtMs,
},
]
: [],
};
},
async stopSandboxProcess(
_workspaceId: string,
_providerId: ProviderId,
sandboxId: string,
processId: string,
_query?: ProcessSignalQuery,
): Promise<SandboxProcessRecord> {
const process = ensureProcessList(sandboxId).find((candidate) => candidate.id === processId);
if (!process) {
throw new Error(`Unknown mock process ${processId}`);
}
process.status = "exited";
process.exitCode = 0;
process.exitedAtMs = nowMs();
process.logText += "\n[stopped]\n";
notifySandbox(sandboxId);
return cloneProcess(process);
},
async killSandboxProcess(
_workspaceId: string,
_providerId: ProviderId,
sandboxId: string,
processId: string,
_query?: ProcessSignalQuery,
): Promise<SandboxProcessRecord> {
const process = ensureProcessList(sandboxId).find((candidate) => candidate.id === processId);
if (!process) {
throw new Error(`Unknown mock process ${processId}`);
}
process.status = "exited";
process.exitCode = 137;
process.exitedAtMs = nowMs();
process.logText += "\n[killed]\n";
notifySandbox(sandboxId);
return cloneProcess(process);
},
async deleteSandboxProcess(
_workspaceId: string,
_providerId: ProviderId,
sandboxId: string,
processId: string,
): Promise<void> {
processesBySandboxId.set(
sandboxId,
ensureProcessList(sandboxId).filter((candidate) => candidate.id !== processId),
);
notifySandbox(sandboxId);
},
subscribeSandboxProcesses(
_workspaceId: string,
_providerId: ProviderId,
sandboxId: string,
listener: () => void,
): () => void {
let listeners = listenersBySandboxId.get(sandboxId);
if (!listeners) {
listeners = new Set();
listenersBySandboxId.set(sandboxId, listeners);
}
listeners.add(listener);
return () => {
const current = listenersBySandboxId.get(sandboxId);
if (!current) {
return;
}
current.delete(listener);
if (current.size === 0) {
listenersBySandboxId.delete(sandboxId);
}
};
},
async sendSandboxPrompt(): Promise<void> {
notSupported("sendSandboxPrompt");
},
async sandboxSessionStatus(sessionId: string): Promise<{ id: string; status: "running" | "idle" | "error" }> {
return { id: sessionId, status: "idle" };
},
async sandboxProviderState(
_workspaceId: string,
_providerId: ProviderId,
sandboxId: string,
): Promise<{ providerId: ProviderId; sandboxId: string; state: string; at: number }> {
return { providerId: "local", sandboxId, state: "running", at: nowMs() };
},
async getSandboxAgentConnection(): Promise<{ endpoint: string; token?: string }> {
return { endpoint: "mock://terminal-unavailable" };
},
async getWorkbench(): Promise<HandoffWorkbenchSnapshot> {
return workbench.getSnapshot();
},
subscribeWorkbench(_workspaceId: string, listener: () => void): () => void {
return workbench.subscribe(listener);
},
async createWorkbenchHandoff(
_workspaceId: string,
input: HandoffWorkbenchCreateHandoffInput,
): Promise<HandoffWorkbenchCreateHandoffResponse> {
return await workbench.createHandoff(input);
},
async markWorkbenchUnread(_workspaceId: string, input: HandoffWorkbenchSelectInput): Promise<void> {
await workbench.markHandoffUnread(input);
},
async renameWorkbenchHandoff(_workspaceId: string, input: HandoffWorkbenchRenameInput): Promise<void> {
await workbench.renameHandoff(input);
},
async renameWorkbenchBranch(_workspaceId: string, input: HandoffWorkbenchRenameInput): Promise<void> {
await workbench.renameBranch(input);
},
async createWorkbenchSession(
_workspaceId: string,
input: HandoffWorkbenchSelectInput & { model?: string },
): Promise<{ tabId: string }> {
return await workbench.addTab(input);
},
async renameWorkbenchSession(_workspaceId: string, input: HandoffWorkbenchRenameSessionInput): Promise<void> {
await workbench.renameSession(input);
},
async setWorkbenchSessionUnread(
_workspaceId: string,
input: HandoffWorkbenchSetSessionUnreadInput,
): Promise<void> {
await workbench.setSessionUnread(input);
},
async updateWorkbenchDraft(_workspaceId: string, input: HandoffWorkbenchUpdateDraftInput): Promise<void> {
await workbench.updateDraft(input);
},
async changeWorkbenchModel(_workspaceId: string, input: HandoffWorkbenchChangeModelInput): Promise<void> {
await workbench.changeModel(input);
},
async sendWorkbenchMessage(_workspaceId: string, input: HandoffWorkbenchSendMessageInput): Promise<void> {
await workbench.sendMessage(input);
},
async stopWorkbenchSession(_workspaceId: string, input: HandoffWorkbenchTabInput): Promise<void> {
await workbench.stopAgent(input);
},
async closeWorkbenchSession(_workspaceId: string, input: HandoffWorkbenchTabInput): Promise<void> {
await workbench.closeTab(input);
},
async publishWorkbenchPr(_workspaceId: string, input: HandoffWorkbenchSelectInput): Promise<void> {
await workbench.publishPr(input);
},
async revertWorkbenchFile(_workspaceId: string, input: HandoffWorkbenchDiffInput): Promise<void> {
await workbench.revertFile(input);
},
async health(): Promise<{ ok: true }> {
return { ok: true };
},
async useWorkspace(workspaceId: string): Promise<{ workspaceId: string }> {
return { workspaceId };
},
};
}