the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add sandboxId to resources and web hooks

Add optional sandboxId to file, secret, variable, and volume models,
and add volume.path and readOnly fields. Change file.delete to accept
an id query parameter (update lexicons/pkl/types). Add addVolume input
schema. Add web API clients and React Query hooks for files, secrets,
variables, and volumes. Expose sandbox preferences endpoints and update
wrangler vars.

+498 -51
+4
apps/api/lexicons/file/defs.json
··· 9 9 "content" 10 10 ], 11 11 "properties": { 12 + "sandboxId": { 13 + "type": "string", 14 + "description": "The ID of the sandbox to which the file belongs. This is used to associate the file with a specific sandbox environment." 15 + }, 12 16 "path": { 13 17 "type": "string", 14 18 "description": "The file path within the sandbox, e.g. '/app/config.json', '/home/user/.ssh/id_rsa', etc."
+9 -12
apps/api/lexicons/file/deleteFile.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "procedure", 7 - "input": { 8 - "encoding": "application/json", 9 - "schema": { 10 - "type": "object", 11 - "required": [ 12 - "file" 13 - ], 14 - "properties": { 15 - "file": { 16 - "type": "ref", 17 - "ref": "io.pocketenv.file.defs#file" 18 - } 7 + "parameters": { 8 + "type": "params", 9 + "required": [ 10 + "id" 11 + ], 12 + "properties": { 13 + "id": { 14 + "type": "string", 15 + "description": "The ID of the file to delete" 19 16 } 20 17 } 21 18 }
+4
apps/api/lexicons/secret/defs.json
··· 22 22 "value" 23 23 ], 24 24 "properties": { 25 + "sandboxId": { 26 + "type": "string", 27 + "description": "The ID of the sandbox to which the secret belongs. This is used to associate the secret with a specific sandbox environment." 28 + }, 25 29 "name": { 26 30 "type": "string", 27 31 "description": "Name of the secret, e.g. 'DATABASE_URL', 'SSH_KEY', etc."
+4
apps/api/lexicons/variable/defs.json
··· 26 26 "value" 27 27 ], 28 28 "properties": { 29 + "sandboxId": { 30 + "type": "string", 31 + "description": "The ID of the sandbox to which the environment variable belongs. This is used to associate the variable with a specific sandbox environment." 32 + }, 29 33 "name": { 30 34 "type": "string", 31 35 "description": "Name of the environment variable, e.g. 'NODE_ENV', 'PORT', etc."
+16 -1
apps/api/lexicons/volume/addVolume.json
··· 3 3 "id": "io.pocketenv.volume.addVolume", 4 4 "defs": { 5 5 "main": { 6 - "type": "procedure" 6 + "type": "procedure", 7 + "input": { 8 + "encoding": "application/json", 9 + "schema": { 10 + "type": "object", 11 + "required": [ 12 + "volume" 13 + ], 14 + "properties": { 15 + "volume": { 16 + "type": "ref", 17 + "ref": "io.pocketenv.volume.defs#volume" 18 + } 19 + } 20 + } 21 + } 7 22 } 8 23 } 9 24 }
+12
apps/api/lexicons/volume/defs.json
··· 29 29 "name" 30 30 ], 31 31 "properties": { 32 + "sandboxId": { 33 + "type": "string", 34 + "description": "The ID of the sandbox to which the volume belongs. This is used to associate the volume with a specific sandbox environment." 35 + }, 32 36 "name": { 33 37 "type": "string", 34 38 "description": "Name of the volume, e.g. 'data-volume', 'logs', etc." 39 + }, 40 + "path": { 41 + "type": "string", 42 + "description": "The path within the sandbox where the volume will be mounted, e.g. '/data', '/logs', etc." 43 + }, 44 + "readOnly": { 45 + "type": "boolean", 46 + "description": "Whether the volume should be mounted as read-only within the sandbox. Defaults to false (read-write)." 35 47 } 36 48 } 37 49 }
+5
apps/api/pkl/defs/file/defs.pkl
··· 7 7 type = "object" 8 8 required = List("path", "content") 9 9 properties { 10 + ["sandboxId"] = new StringType { 11 + type = "string" 12 + description = 13 + "The ID of the sandbox to which the file belongs. This is used to associate the file with a specific sandbox environment." 14 + } 10 15 ["path"] = new StringType { 11 16 type = "string" 12 17 description = "The file path within the sandbox, e.g. '/app/config.json', '/home/user/.ssh/id_rsa', etc."
+7 -10
apps/api/pkl/defs/file/deleteFile.pkl
··· 5 5 defs = new Mapping<String, Procedure> { 6 6 ["main"] { 7 7 type = "procedure" 8 - input { 9 - encoding = "application/json" 10 - schema { 11 - type = "object" 12 - required = List("file") 13 - properties { 14 - ["file"] = new Ref { 15 - type = "ref" 16 - ref = "io.pocketenv.file.defs#file" 17 - } 8 + parameters { 9 + type = "params" 10 + required = List("id") 11 + properties { 12 + ["id"] = new StringType { 13 + type = "string" 14 + description = "The ID of the file to delete" 18 15 } 19 16 } 20 17 }
+5
apps/api/pkl/defs/secret/defs.pkl
··· 20 20 type = "object" 21 21 required = List("name", "value") 22 22 properties { 23 + ["sandboxId"] = new StringType { 24 + type = "string" 25 + description = 26 + "The ID of the sandbox to which the secret belongs. This is used to associate the secret with a specific sandbox environment." 27 + } 23 28 ["name"] = new StringType { 24 29 type = "string" 25 30 description = "Name of the secret, e.g. 'DATABASE_URL', 'SSH_KEY', etc."
+5
apps/api/pkl/defs/variable/defs.pkl
··· 25 25 type = "object" 26 26 required = List("name", "value") 27 27 properties { 28 + ["sandboxId"] = new StringType { 29 + type = "string" 30 + description = 31 + "The ID of the sandbox to which the environment variable belongs. This is used to associate the variable with a specific sandbox environment." 32 + } 28 33 ["name"] = new StringType { 29 34 type = "string" 30 35 description = "Name of the environment variable, e.g. 'NODE_ENV', 'PORT', etc."
+13
apps/api/pkl/defs/volume/addVolume.pkl
··· 5 5 defs = new Mapping<String, Procedure> { 6 6 ["main"] { 7 7 type = "procedure" 8 + input { 9 + encoding = "application/json" 10 + schema { 11 + type = "object" 12 + required = List("volume") 13 + properties { 14 + ["volume"] = new Ref { 15 + type = "ref" 16 + ref = "io.pocketenv.volume.defs#volume" 17 + } 18 + } 19 + } 20 + } 8 21 } 9 22 }
+15
apps/api/pkl/defs/volume/defs.pkl
··· 28 28 type = "object" 29 29 required = List("name") 30 30 properties { 31 + ["sandboxId"] = new StringType { 32 + type = "string" 33 + description = 34 + "The ID of the sandbox to which the volume belongs. This is used to associate the volume with a specific sandbox environment." 35 + } 31 36 ["name"] = new StringType { 32 37 type = "string" 33 38 description = "Name of the volume, e.g. 'data-volume', 'logs', etc." 39 + } 40 + ["path"] = new StringType { 41 + type = "string" 42 + description = 43 + "The path within the sandbox where the volume will be mounted, e.g. '/data', '/logs', etc." 44 + } 45 + ["readOnly"] = new BooleanType { 46 + type = "boolean" 47 + description = 48 + "Whether the volume should be mounted as read-only within the sandbox. Defaults to false (read-write)." 34 49 } 35 50 } 36 51 }
+50 -10
apps/api/src/lexicon/lexicons.ts
··· 237 237 type: "object", 238 238 required: ["path", "content"], 239 239 properties: { 240 + sandboxId: { 241 + type: "string", 242 + description: 243 + "The ID of the sandbox to which the file belongs. This is used to associate the file with a specific sandbox environment.", 244 + }, 240 245 path: { 241 246 type: "string", 242 247 description: ··· 265 270 defs: { 266 271 main: { 267 272 type: "procedure", 268 - input: { 269 - encoding: "application/json", 270 - schema: { 271 - type: "object", 272 - required: ["file"], 273 - properties: { 274 - file: { 275 - type: "ref", 276 - ref: "lex:io.pocketenv.file.defs#file", 277 - }, 273 + parameters: { 274 + type: "params", 275 + required: ["id"], 276 + properties: { 277 + id: { 278 + type: "string", 279 + description: "The ID of the file to delete", 278 280 }, 279 281 }, 280 282 }, ··· 1237 1239 type: "object", 1238 1240 required: ["name", "value"], 1239 1241 properties: { 1242 + sandboxId: { 1243 + type: "string", 1244 + description: 1245 + "The ID of the sandbox to which the secret belongs. This is used to associate the secret with a specific sandbox environment.", 1246 + }, 1240 1247 name: { 1241 1248 type: "string", 1242 1249 description: ··· 1372 1379 type: "object", 1373 1380 required: ["name", "value"], 1374 1381 properties: { 1382 + sandboxId: { 1383 + type: "string", 1384 + description: 1385 + "The ID of the sandbox to which the environment variable belongs. This is used to associate the variable with a specific sandbox environment.", 1386 + }, 1375 1387 name: { 1376 1388 type: "string", 1377 1389 description: ··· 1464 1476 defs: { 1465 1477 main: { 1466 1478 type: "procedure", 1479 + input: { 1480 + encoding: "application/json", 1481 + schema: { 1482 + type: "object", 1483 + required: ["volume"], 1484 + properties: { 1485 + volume: { 1486 + type: "ref", 1487 + ref: "lex:io.pocketenv.volume.defs#volume", 1488 + }, 1489 + }, 1490 + }, 1491 + }, 1467 1492 }, 1468 1493 }, 1469 1494 }, ··· 1496 1521 type: "object", 1497 1522 required: ["name"], 1498 1523 properties: { 1524 + sandboxId: { 1525 + type: "string", 1526 + description: 1527 + "The ID of the sandbox to which the volume belongs. This is used to associate the volume with a specific sandbox environment.", 1528 + }, 1499 1529 name: { 1500 1530 type: "string", 1501 1531 description: "Name of the volume, e.g. 'data-volume', 'logs', etc.", 1532 + }, 1533 + path: { 1534 + type: "string", 1535 + description: 1536 + "The path within the sandbox where the volume will be mounted, e.g. '/data', '/logs', etc.", 1537 + }, 1538 + readOnly: { 1539 + type: "boolean", 1540 + description: 1541 + "Whether the volume should be mounted as read-only within the sandbox. Defaults to false (read-write).", 1502 1542 }, 1503 1543 }, 1504 1544 },
+2
apps/api/src/lexicon/types/io/pocketenv/file/defs.ts
··· 7 7 import { CID } from "multiformats/cid"; 8 8 9 9 export interface File { 10 + /** The ID of the sandbox to which the file belongs. This is used to associate the file with a specific sandbox environment. */ 11 + sandboxId?: string; 10 12 /** The file path within the sandbox, e.g. '/app/config.json', '/home/user/.ssh/id_rsa', etc. */ 11 13 path: string; 12 14 /** The content of the file. This will be written to the specified path within the sandbox. The content should be base64 encoded if it's binary data. */
+5 -10
apps/api/src/lexicon/types/io/pocketenv/file/deleteFile.ts
··· 7 7 import { isObj, hasProp } from "../../../../util"; 8 8 import { CID } from "multiformats/cid"; 9 9 import { type HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 - import type * as IoPocketenvFileDefs from "./defs"; 11 10 12 - export type QueryParams = {}; 13 - 14 - export interface InputSchema { 15 - file: IoPocketenvFileDefs.File; 16 - [k: string]: unknown; 11 + export interface QueryParams { 12 + /** The ID of the file to delete */ 13 + id: string; 17 14 } 18 15 19 - export interface HandlerInput { 20 - encoding: "application/json"; 21 - body: InputSchema; 22 - } 16 + export type InputSchema = undefined; 17 + export type HandlerInput = undefined; 23 18 24 19 export interface HandlerError { 25 20 status: number;
+2
apps/api/src/lexicon/types/io/pocketenv/secret/defs.ts
··· 27 27 } 28 28 29 29 export interface Secret { 30 + /** The ID of the sandbox to which the secret belongs. This is used to associate the secret with a specific sandbox environment. */ 31 + sandboxId?: string; 30 32 /** Name of the secret, e.g. 'DATABASE_URL', 'SSH_KEY', etc. */ 31 33 name: string; 32 34 /** Value of the secret. This will be encrypted at rest and redacted in any API responses. */
+2
apps/api/src/lexicon/types/io/pocketenv/variable/defs.ts
··· 29 29 } 30 30 31 31 export interface Variable { 32 + /** The ID of the sandbox to which the environment variable belongs. This is used to associate the variable with a specific sandbox environment. */ 33 + sandboxId?: string; 32 34 /** Name of the environment variable, e.g. 'NODE_ENV', 'PORT', etc. */ 33 35 name: string; 34 36 /** Value of the environment variable. This will be visible in API responses and should not contain sensitive information. */
+10 -2
apps/api/src/lexicon/types/io/pocketenv/volume/addVolume.ts
··· 7 7 import { isObj, hasProp } from "../../../../util"; 8 8 import { CID } from "multiformats/cid"; 9 9 import { type HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + import type * as IoPocketenvVolumeDefs from "./defs"; 10 11 11 12 export type QueryParams = {}; 12 13 13 - export type InputSchema = undefined; 14 - export type HandlerInput = undefined; 14 + export interface InputSchema { 15 + volume: IoPocketenvVolumeDefs.Volume; 16 + [k: string]: unknown; 17 + } 18 + 19 + export interface HandlerInput { 20 + encoding: "application/json"; 21 + body: InputSchema; 22 + } 15 23 16 24 export interface HandlerError { 17 25 status: number;
+6
apps/api/src/lexicon/types/io/pocketenv/volume/defs.ts
··· 29 29 export type Volumes = Volume[]; 30 30 31 31 export interface Volume { 32 + /** The ID of the sandbox to which the volume belongs. This is used to associate the volume with a specific sandbox environment. */ 33 + sandboxId?: string; 32 34 /** Name of the volume, e.g. 'data-volume', 'logs', etc. */ 33 35 name: string; 36 + /** The path within the sandbox where the volume will be mounted, e.g. '/data', '/logs', etc. */ 37 + path?: string; 38 + /** Whether the volume should be mounted as read-only within the sandbox. Defaults to false (read-write). */ 39 + readOnly?: boolean; 34 40 [k: string]: unknown; 35 41 } 36 42
+4
apps/api/src/xrpc/index.ts
··· 19 19 import addVolume from "./io/pocketenv/volume/addVolume"; 20 20 import deleteVolume from "./io/pocketenv/volume/deleteVolume"; 21 21 import getVolumes from "./io/pocketenv/volume/getVolumes"; 22 + import putPreferences from "./io/pocketenv/sandbox/putPreferences"; 23 + import getPreferences from "./io/pocketenv/sandbox/getPreferences"; 22 24 23 25 export default function (server: Server, ctx: Context) { 24 26 // io.pocketenv ··· 32 34 claimSandbox(server, ctx); 33 35 getProfile(server, ctx); 34 36 getTerminalToken(server, ctx); 37 + putPreferences(server, ctx); 38 + getPreferences(server, ctx); 35 39 addFile(server, ctx); 36 40 deleteFile(server, ctx); 37 41 getFiles(server, ctx);
+3 -2
apps/cf-sandbox/worker-configuration.d.ts
··· 1 1 /* eslint-disable */ 2 - // Generated by Wrangler by running `wrangler types` (hash: 1f7da8ed5305d2520f1123250466b09a) 2 + // Generated by Wrangler by running `wrangler types` (hash: 63d900118c089c42015e56aa5258de5f) 3 3 // Runtime types generated with workerd@1.20260205.0 2025-05-06 nodejs_compat 4 4 declare namespace Cloudflare { 5 5 interface GlobalProps { ··· 8 8 } 9 9 interface Env { 10 10 HYPERDRIVE: Hyperdrive; 11 + SANDBOX_TRANSPORT: "websocket"; 11 12 CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE: string; 12 13 PUBLIC_KEY: string; 13 14 PRIVATE_KEY: string; ··· 20 21 [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; 21 22 }; 22 23 declare namespace NodeJS { 23 - interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE" | "PUBLIC_KEY" | "PRIVATE_KEY" | "JWT_SECRET">> {} 24 + interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "SANDBOX_TRANSPORT" | "CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE" | "PUBLIC_KEY" | "PRIVATE_KEY" | "JWT_SECRET">> {} 24 25 } 25 26 26 27 // Begin runtime types
+1
apps/cf-sandbox/wrangler.jsonc
··· 31 31 * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables 32 32 */ 33 33 // "vars": { "MY_VARIABLE": "production_value" } 34 + "vars": { "SANDBOX_TRANSPORT": "websocket" }, 34 35 /** 35 36 * Note: Use secrets to store sensitive data. 36 37 * https://developers.cloudflare.com/workers/configuration/secrets/
+30
apps/web/src/api/file.ts
··· 1 + import { client } from "."; 2 + 3 + export const addFile = (sandboxId: string, path: string, content: string) => 4 + client.post( 5 + "/xrpc/io.pocketenv.file.addFile", 6 + { 7 + sandboxId, 8 + path, 9 + content, 10 + }, 11 + { 12 + headers: { 13 + Authorization: `Bearer ${localStorage.getItem("token")}`, 14 + }, 15 + }, 16 + ); 17 + 18 + export const deleteFile = (id: string) => 19 + client.post(`/xrpc/io.pocketenv.file.deleteFile?id=${id}`, undefined, { 20 + headers: { 21 + Authorization: `Bearer ${localStorage.getItem("token")}`, 22 + }, 23 + }); 24 + 25 + export const getFiles = () => 26 + client.get("/xrpc/io.pocketenv.file.getFiles", { 27 + headers: { 28 + Authorization: `Bearer ${localStorage.getItem("token")}`, 29 + }, 30 + });
+18
apps/web/src/api/sandbox.ts
··· 72 72 Authorization: `Bearer ${localStorage.getItem("token")}`, 73 73 }, 74 74 }); 75 + 76 + export const putPreferences = () => 77 + client.post( 78 + `/xrpc/io.pocketenv.sandbox.putPreferences`, 79 + {}, 80 + { 81 + headers: { 82 + Authorization: `Bearer ${localStorage.getItem("token")}`, 83 + }, 84 + }, 85 + ); 86 + 87 + export const getPreferences = () => 88 + client.get(`/xrpc/io.pocketenv.sandbox.getPreferences`, { 89 + headers: { 90 + Authorization: `Bearer ${localStorage.getItem("token")}`, 91 + }, 92 + });
+30
apps/web/src/api/secret.ts
··· 1 + import { client } from "."; 2 + 3 + export const addSecret = (sandboxId: string, name: string, value: string) => 4 + client.post( 5 + "/xrpc/io.pocketenv.secret.addSecret", 6 + { 7 + sandboxId, 8 + name, 9 + value, 10 + }, 11 + { 12 + headers: { 13 + Authorization: `Bearer ${localStorage.getItem("token")}`, 14 + }, 15 + }, 16 + ); 17 + 18 + export const deleteSecret = (id: string) => 19 + client.post(`/xrpc/io.pocketenv.secret.deleteSecret?id=${id}`, undefined, { 20 + headers: { 21 + Authorization: `Bearer ${localStorage.getItem("token")}`, 22 + }, 23 + }); 24 + 25 + export const getSecrets = () => 26 + client.get("/xrpc/io.pocketenv.secret.getSecrets", { 27 + headers: { 28 + Authorization: `Bearer ${localStorage.getItem("token")}`, 29 + }, 30 + });
+34
apps/web/src/api/variable.ts
··· 1 + import { client } from "."; 2 + 3 + export const addVariable = (sandboxId: string, name: string, value: string) => 4 + client.post( 5 + "/xrpc/io.pocketenv.variable.addVariable", 6 + { 7 + sandboxId, 8 + name, 9 + value, 10 + }, 11 + { 12 + headers: { 13 + Authorization: `Bearer ${localStorage.getItem("token")}`, 14 + }, 15 + }, 16 + ); 17 + 18 + export const deleteVariable = (id: string) => 19 + client.post( 20 + `/xrpc/io.pocketenv.variable.deleteVariable?id=${id}`, 21 + undefined, 22 + { 23 + headers: { 24 + Authorization: `Bearer ${localStorage.getItem("token")}`, 25 + }, 26 + }, 27 + ); 28 + 29 + export const getVariables = () => 30 + client.get("/xrpc/io.pocketenv.variable.getVariables", { 31 + headers: { 32 + Authorization: `Bearer ${localStorage.getItem("token")}`, 33 + }, 34 + });
+30
apps/web/src/api/volume.ts
··· 1 + import { client } from "."; 2 + 3 + export const addVolume = (sandboxId: string, name: string, path: string) => 4 + client.post( 5 + "/xrpc/io.pocketenv.volume.addVolume", 6 + { 7 + sandboxId, 8 + name, 9 + path, 10 + }, 11 + { 12 + headers: { 13 + Authorization: `Bearer ${localStorage.getItem("token")}`, 14 + }, 15 + }, 16 + ); 17 + 18 + export const deleteVolume = (id: string) => 19 + client.post(`/xrpc/io.pocketenv.volume.deleteVolume?id=${id}`, undefined, { 20 + headers: { 21 + Authorization: `Bearer ${localStorage.getItem("token")}`, 22 + }, 23 + }); 24 + 25 + export const getVolumes = () => 26 + client.get("/xrpc/io.pocketenv.volume.getVolumes", { 27 + headers: { 28 + Authorization: `Bearer ${localStorage.getItem("token")}`, 29 + }, 30 + });
+8 -1
apps/web/src/components/contextmenu/AddEnvironmentVariableModal/AddEnvironmentVariableModal.tsx
··· 1 1 import { useEffect } from "react"; 2 2 import { createPortal } from "react-dom"; 3 + import { useAddVariableMutation } from "../../../hooks/useVariable"; 3 4 4 5 export type AddEnvironmentVariableModalProps = { 5 6 isOpen: boolean; ··· 12 13 onClose, 13 14 sandboxId, 14 15 }: AddEnvironmentVariableModalProps) { 16 + const { mutateAsync } = useAddVariableMutation(sandboxId, "", ""); 15 17 useEffect(() => { 16 18 const handleEscapeKey = (event: KeyboardEvent) => { 17 19 if (event.key === "Escape" && isOpen) { ··· 37 39 }; 38 40 39 41 const handleCloseButton = (e: React.MouseEvent<HTMLButtonElement>) => { 42 + e.stopPropagation(); 43 + onClose(); 44 + }; 45 + 46 + const onAddVariable = (e: React.MouseEvent<HTMLButtonElement>) => { 40 47 e.stopPropagation(); 41 48 onClose(); 42 49 }; ··· 108 115 <div className="modal-footer"> 109 116 <button 110 117 className="btn btn-primary w-35 font-semibold" 111 - onClick={() => {}} 118 + onClick={onAddVariable} 112 119 > 113 120 Add Variable 114 121 </button>
+8 -1
apps/web/src/components/contextmenu/AddFileModal/AddFileModal.tsx
··· 1 1 import { useEffect } from "react"; 2 2 import { createPortal } from "react-dom"; 3 + import { useAddFileMutation } from "../../../hooks/useFile"; 3 4 4 5 export type AddFileModalProps = { 5 6 isOpen: boolean; ··· 8 9 }; 9 10 10 11 function AddFileModal({ isOpen, onClose, sandboxId }: AddFileModalProps) { 12 + const { mutateAsync } = useAddFileMutation(sandboxId, "", ""); 11 13 useEffect(() => { 12 14 const handleEscapeKey = (event: KeyboardEvent) => { 13 15 if (event.key === "Escape" && isOpen) { ··· 33 35 }; 34 36 35 37 const handleCloseButton = (e: React.MouseEvent<HTMLButtonElement>) => { 38 + e.stopPropagation(); 39 + onClose(); 40 + }; 41 + 42 + const onAddFile = (e: React.MouseEvent<HTMLButtonElement>) => { 36 43 e.stopPropagation(); 37 44 onClose(); 38 45 }; ··· 102 109 <div className="modal-footer"> 103 110 <button 104 111 className="btn btn-primary w-35 font-semibold" 105 - onClick={() => {}} 112 + onClick={onAddFile} 106 113 > 107 114 Add File 108 115 </button>
+8 -1
apps/web/src/components/contextmenu/AddSecretModal/AddSecretModal.tsx
··· 1 1 import { useEffect } from "react"; 2 2 import { createPortal } from "react-dom"; 3 + import { useAddSecretMutation } from "../../../hooks/useSecret"; 3 4 4 5 export type AddSecretModalProps = { 5 6 isOpen: boolean; ··· 8 9 }; 9 10 10 11 function AddSecretModal({ isOpen, onClose, sandboxId }: AddSecretModalProps) { 12 + const { mutateAsync } = useAddSecretMutation(sandboxId, "", ""); 11 13 useEffect(() => { 12 14 const handleEscapeKey = (event: KeyboardEvent) => { 13 15 if (event.key === "Escape" && isOpen) { ··· 33 35 }; 34 36 35 37 const handleCloseButton = (e: React.MouseEvent<HTMLButtonElement>) => { 38 + e.stopPropagation(); 39 + onClose(); 40 + }; 41 + 42 + const onAddSecret = (e: React.MouseEvent<HTMLButtonElement>) => { 36 43 e.stopPropagation(); 37 44 onClose(); 38 45 }; ··· 102 109 </> 103 110 </div> 104 111 <div className="modal-footer"> 105 - <button className="btn btn-primary" onClick={() => {}}> 112 + <button className="btn btn-primary" onClick={onAddSecret}> 106 113 Add Secret 107 114 </button> 108 115 </div>
+8 -1
apps/web/src/components/contextmenu/AddVolumeModal/AddVolumeModal.tsx
··· 1 1 import { useEffect } from "react"; 2 2 import { createPortal } from "react-dom"; 3 + import { useAddVolumeMutation } from "../../../hooks/useVolume"; 3 4 4 5 export type AddVolumeModalProps = { 5 6 isOpen: boolean; ··· 8 9 }; 9 10 10 11 function AddVolumeModal({ isOpen, onClose, sandboxId }: AddVolumeModalProps) { 12 + const { mutateAsync } = useAddVolumeMutation(sandboxId, "", ""); 11 13 useEffect(() => { 12 14 const handleEscapeKey = (event: KeyboardEvent) => { 13 15 if (event.key === "Escape" && isOpen) { ··· 33 35 }; 34 36 35 37 const handleCloseButton = (e: React.MouseEvent<HTMLButtonElement>) => { 38 + e.stopPropagation(); 39 + onClose(); 40 + }; 41 + 42 + const onAddVolume = (e: React.MouseEvent<HTMLButtonElement>) => { 36 43 e.stopPropagation(); 37 44 onClose(); 38 45 }; ··· 111 118 <div className="modal-footer"> 112 119 <button 113 120 className="btn btn-primary font-semibold" 114 - onClick={() => {}} 121 + onClick={onAddVolume} 115 122 > 116 123 Add Volume 117 124 </button>
+2
apps/web/src/components/contextmenu/DeleteSandboxModal/DeleteSandboxModal.tsx
··· 1 1 import { useEffect } from "react"; 2 2 import { createPortal } from "react-dom"; 3 + import { useDeleteSandboxMutation } from "../../../hooks/useSandbox"; 3 4 4 5 export type DeleteSandboxModalProps = { 5 6 isOpen: boolean; ··· 12 13 onClose, 13 14 sandboxId, 14 15 }: DeleteSandboxModalProps) { 16 + const { mutateAsync } = useDeleteSandboxMutation(); 15 17 useEffect(() => { 16 18 const handleEscapeKey = (event: KeyboardEvent) => { 17 19 if (event.key === "Escape" && isOpen) {
+35
apps/web/src/hooks/useFile.ts
··· 1 + import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 2 + import { addFile, deleteFile, getFiles } from "../api/file"; 3 + 4 + export const useAddFileMutation = ( 5 + sandboxId: string, 6 + path: string, 7 + content: string, 8 + ) => { 9 + const queryClient = useQueryClient(); 10 + return useMutation({ 11 + mutationKey: ["addFile"], 12 + mutationFn: async () => addFile(sandboxId, path, content), 13 + onSuccess: () => { 14 + queryClient.invalidateQueries({ queryKey: ["files"] }); 15 + }, 16 + }); 17 + }; 18 + 19 + export const useDeleteFileMutation = () => { 20 + const queryClient = useQueryClient(); 21 + return useMutation({ 22 + mutationKey: ["deleteFile"], 23 + mutationFn: async (id: string) => deleteFile(id), 24 + onSuccess: () => { 25 + queryClient.invalidateQueries({ queryKey: ["files"] }); 26 + }, 27 + }); 28 + }; 29 + 30 + export const useFilesQuery = () => 31 + useQuery({ 32 + queryKey: ["files"], 33 + queryFn: () => getFiles(), 34 + select: (response) => response.data, 35 + });
+35
apps/web/src/hooks/useSecret.ts
··· 1 + import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 2 + import { addSecret, deleteSecret, getSecrets } from "../api/secret"; 3 + 4 + export const useAddSecretMutation = ( 5 + sandboxId: string, 6 + name: string, 7 + value: string, 8 + ) => { 9 + const queryClient = useQueryClient(); 10 + return useMutation({ 11 + mutationKey: ["addSecret"], 12 + mutationFn: async () => addSecret(sandboxId, name, value), 13 + onSuccess: () => { 14 + queryClient.invalidateQueries({ queryKey: ["secrets"] }); 15 + }, 16 + }); 17 + }; 18 + 19 + export const useDeleteSecretMutation = () => { 20 + const queryClient = useQueryClient(); 21 + return useMutation({ 22 + mutationKey: ["deleteSecret"], 23 + mutationFn: async (id: string) => deleteSecret(id), 24 + onSuccess: () => { 25 + queryClient.invalidateQueries({ queryKey: ["secrets"] }); 26 + }, 27 + }); 28 + }; 29 + 30 + export const useSecretsQuery = () => 31 + useQuery({ 32 + queryKey: ["secrets"], 33 + queryFn: () => getSecrets(), 34 + select: (response) => response.data, 35 + });
+35
apps/web/src/hooks/useVariable.ts
··· 1 + import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 2 + import { addVariable, deleteVariable, getVariables } from "../api/variable"; 3 + 4 + export const useAddVariableMutation = ( 5 + sandboxId: string, 6 + name: string, 7 + value: string, 8 + ) => { 9 + const queryClient = useQueryClient(); 10 + return useMutation({ 11 + mutationKey: ["addVariable"], 12 + mutationFn: async () => addVariable(sandboxId, name, value), 13 + onSuccess: () => { 14 + queryClient.invalidateQueries({ queryKey: ["variables"] }); 15 + }, 16 + }); 17 + }; 18 + 19 + export const useDeleteVariableMutation = (id: string) => { 20 + const queryClient = useQueryClient(); 21 + return useMutation({ 22 + mutationKey: ["deleteVariable"], 23 + mutationFn: async () => deleteVariable(id), 24 + onSuccess: () => { 25 + queryClient.invalidateQueries({ queryKey: ["variables"] }); 26 + }, 27 + }); 28 + }; 29 + 30 + export const useVariablesQuery = () => 31 + useQuery({ 32 + queryKey: ["variables"], 33 + queryFn: async () => getVariables(), 34 + select: (response) => response.data, 35 + });
+33
apps/web/src/hooks/useVolume.ts
··· 1 + import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 2 + import { addVolume, deleteVolume, getVolumes } from "../api/volume"; 3 + 4 + export const useAddVolumeMutation = ( 5 + sandboxId: string, 6 + name: string, 7 + path: string, 8 + ) => { 9 + const queryClient = useQueryClient(); 10 + return useMutation({ 11 + mutationFn: async () => addVolume(sandboxId, name, path), 12 + onSuccess: () => { 13 + queryClient.invalidateQueries({ queryKey: ["volumes"] }); 14 + }, 15 + }); 16 + }; 17 + 18 + export const useDeleteVolumeMutation = () => { 19 + const queryClient = useQueryClient(); 20 + return useMutation({ 21 + mutationFn: async (id: string) => deleteVolume(id), 22 + onSuccess: () => { 23 + queryClient.invalidateQueries({ queryKey: ["volumes"] }); 24 + }, 25 + }); 26 + }; 27 + 28 + export const useVolumesQuery = () => 29 + useQuery({ 30 + queryKey: ["volumes"], 31 + queryFn: async () => getVolumes(), 32 + select: (response) => response.data, 33 + });