Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"fast-glob": "^3.3.3",
"file-uri-to-path": "^1.0.0",
"framer-motion": "^12.23.26",
"freestyle-sandboxes": "^0.1.3",
"fuse.js": "^7.1.0",
"http-proxy": "^1.18.1",
"idb": "^8.0.3",
Expand Down
182 changes: 182 additions & 0 deletions apps/desktop/src/lib/trpc/routers/cloud-terminal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { cloudWorkspaces } from "@superset/local-db";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { cloudTerminalManager } from "main/lib/cloud-terminal";
import { localDb } from "main/lib/local-db";
import { z } from "zod";
import { publicProcedure, router } from "../..";

/**
* Cloud Terminal router for managing remote terminal sessions on cloud workspaces
*
* Uses the Freestyle SDK to connect to cloud VMs and establish WebSocket-based
* terminal sessions. Sessions are keyed by paneId and linked to cloud workspaces.
*/
export const createCloudTerminalRouter = () => {
return router({
/**
* Create or attach to a cloud terminal session
*/
createOrAttach: publicProcedure
.input(
z.object({
paneId: z.string(),
tabId: z.string(),
cloudWorkspaceId: z.string(),
cols: z.number().optional(),
rows: z.number().optional(),
}),
)
.mutation(async ({ input }) => {
const { paneId, tabId, cloudWorkspaceId, cols, rows } = input;

// Get cloud workspace to find the VM ID
const cloudWorkspace = localDb
.select()
.from(cloudWorkspaces)
.where(eq(cloudWorkspaces.id, cloudWorkspaceId))
.get();

if (!cloudWorkspace) {
throw new Error(`Cloud workspace ${cloudWorkspaceId} not found`);
}

if (!cloudWorkspace.provider_vm_id) {
throw new Error(
`Cloud workspace ${cloudWorkspaceId} does not have a VM assigned`,
);
}

if (cloudWorkspace.status !== "running") {
throw new Error(
`Cloud workspace ${cloudWorkspaceId} is not running (status: ${cloudWorkspace.status})`,
);
}

const result = await cloudTerminalManager.createOrAttach({
paneId,
tabId,
cloudWorkspaceId,
vmId: cloudWorkspace.provider_vm_id,
cols,
rows,
});

return {
paneId,
isNew: result.isNew,
scrollback: result.scrollback,
wasRecovered: result.wasRecovered,
viewportY: result.viewportY,
};
}),

/**
* Write data to cloud terminal
*/
write: publicProcedure
.input(
z.object({
paneId: z.string(),
data: z.string(),
}),
)
.mutation(({ input }) => {
cloudTerminalManager.write(input);
}),
Comment on lines +77 to +86
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling to manager calls.

These mutations delegate to cloudTerminalManager without error handling. Per coding guidelines, errors should not be swallowed silently. If the manager throws, the error should be caught and logged with context.

♻️ Example fix for write mutation
 		write: publicProcedure
 			.input(
 				z.object({
 					paneId: z.string(),
 					data: z.string(),
 				}),
 			)
 			.mutation(({ input }) => {
-				cloudTerminalManager.write(input);
+				try {
+					cloudTerminalManager.write(input);
+				} catch (error) {
+					console.error("[cloud-terminal/write] Failed to write data:", error);
+					throw error;
+				}
 			}),

Also applies to: 91-101, 119-128, 133-141

🤖 Prompt for AI Agents
In @apps/desktop/src/lib/trpc/routers/cloud-terminal/index.ts around lines 77 -
86, The cloud-terminal router mutations (e.g., the write mutation that calls
cloudTerminalManager.write and the other mutations at ranges around lines
91-101, 119-128, 133-141) currently call manager methods without error handling;
update each mutation to wrap the cloudTerminalManager call in a try/catch, log
the caught error with context (include the mutation name and relevant input
fields like paneId) using the existing logger or processLogger, and rethrow or
return a TRPC-friendly error so failures are not silently swallowed.


/**
* Resize cloud terminal
*/
resize: publicProcedure
.input(
z.object({
paneId: z.string(),
cols: z.number(),
rows: z.number(),
}),
)
.mutation(({ input }) => {
cloudTerminalManager.resize(input);
}),

/**
* Kill cloud terminal session
*/
kill: publicProcedure
.input(
z.object({
paneId: z.string(),
}),
)
.mutation(async ({ input }) => {
await cloudTerminalManager.kill(input);
}),

/**
* Detach from cloud terminal (keep session alive)
*/
detach: publicProcedure
.input(
z.object({
paneId: z.string(),
viewportY: z.number().optional(),
}),
)
.mutation(({ input }) => {
cloudTerminalManager.detach(input);
}),

/**
* Clear scrollback buffer for cloud terminal
*/
clearScrollback: publicProcedure
.input(
z.object({
paneId: z.string(),
}),
)
.mutation(({ input }) => {
cloudTerminalManager.clearScrollback(input);
}),

/**
* Get cloud terminal session info
*/
getSession: publicProcedure
.input(z.string())
.query(({ input: paneId }) => {
return cloudTerminalManager.getSession(paneId);
}),

/**
* Stream data from cloud terminal
*/
stream: publicProcedure
.input(z.string())
.subscription(({ input: paneId }) => {
return observable<
| { type: "data"; data: string }
| { type: "exit"; exitCode: number; signal?: number }
>((emit) => {
const onData = (data: string) => {
emit.next({ type: "data", data });
};

const onExit = (exitCode: number, signal?: number) => {
emit.next({ type: "exit", exitCode, signal });
emit.complete();
};

cloudTerminalManager.on(`data:${paneId}`, onData);
cloudTerminalManager.on(`exit:${paneId}`, onExit);

// Cleanup on unsubscribe
return () => {
cloudTerminalManager.off(`data:${paneId}`, onData);
cloudTerminalManager.off(`exit:${paneId}`, onExit);
};
});
}),
});
};
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createAnalyticsRouter } from "./analytics";
import { createAuthRouter } from "./auth";
import { createAutoUpdateRouter } from "./auto-update";
import { createChangesRouter } from "./changes";
import { createCloudTerminalRouter } from "./cloud-terminal";
import { createConfigRouter } from "./config";
import { createExternalRouter } from "./external";
import { createHotkeysRouter } from "./hotkeys";
Expand All @@ -30,6 +31,7 @@ export const createAppRouter = (getWindow: () => BrowserWindow | null) => {
projects: createProjectsRouter(getWindow),
workspaces: createWorkspacesRouter(),
terminal: createTerminalRouter(),
cloudTerminal: createCloudTerminalRouter(),
changes: createChangesRouter(),
notifications: createNotificationsRouter(),
ports: createPortsRouter(),
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/lib/cloud-terminal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { cloudTerminalManager, CloudTerminalManager } from "./manager";
export * from "./types";
Loading
Loading