Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
ac0272c
Add PTY types to shared package
whoiskatrin Dec 18, 2025
99db220
Add PtyManager for container PTY lifecycle
whoiskatrin Dec 18, 2025
4f15ccd
Add PTY handler and route registration
whoiskatrin Dec 18, 2025
020f697
Add PTY message handling to WebSocket adapter
whoiskatrin Dec 18, 2025
6a66c9b
Add PTY methods to transport interface
whoiskatrin Dec 18, 2025
09eca50
Add PtyClient for SDK PTY operations
whoiskatrin Dec 18, 2025
4dede27
Add pty namespace to Sandbox class
whoiskatrin Dec 18, 2025
fbf4fa1
Add E2E tests for PTY workflow
whoiskatrin Dec 18, 2025
d43b3fd
Add changeset for PTY support
whoiskatrin Dec 18, 2025
40258ee
Skip PTY tests when PTY allocation fails
whoiskatrin Dec 19, 2025
405d7c7
Fix pty manager tests
whoiskatrin Dec 19, 2025
b9cc460
fix any types, logger
whoiskatrin Dec 19, 2025
794235b
fix silent logging
whoiskatrin Dec 19, 2025
2534791
fix pty tests for resizing
whoiskatrin Dec 21, 2025
1f311eb
update claude review yml
whoiskatrin Dec 21, 2025
93328a3
revert review change
whoiskatrin Dec 21, 2025
0e2e8ce
update http tests
whoiskatrin Dec 22, 2025
05c2342
more test updates
whoiskatrin Dec 22, 2025
8c0194b
remove the plugin for review
whoiskatrin Dec 22, 2025
f03f896
Add error handling to PTY callbacks and terminal operations
whoiskatrin Dec 22, 2025
2b20949
Improve
whoiskatrin Dec 22, 2025
0a7ca34
add structured exit codes
whoiskatrin Dec 22, 2025
119510e
change fire and forget strategy
whoiskatrin Dec 22, 2025
c9d5f1f
add more e2e tests
whoiskatrin Dec 22, 2025
2e04768
more fixes and tests
whoiskatrin Dec 22, 2025
d256de3
fix ws
whoiskatrin Dec 22, 2025
c85afc7
update error propagation
whoiskatrin Dec 22, 2025
407744d
update resizing tests
whoiskatrin Dec 23, 2025
b81cb3d
add collab terminal example
whoiskatrin Dec 23, 2025
7ddaaa9
Potential fix for code scanning alert no. 40: Insecure randomness
whoiskatrin Dec 23, 2025
76e628e
Potential fix for code scanning alert no. 41: Insecure randomness
whoiskatrin Dec 23, 2025
9c21e86
Update dependency in examples
whoiskatrin Dec 23, 2025
98754c7
Add PTY listeners cleanup
whoiskatrin Dec 23, 2025
7f6bce2
minor nits
whoiskatrin Dec 23, 2025
e213b13
update tests setup
whoiskatrin Dec 23, 2025
edd6a00
Add logging for PTY listener registration errors and improve error ha…
whoiskatrin Dec 23, 2025
76ceaa4
Enhance error handling and logging in WebSocketTransport and PtyHandl…
whoiskatrin Dec 23, 2025
bc159c6
Add connection-specific PTY listener cleanup on WebSocket close
whoiskatrin Dec 23, 2025
3cfc363
Remove outdated comment regarding connection cleanup functions in Web…
whoiskatrin Dec 23, 2025
51ee876
Fix error handling in PTY management by updating kill method to retur…
whoiskatrin Dec 23, 2025
d51f141
implement signal handling for Ctrl+C, Ctrl+Z, and Ctrl+\ in the PTY m…
whoiskatrin Jan 5, 2026
1ea663e
Merge main into pty-support
whoiskatrin Jan 5, 2026
499e3b7
Potential fix for code scanning alert no. 43: Insecure randomness
whoiskatrin Jan 5, 2026
bed6e22
Changes based on review comments
whoiskatrin Jan 8, 2026
b11b99f
Update dependencies and improve PTY handling in collaborative termina…
whoiskatrin Jan 8, 2026
1f0f7f6
Update PTY workflow tests to expect correct HTTP status codes for err…
whoiskatrin Jan 8, 2026
84a20a7
extractPtyId method to retrieve PTY IDs from responses, and update ha…
whoiskatrin Jan 8, 2026
0aa8995
Update PTY workflow tests to expect 'message' field in error response…
whoiskatrin Jan 8, 2026
fd1adc5
Fixed handlers for tests
whoiskatrin Jan 9, 2026
5918151
Use PR-specific Docker build cache scope to avoid cross-PR cache poll…
whoiskatrin Jan 9, 2026
a841501
Add debug logging to router for route registration and matching
whoiskatrin Jan 9, 2026
96edf05
Add INFO-level route logging to debug container caching issues
whoiskatrin Jan 9, 2026
a2898ea
Add retry logic for WebSocket server readiness in e2e tests
whoiskatrin Jan 9, 2026
f5f612a
Remove debug logging added during PTY route investigation
whoiskatrin Jan 9, 2026
099bb36
Fix sync-docs workflow to handle PR bodies with special characters
whoiskatrin Jan 9, 2026
a3f8b02
Fix sync-docs workflow shell escaping for opencode run
whoiskatrin Jan 9, 2026
10e3f36
Merge main into pty-support, resolve sync-docs conflict
whoiskatrin Jan 9, 2026
491b642
Merge remote-tracking branch 'origin/main' into pty-support
ghostwriternr Jan 13, 2026
5c7de26
Fix lint errors and align env type signatures
ghostwriternr Jan 13, 2026
07f3499
send heartbeat events to keep container alive
deathbyknowledge Jan 20, 2026
a152302
Merge remote-tracking branch 'origin/main' into pty-support
ghostwriternr Feb 6, 2026
961caa2
Add PTY terminal passthrough for browser clients
ghostwriternr Feb 6, 2026
9f6d732
Add tests and infrastructure for PTY terminal (#375)
ghostwriternr Feb 6, 2026
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
29 changes: 29 additions & 0 deletions .changeset/pty-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@cloudflare/sandbox': patch
---

Add terminal support for browser-based terminal UIs.

Build interactive terminal experiences by connecting xterm.js to container PTYs via WebSocket. Terminals reconnect automatically with output history preserved, and each session gets its own isolated terminal.

```typescript
// Proxy WebSocket to container terminal
return sandbox.terminal(request, { cols: 80, rows: 24 });

// Multiple isolated terminals in the same sandbox
const session = await sandbox.getSession('dev');
return session.terminal(request);
```

Also exports `@cloudflare/sandbox/xterm` with a `SandboxAddon` for xterm.js — handles WebSocket connection, reconnection with exponential backoff, and terminal resize forwarding.

```typescript
import { SandboxAddon } from '@cloudflare/sandbox/xterm';

const addon = new SandboxAddon({
getWebSocketUrl: ({ sandboxId, origin }) =>
`${origin}/ws/terminal?id=${sandboxId}`
});
terminal.loadAddon(addon);
addon.connect({ sandboxId: 'my-sandbox' });
```
9 changes: 7 additions & 2 deletions .github/workflows/pullrequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,16 @@ jobs:
run: |
echo "worker_url=https://${{ steps.env-name.outputs.worker_name }}.${{ env.CF_ACCOUNT_SUBDOMAIN }}.workers.dev" >> $GITHUB_OUTPUT

# Run E2E tests against deployed worker
- name: Install Playwright browsers
run: npx playwright install chromium

- name: Run E2E tests
run: npx vitest run --config vitest.e2e.config.ts
run: |
npx vitest run --config vitest.e2e.config.ts
npx playwright test --config tests/e2e/browser/playwright.config.ts
env:
TEST_WORKER_URL: ${{ steps.get-url.outputs.worker_url }}
TEST_SANDBOX_ID: browser-test-${{ github.run_id }}-${{ matrix.transport }}
CI: true
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,16 @@ jobs:
workingDirectory: tests/e2e/test-worker
command: deploy --name ${{ env.E2E_WORKER_NAME }}

- name: Install Playwright browsers
run: npx playwright install chromium

- name: Run E2E tests
env:
TEST_WORKER_URL: https://${{ env.E2E_WORKER_NAME }}.${{ env.CF_ACCOUNT_SUBDOMAIN }}.workers.dev
run: npm run test:e2e
TEST_SANDBOX_ID: browser-test-release-${{ github.run_id }}
run: |
npx vitest run --config vitest.e2e.config.ts
npx playwright test --config tests/e2e/browser/playwright.config.ts

- name: Cleanup test deployment
if: always()
Expand Down
9 changes: 6 additions & 3 deletions .opencode/skill/testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,14 @@ npm test -w @repo/sandbox-container # Container tests only
**Commands**:

```bash
npm run test:e2e # All E2E tests
npm run test:e2e -- -- tests/e2e/process-lifecycle-workflow.test.ts # Single file
npm run test:e2e -- -- tests/e2e/git-clone-workflow.test.ts -t 'test name' # Single test
npm run test:e2e # All E2E tests (vitest + browser)
npm run test:e2e:vitest -- -- tests/e2e/process-lifecycle-workflow.test.ts # Single vitest file
npm run test:e2e:vitest -- -- tests/e2e/git-clone-workflow.test.ts -t 'test name' # Single vitest test
npm run test:e2e:browser # Browser tests only (Playwright)
```

**Note**: Use `test:e2e:vitest` when filtering tests. The `test:e2e` wrapper doesn't support argument passthrough.

**Key patterns**:

- All tests share ONE container for performance
Expand Down
17 changes: 10 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,18 @@ npm run build:clean # Force rebuild without cache
See the **testing** skill for detailed guidance on unit vs E2E tests.

```bash
npm test # All unit tests
npm test -w @cloudflare/sandbox # SDK unit tests only
npm test -w @repo/sandbox-container # Container unit tests only

npm run test:e2e # All E2E tests (requires Docker)
npm run test:e2e -- -- tests/e2e/file.ts # Single E2E file
npm run test:e2e -- -- tests/e2e/file.ts -t 'test name' # Single test
npm test # All unit tests
npm test -w @cloudflare/sandbox # SDK unit tests only
npm test -w @repo/sandbox-container # Container unit tests only

npm run test:e2e # All E2E tests (vitest + browser, requires Docker)
npm run test:e2e:vitest -- -- tests/e2e/file.ts # Single vitest E2E file
npm run test:e2e:vitest -- -- tests/e2e/file.ts -t 'test name' # Single vitest E2E test
npm run test:e2e:browser # Browser E2E tests only (Playwright)
```

**Note**: Use `test:e2e:vitest` when filtering tests. The `test:e2e` wrapper doesn't support argument passthrough.

### Code Quality

```bash
Expand Down
13 changes: 9 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,18 @@ Located in `tests/e2e/`:
Run with: `npm run test:e2e`

```bash
# Run a single E2E test file
npm run test:e2e -- -- tests/e2e/process-lifecycle-workflow.test.ts
# Run a single vitest E2E test file
npm run test:e2e:vitest -- -- tests/e2e/process-lifecycle-workflow.test.ts

# Run a specific test within a file
npm run test:e2e -- -- tests/e2e/git-clone-workflow.test.ts -t 'should handle cloning to default directory'
# Run a specific vitest E2E test within a file
npm run test:e2e:vitest -- -- tests/e2e/git-clone-workflow.test.ts -t 'should handle cloning to default directory'

# Run browser E2E tests only (Playwright)
npm run test:e2e:browser
```

**Note**: Use `test:e2e:vitest` when filtering tests. The `test:e2e` wrapper runs both vitest and browser tests sequentially but doesn't support argument passthrough.

**See `docs/E2E_TESTING.md` for the complete guide on writing E2E tests.**

### Writing Tests
Expand Down
15 changes: 10 additions & 5 deletions docs/E2E_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,21 @@ test('should start server', async () => {
## Running Tests

```bash
# All E2E tests
# All E2E tests (runs vitest E2E tests, then browser E2E tests sequentially)
npm run test:e2e

# Single file
npm run test:e2e -- -- tests/e2e/process-lifecycle-workflow.test.ts
# Single vitest E2E file
npm run test:e2e:vitest -- -- tests/e2e/process-lifecycle-workflow.test.ts

# Single test by name
npm run test:e2e -- -- tests/e2e/git-clone-workflow.test.ts -t 'should clone repo'
# Single vitest E2E test by name
npm run test:e2e:vitest -- -- tests/e2e/git-clone-workflow.test.ts -t 'should clone repo'

# Browser E2E tests only (Playwright)
npm run test:e2e:browser
```

**Note on argument passthrough**: Use `test:e2e:vitest` (not `test:e2e`) when passing arguments to filter tests. The `test:e2e` script runs both vitest and browser tests sequentially but doesn't support argument passthrough due to turborepo limitations.

## Debugging

- Tests auto-retry once on failure (`retry: 1` in config)
Expand Down
18 changes: 18 additions & 0 deletions examples/collaborative-terminal/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.DS_Store
.env
/node_modules/
*.tsbuildinfo

# React Router
/.react-router/
/build/

# Cloudflare
.mf
.wrangler
.dev.vars*
worker-configuration.d.ts


!.dev.vars.example
!.env.example
5 changes: 5 additions & 0 deletions examples/collaborative-terminal/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"files.associations": {
"wrangler.json": "jsonc"
}
}
4 changes: 4 additions & 0 deletions examples/collaborative-terminal/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM docker.io/cloudflare/sandbox:0.7.0

# Required during local development to access exposed ports
EXPOSE 8080
111 changes: 111 additions & 0 deletions examples/collaborative-terminal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Collaborative Terminal

Real-time terminal sharing powered by Cloudflare Sandbox. Like Google Docs, but for your shell.

Multiple users join a room, share a single sandbox terminal, and see each other's input in real-time with presence indicators and typing notifications.

## Features

- **Shared terminal**: Every participant sees the same PTY output as it happens
- **Room system**: Create rooms, share links, browse and join active rooms
- **Presence**: See who's in the room with colored avatars and typing indicators
- **Session isolation**: Each room gets its own sandbox session so rooms don't interfere
- **Live room list**: Homepage updates in real-time as rooms are created or emptied

## Architecture

The example uses three Durable Objects working together:

```
Browser (xterm.js + SandboxAddon)
|
|-- /ws/room/:id ----> Room DO Presence, user list, typing
|
\-- /ws/terminal/:sessionId
|
v
Sandbox DO <---> Container PTY Direct WebSocket passthrough
|
RoomRegistry DO Tracks active rooms globally
```

**Terminal connection**: The browser connects directly to the sandbox container's PTY through a WebSocket that the SDK proxies transparently. There's no JSON protocol for terminal I/O — raw bytes flow between xterm.js and the container's PTY via `SandboxAddon`.

**Room connection**: A separate WebSocket to the Room DO handles presence (joins, leaves, typing indicators). This keeps the collaboration layer decoupled from terminal I/O.

## How It Works

### Server side

The Worker routes requests to the appropriate Durable Object:

```typescript
// Terminal: proxy WebSocket directly to a sandbox session's PTY
const sandbox = getSandbox(env.Sandbox, 'shared-terminal');
const session = await sandbox.getSession(sessionId);
return session.terminal(request);
```

Each room maps to a session ID (`room-${roomId}`), so different rooms get isolated shell environments within the same sandbox container.

### Client side

The terminal component uses `SandboxAddon` from `@cloudflare/sandbox/xterm` to handle the WebSocket connection, resize events, and reconnection:

```typescript
import { SandboxAddon } from '@cloudflare/sandbox/xterm';

const sandboxAddon = new SandboxAddon({
getWebSocketUrl: ({ origin, sessionId }) =>
`${origin}/ws/terminal/${sessionId}`,
onStateChange: (state) => setState(state)
});

terminal.loadAddon(sandboxAddon);
sandboxAddon.connect({ sandboxId: 'shared-terminal', sessionId });
```

## Getting Started

### Prerequisites

- Node.js 20+
- Docker (for local development)
- Cloudflare account with container access

### Install and run

```bash
npm install
npm run dev
```

Open http://localhost:5173, create a room, and share the link.

### Deploy

```bash
npm run deploy
```

After first deployment, wait 2-3 minutes for container provisioning before making requests.

## Project Structure

```
workers/
app.ts Main Worker — routes requests, creates rooms
room.ts Room DO — manages connected users and presence
registry.ts RoomRegistry DO — tracks active rooms globally
types/protocol.ts WebSocket message types for the room protocol

app/
routes/home.tsx Homepage with room creation and active room list
routes/room.tsx Room page with terminal, sidebar, and presence
components/
Terminal.client.tsx xterm.js setup with SandboxAddon
UserAvatars.tsx Colored avatar circles with typing indicators
hooks/
usePresence.ts WebSocket hook for room presence state
useActiveRooms.ts WebSocket hook for live room list from registry
```
14 changes: 14 additions & 0 deletions examples/collaborative-terminal/app/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@import "tailwindcss";

@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: "JetBrains Mono", "Fira Code", "SF Mono", Menlo, Monaco,
"Courier New", monospace;
}

html,
body {
@apply bg-zinc-950 text-zinc-100;
color-scheme: dark;
}
Loading
Loading