Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/cloudflare-exec-subshell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-cloudflare': patch
---

Run each `exec` and `spawn` command in a subshell. Cloudflare runs every command in one shared shell session, so a command that called `exit` ended the session and every later command failed. This broke the `@tanstack/ai-computer-sandbox` driver on Cloudflare.
5 changes: 5 additions & 0 deletions .changeset/computer-sandbox-keep-desktop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-computer-sandbox': minor
---

Add the `sandboxIds` option to `sandboxComputer()`. It keeps the sandbox of each thread and reconnects to it later with `provider.resume`, so a user who comes back to a thread gets the same desktop. A kept sandbox is not destroyed when its thread closes or goes idle.
8 changes: 8 additions & 0 deletions .changeset/computer-sandbox-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tanstack/ai-computer-sandbox': minor
'@tanstack/ai-sandbox': minor
---

`sandboxComputer()` takes a `snapshots` option. The driver saves the files of one folder, like a browser profile, when a thread closes or goes idle. It restores them into the next new sandbox of the thread, so a sign-in survives a sandbox that is gone. This works with every provider. Call `saveComputerSnapshot()` to save right away, for example after a sign-in.

`@tanstack/ai-sandbox` exports `captureSandboxFiles` and `restoreSandboxFiles`, the file capture and restore of portable snapshots.
98 changes: 97 additions & 1 deletion docs/computer-use/drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,13 @@ const computerUse = createComputerUse({

- `provider`: the sandbox provider. It makes one sandbox for each thread.
- `display`: the screen size of each desktop in pixels. The default is 1024 x 768.
- `sandboxIds`: keep the desktop of each thread and reconnect to it later. See [Keep the desktop for later](#keep-the-desktop-for-later).
- `snapshots`: save the files of a folder, and restore them into the next new sandbox of the thread. See [Restore files into a new sandbox](#restore-files-into-a-new-sandbox).

How the sandbox driver behaves:

- There is no `navigate` tool, because a desktop has no address. The model opens the browser on the desktop.
- When a thread closes or goes idle, `createComputerUse` destroys its sandbox.
- When a thread closes or goes idle, `createComputerUse` destroys its sandbox, unless you set `sandboxIds`. With `snapshots`, the driver saves the files first.
- One scroll step is 100 pixels.
- The driver quotes every key name and all typed text before they go to the shell.

Expand All @@ -155,6 +157,100 @@ RUN apt-get update && apt-get install -y xvfb xdotool scrot x11-utils

If a tool is missing, the driver throws an error with its name.

### Keep the desktop for later

Give `sandboxComputer()` a `sandboxIds` store. A user who comes back to a thread a day later gets the same desktop, for example with the browser still signed in.

1. Store one sandbox id for each thread, next to your threads.
2. Set the provider to pause the sandbox, not to kill it. With E2B, use `onTimeout: 'pause'`.

```ts
import { createComputerUse } from '@tanstack/ai-computer-use'
import { sandboxComputer } from '@tanstack/ai-computer-sandbox'
import { e2bSandbox } from '@tanstack/ai-sandbox-e2b'

declare const db: {
getSandboxId: (threadId: string) => Promise<string | undefined>
setSandboxId: (threadId: string, sandboxId: string) => Promise<void>
}

const computerUse = createComputerUse({
driver: sandboxComputer({
provider: e2bSandbox({
template: 'desktop',
timeoutMs: 15 * 60_000,
onTimeout: 'pause',
}),
sandboxIds: {
get: (threadId) => db.getSandboxId(threadId),
set: (threadId, sandboxId) => db.setSandboxId(threadId, sandboxId),
},
}),
})
```

With `sandboxIds`, the driver works like this:

- The first call of a thread creates a sandbox and stores its id.
- A later call reconnects to that sandbox with `provider.resume`. If the sandbox is gone, the driver creates a new one and stores the new id.
- The driver never destroys a kept sandbox. The provider timeout pauses or removes it.
- If the setup of a reconnected sandbox fails, the driver keeps the sandbox and throws the error. The next call tries again.

What each provider keeps:

- E2B with `onTimeout: 'pause'` keeps the memory and the disk. Open apps come back as they were.
- Docker keeps the container. A stopped container starts again with its disk, and the driver starts the display again.

> [!WARNING]
> A kept desktop holds what the user did in it, like signed-in browser sessions. Delete the sandbox when the user deletes the thread.

### Restore files into a new sandbox

Give `sandboxComputer()` a `snapshots` option. The driver saves the files of one folder, like the browser profile. When the sandbox of a thread is gone, the next new sandbox gets those files, so the browser is still signed in. This works with every provider, also when its disk does not last, like Cloudflare.

```ts
import { createComputerUse } from '@tanstack/ai-computer-use'
import { sandboxComputer } from '@tanstack/ai-computer-sandbox'
import { memorySandboxSnapshots } from '@tanstack/ai-sandbox'
import { e2bSandbox } from '@tanstack/ai-sandbox-e2b'
import type { SandboxHandle } from '@tanstack/ai-sandbox'

async function run(sandbox: SandboxHandle, command: string) {
const result = await sandbox.process.exec(command)
if (result.exitCode !== 0) throw new Error(result.stderr)
}

const computerUse = createComputerUse({
driver: sandboxComputer({
provider: e2bSandbox({ template: 'my-desktop' }),
snapshots: {
store: await memorySandboxSnapshots(),
root: '/home/user/browser-profile',
// Skip the caches. They are large, and the browser makes them again.
policy: { exclude: (path) => path.includes('Cache') },
// The browser writes its profile all the time. Close it cleanly during
// a save. A kill signal can stop it before it writes its cookies.
before: (sandbox) => run(sandbox, 'stop-browser'),
after: (sandbox) => run(sandbox, 'start-browser'),
},
}),
})
```

With `snapshots`, the driver works like this:

- When a thread closes or goes idle, the driver runs `before`, saves the files under `root`, then runs `after`.
- When the driver creates a new sandbox for a thread with a saved snapshot, it runs `before`, restores the files, then runs `after`.
- A sandbox that the driver reconnects to with `sandboxIds` keeps its own files. The driver restores nothing into it.
- Each save becomes the newest checkpoint of the thread. Use these stores only for computers.

To save at a moment that you must keep, like right after a sign-in, call `saveComputerSnapshot(sandbox, threadId, snapshots)` with the same options. The [sign-in tutorial](../tutorials/computer-use-login) does this.

`memorySandboxSnapshots()` keeps the files in memory, so they are lost when the server stops. For a user who comes back the next day, build the stores with `createSandboxSnapshots` on your database.

> [!WARNING]
> A snapshot of a browser profile holds its session cookies. A person with the cookies can use the signed-in accounts. Keep the blob store secret, and encrypt it in production.

Do you already have a desktop sandbox? See [Share one sandbox](#share-one-sandbox).

## Your own computer
Expand Down
16 changes: 13 additions & 3 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"label": "Overview",
"to": "getting-started/overview",
"addedAt": "2026-04-15",
"updatedAt": "2026-10-22"
"updatedAt": "2026-11-03"
},
{
"label": "Quick Start",
Expand Down Expand Up @@ -98,7 +98,12 @@
"label": "Computer Use",
"to": "tutorials/computer-use",
"addedAt": "2026-10-21",
"updatedAt": "2026-10-22"
"updatedAt": "2026-11-03"
},
{
"label": "Sign In Without the Model",
"to": "tutorials/computer-use-login",
"addedAt": "2026-11-03"
},
{
"label": "Subscribe to MCP Events",
Expand Down Expand Up @@ -151,6 +156,11 @@
"to": "framework/react/examples/computer-use",
"addedAt": "2026-10-21"
},
{
"label": "Computer Use Login",
"to": "framework/react/examples/computer-use-login",
"addedAt": "2026-11-03"
},
{
"label": "MCP Events",
"to": "framework/react/examples/mcp-events",
Expand Down Expand Up @@ -1049,7 +1059,7 @@
"label": "Drivers",
"to": "computer-use/drivers",
"addedAt": "2026-10-20",
"updatedAt": "2026-10-22"
"updatedAt": "2026-11-03"
},
{
"label": "Safety Checks",
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Each tutorial builds one small app from start to finish:
- [Subagents](../tutorials/subagents): a researcher, an SEO agent, and a writer in one blog-writing chat.
- [Build an MCP Server](../tutorials/mcp-server): serve a tool, a resource, and a prompt.
- [Computer Use](../tutorials/computer-use): let the model use a web page from screenshots.
- [Sign In Without the Model](../tutorials/computer-use-login): let the model sign in to a site, and keep the password away from it.

## Reference

Expand Down
Loading
Loading