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 sandbox base and collapsible sidebar

Order actor sandboxes by creation time and include the base field in
API responses. Update UI to support a collapsible sidebar (72px when
collapsed): hide logo, center toggle, and adjust layout margins/classes.

Add a "Base" column to Projects and Sandbox views, format createdAt with
dayjs, and wire start/stop sandbox actions with loading state and query
invalidation. Add baseSandbox to the Sandbox type.

+106 -45
+2 -1
apps/api/src/xrpc/io/pocketenv/actor/getActorSandboxes.ts
··· 56 56 eq(schema.users.handle, params.did), 57 57 ), 58 58 ) 59 - .orderBy(desc(schema.sandboxes.installs)) 59 + .orderBy(desc(schema.sandboxes.createdAt)) 60 60 .limit(params.limit ?? 30) 61 61 .offset(params.offset ?? 0) 62 62 .execute() ··· 89 89 sandboxes: sandboxes.map((sandbox) => ({ 90 90 id: sandbox.id, 91 91 name: sandbox.name, 92 + base: sandbox.base, 92 93 displayName: sandbox.displayName, 93 94 description: sandbox.description!, 94 95 logo: sandbox.logo!,
+44 -32
apps/web/src/components/sidebar/Sidebar.tsx
··· 23 23 return ( 24 24 <div> 25 25 <aside 26 - className={`fixed z-50 h-screen bg-base-100 w-64 transition-transform duration-300 ease-in-out ${ 27 - isCollapsed ? "-translate-x-48" : "translate-x-0" 26 + className={`fixed z-50 h-screen bg-base-100 ${ 27 + isCollapsed ? "w-[72px]" : "w-64" 28 28 }`} 29 29 role="dialog" 30 30 tabIndex={-1} 31 31 > 32 - <div className="drawer-body px-2 pt-4 bg-base-100 h-full"> 32 + <div className="drawer-body px-2 pt-4 bg-base-100 overflow-hidden"> 33 33 <div className="flex items-center"> 34 - <div className="flex-1"> 35 - <Link to="/projects"> 36 - <div className="mb-[30px] ml-[5px]"> 37 - <img src={Logo} className="max-h-[40px] mr-[15px]" /> 38 - </div> 39 - </Link> 40 - </div> 34 + {!isCollapsed && ( 35 + <div className="flex-1"> 36 + <Link to="/projects"> 37 + <div className="mb-[30px] ml-[5px]"> 38 + <img src={Logo} className="max-h-[40px] mr-[15px]" /> 39 + </div> 40 + </Link> 41 + </div> 42 + )} 41 43 <button 42 - className="mb-[25px] opacity-70 hover:opacity-100 transition-opacity" 44 + className={`mb-[25px] opacity-70 hover:opacity-100 ${ 45 + isCollapsed ? "mx-auto mt-2" : "" 46 + }`} 43 47 onClick={toggleSidebar} 44 48 aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"} 45 49 > 46 50 <span 47 - className={`icon-[hugeicons--panel-left] size-5.5 text-white transition-transform duration-300 ${ 51 + className={`icon-[hugeicons--panel-left] size-5.5 text-white ${ 48 52 isCollapsed ? "rotate-180" : "" 49 53 }`} 50 54 ></span> ··· 54 58 <li> 55 59 <Link 56 60 to="/projects" 57 - className={ 61 + className={`${ 58 62 isActive("/projects") 59 63 ? "active bg-white/7 text-[#00e8c6]! font-semibold rounded-full" 60 64 : "rounded-full hover:text-white" 61 - } 62 - title="Projects" 65 + } ${isCollapsed ? "justify-center px-2" : ""}`} 66 + title={isCollapsed ? "Projects" : undefined} 63 67 > 64 - <span className="icon-[tabler--box] size-6 mr-2"></span> 65 - Projects 68 + <span 69 + className={`icon-[tabler--box] size-6 ${isCollapsed ? "" : "mr-2"}`} 70 + ></span> 71 + {!isCollapsed && "Projects"} 66 72 </Link> 67 73 </li> 68 74 <li> 69 75 <Link 70 76 to="/snapshots" 71 - className={ 77 + className={`${ 72 78 isActive("/snapshots") 73 79 ? "active bg-white/7 text-[#00e8c6]! font-semibold rounded-full" 74 80 : "rounded-full hover:text-white" 75 - } 76 - title="Snapshots" 81 + } ${isCollapsed ? "justify-center px-2" : ""}`} 82 + title={isCollapsed ? "Snapshots" : undefined} 77 83 > 78 - <span className="icon-[tabler--device-floppy] size-6 mr-2"></span> 79 - Snapshots 84 + <span 85 + className={`icon-[tabler--device-floppy] size-6 ${isCollapsed ? "" : "mr-2"}`} 86 + ></span> 87 + {!isCollapsed && "Snapshots"} 80 88 </Link> 81 89 </li> 82 90 <li> 83 91 <Link 84 92 to="/volumes" 85 - className={ 93 + className={`${ 86 94 isActive("/volumes") 87 95 ? "active bg-white/7 text-[#00e8c6]! font-semibold rounded-full" 88 96 : "rounded-full hover:text-white" 89 - } 90 - title="Volumes" 97 + } ${isCollapsed ? "justify-center px-2" : ""}`} 98 + title={isCollapsed ? "Volumes" : undefined} 91 99 > 92 - <span className="icon-[icon-park-outline--hard-disk] size-5 mr-2"></span> 93 - Volumes 100 + <span 101 + className={`icon-[icon-park-outline--hard-disk] size-5 ${isCollapsed ? "" : "mr-2"}`} 102 + ></span> 103 + {!isCollapsed && "Volumes"} 94 104 </Link> 95 105 </li> 96 106 <li> 97 107 <Link 98 108 to="/secrets" 99 - className={ 109 + className={`${ 100 110 isActive("/secrets") 101 111 ? "active bg-white/7 text-[#00e8c6]! font-semibold rounded-full" 102 112 : "rounded-full hover:text-white" 103 - } 104 - title="Secrets" 113 + } ${isCollapsed ? "justify-center px-2" : ""}`} 114 + title={isCollapsed ? "Secrets" : undefined} 105 115 > 106 - <span className="icon-[tabler--key] size-6 mr-2"></span> 107 - Secrets 116 + <span 117 + className={`icon-[tabler--key] size-6 ${isCollapsed ? "" : "mr-2"}`} 118 + ></span> 119 + {!isCollapsed && "Secrets"} 108 120 </Link> 109 121 </li> 110 122 </ul>
+2 -2
apps/web/src/layouts/Main.tsx
··· 37 37 <div className="flex min-h-screen bg-base-100"> 38 38 <Sidebar /> 39 39 <div 40 - className={`flex flex-col flex-1 bg-base-100 transition-all duration-300 ease-in-out ${ 41 - isCollapsed ? "sm:ml-16" : "sm:ml-64" 40 + className={`flex flex-col flex-1 bg-base-100 ${ 41 + isCollapsed ? "sm:ml-[72px]" : "sm:ml-64" 42 42 }`} 43 43 > 44 44 <Navbar title={title} />
+54 -10
apps/web/src/pages/projects/Project/Project.tsx
··· 1 + import { useNavigate } from "@tanstack/react-router"; 1 2 import type { Sandbox } from "../../../types/sandbox"; 2 3 import _ from "lodash"; 4 + import dayjs from "dayjs"; 5 + import { useState } from "react"; 6 + import { 7 + useStartSandboxMutation, 8 + useStopSandboxMutation, 9 + } from "../../../hooks/useSandbox"; 10 + import { useQueryClient } from "@tanstack/react-query"; 11 + import { useAtomValue } from "jotai"; 12 + import { profileAtom } from "../../../atoms/profile"; 3 13 4 14 export type ProjectProps = { 5 15 sandbox: Sandbox; 6 16 }; 7 17 8 18 function Project({ sandbox }: ProjectProps) { 9 - const onPlay = (e: React.MouseEvent) => { 19 + const navigate = useNavigate(); 20 + const queryClient = useQueryClient(); 21 + const profile = useAtomValue(profileAtom); 22 + const { mutateAsync: stopSandbox } = useStopSandboxMutation(); 23 + const { mutateAsync: startSandbox } = useStartSandboxMutation(); 24 + const [displayLoading, setDisplayLoading] = useState(false); 25 + 26 + const onPlay = async (e: React.MouseEvent) => { 10 27 e.stopPropagation(); 28 + setDisplayLoading(true); 29 + await startSandbox(sandbox.id); 30 + queryClient.invalidateQueries({ 31 + queryKey: ["actorSandboxes", profile?.did], 32 + }); 33 + setDisplayLoading(false); 11 34 }; 12 35 13 - const onStop = (e: React.MouseEvent) => { 36 + const onStop = async (e: React.MouseEvent) => { 14 37 e.stopPropagation(); 38 + setDisplayLoading(true); 39 + await stopSandbox(sandbox.id); 40 + queryClient.invalidateQueries({ 41 + queryKey: ["actorSandboxes", profile?.did], 42 + }); 43 + setDisplayLoading(false); 15 44 }; 16 45 17 - const onOpenProject = () => {}; 46 + const onOpenProject = () => { 47 + navigate({ 48 + to: `/${sandbox.uri.split("at://")[1].replace("io.pocketenv.", "")}`, 49 + }); 50 + }; 51 + 52 + const onOpenContextMenu = (e: React.MouseEvent) => { 53 + e.stopPropagation(); 54 + }; 18 55 19 56 return ( 20 57 <tr className="cursor-pointer" onClick={onOpenProject}> 21 58 <td>{sandbox.name}</td> 59 + <td>{sandbox.base}</td> 22 60 <td> 23 61 <span 24 62 className={`badge badge-soft ${sandbox?.status === "RUNNING" ? "badge-success" : ""} rounded-full ${sandbox.status === "RUNNING" ? "bg-green-400/10" : "bg-white/15 rounded"}`} ··· 34 72 {sandbox?.memory} GiB RAM 35 73 </span> 36 74 </td> 37 - <td>March 1, 2024</td> 75 + <td>{dayjs(sandbox.createdAt).format("M/D/YYYY, h:mm:ss A")}</td> 38 76 <td> 39 - {sandbox.status === "RUNNING" && ( 77 + {!displayLoading && sandbox.status === "RUNNING" && ( 40 78 <button className="btn btn-circle btn-text btn-sm" onClick={onStop}> 41 - <span className="icon-[tabler--player-stop] size-5"></span> 79 + <span className="icon-[tabler--player-stop] size-5 hover:text-white"></span> 42 80 </button> 43 81 )} 44 - {sandbox.status !== "RUNNING" && ( 82 + {!displayLoading && sandbox.status !== "RUNNING" && ( 45 83 <button className="btn btn-circle btn-text btn-sm" onClick={onPlay}> 46 - <span className="icon-[tabler--player-play] size-5"></span> 84 + <span className="icon-[tabler--player-play] size-5 hover:text-white"></span> 47 85 </button> 48 86 )} 49 - <button className="btn btn-circle btn-text btn-sm"> 50 - <span className="icon-[tabler--dots-vertical] size-5"></span> 87 + {displayLoading && ( 88 + <span className="loading loading-spinner loading-sm btn-text mr-[10px]"></span> 89 + )} 90 + <button 91 + className="btn btn-circle btn-text btn-sm" 92 + onClick={onOpenContextMenu} 93 + > 94 + <span className="icon-[tabler--dots-vertical] size-5 hover:text-white"></span> 51 95 </button> 52 96 </td> 53 97 </tr>
+1
apps/web/src/pages/projects/Projects.tsx
··· 16 16 <thead> 17 17 <tr> 18 18 <th className="normal-case text-[14px]">Name</th> 19 + <th className="normal-case text-[14px]">Base</th> 19 20 <th className="normal-case text-[14px]">State</th> 20 21 <th className="normal-case text-[14px]">Resources</th> 21 22 <th className="normal-case text-[14px]">Created At</th>
+2
apps/web/src/pages/sandbox/Sandbox.tsx
··· 99 99 <thead> 100 100 <tr> 101 101 <th>Status</th> 102 + <th>Base</th> 102 103 <th>Started</th> 103 104 <th>Timeout</th> 104 105 <th>Resources</th> ··· 113 114 {_.upperFirst(_.camelCase(data.sandbox.status))} 114 115 </span> 115 116 </td> 117 + <td>{data.sandbox.baseSandbox}</td> 116 118 <td> 117 119 {data?.sandbox?.startedAt 118 120 ? dayjs(data.sandbox.startedAt).fromNow()
+1
apps/web/src/types/sandbox.ts
··· 3 3 export type Sandbox = { 4 4 id: string; 5 5 name: string; 6 + baseSandbox: string; 6 7 displayName: string; 7 8 uri: string; 8 9 description?: string;