chore: recover edinburgh workspace state

This commit is contained in:
Nathan Flurry 2026-03-09 19:59:35 -07:00
parent 5d65013aa5
commit bda9c50693
179 changed files with 0 additions and 44514 deletions

View file

@ -1,82 +0,0 @@
[workspace]
resolver = "2"
members = ["server/packages/*", "gigacode"]
[workspace.package]
version = "0.3.0"
edition = "2021"
authors = [ "Rivet Gaming, LLC <developer@rivet.gg>" ]
license = "Apache-2.0"
repository = "https://github.com/rivet-dev/sandbox-agent"
description = "Universal API for automatic coding agents in sandboxes. Supports Claude Code, Codex, OpenCode, and Amp."
[workspace.dependencies]
# Internal crates
sandbox-agent = { version = "0.3.0", path = "server/packages/sandbox-agent" }
sandbox-agent-error = { version = "0.3.0", path = "server/packages/error" }
sandbox-agent-agent-management = { version = "0.3.0", path = "server/packages/agent-management" }
sandbox-agent-agent-credentials = { version = "0.3.0", path = "server/packages/agent-credentials" }
sandbox-agent-opencode-adapter = { version = "0.3.0", path = "server/packages/opencode-adapter" }
sandbox-agent-opencode-server-manager = { version = "0.3.0", path = "server/packages/opencode-server-manager" }
acp-http-adapter = { version = "0.3.0", path = "server/packages/acp-http-adapter" }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Error handling
thiserror = "1.0"
# Schema generation
schemars = "0.8"
utoipa = { version = "4.2", features = ["axum_extras"] }
# Web framework
axum = { version = "0.7", features = ["ws"] }
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.5", features = ["cors", "trace"] }
# Async runtime
tokio = { version = "1.36", features = ["macros", "rt-multi-thread", "signal", "time"] }
tokio-stream = { version = "0.1", features = ["sync"] }
futures = "0.3"
# HTTP client
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls", "stream"] }
# CLI
clap = { version = "4.5", features = ["derive"] }
# Logging
tracing = "0.1"
tracing-logfmt = "0.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Time/date
time = { version = "0.3", features = ["parsing", "formatting"] }
chrono = { version = "0.4", features = ["serde"] }
# Filesystem/paths
dirs = "5.0"
tempfile = "3.10"
# Archive handling
flate2 = "1.0"
tar = "0.4"
zip = { version = "0.6", default-features = false, features = ["deflate"] }
# Misc
url = "2.5"
regress = "0.10"
include_dir = "0.7"
base64 = "0.22"
toml_edit = "0.22"
# Code generation (build deps)
typify = "0.4"
prettyplease = "0.2"
syn = "2.0"
# Testing
http-body-util = "0.1"
insta = { version = "1.41", features = ["yaml"] }

190
LICENSE
View file

@ -1,190 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2026 Sandbox Agent Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,76 +0,0 @@
#!/bin/bash
set -euo pipefail
TARGET=${1:-x86_64-unknown-linux-musl}
VERSION=${2:-}
# Build arguments for Docker
BUILD_ARGS=""
if [ -n "$VERSION" ]; then
BUILD_ARGS="--build-arg SANDBOX_AGENT_VERSION=$VERSION"
echo "Building with version: $VERSION"
fi
case $TARGET in
x86_64-unknown-linux-musl)
echo "Building for Linux x86_64 musl"
DOCKERFILE="linux-x86_64.Dockerfile"
TARGET_STAGE="x86_64-builder"
BINARY="sandbox-agent-$TARGET"
GIGACODE="gigacode-$TARGET"
;;
aarch64-unknown-linux-musl)
echo "Building for Linux aarch64 musl"
DOCKERFILE="linux-aarch64.Dockerfile"
TARGET_STAGE="aarch64-builder"
BINARY="sandbox-agent-$TARGET"
GIGACODE="gigacode-$TARGET"
;;
x86_64-pc-windows-gnu)
echo "Building for Windows x86_64"
DOCKERFILE="windows.Dockerfile"
TARGET_STAGE=""
BINARY="sandbox-agent-$TARGET.exe"
GIGACODE="gigacode-$TARGET.exe"
;;
x86_64-apple-darwin)
echo "Building for macOS x86_64"
DOCKERFILE="macos-x86_64.Dockerfile"
TARGET_STAGE="x86_64-builder"
BINARY="sandbox-agent-$TARGET"
GIGACODE="gigacode-$TARGET"
;;
aarch64-apple-darwin)
echo "Building for macOS aarch64"
DOCKERFILE="macos-aarch64.Dockerfile"
TARGET_STAGE="aarch64-builder"
BINARY="sandbox-agent-$TARGET"
GIGACODE="gigacode-$TARGET"
;;
*)
echo "Unsupported target: $TARGET"
exit 1
;;
esac
DOCKER_BUILDKIT=1
if [ -n "$TARGET_STAGE" ]; then
docker build --target "$TARGET_STAGE" $BUILD_ARGS -f "docker/release/$DOCKERFILE" -t "sandbox-agent-builder-$TARGET" .
else
docker build $BUILD_ARGS -f "docker/release/$DOCKERFILE" -t "sandbox-agent-builder-$TARGET" .
fi
CONTAINER_ID=$(docker create "sandbox-agent-builder-$TARGET")
mkdir -p dist
docker cp "$CONTAINER_ID:/artifacts/$BINARY" "dist/"
docker cp "$CONTAINER_ID:/artifacts/$GIGACODE" "dist/"
docker rm "$CONTAINER_ID"
if [[ "$BINARY" != *.exe ]]; then
chmod +x "dist/$BINARY"
chmod +x "dist/$GIGACODE"
fi
echo "Binary saved to: dist/$BINARY"
echo "Binary saved to: dist/$GIGACODE"

View file

@ -1,84 +0,0 @@
# syntax=docker/dockerfile:1.10.0
# Build inspector frontend
FROM node:22-alpine AS inspector-build
WORKDIR /app
RUN npm install -g pnpm
# Copy package files for workspaces
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
COPY sdks/react/package.json ./sdks/react/
COPY sdks/typescript/package.json ./sdks/typescript/
# Install dependencies
RUN pnpm install --filter @sandbox-agent/inspector...
# Copy SDK source (with pre-generated types from docs/openapi.json)
COPY docs/openapi.json ./docs/
COPY sdks/cli-shared ./sdks/cli-shared
COPY sdks/acp-http-client ./sdks/acp-http-client
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
COPY sdks/react ./sdks/react
COPY sdks/typescript ./sdks/typescript
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
RUN cd sdks/cli-shared && pnpm exec tsup
RUN cd sdks/acp-http-client && pnpm exec tsup
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
RUN cd sdks/persist-indexeddb && pnpm exec tsup
RUN cd sdks/react && pnpm exec tsup
# Copy inspector source and build
COPY frontend/packages/inspector ./frontend/packages/inspector
RUN cd frontend/packages/inspector && pnpm exec vite build
# Use Alpine with native musl for ARM64 builds (runs natively on ARM64 runner)
FROM rust:1.88-alpine AS aarch64-builder
# Accept version as build arg
ARG SANDBOX_AGENT_VERSION
ENV SANDBOX_AGENT_VERSION=${SANDBOX_AGENT_VERSION}
# Install dependencies
RUN apk add --no-cache \
musl-dev \
clang \
llvm-dev \
openssl-dev \
openssl-libs-static \
pkgconfig \
git \
curl \
build-base
# Add musl target
RUN rustup target add aarch64-unknown-linux-musl
# Set environment variables for native musl build
ENV CARGO_INCREMENTAL=0 \
CARGO_NET_GIT_FETCH_WITH_CLI=true \
RUSTFLAGS="-C target-feature=+crt-static"
WORKDIR /build
# Copy the source code
COPY . .
# Copy pre-built inspector frontend
COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist
# Build for Linux with musl (static binary) - aarch64
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \
cargo build -p sandbox-agent -p gigacode --release --target aarch64-unknown-linux-musl && \
mkdir -p /artifacts && \
cp target/aarch64-unknown-linux-musl/release/sandbox-agent /artifacts/sandbox-agent-aarch64-unknown-linux-musl && \
cp target/aarch64-unknown-linux-musl/release/gigacode /artifacts/gigacode-aarch64-unknown-linux-musl
# Default command to show help
CMD ["ls", "-la", "/artifacts"]

View file

@ -1,118 +0,0 @@
# syntax=docker/dockerfile:1.10.0
# Build inspector frontend
FROM node:22-alpine AS inspector-build
WORKDIR /app
RUN npm install -g pnpm
# Copy package files for workspaces
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
COPY sdks/react/package.json ./sdks/react/
COPY sdks/typescript/package.json ./sdks/typescript/
# Install dependencies
RUN pnpm install --filter @sandbox-agent/inspector...
# Copy SDK source (with pre-generated types from docs/openapi.json)
COPY docs/openapi.json ./docs/
COPY sdks/cli-shared ./sdks/cli-shared
COPY sdks/acp-http-client ./sdks/acp-http-client
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
COPY sdks/react ./sdks/react
COPY sdks/typescript ./sdks/typescript
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
RUN cd sdks/cli-shared && pnpm exec tsup
RUN cd sdks/acp-http-client && pnpm exec tsup
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
RUN cd sdks/persist-indexeddb && pnpm exec tsup
RUN cd sdks/react && pnpm exec tsup
# Copy inspector source and build
COPY frontend/packages/inspector ./frontend/packages/inspector
RUN cd frontend/packages/inspector && pnpm exec vite build
FROM rust:1.88.0 AS base
# Install dependencies
RUN apt-get update && apt-get install -y \
musl-tools \
musl-dev \
llvm-14-dev \
libclang-14-dev \
clang-14 \
libssl-dev \
pkg-config \
ca-certificates \
g++ \
g++-multilib \
git \
curl && \
rm -rf /var/lib/apt/lists/* && \
wget -q https://github.com/cross-tools/musl-cross/releases/latest/download/x86_64-unknown-linux-musl.tar.xz && \
tar -xf x86_64-unknown-linux-musl.tar.xz -C /opt/ && \
rm x86_64-unknown-linux-musl.tar.xz
# Install musl targets
RUN rustup target add x86_64-unknown-linux-musl
# Set environment variables
ENV PATH="/opt/x86_64-unknown-linux-musl/bin:$PATH" \
LIBCLANG_PATH=/usr/lib/llvm-14/lib \
CLANG_PATH=/usr/bin/clang-14 \
CC_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-gcc \
CXX_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-g++ \
AR_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-ar \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-unknown-linux-musl-gcc \
CARGO_INCREMENTAL=0 \
RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-static-libgcc" \
CARGO_NET_GIT_FETCH_WITH_CLI=true
# Set working directory
WORKDIR /build
# Build for x86_64
FROM base AS x86_64-builder
# Accept version as build arg
ARG SANDBOX_AGENT_VERSION
ENV SANDBOX_AGENT_VERSION=${SANDBOX_AGENT_VERSION}
# Set up OpenSSL for x86_64 musl target
ENV SSL_VER=1.1.1w
RUN wget https://www.openssl.org/source/openssl-$SSL_VER.tar.gz \
&& tar -xzf openssl-$SSL_VER.tar.gz \
&& cd openssl-$SSL_VER \
&& ./Configure no-shared no-async --prefix=/musl --openssldir=/musl/ssl linux-x86_64 \
&& make -j$(nproc) \
&& make install_sw \
&& cd .. \
&& rm -rf openssl-$SSL_VER*
# Configure OpenSSL env vars for the build
ENV OPENSSL_DIR=/musl \
OPENSSL_INCLUDE_DIR=/musl/include \
OPENSSL_LIB_DIR=/musl/lib \
PKG_CONFIG_ALLOW_CROSS=1
# Copy the source code
COPY . .
# Copy pre-built inspector frontend
COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist
# Build for Linux with musl (static binary) - x86_64
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \
cargo build -p sandbox-agent -p gigacode --release --target x86_64-unknown-linux-musl && \
mkdir -p /artifacts && \
cp target/x86_64-unknown-linux-musl/release/sandbox-agent /artifacts/sandbox-agent-x86_64-unknown-linux-musl && \
cp target/x86_64-unknown-linux-musl/release/gigacode /artifacts/gigacode-x86_64-unknown-linux-musl
# Default command to show help
CMD ["ls", "-la", "/artifacts"]

View file

@ -1,116 +0,0 @@
# syntax=docker/dockerfile:1.10.0
# Build inspector frontend
FROM node:22-alpine AS inspector-build
WORKDIR /app
RUN npm install -g pnpm
# Copy package files for workspaces
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
COPY sdks/react/package.json ./sdks/react/
COPY sdks/typescript/package.json ./sdks/typescript/
# Install dependencies
RUN pnpm install --filter @sandbox-agent/inspector...
# Copy SDK source (with pre-generated types from docs/openapi.json)
COPY docs/openapi.json ./docs/
COPY sdks/cli-shared ./sdks/cli-shared
COPY sdks/acp-http-client ./sdks/acp-http-client
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
COPY sdks/react ./sdks/react
COPY sdks/typescript ./sdks/typescript
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
RUN cd sdks/cli-shared && pnpm exec tsup
RUN cd sdks/acp-http-client && pnpm exec tsup
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
RUN cd sdks/persist-indexeddb && pnpm exec tsup
RUN cd sdks/react && pnpm exec tsup
# Copy inspector source and build
COPY frontend/packages/inspector ./frontend/packages/inspector
RUN cd frontend/packages/inspector && pnpm exec vite build
FROM rust:1.88.0 AS base
# Install dependencies
RUN apt-get update && apt-get install -y \
clang \
cmake \
patch \
libxml2-dev \
wget \
xz-utils \
curl \
git && \
rm -rf /var/lib/apt/lists/*
# Install osxcross
RUN git config --global --add safe.directory '*' && \
git clone https://github.com/tpoechtrager/osxcross /root/osxcross && \
cd /root/osxcross && \
wget -nc https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.3.sdk.tar.xz && \
mv MacOSX11.3.sdk.tar.xz tarballs/ && \
UNATTENDED=yes OSX_VERSION_MIN=10.7 ./build.sh
# Add osxcross to PATH
ENV PATH="/root/osxcross/target/bin:$PATH"
# Tell Clang/bindgen to use the macOS SDK, and nudge Clang to prefer osxcross binutils.
ENV OSXCROSS_SDK=MacOSX11.3.sdk \
SDKROOT=/root/osxcross/target/SDK/MacOSX11.3.sdk \
BINDGEN_EXTRA_CLANG_ARGS_aarch64_apple_darwin="--sysroot=/root/osxcross/target/SDK/MacOSX11.3.sdk -isystem /root/osxcross/target/SDK/MacOSX11.3.sdk/usr/include" \
CFLAGS_aarch64_apple_darwin="-B/root/osxcross/target/bin" \
CXXFLAGS_aarch64_apple_darwin="-B/root/osxcross/target/bin" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=aarch64-apple-darwin20.4-clang \
CC_aarch64_apple_darwin=aarch64-apple-darwin20.4-clang \
CXX_aarch64_apple_darwin=aarch64-apple-darwin20.4-clang++ \
AR_aarch64_apple_darwin=aarch64-apple-darwin20.4-ar \
RANLIB_aarch64_apple_darwin=aarch64-apple-darwin20.4-ranlib \
MACOSX_DEPLOYMENT_TARGET=10.14 \
CARGO_INCREMENTAL=0 \
CARGO_NET_GIT_FETCH_WITH_CLI=true
# Set working directory
WORKDIR /build
# Build for ARM64 macOS
FROM base AS aarch64-builder
# Accept version as build arg
ARG SANDBOX_AGENT_VERSION
ENV SANDBOX_AGENT_VERSION=${SANDBOX_AGENT_VERSION}
# Install macOS ARM64 target
RUN rustup target add aarch64-apple-darwin
# Configure Cargo for cross-compilation (ARM64)
RUN mkdir -p /root/.cargo && \
echo '\
[target.aarch64-apple-darwin]\n\
linker = "aarch64-apple-darwin20.4-clang"\n\
ar = "aarch64-apple-darwin20.4-ar"\n\
' > /root/.cargo/config.toml
# Copy the source code
COPY . .
# Copy pre-built inspector frontend
COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist
# Build for ARM64 macOS
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \
cargo build -p sandbox-agent -p gigacode --release --target aarch64-apple-darwin && \
mkdir -p /artifacts && \
cp target/aarch64-apple-darwin/release/sandbox-agent /artifacts/sandbox-agent-aarch64-apple-darwin && \
cp target/aarch64-apple-darwin/release/gigacode /artifacts/gigacode-aarch64-apple-darwin
# Default command to show help
CMD ["ls", "-la", "/artifacts"]

View file

@ -1,116 +0,0 @@
# syntax=docker/dockerfile:1.10.0
# Build inspector frontend
FROM node:22-alpine AS inspector-build
WORKDIR /app
RUN npm install -g pnpm
# Copy package files for workspaces
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
COPY sdks/react/package.json ./sdks/react/
COPY sdks/typescript/package.json ./sdks/typescript/
# Install dependencies
RUN pnpm install --filter @sandbox-agent/inspector...
# Copy SDK source (with pre-generated types from docs/openapi.json)
COPY docs/openapi.json ./docs/
COPY sdks/cli-shared ./sdks/cli-shared
COPY sdks/acp-http-client ./sdks/acp-http-client
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
COPY sdks/react ./sdks/react
COPY sdks/typescript ./sdks/typescript
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
RUN cd sdks/cli-shared && pnpm exec tsup
RUN cd sdks/acp-http-client && pnpm exec tsup
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
RUN cd sdks/persist-indexeddb && pnpm exec tsup
RUN cd sdks/react && pnpm exec tsup
# Copy inspector source and build
COPY frontend/packages/inspector ./frontend/packages/inspector
RUN cd frontend/packages/inspector && pnpm exec vite build
FROM rust:1.88.0 AS base
# Install dependencies
RUN apt-get update && apt-get install -y \
clang \
cmake \
patch \
libxml2-dev \
wget \
xz-utils \
curl \
git && \
rm -rf /var/lib/apt/lists/*
# Install osxcross
RUN git config --global --add safe.directory '*' && \
git clone https://github.com/tpoechtrager/osxcross /root/osxcross && \
cd /root/osxcross && \
wget -nc https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.3.sdk.tar.xz && \
mv MacOSX11.3.sdk.tar.xz tarballs/ && \
UNATTENDED=yes OSX_VERSION_MIN=10.7 ./build.sh
# Add osxcross to PATH
ENV PATH="/root/osxcross/target/bin:$PATH"
# Tell Clang/bindgen to use the macOS SDK, and nudge Clang to prefer osxcross binutils.
ENV OSXCROSS_SDK=MacOSX11.3.sdk \
SDKROOT=/root/osxcross/target/SDK/MacOSX11.3.sdk \
BINDGEN_EXTRA_CLANG_ARGS_X86_64_apple_darwin="--sysroot=/root/osxcross/target/SDK/MacOSX11.3.sdk -isystem /root/osxcross/target/SDK/MacOSX11.3.sdk/usr/include" \
CFLAGS_X86_64_apple_darwin="-B/root/osxcross/target/bin" \
CXXFLAGS_X86_64_apple_darwin="-B/root/osxcross/target/bin" \
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER=x86_64-apple-darwin20.4-clang \
CC_x86_64_apple_darwin=x86_64-apple-darwin20.4-clang \
CXX_x86_64_apple_darwin=x86_64-apple-darwin20.4-clang++ \
AR_X86_64_apple_darwin=x86_64-apple-darwin20.4-ar \
RANLIB_X86_64_apple_darwin=x86_64-apple-darwin20.4-ranlib \
MACOSX_DEPLOYMENT_TARGET=10.14 \
CARGO_INCREMENTAL=0 \
CARGO_NET_GIT_FETCH_WITH_CLI=true
# Set working directory
WORKDIR /build
# Build for x86_64 macOS
FROM base AS x86_64-builder
# Accept version as build arg
ARG SANDBOX_AGENT_VERSION
ENV SANDBOX_AGENT_VERSION=${SANDBOX_AGENT_VERSION}
# Install macOS x86_64 target
RUN rustup target add x86_64-apple-darwin
# Configure Cargo for cross-compilation (x86_64)
RUN mkdir -p /root/.cargo && \
echo '\
[target.x86_64-apple-darwin]\n\
linker = "x86_64-apple-darwin20.4-clang"\n\
ar = "x86_64-apple-darwin20.4-ar"\n\
' > /root/.cargo/config.toml
# Copy the source code
COPY . .
# Copy pre-built inspector frontend
COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist
# Build for x86_64 macOS
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \
cargo build -p sandbox-agent -p gigacode --release --target x86_64-apple-darwin && \
mkdir -p /artifacts && \
cp target/x86_64-apple-darwin/release/sandbox-agent /artifacts/sandbox-agent-x86_64-apple-darwin && \
cp target/x86_64-apple-darwin/release/gigacode /artifacts/gigacode-x86_64-apple-darwin
# Default command to show help
CMD ["ls", "-la", "/artifacts"]

View file

@ -1,102 +0,0 @@
# syntax=docker/dockerfile:1.10.0
# Build inspector frontend
FROM node:22-alpine AS inspector-build
WORKDIR /app
RUN npm install -g pnpm
# Copy package files for workspaces
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
COPY sdks/react/package.json ./sdks/react/
COPY sdks/typescript/package.json ./sdks/typescript/
# Install dependencies
RUN pnpm install --filter @sandbox-agent/inspector...
# Copy SDK source (with pre-generated types from docs/openapi.json)
COPY docs/openapi.json ./docs/
COPY sdks/cli-shared ./sdks/cli-shared
COPY sdks/acp-http-client ./sdks/acp-http-client
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
COPY sdks/react ./sdks/react
COPY sdks/typescript ./sdks/typescript
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
RUN cd sdks/cli-shared && pnpm exec tsup
RUN cd sdks/acp-http-client && pnpm exec tsup
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
RUN cd sdks/persist-indexeddb && pnpm exec tsup
RUN cd sdks/react && pnpm exec tsup
# Copy inspector source and build
COPY frontend/packages/inspector ./frontend/packages/inspector
RUN cd frontend/packages/inspector && pnpm exec vite build
FROM rust:1.88.0
# Accept version as build arg
ARG SANDBOX_AGENT_VERSION
ENV SANDBOX_AGENT_VERSION=${SANDBOX_AGENT_VERSION}
# Install dependencies
RUN apt-get update && apt-get install -y \
llvm-14-dev \
libclang-14-dev \
clang-14 \
gcc-mingw-w64-x86-64 \
g++-mingw-w64-x86-64 \
binutils-mingw-w64-x86-64 \
ca-certificates \
curl \
git && \
rm -rf /var/lib/apt/lists/*
# Switch MinGW-w64 to the POSIX threading model toolchain
RUN update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix && \
update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix
# Install target
RUN rustup target add x86_64-pc-windows-gnu
# Configure Cargo for Windows cross-compilation
RUN mkdir -p /root/.cargo && \
echo '\
[target.x86_64-pc-windows-gnu]\n\
linker = "x86_64-w64-mingw32-gcc"\n\
' > /root/.cargo/config.toml
# Set environment variables for cross-compilation
ENV CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc \
CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc \
CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++ \
CC_x86_64-pc-windows-gnu=x86_64-w64-mingw32-gcc \
CXX_x86_64-pc-windows-gnu=x86_64-w64-mingw32-g++ \
LIBCLANG_PATH=/usr/lib/llvm-14/lib \
CLANG_PATH=/usr/bin/clang-14 \
CARGO_INCREMENTAL=0 \
CARGO_NET_GIT_FETCH_WITH_CLI=true
# Set working directory
WORKDIR /build
# Copy the source code
COPY . .
# Copy pre-built inspector frontend
COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist
# Build for Windows
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \
cargo build -p sandbox-agent -p gigacode --release --target x86_64-pc-windows-gnu && \
mkdir -p /artifacts && \
cp target/x86_64-pc-windows-gnu/release/sandbox-agent.exe /artifacts/sandbox-agent-x86_64-pc-windows-gnu.exe && \
cp target/x86_64-pc-windows-gnu/release/gigacode.exe /artifacts/gigacode-x86_64-pc-windows-gnu.exe
# Default command to show help
CMD ["ls", "-la", "/artifacts"]

View file

@ -1,170 +0,0 @@
# syntax=docker/dockerfile:1.10.0
# ============================================================================
# Build inspector frontend
# ============================================================================
FROM node:22-alpine AS inspector-build
WORKDIR /app
RUN npm install -g pnpm
# Copy package files for workspaces
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
COPY sdks/react/package.json ./sdks/react/
COPY sdks/typescript/package.json ./sdks/typescript/
# Install dependencies
RUN pnpm install --filter @sandbox-agent/inspector...
# Copy SDK source (with pre-generated types from docs/openapi.json)
COPY docs/openapi.json ./docs/
COPY sdks/cli-shared ./sdks/cli-shared
COPY sdks/acp-http-client ./sdks/acp-http-client
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
COPY sdks/react ./sdks/react
COPY sdks/typescript ./sdks/typescript
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
RUN cd sdks/cli-shared && pnpm exec tsup
RUN cd sdks/acp-http-client && pnpm exec tsup
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
RUN cd sdks/persist-indexeddb && pnpm exec tsup
RUN cd sdks/react && pnpm exec tsup
# Copy inspector source and build
COPY frontend/packages/inspector ./frontend/packages/inspector
RUN cd frontend/packages/inspector && pnpm exec vite build
# ============================================================================
# AMD64 Builder - Uses cross-tools musl toolchain
# ============================================================================
FROM --platform=linux/amd64 rust:1.88.0 AS builder-amd64
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
musl-tools \
musl-dev \
llvm-14-dev \
libclang-14-dev \
clang-14 \
libssl-dev \
pkg-config \
ca-certificates \
g++ \
g++-multilib \
git \
curl \
wget && \
rm -rf /var/lib/apt/lists/*
# Download cross-tools musl toolchain
RUN wget -q https://github.com/cross-tools/musl-cross/releases/latest/download/x86_64-unknown-linux-musl.tar.xz && \
tar -xf x86_64-unknown-linux-musl.tar.xz -C /opt/ && \
rm x86_64-unknown-linux-musl.tar.xz && \
rustup target add x86_64-unknown-linux-musl
ENV PATH="/opt/x86_64-unknown-linux-musl/bin:$PATH" \
LIBCLANG_PATH=/usr/lib/llvm-14/lib \
CLANG_PATH=/usr/bin/clang-14 \
CC_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-gcc \
CXX_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-g++ \
AR_x86_64_unknown_linux_musl=x86_64-unknown-linux-musl-ar \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-unknown-linux-musl-gcc \
CARGO_INCREMENTAL=0 \
CARGO_NET_GIT_FETCH_WITH_CLI=true
# Build OpenSSL for musl
ENV SSL_VER=1.1.1w
RUN wget https://www.openssl.org/source/openssl-$SSL_VER.tar.gz && \
tar -xzf openssl-$SSL_VER.tar.gz && \
cd openssl-$SSL_VER && \
./Configure no-shared no-async --prefix=/musl --openssldir=/musl/ssl linux-x86_64 && \
make -j$(nproc) && \
make install_sw && \
cd .. && \
rm -rf openssl-$SSL_VER*
ENV OPENSSL_DIR=/musl \
OPENSSL_INCLUDE_DIR=/musl/include \
OPENSSL_LIB_DIR=/musl/lib \
PKG_CONFIG_ALLOW_CROSS=1 \
RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-static-libgcc"
WORKDIR /build
COPY . .
# Copy pre-built inspector frontend
COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \
cargo build -p sandbox-agent --release --target x86_64-unknown-linux-musl && \
cp target/x86_64-unknown-linux-musl/release/sandbox-agent /sandbox-agent
# ============================================================================
# ARM64 Builder - Uses Alpine with native musl
# ============================================================================
FROM --platform=linux/arm64 rust:1.88-alpine AS builder-arm64
RUN apk add --no-cache \
musl-dev \
clang \
llvm-dev \
openssl-dev \
openssl-libs-static \
pkgconfig \
git \
curl \
build-base
RUN rustup target add aarch64-unknown-linux-musl
ENV CARGO_INCREMENTAL=0 \
CARGO_NET_GIT_FETCH_WITH_CLI=true \
RUSTFLAGS="-C target-feature=+crt-static"
WORKDIR /build
COPY . .
# Copy pre-built inspector frontend
COPY --from=inspector-build /app/frontend/packages/inspector/dist ./frontend/packages/inspector/dist
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/build/target \
cargo build -p sandbox-agent --release --target aarch64-unknown-linux-musl && \
cp target/aarch64-unknown-linux-musl/release/sandbox-agent /sandbox-agent
# ============================================================================
# Select the appropriate builder based on target architecture
# ============================================================================
ARG TARGETARCH
FROM builder-${TARGETARCH} AS builder
# Runtime stage - minimal image
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
curl \
git && \
rm -rf /var/lib/apt/lists/*
# Copy the binary from builder
COPY --from=builder /sandbox-agent /usr/local/bin/sandbox-agent
RUN chmod +x /usr/local/bin/sandbox-agent
# Create non-root user
RUN useradd -m -s /bin/bash sandbox
USER sandbox
WORKDIR /home/sandbox
EXPOSE 2468
ENTRYPOINT ["sandbox-agent"]
CMD ["--host", "0.0.0.0", "--port", "2468"]

View file

@ -1 +0,0 @@
CLAUDE.md

View file

@ -1,4 +0,0 @@
# Frontend Instructions
- When the user asks for UI changes, capture screenshots of the updated UI after implementation and verification.
- At the end, offer to open those screenshots for the user and provide absolute filesystem paths to the screenshot files.

View file

@ -1,52 +0,0 @@
FROM node:22-alpine AS build
WORKDIR /app
RUN npm install -g pnpm@9
# Copy package files for all workspaces
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
COPY sdks/typescript/package.json ./sdks/typescript/
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
COPY sdks/react/package.json ./sdks/react/
# Install dependencies
RUN pnpm install --filter @sandbox-agent/inspector...
# Copy cli-shared source and build it
COPY sdks/cli-shared ./sdks/cli-shared
RUN cd sdks/cli-shared && pnpm exec tsup
# Copy acp-http-client source and build it
COPY sdks/acp-http-client ./sdks/acp-http-client
RUN cd sdks/acp-http-client && pnpm exec tsup
# Copy SDK source (with pre-generated types) and build
COPY sdks/typescript ./sdks/typescript
RUN cd sdks/typescript && pnpm exec tsup
# Copy persist-indexeddb and build (depends on SDK)
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
RUN cd sdks/persist-indexeddb && pnpm exec tsup
# Copy react and build (depends on SDK)
COPY sdks/react ./sdks/react
RUN cd sdks/react && pnpm exec tsup
# Copy inspector source
COPY frontend/packages/inspector ./frontend/packages/inspector
# Build inspector
RUN cd frontend/packages/inspector && pnpm exec vite build
FROM caddy:alpine
COPY --from=build /app/frontend/packages/inspector/dist /srv/ui
RUN cat > /etc/caddy/Caddyfile <<'EOF'
:80 {
root * /srv
file_server
try_files {path} /ui/index.html
}
EOF
EXPOSE 80

File diff suppressed because it is too large Load diff

View file

@ -1,31 +0,0 @@
{
"name": "@sandbox-agent/inspector",
"private": true,
"version": "0.0.0",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "SKIP_OPENAPI_GEN=1 pnpm --filter @sandbox-agent/persist-indexeddb build && pnpm --filter @sandbox-agent/react build && vite build",
"preview": "vite preview",
"typecheck": "SKIP_OPENAPI_GEN=1 pnpm --filter @sandbox-agent/persist-indexeddb build && pnpm --filter @sandbox-agent/react build && tsc --noEmit",
"test": "SKIP_OPENAPI_GEN=1 pnpm --filter @sandbox-agent/persist-indexeddb build && pnpm --filter @sandbox-agent/react build && vitest run"
},
"devDependencies": {
"@sandbox-agent/react": "workspace:*",
"sandbox-agent": "workspace:*",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"fake-indexeddb": "^6.2.4",
"typescript": "^5.7.3",
"vite": "^5.4.7",
"vitest": "^3.0.0"
},
"dependencies": {
"@sandbox-agent/persist-indexeddb": "workspace:*",
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
}

View file

@ -1 +0,0 @@
<svg width="128" height="128" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="126" height="126" rx="44" fill="#0F0F0F"/><rect x="18.25" y="18.25" width="91.5" height="91.5" rx="25.75" stroke="#F0F0F0" stroke-width="8.5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.694 43.098c0-.622-.505-1.126-1.127-1.126h-8.444a5.114 5.114 0 0 0-5.112 5.111v33.824a5.114 5.114 0 0 0 5.112 5.112h8.444c.622 0 1.127-.505 1.127-1.127V43.098Zm24.424 27.869c-1.238-2.222-4.047-4.026-6.27-4.026H62.923c-.684 0-.93.555-.549 1.239l7.703 13.822c1.239 2.223 4.048 4.026 6.27 4.026h12.927c.683 0 .93-.555.548-1.239l-7.703-13.822Zm.538-18.718c0-5.672-4.605-10.277-10.277-10.277H63.31a1.21 1.21 0 0 0-1.209 1.209v18.137c0 .667.542 1.209 1.21 1.209h9.068c5.672 0 10.277-4.605 10.277-10.278Z" fill="#F0F0F0"/></svg>

Before

Width:  |  Height:  |  Size: 818 B

View file

@ -1 +0,0 @@
<svg viewBox="0 0 281 124" fill="#ffffff" xmlns="http://www.w3.org/2000/svg"><path d="M236.014 0C260.431 9.71106e-05 280.602 17.4115 280.603 44.7432C280.602 73.5337 260.065 94.1657 233.52 94.166C224.158 94.166 215.639 92.4222 208.63 88.4902C202.886 85.2698 198.203 80.6054 194.919 74.3379L188.115 121.822L187.946 123.016H174.214L174.448 121.423L191.772 2.49414H205.372L203.937 11.3369C212.143 3.86078 223.2 0.000153635 236.014 0ZM47.082 0.154297C56.4435 0.154297 65.0012 1.8991 72.0488 5.84863C77.8222 9.08305 82.5323 13.7713 85.8271 20.085L88.1201 3.69238L88.2861 2.49316H101.863L89.1611 90.6328L88.9873 91.8262H75.4092L76.7227 82.8555C68.5854 90.4564 57.3981 94.3231 44.5889 94.3232C20.1709 94.3232 0.000167223 76.9087 0 49.5771C0.000149745 20.7854 20.54 0.154871 47.082 0.154297ZM116.234 90.6357L116.061 91.8271H102.485L115.351 3.68555L115.521 2.49414H129.083L116.234 90.6357ZM140.673 90.6357L140.499 91.8271H126.924L139.789 3.68555L139.96 2.49414H153.521L140.673 90.6357ZM177.958 2.49414L165.108 90.6357L164.935 91.8271H151.36L164.225 3.68555L164.396 2.49414H177.958ZM48.4854 11.9844C27.8638 11.985 14.0133 28.3799 14.0127 48.9521C14.0127 57.7907 16.8094 66.1771 22.3145 72.334C27.7973 78.4657 36.0631 82.4932 47.2402 82.4932C67.8534 82.4925 81.7122 65.9487 81.7129 45.3682C81.7129 35.4076 78.2493 27.0792 72.4131 21.2441C66.5794 15.4088 58.2871 11.9844 48.4854 11.9844ZM233.362 11.8291C212.749 11.8297 198.89 28.3716 198.89 48.9521C198.89 58.9123 202.356 67.2403 208.189 73.0742C214.023 78.9107 222.315 82.3358 232.116 82.3359C252.738 82.3355 266.589 65.9407 266.59 45.3682C266.59 36.5296 263.795 28.1424 258.29 21.9863C252.807 15.8551 244.542 11.8291 233.362 11.8291Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.17 -->
<svg width="1200" height="1200" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<g id="g314">
<path id="path147" fill="#d97757" stroke="none" d="M 233.959793 800.214905 L 468.644287 668.536987 L 472.590637 657.100647 L 468.644287 650.738403 L 457.208069 650.738403 L 417.986633 648.322144 L 283.892639 644.69812 L 167.597321 639.865845 L 54.926208 633.825623 L 26.577238 627.785339 L 3.3e-05 592.751709 L 2.73832 575.27533 L 26.577238 559.248352 L 60.724873 562.228149 L 136.187973 567.382629 L 249.422867 575.194763 L 331.570496 580.026978 L 453.261841 592.671082 L 472.590637 592.671082 L 475.328857 584.859009 L 468.724915 580.026978 L 463.570557 575.194763 L 346.389313 495.785217 L 219.543671 411.865906 L 153.100723 363.543762 L 117.181267 339.060425 L 99.060455 316.107361 L 91.248367 266.01355 L 123.865784 230.093994 L 167.677887 233.073853 L 178.872513 236.053772 L 223.248367 270.201477 L 318.040283 343.570496 L 441.825592 434.738342 L 459.946411 449.798706 L 467.194672 444.64447 L 468.080597 441.020203 L 459.946411 427.409485 L 392.617493 305.718323 L 320.778564 181.932983 L 288.80542 130.630859 L 280.348999 99.865845 C 277.369171 87.221436 275.194641 76.590698 275.194641 63.624268 L 312.322174 13.20813 L 332.8591 6.604126 L 382.389313 13.20813 L 403.248352 31.328979 L 434.013519 101.71814 L 483.865753 212.537048 L 561.181274 363.221497 L 583.812134 407.919434 L 595.892639 449.315491 L 600.40271 461.959839 L 608.214783 461.959839 L 608.214783 454.711609 L 614.577271 369.825623 L 626.335632 265.61084 L 637.771851 131.516846 L 641.718201 93.745117 L 660.402832 48.483276 L 697.530334 24.000122 L 726.52356 37.852417 L 750.362549 72 L 747.060486 94.067139 L 732.886047 186.201416 L 705.100708 330.52356 L 686.979919 427.167847 L 697.530334 427.167847 L 709.61084 415.087341 L 758.496704 350.174561 L 840.644348 247.490051 L 876.885925 206.738342 L 919.167847 161.71814 L 946.308838 140.29541 L 997.61084 140.29541 L 1035.38269 196.429626 L 1018.469849 254.416199 L 965.637634 321.422852 L 921.825562 378.201538 L 859.006714 462.765259 L 819.785278 530.41626 L 823.409424 535.812073 L 832.75177 534.92627 L 974.657776 504.724915 L 1051.328979 490.872559 L 1142.818848 475.167786 L 1184.214844 494.496582 L 1188.724854 514.147644 L 1172.456421 554.335693 L 1074.604126 578.496765 L 959.838989 601.449829 L 788.939636 641.879272 L 786.845764 643.409485 L 789.261841 646.389343 L 866.255127 653.637634 L 899.194702 655.409424 L 979.812134 655.409424 L 1129.932861 666.604187 L 1169.154419 692.537109 L 1192.671265 724.268677 L 1188.724854 748.429688 L 1128.322144 779.194641 L 1046.818848 759.865845 L 856.590759 714.604126 L 791.355774 698.335754 L 782.335693 698.335754 L 782.335693 703.731567 L 836.69812 756.885986 L 936.322205 846.845581 L 1061.073975 962.81897 L 1067.436279 991.490112 L 1051.409424 1014.120911 L 1034.496704 1011.704712 L 924.885986 929.234924 L 882.604126 892.107544 L 786.845764 811.48999 L 780.483276 811.48999 L 780.483276 819.946289 L 802.550415 852.241699 L 919.087341 1027.409424 L 925.127625 1081.127686 L 916.671204 1098.604126 L 886.469849 1109.154419 L 853.288696 1103.114136 L 785.073914 1007.355835 L 714.684631 899.516785 L 657.906067 802.872498 L 650.979858 806.81897 L 617.476624 1167.704834 L 601.771851 1186.147705 L 565.530212 1200 L 535.328857 1177.046997 L 519.302124 1139.919556 L 535.328857 1066.550537 L 554.657776 970.792053 L 570.362488 894.68457 L 584.536926 800.134277 L 592.993347 768.724976 L 592.429626 766.630859 L 585.503479 767.516968 L 514.22821 865.369263 L 405.825531 1011.865906 L 320.053711 1103.677979 L 299.516815 1111.812256 L 263.919525 1093.369263 L 267.221497 1060.429688 L 287.114136 1031.114136 L 405.825531 880.107361 L 477.422913 786.52356 L 523.651062 732.483276 L 523.328918 724.671265 L 520.590698 724.671265 L 205.288605 929.395935 L 149.154434 936.644409 L 124.993355 914.01355 L 127.973183 876.885986 L 139.409409 864.80542 L 234.201385 799.570435 L 233.879227 799.8927 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>OpenAI icon</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v 2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v 2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1 +0,0 @@
<svg width='32' height='40' viewBox='0 0 32 40' fill='none' xmlns='http://www.w3.org/2000/svg'><g clip-path='url(#clip0_1311_94973)'><path d='M24 32H8V16H24V32Z' fill='#4B4646'/><path d='M24 8H8V32H24V8ZM32 40H0V0H32V40Z' fill='#F1ECEC'/></g><defs><clipPath id='clip0_1311_94973'><rect width='32' height='40' fill='white'/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 347 B

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
<!-- P shape: outer boundary clockwise, inner hole counter-clockwise -->
<path fill="#fff" fill-rule="evenodd" d="
M165.29 165.29
H517.36
V400
H400
V517.36
H282.65
V634.72
H165.29
Z
M282.65 282.65
V400
H400
V282.65
Z
"/>
<!-- i dot -->
<path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 473 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because it is too large Load diff

View file

@ -1,128 +0,0 @@
import { AlertTriangle, BookOpen, Zap } from "lucide-react";
import { isHttpsToHttpConnection, isLocalNetworkTarget } from "../lib/permissions";
const logoUrl = `${import.meta.env.BASE_URL}logos/sandboxagent.svg`;
const ConnectScreen = ({
endpoint,
token,
connectError,
connecting,
onEndpointChange,
onTokenChange,
onConnect,
reportUrl,
docsUrl,
discordUrl,
}: {
endpoint: string;
token: string;
connectError: string | null;
connecting: boolean;
onEndpointChange: (value: string) => void;
onTokenChange: (value: string) => void;
onConnect: () => void;
reportUrl?: string;
docsUrl?: string;
discordUrl?: string;
}) => {
return (
<div className="app">
<header className="header">
<div className="header-left">
<img src={logoUrl} alt="Sandbox Agent" className="logo-text" style={{ height: '20px', width: 'auto' }} />
</div>
{(docsUrl || discordUrl || reportUrl) && (
<div className="header-right">
{docsUrl && (
<a className="header-link" href={docsUrl} target="_blank" rel="noreferrer">
<BookOpen size={12} />
Docs
</a>
)}
{discordUrl && (
<a className="header-link" href={discordUrl} target="_blank" rel="noreferrer">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
Discord
</a>
)}
{reportUrl && (
<a className="header-link" href={reportUrl} target="_blank" rel="noreferrer">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
Issues
</a>
)}
</div>
)}
</header>
<main className="landing">
<div className="landing-container">
<div className="landing-hero">
<img src={logoUrl} alt="Sandbox Agent" style={{ height: '32px', width: 'auto', marginBottom: '20px' }} />
</div>
<div className="connect-card">
<div className="connect-card-title">Connect to Server</div>
{connectError && <div className="banner error">{connectError}</div>}
{isHttpsToHttpConnection(window.location.href, endpoint) &&
isLocalNetworkTarget(endpoint) && (
<div className="banner warning">
<AlertTriangle size={16} />
<span>
Connecting from HTTPS to a local HTTP server requires{" "}
<strong>local network access</strong> permission. Your browser may prompt you to
allow this connection.
</span>
</div>
)}
<label className="field">
<span className="label">Endpoint</span>
<input
className="input"
type="text"
placeholder="http://localhost:2468"
value={endpoint}
onChange={(event) => onEndpointChange(event.target.value)}
/>
</label>
<label className="field">
<span className="label">Token (optional)</span>
<input
className="input"
type="password"
placeholder="Bearer token"
value={token}
onChange={(event) => onTokenChange(event.target.value)}
/>
</label>
<button className="button primary" onClick={onConnect} disabled={connecting}>
{connecting ? (
<>
<span className="spinner" />
Connecting...
</>
) : (
<>
<Zap className="button-icon" />
Connect
</>
)}
</button>
<p className="hint">
Having trouble connecting? See the <a href="https://sandboxagent.dev/docs/cors" target="_blank" rel="noreferrer">CORS documentation</a>.
</p>
</div>
</div>
</main>
</div>
);
};
export default ConnectScreen;

View file

@ -1,300 +0,0 @@
import { ArrowLeft, ArrowRight } from "lucide-react";
import { useEffect, useState } from "react";
import type { AgentInfo } from "sandbox-agent";
type AgentModeInfo = { id: string; name: string; description: string };
type AgentModelInfo = { id: string; name?: string };
export type SessionConfig = {
agentMode: string;
model: string;
};
const CUSTOM_MODEL_VALUE = "__custom__";
const agentLabels: Record<string, string> = {
claude: "Claude Code",
codex: "Codex",
opencode: "OpenCode",
amp: "Amp",
pi: "Pi",
cursor: "Cursor"
};
const agentLogos: Record<string, string> = {
claude: `${import.meta.env.BASE_URL}logos/claude.svg`,
codex: `${import.meta.env.BASE_URL}logos/openai.svg`,
opencode: `${import.meta.env.BASE_URL}logos/opencode.svg`,
amp: `${import.meta.env.BASE_URL}logos/amp.svg`,
pi: `${import.meta.env.BASE_URL}logos/pi.svg`,
};
const SessionCreateMenu = ({
agents,
agentsLoading,
agentsError,
modesByAgent,
modelsByAgent,
defaultModelByAgent,
onCreateSession,
onSelectAgent,
open,
onClose
}: {
agents: AgentInfo[];
agentsLoading: boolean;
agentsError: string | null;
modesByAgent: Record<string, AgentModeInfo[]>;
modelsByAgent: Record<string, AgentModelInfo[]>;
defaultModelByAgent: Record<string, string>;
onCreateSession: (agentId: string, config: SessionConfig) => Promise<void>;
onSelectAgent: (agentId: string) => Promise<void>;
open: boolean;
onClose: () => void;
}) => {
const [phase, setPhase] = useState<"agent" | "config" | "loading-config">("agent");
const [selectedAgent, setSelectedAgent] = useState("");
const [agentMode, setAgentMode] = useState("");
const [selectedModel, setSelectedModel] = useState("");
const [customModel, setCustomModel] = useState("");
const [isCustomModel, setIsCustomModel] = useState(false);
const [creating, setCreating] = useState(false);
// Reset state when menu closes
useEffect(() => {
if (!open) {
setPhase("agent");
setSelectedAgent("");
setAgentMode("");
setSelectedModel("");
setCustomModel("");
setIsCustomModel(false);
setCreating(false);
}
}, [open]);
// Auto-select first mode when modes load for selected agent
useEffect(() => {
if (!selectedAgent) return;
const modes = modesByAgent[selectedAgent];
if (modes && modes.length > 0 && !agentMode) {
setAgentMode(modes[0].id);
}
}, [modesByAgent, selectedAgent, agentMode]);
// Agent-specific config should not leak between agent selections.
useEffect(() => {
setAgentMode("");
setSelectedModel("");
setCustomModel("");
setIsCustomModel(false);
}, [selectedAgent]);
// Auto-select default model when agent is selected
useEffect(() => {
if (!selectedAgent) return;
if (selectedModel) return;
const defaultModel = defaultModelByAgent[selectedAgent];
if (defaultModel) {
setSelectedModel(defaultModel);
} else {
const models = modelsByAgent[selectedAgent];
if (models && models.length > 0) {
setSelectedModel(models[0].id);
}
}
}, [modelsByAgent, defaultModelByAgent, selectedAgent, selectedModel]);
if (!open) return null;
const handleAgentClick = (agentId: string) => {
setSelectedAgent(agentId);
setPhase("config");
// Load agent config in background; creation should not block on this call.
void onSelectAgent(agentId).catch((error) => {
console.error("[SessionCreateMenu] Failed to load agent config:", error);
});
};
const handleBack = () => {
if (creating) return;
setPhase("agent");
setSelectedAgent("");
setAgentMode("");
setSelectedModel("");
setCustomModel("");
setIsCustomModel(false);
};
const handleModelSelectChange = (value: string) => {
if (value === CUSTOM_MODEL_VALUE) {
setIsCustomModel(true);
setSelectedModel("");
} else {
setIsCustomModel(false);
setCustomModel("");
setSelectedModel(value);
}
};
const resolvedModel = isCustomModel ? customModel : selectedModel;
const handleCreate = async () => {
if (!selectedAgent) return;
setCreating(true);
try {
await onCreateSession(selectedAgent, { agentMode, model: resolvedModel });
onClose();
} catch (error) {
console.error("[SessionCreateMenu] Failed to create session:", error);
} finally {
setCreating(false);
}
};
if (phase === "agent") {
return (
<div className="session-create-menu">
{agentsLoading && <div className="sidebar-add-status">Loading agents...</div>}
{agentsError && <div className="sidebar-add-status error">{agentsError}</div>}
{!agentsLoading && !agentsError && agents.length === 0 && (
<div className="sidebar-add-status">No agents available.</div>
)}
{!agentsLoading && !agentsError && (() => {
const codingAgents = agents.filter((a) => a.id !== "mock");
const mockAgent = agents.find((a) => a.id === "mock");
return (
<>
{codingAgents.map((agent) => (
<button
key={agent.id}
className="sidebar-add-option"
onClick={() => handleAgentClick(agent.id)}
>
<div className="agent-option-left">
{agentLogos[agent.id] && (
<img src={agentLogos[agent.id]} alt="" className="agent-option-logo" />
)}
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
{agent.version && <span className="agent-option-version">{agent.version}</span>}
</div>
<div className="agent-option-badges">
{agent.installed && <span className="agent-badge installed">Installed</span>}
<ArrowRight size={12} className="agent-option-arrow" />
</div>
</button>
))}
{mockAgent && (
<>
<div className="agent-divider" />
<button
className="sidebar-add-option"
onClick={() => handleAgentClick(mockAgent.id)}
>
<div className="agent-option-left">
<span className="agent-option-name">{agentLabels[mockAgent.id] ?? mockAgent.id}</span>
{mockAgent.version && <span className="agent-option-version">{mockAgent.version}</span>}
</div>
<div className="agent-option-badges">
{mockAgent.installed && <span className="agent-badge installed">Installed</span>}
<ArrowRight size={12} className="agent-option-arrow" />
</div>
</button>
</>
)}
</>
);
})()}
</div>
);
}
const agentLabel = agentLabels[selectedAgent] ?? selectedAgent;
// Phase 2: config form
const activeModes = modesByAgent[selectedAgent] ?? [];
const activeModels = modelsByAgent[selectedAgent] ?? [];
return (
<div className="session-create-menu">
<div className="session-create-header">
<button className="session-create-back" onClick={handleBack} title="Back to agents">
<ArrowLeft size={14} />
</button>
<span className="session-create-agent-name">{agentLabel}</span>
</div>
<div className="session-create-form">
<div className="setup-field">
<span className="setup-label">Model</span>
{isCustomModel ? (
<input
className="setup-input"
type="text"
value={customModel}
onChange={(e) => setCustomModel(e.target.value)}
placeholder="Enter model name..."
autoFocus
/>
) : (
<select
className="setup-select"
value={selectedModel}
onChange={(e) => handleModelSelectChange(e.target.value)}
title="Model"
>
{activeModels.map((m) => (
<option key={m.id} value={m.id}>
{m.name || m.id}
</option>
))}
<option value={CUSTOM_MODEL_VALUE}>Custom...</option>
</select>
)}
{isCustomModel && (
<button
className="setup-custom-back"
onClick={() => {
setIsCustomModel(false);
setCustomModel("");
const defaultModel = defaultModelByAgent[selectedAgent];
setSelectedModel(
defaultModel || (activeModels.length > 0 ? activeModels[0].id : "")
);
}}
title="Back to model list"
type="button"
>
List
</button>
)}
</div>
{activeModes.length > 0 && (
<div className="setup-field">
<span className="setup-label">Mode</span>
<select
className="setup-select"
value={agentMode}
onChange={(e) => setAgentMode(e.target.value)}
title="Mode"
>
{activeModes.map((m) => (
<option key={m.id} value={m.id}>
{m.name || m.id}
</option>
))}
</select>
</div>
)}
</div>
<div className="session-create-actions">
<button className="button primary" onClick={() => void handleCreate()} disabled={creating}>
{creating ? "Creating..." : "Create Session"}
</button>
</div>
</div>
);
};
export default SessionCreateMenu;

View file

@ -1,201 +0,0 @@
import { Archive, ArrowLeft, ArrowUpRight, Plus, RefreshCw } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { AgentInfo } from "sandbox-agent";
import { formatShortId } from "../utils/format";
type AgentModeInfo = { id: string; name: string; description: string };
type AgentModelInfo = { id: string; name?: string };
import SessionCreateMenu, { type SessionConfig } from "./SessionCreateMenu";
type SessionListItem = {
sessionId: string;
agent: string;
ended: boolean;
archived: boolean;
};
const agentLabels: Record<string, string> = {
claude: "Claude Code",
codex: "Codex",
opencode: "OpenCode",
amp: "Amp",
pi: "Pi",
cursor: "Cursor"
};
const persistenceDocsUrl = "https://sandboxagent.dev/docs/session-persistence";
const MIN_REFRESH_SPIN_MS = 350;
const SessionSidebar = ({
sessions,
selectedSessionId,
onSelectSession,
onRefresh,
onCreateSession,
onSelectAgent,
agents,
agentsLoading,
agentsError,
sessionsLoading,
sessionsError,
modesByAgent,
modelsByAgent,
defaultModelByAgent,
}: {
sessions: SessionListItem[];
selectedSessionId: string;
onSelectSession: (session: SessionListItem) => void;
onRefresh: () => void;
onCreateSession: (agentId: string, config: SessionConfig) => Promise<void>;
onSelectAgent: (agentId: string) => Promise<void>;
agents: AgentInfo[];
agentsLoading: boolean;
agentsError: string | null;
sessionsLoading: boolean;
sessionsError: string | null;
modesByAgent: Record<string, AgentModeInfo[]>;
modelsByAgent: Record<string, AgentModelInfo[]>;
defaultModelByAgent: Record<string, string>;
}) => {
const [showMenu, setShowMenu] = useState(false);
const [showArchived, setShowArchived] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
const archivedCount = sessions.filter((session) => session.archived).length;
const activeSessions = sessions.filter((session) => !session.archived);
const archivedSessions = sessions.filter((session) => session.archived);
const visibleSessions = showArchived ? archivedSessions : activeSessions;
const orderedVisibleSessions = showArchived
? [...visibleSessions].sort((a, b) => Number(a.ended) - Number(b.ended))
: visibleSessions;
useEffect(() => {
if (!showMenu) return;
const handler = (event: MouseEvent) => {
if (!menuRef.current) return;
if (!menuRef.current.contains(event.target as Node)) {
setShowMenu(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [showMenu]);
useEffect(() => {
// Prevent getting stuck in archived view when there are no archived sessions.
if (!showArchived) return;
if (archivedSessions.length === 0) {
setShowArchived(false);
}
}, [showArchived, archivedSessions.length]);
const handleRefresh = async () => {
if (refreshing) return;
const startedAt = Date.now();
setRefreshing(true);
try {
await Promise.resolve(onRefresh());
} finally {
const elapsedMs = Date.now() - startedAt;
if (elapsedMs < MIN_REFRESH_SPIN_MS) {
await new Promise((resolve) => window.setTimeout(resolve, MIN_REFRESH_SPIN_MS - elapsedMs));
}
setRefreshing(false);
}
};
return (
<div className="session-sidebar">
<div className="sidebar-header">
<span className="sidebar-title">Sessions</span>
<div className="sidebar-header-actions">
{archivedCount > 0 && (
<button
className={`button secondary small ${showArchived ? "active" : ""}`}
onClick={() => setShowArchived((value) => !value)}
title={showArchived ? "Hide archived sessions" : `Show archived sessions (${archivedCount})`}
>
{showArchived ? (
<ArrowLeft size={12} className="button-icon" />
) : (
<Archive size={12} className="button-icon" />
)}
</button>
)}
<button
className="button secondary small"
onClick={() => void handleRefresh()}
title="Refresh sessions"
disabled={sessionsLoading || refreshing}
>
<RefreshCw size={12} className={`button-icon ${sessionsLoading || refreshing ? "spinner-icon" : ""}`} />
</button>
<div className="sidebar-add-menu-wrapper" ref={menuRef}>
<button
className="sidebar-add-btn"
onClick={() => setShowMenu((value) => !value)}
title="New session"
>
<Plus size={14} />
</button>
<SessionCreateMenu
agents={agents}
agentsLoading={agentsLoading}
agentsError={agentsError}
modesByAgent={modesByAgent}
modelsByAgent={modelsByAgent}
defaultModelByAgent={defaultModelByAgent}
onCreateSession={onCreateSession}
onSelectAgent={onSelectAgent}
open={showMenu}
onClose={() => setShowMenu(false)}
/>
</div>
</div>
</div>
<div className="session-list">
{sessionsLoading ? (
<div className="sidebar-empty">Loading sessions...</div>
) : sessionsError ? (
<div className="sidebar-empty error">{sessionsError}</div>
) : visibleSessions.length === 0 ? (
<div className="sidebar-empty">{showArchived ? "No archived sessions." : "No sessions yet."}</div>
) : (
<>
{showArchived && <div className="sidebar-empty">Archived Sessions</div>}
{orderedVisibleSessions.map((session) => (
<div
key={session.sessionId}
className={`session-item ${session.sessionId === selectedSessionId ? "active" : ""} ${session.ended ? "ended" : ""} ${session.archived ? "ended" : ""}`}
>
<button
className="session-item-content"
onClick={() => onSelectSession(session)}
>
<div className="session-item-id" title={session.sessionId}>
{formatShortId(session.sessionId)}
</div>
<div className="session-item-meta">
<span className="session-item-agent">
{agentLabels[session.agent] ?? session.agent}
</span>
{(session.archived || session.ended) && <span className="session-item-ended">ended</span>}
</div>
</button>
</div>
))}
</>
)}
</div>
<div className="session-persistence-note">
Sessions are persisted in your browser using IndexedDB. These sessions are only from your browser; your SDK sessions are separate. Adding inspector support for SDK soon.{" "}
<a href={persistenceDocsUrl} target="_blank" rel="noreferrer" style={{ display: "inline-flex", alignItems: "center", gap: 2 }}>
Configure persistence
<ArrowUpRight size={10} />
</a>
</div>
</div>
);
};
export default SessionSidebar;

View file

@ -1,63 +0,0 @@
import type { ComponentType } from "react";
import {
Activity,
AlertTriangle,
Brain,
CircleDot,
Download,
FileDiff,
Gauge,
GitBranch,
HelpCircle,
Image,
Layers,
MessageSquare,
Paperclip,
PlayCircle,
Plug,
Shield,
Terminal,
Wrench
} from "lucide-react";
import type { FeatureCoverageView } from "../../types/agents";
const badges = [
{ key: "planMode", label: "Plan", icon: GitBranch },
{ key: "permissions", label: "Perms", icon: Shield },
{ key: "questions", label: "Q&A", icon: HelpCircle },
{ key: "toolCalls", label: "Tool Calls", icon: Wrench },
{ key: "toolResults", label: "Tool Results", icon: Download },
{ key: "textMessages", label: "Text", icon: MessageSquare },
{ key: "images", label: "Images", icon: Image },
{ key: "fileAttachments", label: "Files", icon: Paperclip },
{ key: "sessionLifecycle", label: "Lifecycle", icon: PlayCircle },
{ key: "errorEvents", label: "Errors", icon: AlertTriangle },
{ key: "reasoning", label: "Reasoning", icon: Brain },
{ key: "status", label: "Status", icon: Gauge },
{ key: "commandExecution", label: "Commands", icon: Terminal },
{ key: "fileChanges", label: "File Changes", icon: FileDiff },
{ key: "mcpTools", label: "MCP", icon: Plug },
{ key: "streamingDeltas", label: "Deltas", icon: Activity },
{ key: "itemStarted", label: "Item Start", icon: CircleDot },
{ key: "variants", label: "Variants", icon: Layers }
] as const;
type BadgeItem = (typeof badges)[number];
const getEnabled = (featureCoverage: FeatureCoverageView, key: BadgeItem["key"]) =>
Boolean((featureCoverage as unknown as Record<string, boolean | undefined>)[key]);
const FeatureCoverageBadges = ({ featureCoverage }: { featureCoverage: FeatureCoverageView }) => {
return (
<div className="feature-coverage-badges">
{badges.map(({ key, label, icon: Icon }) => (
<span key={key} className={`feature-coverage-badge ${getEnabled(featureCoverage, key) ? "enabled" : "disabled"}`}>
<Icon size={12} />
<span>{label}</span>
</span>
))}
</div>
);
};
export default FeatureCoverageBadges;

View file

@ -1,37 +0,0 @@
import { Send } from "lucide-react";
const ChatInput = ({
message,
onMessageChange,
onSendMessage,
onKeyDown,
placeholder,
disabled
}: {
message: string;
onMessageChange: (value: string) => void;
onSendMessage: () => void;
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
placeholder: string;
disabled: boolean;
}) => {
return (
<div className="input-container">
<div className="input-wrapper">
<textarea
value={message}
onChange={(event) => onMessageChange(event.target.value)}
onKeyDown={onKeyDown}
placeholder={placeholder}
rows={1}
disabled={disabled}
/>
<button className="send-button" onClick={onSendMessage} disabled={disabled || !message.trim()}>
<Send />
</button>
</div>
</div>
);
};
export default ChatInput;

View file

@ -1,292 +0,0 @@
import { useState } from "react";
import { getMessageClass } from "./messageUtils";
import type { TimelineEntry } from "./types";
import { AlertTriangle, ChevronRight, ChevronDown, Wrench, Brain, Info, ExternalLink, PlayCircle } from "lucide-react";
import MarkdownText from "./MarkdownText";
const ToolItem = ({
entry,
isLast,
onEventClick
}: {
entry: TimelineEntry;
isLast: boolean;
onEventClick?: (eventId: string) => void;
}) => {
const [expanded, setExpanded] = useState(false);
const isTool = entry.kind === "tool";
const isReasoning = entry.kind === "reasoning";
const isMeta = entry.kind === "meta";
const isComplete = isTool && (entry.toolStatus === "completed" || entry.toolStatus === "failed");
const isFailed = isTool && entry.toolStatus === "failed";
const isInProgress = isTool && entry.toolStatus === "in_progress";
let label = "";
let icon = <Info size={12} />;
if (isTool) {
const statusLabel = entry.toolStatus && entry.toolStatus !== "completed"
? ` (${entry.toolStatus.replace("_", " ")})`
: "";
label = `${entry.toolName ?? "tool"}${statusLabel}`;
icon = <Wrench size={12} />;
} else if (isReasoning) {
label = `Reasoning${entry.reasoning?.visibility ? ` (${entry.reasoning.visibility})` : ""}`;
icon = <Brain size={12} />;
} else if (isMeta) {
label = entry.meta?.title ?? "Status";
icon = entry.meta?.severity === "error" ? <AlertTriangle size={12} /> : <Info size={12} />;
}
const hasContent = isTool
? Boolean(entry.toolInput || entry.toolOutput)
: isReasoning
? Boolean(entry.reasoning?.text?.trim())
: Boolean(entry.meta?.detail?.trim());
const canOpenEvent = Boolean(
entry.eventId &&
onEventClick &&
!(isMeta && entry.meta?.title === "Available commands update"),
);
return (
<div className={`tool-item ${isLast ? "last" : ""} ${isFailed ? "failed" : ""}`}>
<div className="tool-item-connector">
<div className="tool-item-dot" />
{!isLast && <div className="tool-item-line" />}
</div>
<div className="tool-item-content">
<button
className={`tool-item-header ${expanded ? "expanded" : ""}`}
onClick={() => hasContent && setExpanded(!expanded)}
disabled={!hasContent}
>
<span className="tool-item-icon">{icon}</span>
<span className="tool-item-label">{label}</span>
{isInProgress && (
<span className="tool-item-spinner">
<span className="thinking-dot" />
<span className="thinking-dot" />
<span className="thinking-dot" />
</span>
)}
{canOpenEvent && (
<span
className="tool-item-link"
onClick={(e) => {
e.stopPropagation();
onEventClick?.(entry.eventId!);
}}
title="View in Events"
>
<ExternalLink size={10} />
</span>
)}
{hasContent && (
<span className="tool-item-chevron">
{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
</span>
)}
</button>
{expanded && hasContent && (
<div className="tool-item-body">
{isTool && entry.toolInput && (
<div className="tool-section">
<div className="tool-section-title">Input</div>
<pre className="tool-code">{entry.toolInput}</pre>
</div>
)}
{isTool && isComplete && entry.toolOutput && (
<div className="tool-section">
<div className="tool-section-title">Output</div>
<pre className="tool-code">{entry.toolOutput}</pre>
</div>
)}
{isReasoning && entry.reasoning?.text && (
<div className="tool-section">
<pre className="tool-code muted">{entry.reasoning.text}</pre>
</div>
)}
{isMeta && entry.meta?.detail && (
<div className="tool-section">
<pre className="tool-code">{entry.meta.detail}</pre>
</div>
)}
</div>
)}
</div>
</div>
);
};
const ToolGroup = ({ entries, onEventClick }: { entries: TimelineEntry[]; onEventClick?: (eventId: string) => void }) => {
const [expanded, setExpanded] = useState(false);
// If only one item, render it directly without macro wrapper
if (entries.length === 1) {
return (
<div className="tool-group-single">
<ToolItem entry={entries[0]} isLast={true} onEventClick={onEventClick} />
</div>
);
}
const totalCount = entries.length;
const summary = `${totalCount} Event${totalCount > 1 ? "s" : ""}`;
// Check if any are in progress
const hasInProgress = entries.some(e => e.kind === "tool" && e.toolStatus === "in_progress");
const hasFailed = entries.some(e => e.kind === "tool" && e.toolStatus === "failed");
return (
<div className={`tool-group-container ${hasFailed ? "failed" : ""}`}>
<button
className={`tool-group-header ${expanded ? "expanded" : ""}`}
onClick={() => setExpanded(!expanded)}
>
<span className="tool-group-icon">
<PlayCircle size={14} />
</span>
<span className="tool-group-label">{summary}</span>
<span className="tool-group-chevron">
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
</button>
{expanded && (
<div className="tool-group">
{entries.map((entry, idx) => (
<ToolItem
key={entry.id}
entry={entry}
isLast={idx === entries.length - 1}
onEventClick={onEventClick}
/>
))}
</div>
)}
</div>
);
};
const agentLogos: Record<string, string> = {
claude: `${import.meta.env.BASE_URL}logos/claude.svg`,
codex: `${import.meta.env.BASE_URL}logos/openai.svg`,
opencode: `${import.meta.env.BASE_URL}logos/opencode.svg`,
amp: `${import.meta.env.BASE_URL}logos/amp.svg`,
pi: `${import.meta.env.BASE_URL}logos/pi.svg`,
};
const ChatMessages = ({
entries,
sessionError,
eventError,
messagesEndRef,
onEventClick,
isThinking,
agentId
}: {
entries: TimelineEntry[];
sessionError: string | null;
eventError?: string | null;
messagesEndRef: React.RefObject<HTMLDivElement>;
onEventClick?: (eventId: string) => void;
isThinking?: boolean;
agentId?: string;
}) => {
// Group consecutive tool/reasoning/meta entries together
const groupedEntries: Array<{ type: "message" | "tool-group" | "divider"; entries: TimelineEntry[] }> = [];
let currentToolGroup: TimelineEntry[] = [];
const flushToolGroup = () => {
if (currentToolGroup.length > 0) {
groupedEntries.push({ type: "tool-group", entries: currentToolGroup });
currentToolGroup = [];
}
};
for (const entry of entries) {
const isStatusDivider = entry.kind === "meta" &&
["Session Started", "Turn Started", "Turn Ended"].includes(entry.meta?.title ?? "");
if (isStatusDivider) {
flushToolGroup();
groupedEntries.push({ type: "divider", entries: [entry] });
} else if (entry.kind === "tool" || entry.kind === "reasoning" || (entry.kind === "meta" && entry.meta?.detail)) {
currentToolGroup.push(entry);
} else if (entry.kind === "meta" && !entry.meta?.detail) {
// Simple meta without detail - add to tool group as single item
currentToolGroup.push(entry);
} else {
// Regular message
flushToolGroup();
groupedEntries.push({ type: "message", entries: [entry] });
}
}
flushToolGroup();
return (
<div className="messages">
{groupedEntries.map((group, idx) => {
if (group.type === "divider") {
const entry = group.entries[0];
const title = entry.meta?.title ?? "Status";
return (
<div key={entry.id} className="status-divider">
<div className="status-divider-line" />
<span className="status-divider-text">{title}</span>
<div className="status-divider-line" />
</div>
);
}
if (group.type === "tool-group") {
return <ToolGroup key={`group-${idx}`} entries={group.entries} onEventClick={onEventClick} />;
}
// Regular message
const entry = group.entries[0];
const messageClass = getMessageClass(entry);
return (
<div key={entry.id} className={`message ${messageClass} no-avatar`}>
<div className="message-content">
{entry.text ? (
<MarkdownText text={entry.text} />
) : (
<span className="thinking-indicator">
<span className="thinking-dot" />
<span className="thinking-dot" />
<span className="thinking-dot" />
</span>
)}
</div>
</div>
);
})}
{sessionError && <div className="message-error">{sessionError}</div>}
{eventError && <div className="message-error">{eventError}</div>}
{isThinking && (
<div className="thinking-row">
<div className="thinking-avatar">
{agentId && agentLogos[agentId] ? (
<img src={agentLogos[agentId]} alt="" className="thinking-avatar-img" />
) : (
<span className="ai-label">AI</span>
)}
</div>
<span className="thinking-indicator">
<span className="thinking-dot" />
<span className="thinking-dot" />
<span className="thinking-dot" />
</span>
</div>
)}
<div ref={messagesEndRef} />
</div>
);
};
export default ChatMessages;

View file

@ -1,277 +0,0 @@
import { AlertTriangle, Archive, CheckSquare, MessageSquare, Plus, Square, Terminal } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { AgentInfo } from "sandbox-agent";
import { formatShortId } from "../../utils/format";
type AgentModeInfo = { id: string; name: string; description: string };
type AgentModelInfo = { id: string; name?: string };
import SessionCreateMenu, { type SessionConfig } from "../SessionCreateMenu";
import ChatInput from "./ChatInput";
import ChatMessages from "./ChatMessages";
import type { TimelineEntry } from "./types";
const HistoryLoadingSkeleton = () => (
<div className="chat-loading-skeleton" aria-hidden>
<div className="chat-skeleton-row assistant">
<div className="chat-skeleton-bubble w-lg" />
</div>
<div className="chat-skeleton-row user">
<div className="chat-skeleton-bubble w-md" />
</div>
<div className="chat-skeleton-row assistant">
<div className="chat-skeleton-bubble w-xl" />
</div>
<div className="chat-skeleton-row assistant">
<div className="chat-skeleton-bubble w-sm" />
</div>
</div>
);
const ChatPanel = ({
sessionId,
transcriptEntries,
isLoadingHistory,
sessionError,
message,
onMessageChange,
onSendMessage,
onKeyDown,
onCreateSession,
onSelectAgent,
agents,
agentsLoading,
agentsError,
messagesEndRef,
agentLabel,
modelLabel,
currentAgentVersion,
sessionEnded,
sessionArchived,
onEndSession,
onArchiveSession,
onUnarchiveSession,
modesByAgent,
modelsByAgent,
defaultModelByAgent,
onEventClick,
isThinking,
agentId,
tokenUsage,
}: {
sessionId: string;
transcriptEntries: TimelineEntry[];
isLoadingHistory?: boolean;
sessionError: string | null;
message: string;
onMessageChange: (value: string) => void;
onSendMessage: () => void;
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onCreateSession: (agentId: string, config: SessionConfig) => Promise<void>;
onSelectAgent: (agentId: string) => Promise<void>;
agents: AgentInfo[];
agentsLoading: boolean;
agentsError: string | null;
messagesEndRef: React.RefObject<HTMLDivElement>;
agentLabel: string;
modelLabel?: string | null;
currentAgentVersion?: string | null;
sessionEnded: boolean;
sessionArchived: boolean;
onEndSession: () => void;
onArchiveSession: () => void;
onUnarchiveSession: () => void;
modesByAgent: Record<string, AgentModeInfo[]>;
modelsByAgent: Record<string, AgentModelInfo[]>;
defaultModelByAgent: Record<string, string>;
onEventClick?: (eventId: string) => void;
isThinking?: boolean;
agentId?: string;
tokenUsage?: { used: number; size: number; cost?: number } | null;
}) => {
const [showAgentMenu, setShowAgentMenu] = useState(false);
const [copiedSessionId, setCopiedSessionId] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!showAgentMenu) return;
const handler = (event: MouseEvent) => {
if (!menuRef.current) return;
if (!menuRef.current.contains(event.target as Node)) {
setShowAgentMenu(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [showAgentMenu]);
const copySessionId = async () => {
if (!sessionId) return;
const onSuccess = () => {
setCopiedSessionId(true);
window.setTimeout(() => setCopiedSessionId(false), 1200);
};
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(sessionId);
onSuccess();
return;
}
} catch {
// Fallback below for older/insecure contexts.
}
const textarea = document.createElement("textarea");
textarea.value = sessionId;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
onSuccess();
} finally {
document.body.removeChild(textarea);
}
};
const handleArchiveSession = () => {
if (!sessionId) return;
onArchiveSession();
};
const handleUnarchiveSession = () => {
if (!sessionId) return;
onUnarchiveSession();
};
return (
<div className="chat-panel">
<div className="panel-header">
<div className="panel-header-left">
<MessageSquare className="button-icon" />
<span className="panel-title">{sessionId ? agentLabel : "No Session"}</span>
{sessionId && modelLabel && (
<span className="header-meta-pill" title={modelLabel}>
{modelLabel}
</span>
)}
{sessionId && currentAgentVersion && (
<span className="header-meta-pill">v{currentAgentVersion}</span>
)}
{sessionId && (
<button
type="button"
className="session-id-display"
title={copiedSessionId ? "Copied" : `${sessionId} (click to copy)`}
onClick={() => void copySessionId()}
>
{copiedSessionId ? "Copied" : formatShortId(sessionId)}
</button>
)}
</div>
<div className="panel-header-right">
{sessionId && tokenUsage && (
<span className="token-pill">{tokenUsage.used.toLocaleString()} tokens</span>
)}
{sessionId && (
sessionEnded ? (
<>
<span className="button ghost small session-ended-status" title="Session ended">
<CheckSquare size={12} />
Ended
</span>
<button
type="button"
className="button ghost small"
onClick={sessionArchived ? handleUnarchiveSession : handleArchiveSession}
title={sessionArchived ? "Unarchive session" : "Archive session"}
>
<Archive size={12} />
{sessionArchived ? "Unarchive" : "Archive"}
</button>
</>
) : (
<button
type="button"
className="button ghost small"
onClick={onEndSession}
title="End session"
>
<Square size={12} />
End
</button>
)
)}
</div>
</div>
{sessionError && (
<div className="error-banner">
<AlertTriangle size={14} />
<span>{sessionError}</span>
</div>
)}
<div className="messages-container">
{!sessionId ? (
<div className="empty-state">
<div className="empty-state-title">No Session Selected</div>
<p className="empty-state-text no-session-subtext">Create a new session to start chatting with an agent.</p>
<div className="empty-state-menu-wrapper" ref={menuRef}>
<button
className="button primary"
onClick={() => setShowAgentMenu((value) => !value)}
>
<Plus className="button-icon" />
Create Session
</button>
<SessionCreateMenu
agents={agents}
agentsLoading={agentsLoading}
agentsError={agentsError}
modesByAgent={modesByAgent}
modelsByAgent={modelsByAgent}
defaultModelByAgent={defaultModelByAgent}
onCreateSession={onCreateSession}
onSelectAgent={onSelectAgent}
open={showAgentMenu}
onClose={() => setShowAgentMenu(false)}
/>
</div>
</div>
) : transcriptEntries.length === 0 && !sessionError ? (
isLoadingHistory ? (
<HistoryLoadingSkeleton />
) : (
<div className="empty-state">
<Terminal className="empty-state-icon" />
<div className="empty-state-title">Ready to Chat</div>
<p className="empty-state-text">Send a message to start a conversation with the agent.</p>
</div>
)
) : (
<ChatMessages
entries={transcriptEntries}
sessionError={sessionError}
eventError={null}
messagesEndRef={messagesEndRef}
onEventClick={onEventClick}
isThinking={isThinking}
agentId={agentId}
/>
)}
</div>
<ChatInput
message={message}
onMessageChange={onMessageChange}
onSendMessage={onSendMessage}
onKeyDown={onKeyDown}
placeholder={sessionEnded ? "Session ended" : sessionId ? "Send a message..." : "Select or create a session first"}
disabled={!sessionId || sessionEnded}
/>
</div>
);
};
export default ChatPanel;

View file

@ -1,206 +0,0 @@
import type { ReactNode } from "react";
const SAFE_URL_RE = /^(https?:\/\/|mailto:)/i;
const isSafeUrl = (url: string): boolean => SAFE_URL_RE.test(url.trim());
const inlineTokenRe = /(`[^`\n]+`|\[[^\]\n]+\]\(([^)\s]+)(?:\s+"[^"]*")?\)|\*\*[^*\n]+\*\*|__[^_\n]+__|\*[^*\n]+\*|_[^_\n]+_|~~[^~\n]+~~)/g;
const parseInline = (text: string, keyPrefix: string): ReactNode[] => {
const out: ReactNode[] = [];
let lastIndex = 0;
let tokenIndex = 0;
for (const match of text.matchAll(inlineTokenRe)) {
const token = match[0];
const idx = match.index ?? 0;
if (idx > lastIndex) {
out.push(text.slice(lastIndex, idx));
}
const key = `${keyPrefix}-t-${tokenIndex++}`;
if (token.startsWith("`") && token.endsWith("`")) {
out.push(<code key={key}>{token.slice(1, -1)}</code>);
} else if (token.startsWith("**") && token.endsWith("**")) {
out.push(<strong key={key}>{token.slice(2, -2)}</strong>);
} else if (token.startsWith("__") && token.endsWith("__")) {
out.push(<strong key={key}>{token.slice(2, -2)}</strong>);
} else if (token.startsWith("*") && token.endsWith("*")) {
out.push(<em key={key}>{token.slice(1, -1)}</em>);
} else if (token.startsWith("_") && token.endsWith("_")) {
out.push(<em key={key}>{token.slice(1, -1)}</em>);
} else if (token.startsWith("~~") && token.endsWith("~~")) {
out.push(<del key={key}>{token.slice(2, -2)}</del>);
} else if (token.startsWith("[") && token.includes("](") && token.endsWith(")")) {
const linkMatch = token.match(/^\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/);
if (!linkMatch) {
out.push(token);
} else {
const label = linkMatch[1];
const href = linkMatch[2];
if (isSafeUrl(href)) {
out.push(
<a key={key} href={href} target="_blank" rel="noreferrer">
{label}
</a>,
);
} else {
out.push(label);
}
}
} else {
out.push(token);
}
lastIndex = idx + token.length;
}
if (lastIndex < text.length) {
out.push(text.slice(lastIndex));
}
return out;
};
const renderInlineLines = (text: string, keyPrefix: string): ReactNode[] => {
const lines = text.split("\n");
const out: ReactNode[] = [];
lines.forEach((line, idx) => {
if (idx > 0) out.push(<br key={`${keyPrefix}-br-${idx}`} />);
out.push(...parseInline(line, `${keyPrefix}-l-${idx}`));
});
return out;
};
const isUnorderedListItem = (line: string): boolean => /^\s*[-*+]\s+/.test(line);
const isOrderedListItem = (line: string): boolean => /^\s*\d+\.\s+/.test(line);
const MarkdownText = ({ text }: { text: string }) => {
const source = text.replace(/\r\n?/g, "\n");
const lines = source.split("\n");
const nodes: ReactNode[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
const trimmed = line.trim();
if (!trimmed) {
i += 1;
continue;
}
if (trimmed.startsWith("```")) {
const lang = trimmed.slice(3).trim();
const codeLines: string[] = [];
i += 1;
while (i < lines.length && !lines[i].trim().startsWith("```")) {
codeLines.push(lines[i]);
i += 1;
}
if (i < lines.length && lines[i].trim().startsWith("```")) i += 1;
nodes.push(
<pre key={`code-${nodes.length}`} className="md-pre">
<code className={lang ? `language-${lang}` : undefined}>{codeLines.join("\n")}</code>
</pre>,
);
continue;
}
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const content = headingMatch[2];
const key = `h-${nodes.length}`;
if (level === 1) nodes.push(<h1 key={key}>{renderInlineLines(content, key)}</h1>);
else if (level === 2) nodes.push(<h2 key={key}>{renderInlineLines(content, key)}</h2>);
else if (level === 3) nodes.push(<h3 key={key}>{renderInlineLines(content, key)}</h3>);
else if (level === 4) nodes.push(<h4 key={key}>{renderInlineLines(content, key)}</h4>);
else if (level === 5) nodes.push(<h5 key={key}>{renderInlineLines(content, key)}</h5>);
else nodes.push(<h6 key={key}>{renderInlineLines(content, key)}</h6>);
i += 1;
continue;
}
if (trimmed.startsWith(">")) {
const quoteLines: string[] = [];
while (i < lines.length && lines[i].trim().startsWith(">")) {
quoteLines.push(lines[i].trim().replace(/^>\s?/, ""));
i += 1;
}
const content = quoteLines.join("\n");
const key = `q-${nodes.length}`;
nodes.push(<blockquote key={key}>{renderInlineLines(content, key)}</blockquote>);
continue;
}
if (isUnorderedListItem(line) || isOrderedListItem(line)) {
const ordered = isOrderedListItem(line);
const items: string[] = [];
while (i < lines.length) {
const candidate = lines[i];
if (ordered && isOrderedListItem(candidate)) {
items.push(candidate.replace(/^\s*\d+\.\s+/, ""));
i += 1;
continue;
}
if (!ordered && isUnorderedListItem(candidate)) {
items.push(candidate.replace(/^\s*[-*+]\s+/, ""));
i += 1;
continue;
}
if (!candidate.trim()) {
i += 1;
break;
}
break;
}
const key = `list-${nodes.length}`;
if (ordered) {
nodes.push(
<ol key={key}>
{items.map((item, idx) => (
<li key={`${key}-i-${idx}`}>{renderInlineLines(item, `${key}-i-${idx}`)}</li>
))}
</ol>,
);
} else {
nodes.push(
<ul key={key}>
{items.map((item, idx) => (
<li key={`${key}-i-${idx}`}>{renderInlineLines(item, `${key}-i-${idx}`)}</li>
))}
</ul>,
);
}
continue;
}
const paragraphLines: string[] = [];
while (i < lines.length) {
const current = lines[i];
const currentTrimmed = current.trim();
if (!currentTrimmed) break;
if (
currentTrimmed.startsWith("```") ||
currentTrimmed.startsWith(">") ||
/^(#{1,6})\s+/.test(currentTrimmed) ||
isUnorderedListItem(current) ||
isOrderedListItem(current)
) {
break;
}
paragraphLines.push(current);
i += 1;
}
const content = paragraphLines.join("\n");
const key = `p-${nodes.length}`;
nodes.push(<p key={key}>{renderInlineLines(content, key)}</p>);
}
return <div className="markdown-body">{nodes}</div>;
};
export default MarkdownText;

View file

@ -1,19 +0,0 @@
import type { TimelineEntry } from "./types";
import { Settings, AlertTriangle } from "lucide-react";
import type { ReactNode } from "react";
export const getMessageClass = (entry: TimelineEntry) => {
if (entry.kind === "tool") return "tool";
if (entry.kind === "meta") return entry.meta?.severity === "error" ? "error" : "system";
if (entry.kind === "reasoning") return "assistant";
if (entry.role === "user") return "user";
return "assistant";
};
export const getAvatarLabel = (messageClass: string): ReactNode => {
if (messageClass === "user") return null;
if (messageClass === "tool") return "T";
if (messageClass === "system") return <Settings size={14} />;
if (messageClass === "error") return <AlertTriangle size={14} />;
return "AI";
};

View file

@ -1,18 +0,0 @@
export type TimelineEntry = {
id: string;
eventId?: string; // Links back to the original event for navigation
kind: "message" | "tool" | "meta" | "reasoning";
time: string;
// For messages:
role?: "user" | "assistant";
text?: string;
// For tool calls:
toolName?: string;
toolInput?: string;
toolOutput?: string;
toolStatus?: string;
// For reasoning:
reasoning?: { text: string; visibility?: string };
// For meta:
meta?: { title: string; detail?: string; severity?: "info" | "error" };
};

View file

@ -1,127 +0,0 @@
import { Download, Loader2, RefreshCw } from "lucide-react";
import { useState } from "react";
import type { AgentInfo } from "sandbox-agent";
type AgentModeInfo = { id: string; name: string; description: string };
import FeatureCoverageBadges from "../agents/FeatureCoverageBadges";
import { emptyFeatureCoverage } from "../../types/agents";
const MIN_REFRESH_SPIN_MS = 350;
const AgentsTab = ({
agents,
defaultAgents,
modesByAgent,
onRefresh,
onInstall,
loading,
error
}: {
agents: AgentInfo[];
defaultAgents: string[];
modesByAgent: Record<string, AgentModeInfo[]>;
onRefresh: () => void;
onInstall: (agentId: string, reinstall: boolean) => Promise<void>;
loading: boolean;
error: string | null;
}) => {
const [installingAgent, setInstallingAgent] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
const handleInstall = async (agentId: string, reinstall: boolean) => {
setInstallingAgent(agentId);
try {
await onInstall(agentId, reinstall);
} finally {
setInstallingAgent(null);
}
};
const handleRefresh = async () => {
if (refreshing) return;
const startedAt = Date.now();
setRefreshing(true);
try {
await Promise.resolve(onRefresh());
} finally {
const elapsedMs = Date.now() - startedAt;
if (elapsedMs < MIN_REFRESH_SPIN_MS) {
await new Promise((resolve) => window.setTimeout(resolve, MIN_REFRESH_SPIN_MS - elapsedMs));
}
setRefreshing(false);
}
};
return (
<>
<div className="inline-row" style={{ marginBottom: 16 }}>
<button className="button secondary small" onClick={() => void handleRefresh()} disabled={loading || refreshing}>
<RefreshCw className={`button-icon ${loading || refreshing ? "spinner-icon" : ""}`} /> Refresh
</button>
</div>
{error && <div className="banner error">{error}</div>}
{!loading && agents.length === 0 && (
<div className="card-meta">No agents reported. Click refresh to check.</div>
)}
{(agents.length
? agents
: defaultAgents.map((id) => ({
id,
installed: false,
credentialsAvailable: false,
version: undefined as string | undefined,
path: undefined as string | undefined,
capabilities: emptyFeatureCoverage as AgentInfo["capabilities"],
}))).map((agent) => {
const isInstalling = installingAgent === agent.id;
return (
<div key={agent.id} className="card">
<div className="card-header">
<span className="card-title">{agent.id}</span>
<div className="card-header-pills">
<span className={`pill ${agent.installed ? "success" : "danger"}`}>
{agent.installed ? "Installed" : "Missing"}
</span>
<span className={`pill ${agent.credentialsAvailable ? "success" : "warning"}`}>
{agent.credentialsAvailable ? "Authenticated" : "No Credentials"}
</span>
</div>
</div>
<div className="card-meta">
{agent.version ?? "Version unknown"}
{agent.path && <span className="mono muted" style={{ marginLeft: 8 }}>{agent.path}</span>}
</div>
<div className="card-meta" style={{ marginTop: 8 }}>
Feature coverage
</div>
<div style={{ marginTop: 8 }}>
<FeatureCoverageBadges featureCoverage={agent.capabilities ?? emptyFeatureCoverage} />
</div>
{modesByAgent[agent.id] && modesByAgent[agent.id].length > 0 && (
<div className="card-meta" style={{ marginTop: 8 }}>
Modes: {modesByAgent[agent.id].map((mode) => mode.id).join(", ")}
</div>
)}
<div className="card-actions">
<button
className="button secondary small"
onClick={() => handleInstall(agent.id, agent.installed)}
disabled={isInstalling}
>
{isInstalling ? (
<Loader2 className="button-icon spinner-icon" />
) : (
<Download className="button-icon" />
)}
{isInstalling ? "Installing..." : agent.installed ? "Reinstall" : "Install"}
</button>
</div>
</div>
);
})}
</>
);
};
export default AgentsTab;

View file

@ -1,151 +0,0 @@
import { ChevronLeft, ChevronRight, Cloud, Play, PlayCircle, Server, Terminal, Wrench } from "lucide-react";
import type { AgentInfo, SandboxAgent, SessionEvent } from "sandbox-agent";
type AgentModeInfo = { id: string; name: string; description: string };
import AgentsTab from "./AgentsTab";
import EventsTab from "./EventsTab";
import McpTab from "./McpTab";
import ProcessesTab from "./ProcessesTab";
import ProcessRunTab from "./ProcessRunTab";
import SkillsTab from "./SkillsTab";
import RequestLogTab from "./RequestLogTab";
import type { RequestLog } from "../../types/requestLog";
export type DebugTab = "log" | "events" | "agents" | "mcp" | "skills" | "processes" | "run-process";
const DebugPanel = ({
debugTab,
onDebugTabChange,
events,
onResetEvents,
highlightedEventId,
onClearHighlight,
requestLog,
copiedLogId,
onClearRequestLog,
onCopyRequestLog,
agents,
defaultAgents,
modesByAgent,
onRefreshAgents,
onInstallAgent,
agentsLoading,
agentsError,
getClient,
collapsed,
onToggleCollapse,
}: {
debugTab: DebugTab;
onDebugTabChange: (tab: DebugTab) => void;
events: SessionEvent[];
onResetEvents: () => void;
highlightedEventId?: string | null;
onClearHighlight?: () => void;
requestLog: RequestLog[];
copiedLogId: number | null;
onClearRequestLog: () => void;
onCopyRequestLog: (entry: RequestLog) => void;
agents: AgentInfo[];
defaultAgents: string[];
modesByAgent: Record<string, AgentModeInfo[]>;
onRefreshAgents: () => void;
onInstallAgent: (agentId: string, reinstall: boolean) => Promise<void>;
agentsLoading: boolean;
agentsError: string | null;
getClient: () => SandboxAgent;
collapsed?: boolean;
onToggleCollapse?: () => void;
}) => {
return (
<div className={`debug-panel ${collapsed ? "collapsed" : ""}`}>
<div className="debug-tabs">
<button
className="debug-collapse-btn"
onClick={onToggleCollapse}
title={collapsed ? "Expand panel" : "Collapse panel"}
>
{collapsed ? <ChevronLeft size={14} /> : <ChevronRight size={14} />}
</button>
<button className={`debug-tab ${debugTab === "events" ? "active" : ""}`} onClick={() => onDebugTabChange("events")}>
<PlayCircle className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
Events
{events.length > 0 && <span className="debug-tab-badge">{events.length}</span>}
</button>
<button className={`debug-tab ${debugTab === "log" ? "active" : ""}`} onClick={() => onDebugTabChange("log")}>
<Terminal className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
Request Log
</button>
<button className={`debug-tab ${debugTab === "agents" ? "active" : ""}`} onClick={() => onDebugTabChange("agents")}>
<Cloud className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
Agents
</button>
<button className={`debug-tab ${debugTab === "mcp" ? "active" : ""}`} onClick={() => onDebugTabChange("mcp")}>
<Server className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
MCP
</button>
<button className={`debug-tab ${debugTab === "processes" ? "active" : ""}`} onClick={() => onDebugTabChange("processes")}>
<Terminal className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
Processes
</button>
<button className={`debug-tab ${debugTab === "run-process" ? "active" : ""}`} onClick={() => onDebugTabChange("run-process")}>
<Play className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
Run Once
</button>
<button className={`debug-tab ${debugTab === "skills" ? "active" : ""}`} onClick={() => onDebugTabChange("skills")}>
<Wrench className="button-icon" style={{ marginRight: 4, width: 12, height: 12 }} />
Skills
</button>
</div>
<div className="debug-content">
{debugTab === "log" && (
<RequestLogTab
requestLog={requestLog}
copiedLogId={copiedLogId}
onClear={onClearRequestLog}
onCopy={onCopyRequestLog}
/>
)}
{debugTab === "events" && (
<EventsTab
events={events}
onClear={onResetEvents}
highlightedEventId={highlightedEventId}
onClearHighlight={onClearHighlight}
/>
)}
{debugTab === "agents" && (
<AgentsTab
agents={agents}
defaultAgents={defaultAgents}
modesByAgent={modesByAgent}
onRefresh={onRefreshAgents}
onInstall={onInstallAgent}
loading={agentsLoading}
error={agentsError}
/>
)}
{debugTab === "mcp" && (
<McpTab getClient={getClient} />
)}
{debugTab === "processes" && (
<ProcessesTab getClient={getClient} />
)}
{debugTab === "run-process" && (
<ProcessRunTab getClient={getClient} />
)}
{debugTab === "skills" && (
<SkillsTab getClient={getClient} />
)}
</div>
</div>
);
};
export default DebugPanel;

View file

@ -1,269 +0,0 @@
import {
Ban,
Bot,
Brain,
ChevronDown,
ChevronRight,
Circle,
CircleX,
Command,
CornerDownLeft,
FilePen,
FileText,
FolderOpen,
Hourglass,
KeyRound,
ListChecks,
MessageSquare,
Plug,
Radio,
ScrollText,
Settings,
ShieldCheck,
SquarePlus,
SquareTerminal,
ToggleLeft,
Trash2,
Unplug,
Wrench,
type LucideIcon,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { SessionEvent } from "sandbox-agent";
import { formatJson, formatShortId, formatTime } from "../../utils/format";
type EventIconInfo = { Icon: LucideIcon; category: string };
function getEventIcon(method: string, payload: Record<string, unknown>): EventIconInfo {
if (method === "session/update") {
const params = payload.params as Record<string, unknown> | undefined;
const update = params?.update as Record<string, unknown> | undefined;
const updateType = update?.sessionUpdate as string | undefined;
switch (updateType) {
case "user_message_chunk":
return { Icon: MessageSquare, category: "prompt" };
case "agent_message_chunk":
return { Icon: Bot, category: "update" };
case "agent_thought_chunk":
return { Icon: Brain, category: "update" };
case "tool_call":
case "tool_call_update":
return { Icon: Wrench, category: "tool" };
case "plan":
return { Icon: ListChecks, category: "config" };
case "available_commands_update":
return { Icon: Command, category: "config" };
case "current_mode_update":
return { Icon: ToggleLeft, category: "config" };
case "config_option_update":
return { Icon: Settings, category: "config" };
default:
return { Icon: Radio, category: "update" };
}
}
switch (method) {
case "initialize":
return { Icon: Plug, category: "connection" };
case "authenticate":
return { Icon: KeyRound, category: "connection" };
case "session/new":
return { Icon: SquarePlus, category: "session" };
case "session/load":
return { Icon: FolderOpen, category: "session" };
case "session/prompt":
return { Icon: MessageSquare, category: "prompt" };
case "session/cancel":
return { Icon: Ban, category: "cancel" };
case "session/set_mode":
return { Icon: ToggleLeft, category: "config" };
case "session/set_config_option":
return { Icon: Settings, category: "config" };
case "session/request_permission":
return { Icon: ShieldCheck, category: "permission" };
case "fs/read_text_file":
return { Icon: FileText, category: "filesystem" };
case "fs/write_text_file":
return { Icon: FilePen, category: "filesystem" };
case "terminal/create":
return { Icon: SquareTerminal, category: "terminal" };
case "terminal/kill":
return { Icon: CircleX, category: "terminal" };
case "terminal/output":
return { Icon: ScrollText, category: "terminal" };
case "terminal/release":
return { Icon: Trash2, category: "terminal" };
case "terminal/wait_for_exit":
return { Icon: Hourglass, category: "terminal" };
case "_sandboxagent/session/detach":
return { Icon: Unplug, category: "session" };
case "(response)":
return { Icon: CornerDownLeft, category: "response" };
default:
if (method.startsWith("_sandboxagent/")) {
return { Icon: Radio, category: "connection" };
}
return { Icon: Circle, category: "response" };
}
}
const EventsTab = ({
events,
onClear,
highlightedEventId,
onClearHighlight,
}: {
events: SessionEvent[];
onClear: () => void;
highlightedEventId?: string | null;
onClearHighlight?: () => void;
}) => {
const [collapsedEvents, setCollapsedEvents] = useState<Record<string, boolean>>({});
const [copied, setCopied] = useState(false);
const handleCopy = () => {
const text = JSON.stringify(events, null, 2);
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}).catch(() => {
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
};
const fallbackCopy = (text: string) => {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy events:", err);
}
document.body.removeChild(textarea);
};
useEffect(() => {
if (events.length === 0) {
setCollapsedEvents({});
}
}, [events.length]);
// Scroll to highlighted event (with delay to ensure DOM is ready after tab switch)
useEffect(() => {
if (highlightedEventId) {
const scrollToEvent = () => {
const el = document.getElementById(`event-${highlightedEventId}`);
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "center" });
// Clear highlight after animation
setTimeout(() => {
onClearHighlight?.();
}, 2000);
}
};
// Small delay to ensure tab switch and DOM render completes
const timer = setTimeout(scrollToEvent, 100);
return () => clearTimeout(timer);
}
}, [highlightedEventId, onClearHighlight]);
const getMethod = (event: SessionEvent): string => {
const payload = event.payload as Record<string, unknown>;
return typeof payload.method === "string" ? payload.method : "(response)";
};
return (
<>
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
<span className="card-meta">{events.length} events</span>
<div className="inline-row">
<button
type="button"
className="button ghost small"
onClick={handleCopy}
disabled={events.length === 0}
title="Copy all events as JSON"
>
{copied ? "Copied" : "Copy JSON"}
</button>
<button className="button ghost small" onClick={onClear}>
Clear
</button>
</div>
</div>
{events.length === 0 ? (
<div className="card-meta">
No events yet. Create a session and send a message.
</div>
) : (
<div className="event-list">
{[...events].reverse().map((event) => {
const eventKey = event.id;
const isCollapsed = collapsedEvents[eventKey] ?? true;
const toggleCollapsed = () =>
setCollapsedEvents((prev) => ({
...prev,
[eventKey]: !(prev[eventKey] ?? true)
}));
const method = getMethod(event);
const payload = event.payload as Record<string, unknown>;
const { Icon, category } = getEventIcon(method, payload);
const time = formatTime(new Date(event.createdAt).toISOString());
const senderClass = event.sender === "client" ? "client" : "agent";
const isHighlighted = highlightedEventId === event.id;
return (
<div
key={eventKey}
id={`event-${event.id}`}
className={`event-item ${isCollapsed ? "collapsed" : "expanded"} ${isHighlighted ? "highlighted" : ""}`}
>
<button
className="event-summary"
type="button"
onClick={toggleCollapsed}
title={isCollapsed ? "Expand payload" : "Collapse payload"}
>
<span className={`event-icon ${category}`}>
<Icon size={14} />
</span>
<div className="event-summary-main">
<div className="event-title-row">
<span className={`event-type ${category}`}>{method}</span>
<span className={`pill ${senderClass === "client" ? "accent" : "success"}`}>
{event.sender}
</span>
<span className="event-time">{time}</span>
</div>
<div className="event-id" title={event.id}>
{formatShortId(event.id)}
</div>
</div>
<span className="event-chevron">
{isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
</span>
</button>
{!isCollapsed && <pre className="code-block event-payload">{formatJson(event.payload)}</pre>}
</div>
);
})}
</div>
)}
</>
);
};
export default EventsTab;

View file

@ -1,243 +0,0 @@
import { ChevronDown, ChevronRight, FolderOpen, Loader2, Plus, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import type { SandboxAgent } from "sandbox-agent";
import { formatJson } from "../../utils/format";
type McpEntry = {
name: string;
config: Record<string, unknown>;
};
const MCP_DIRECTORY_STORAGE_KEY = "sandbox-agent-inspector-mcp-directory";
const McpTab = ({
getClient,
}: {
getClient: () => SandboxAgent;
}) => {
const [directory, setDirectory] = useState(() => {
if (typeof window === "undefined") return "/";
try {
return window.localStorage.getItem(MCP_DIRECTORY_STORAGE_KEY) ?? "/";
} catch {
return "/";
}
});
const [entries, setEntries] = useState<McpEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [collapsedServers, setCollapsedServers] = useState<Record<string, boolean>>({});
// Add/edit form state
const [editing, setEditing] = useState(false);
const [editName, setEditName] = useState("");
const [editJson, setEditJson] = useState("");
const [editError, setEditError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const loadAll = useCallback(async (dir: string) => {
setLoading(true);
setError(null);
try {
const configPath = `${dir === "/" ? "" : dir}/.sandbox-agent/config/mcp.json`;
const bytes = await getClient().readFsFile({ path: configPath });
const text = new TextDecoder().decode(bytes);
if (!text.trim()) {
setEntries([]);
return;
}
const map = JSON.parse(text) as Record<string, Record<string, unknown>>;
setEntries(
Object.entries(map).map(([name, config]) => ({ name, config })),
);
} catch {
// File doesn't exist yet or is empty — that's fine
setEntries([]);
} finally {
setLoading(false);
}
}, [getClient]);
useEffect(() => {
loadAll(directory);
}, [directory, loadAll]);
useEffect(() => {
try {
window.localStorage.setItem(MCP_DIRECTORY_STORAGE_KEY, directory);
} catch {
// Ignore storage failures.
}
}, [directory]);
const startAdd = () => {
setEditing(true);
setEditName("");
setEditJson('{\n "type": "local",\n "command": "npx",\n "args": ["@modelcontextprotocol/server-everything"]\n}');
setEditError(null);
};
const cancelEdit = () => {
setEditing(false);
setEditName("");
setEditJson("");
setEditError(null);
};
const save = async () => {
const name = editName.trim();
if (!name) {
setEditError("Name is required");
return;
}
let parsed: Record<string, unknown>;
try {
parsed = JSON.parse(editJson.trim());
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
setEditError("Must be a JSON object");
return;
}
} catch {
setEditError("Invalid JSON");
return;
}
setSaving(true);
setEditError(null);
try {
await getClient().setMcpConfig(
{ directory, mcpName: name },
parsed as Parameters<SandboxAgent["setMcpConfig"]>[1],
);
cancelEdit();
await loadAll(directory);
} catch (err) {
setEditError(err instanceof Error ? err.message : "Failed to save");
} finally {
setSaving(false);
}
};
const remove = async (name: string) => {
try {
await getClient().deleteMcpConfig({ directory, mcpName: name });
await loadAll(directory);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete");
}
};
return (
<>
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
<span className="card-meta">MCP Server Configuration</span>
<div className="inline-row">
{!editing && (
<button className="button secondary small" onClick={startAdd}>
<Plus className="button-icon" style={{ width: 12, height: 12 }} />
Add
</button>
)}
</div>
</div>
<div className="inline-row" style={{ marginBottom: 12, gap: 6 }}>
<FolderOpen size={14} className="muted" style={{ flexShrink: 0 }} />
<input
className="setup-input mono"
value={directory}
onChange={(e) => setDirectory(e.target.value)}
placeholder="/"
style={{ flex: 1, fontSize: 11 }}
/>
</div>
{error && <div className="banner error">{error}</div>}
{loading && <div className="card-meta">Loading...</div>}
{editing && (
<div className="card" style={{ marginBottom: 12 }}>
<div className="card-header">
<span className="card-title">
{editName ? `Edit: ${editName}` : "Add MCP Server"}
</span>
</div>
<div style={{ marginTop: 8 }}>
<input
className="setup-input"
value={editName}
onChange={(e) => { setEditName(e.target.value); setEditError(null); }}
placeholder="server-name"
style={{ marginBottom: 8, width: "100%", boxSizing: "border-box" }}
/>
<textarea
className="setup-input mono"
value={editJson}
onChange={(e) => { setEditJson(e.target.value); setEditError(null); }}
rows={6}
style={{ width: "100%", boxSizing: "border-box", fontFamily: "monospace", fontSize: 11, resize: "vertical" }}
/>
{editError && <div className="banner error" style={{ marginTop: 4 }}>{editError}</div>}
</div>
<div className="card-actions">
<button className="button primary small" onClick={save} disabled={saving}>
{saving ? <Loader2 className="button-icon spinner-icon" /> : null}
Save
</button>
<button className="button ghost small" onClick={cancelEdit}>
Cancel
</button>
</div>
</div>
)}
{entries.length === 0 && !editing && !loading && (
<div className="card-meta">
No MCP servers configured in this directory.
</div>
)}
{entries.map((entry) => {
const isCollapsed = collapsedServers[entry.name] ?? true;
return (
<div key={entry.name} className="card" style={{ marginBottom: 8 }}>
<div className="card-header">
<div className="inline-row" style={{ gap: 6 }}>
<button
className="button ghost small"
onClick={() => setCollapsedServers((prev) => ({ ...prev, [entry.name]: !(prev[entry.name] ?? true) }))}
title={isCollapsed ? "Expand" : "Collapse"}
style={{ padding: "2px 4px" }}
>
{isCollapsed ? <ChevronRight size={12} /> : <ChevronDown size={12} />}
</button>
<span className="card-title">{entry.name}</span>
</div>
<div className="card-header-pills">
<span className="pill accent">
{(entry.config as { type?: string }).type ?? "unknown"}
</span>
<button
className="button ghost small"
onClick={() => remove(entry.name)}
title="Remove"
style={{ padding: "2px 4px" }}
>
<Trash2 size={12} />
</button>
</div>
</div>
{!isCollapsed && (
<pre className="code-block" style={{ marginTop: 4, fontSize: 10 }}>
{formatJson(entry.config)}
</pre>
)}
</div>
);
})}
</>
);
};
export default McpTab;

View file

@ -1,165 +0,0 @@
import { ChevronDown, ChevronRight, Loader2, Play } from "lucide-react";
import { useState } from "react";
import { SandboxAgentError } from "sandbox-agent";
import type { ProcessRunResponse, SandboxAgent } from "sandbox-agent";
const parseArgs = (value: string): string[] => value.split("\n").map((part) => part.trim()).filter(Boolean);
const ProcessRunTab = ({
getClient,
}: {
getClient: () => SandboxAgent;
}) => {
const [command, setCommand] = useState("");
const [argsText, setArgsText] = useState("");
const [cwd, setCwd] = useState("");
const [timeoutMs, setTimeoutMs] = useState("30000");
const [maxOutputBytes, setMaxOutputBytes] = useState("");
const [showAdvanced, setShowAdvanced] = useState(false);
const [running, setRunning] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<ProcessRunResponse | null>(null);
const handleRun = async () => {
const trimmedCommand = command.trim();
if (!trimmedCommand) {
setError("Command is required.");
return;
}
setRunning(true);
setError(null);
try {
const response = await getClient().runProcess({
command: trimmedCommand,
args: parseArgs(argsText),
cwd: cwd.trim() || undefined,
timeoutMs: timeoutMs.trim() ? Number(timeoutMs) : undefined,
maxOutputBytes: maxOutputBytes.trim() ? Number(maxOutputBytes) : undefined,
});
setResult(response);
} catch (runError) {
const detail = runError instanceof SandboxAgentError ? runError.problem?.detail : undefined;
setError(detail || (runError instanceof Error ? runError.message : "Unable to run process."));
setResult(null);
} finally {
setRunning(false);
}
};
return (
<div className="process-run-container">
<div className="process-run-form">
<div className="process-run-row">
<div className="process-run-field process-run-field-grow">
<label className="label">Command</label>
<input
className="setup-input mono"
value={command}
onChange={(event) => {
setCommand(event.target.value);
setError(null);
}}
placeholder="ls"
/>
</div>
<div className="process-run-field process-run-field-grow">
<label className="label">Working Directory</label>
<input
className="setup-input mono"
value={cwd}
onChange={(event) => {
setCwd(event.target.value);
setError(null);
}}
placeholder="/workspace"
/>
</div>
</div>
<div className="process-run-field">
<label className="label">Arguments</label>
<textarea
className="setup-input mono"
rows={2}
value={argsText}
onChange={(event) => {
setArgsText(event.target.value);
setError(null);
}}
placeholder={"One argument per line, e.g.\n-lc"}
/>
</div>
<button
className="process-advanced-toggle"
onClick={() => setShowAdvanced((prev) => !prev)}
type="button"
>
{showAdvanced ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
Advanced
</button>
{showAdvanced && (
<div className="process-run-row">
<div className="process-run-field process-run-field-grow">
<label className="label">Timeout (ms)</label>
<input
className="setup-input mono"
value={timeoutMs}
onChange={(event) => {
setTimeoutMs(event.target.value);
setError(null);
}}
placeholder="30000"
/>
</div>
<div className="process-run-field process-run-field-grow">
<label className="label">Max Output Bytes</label>
<input
className="setup-input mono"
value={maxOutputBytes}
onChange={(event) => {
setMaxOutputBytes(event.target.value);
setError(null);
}}
placeholder="Default"
/>
</div>
</div>
)}
{error ? <div className="banner error">{error}</div> : null}
<button className="button primary small" onClick={() => void handleRun()} disabled={running} style={{ alignSelf: "flex-start" }}>
{running ? <Loader2 className="button-icon spinner-icon" /> : <Play className="button-icon" />}
{running ? "Running..." : "Run"}
</button>
</div>
{result ? (
<div className="process-run-result">
<div className="process-run-result-header">
<span className={`pill ${result.timedOut ? "warning" : result.exitCode === 0 ? "success" : "danger"}`}>
{result.timedOut ? "Timed Out" : `exit ${result.exitCode ?? "?"}`}
</span>
<span className="card-meta">{result.durationMs}ms</span>
</div>
<div className="process-run-output">
<div className="process-run-output-section">
<div className="process-run-output-label">stdout{result.stdoutTruncated ? " (truncated)" : ""}</div>
<pre className="process-log-block">{result.stdout || "(empty)"}</pre>
</div>
<div className="process-run-output-section">
<div className="process-run-output-label">stderr{result.stderrTruncated ? " (truncated)" : ""}</div>
<pre className="process-log-block">{result.stderr || "(empty)"}</pre>
</div>
</div>
</div>
) : null}
</div>
);
};
export default ProcessRunTab;

View file

@ -1,433 +0,0 @@
import { ChevronDown, ChevronRight, Loader2, Play, RefreshCw, Skull, SquareTerminal, Trash2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { ProcessTerminal } from "@sandbox-agent/react";
import { SandboxAgentError } from "sandbox-agent";
import type { ProcessInfo, SandboxAgent } from "sandbox-agent";
const extractErrorMessage = (error: unknown, fallback: string): string => {
if (error instanceof SandboxAgentError && error.problem?.detail) return error.problem.detail;
if (error instanceof Error) return error.message;
return fallback;
};
const decodeBase64Utf8 = (value: string): string => {
try {
const bytes = Uint8Array.from(window.atob(value), (char) => char.charCodeAt(0));
return new TextDecoder().decode(bytes);
} catch {
return value;
}
};
const formatDateTime = (value: number | null | undefined): string => {
if (!value) {
return "Unknown";
}
return new Date(value).toLocaleString();
};
const parseArgs = (value: string): string[] => value.split("\n").map((part) => part.trim()).filter(Boolean);
const formatCommandSummary = (process: Pick<ProcessInfo, "command" | "args">): string => {
return [process.command, ...process.args].join(" ").trim();
};
const canOpenTerminal = (process: ProcessInfo | null | undefined): boolean => {
return Boolean(process && process.status === "running" && process.interactive && process.tty);
};
const ProcessesTab = ({
getClient,
}: {
getClient: () => SandboxAgent;
}) => {
const [processes, setProcesses] = useState<ProcessInfo[]>([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [command, setCommand] = useState("");
const [argsText, setArgsText] = useState("");
const [cwd, setCwd] = useState("");
const [interactive, setInteractive] = useState(true);
const [tty, setTty] = useState(true);
const [creating, setCreating] = useState(false);
const [createError, setCreateError] = useState<string | null>(null);
const [showCreateForm, setShowCreateForm] = useState(true);
const [selectedProcessId, setSelectedProcessId] = useState<string | null>(null);
const [logsText, setLogsText] = useState("");
const [logsLoading, setLogsLoading] = useState(false);
const [logsError, setLogsError] = useState<string | null>(null);
const [terminalOpen, setTerminalOpen] = useState(false);
const [actingProcessId, setActingProcessId] = useState<string | null>(null);
const loadProcesses = useCallback(async (mode: "initial" | "refresh" = "initial") => {
if (mode === "initial") {
setLoading(true);
} else {
setRefreshing(true);
}
setError(null);
try {
const response = await getClient().listProcesses();
setProcesses(response.processes);
setSelectedProcessId((current) => {
if (!current) {
return response.processes[0]?.id ?? null;
}
return response.processes.some((listedProcess) => listedProcess.id === current)
? current
: response.processes[0]?.id ?? null;
});
} catch (loadError) {
setError(extractErrorMessage(loadError, "Unable to load processes."));
} finally {
setLoading(false);
setRefreshing(false);
}
}, [getClient]);
const loadSelectedLogs = useCallback(async (process: ProcessInfo | null) => {
if (!process) {
setLogsText("");
setLogsError(null);
return;
}
setLogsLoading(true);
setLogsError(null);
try {
const response = await getClient().getProcessLogs(process.id, {
stream: process.tty ? "pty" : "combined",
tail: 200,
});
const text = response.entries.map((logEntry) => decodeBase64Utf8(logEntry.data)).join("");
setLogsText(text);
} catch (loadError) {
setLogsError(extractErrorMessage(loadError, "Unable to load process logs."));
setLogsText("");
} finally {
setLogsLoading(false);
}
}, [getClient]);
useEffect(() => {
void loadProcesses();
}, [loadProcesses]);
const selectedProcess = useMemo(
() => processes.find((process) => process.id === selectedProcessId) ?? null,
[processes, selectedProcessId]
);
useEffect(() => {
void loadSelectedLogs(selectedProcess);
if (!canOpenTerminal(selectedProcess)) {
setTerminalOpen(false);
}
}, [loadSelectedLogs, selectedProcess]);
const handleCreateProcess = async () => {
const trimmedCommand = command.trim();
if (!trimmedCommand) {
setCreateError("Command is required.");
return;
}
setCreating(true);
setCreateError(null);
try {
const created = await getClient().createProcess({
command: trimmedCommand,
args: parseArgs(argsText),
cwd: cwd.trim() || undefined,
interactive,
tty,
});
await loadProcesses("refresh");
setSelectedProcessId(created.id);
setTerminalOpen(created.interactive && created.tty);
setCommand("");
setArgsText("");
setCwd("");
setInteractive(true);
setTty(true);
} catch (createFailure) {
setCreateError(extractErrorMessage(createFailure, "Unable to create process."));
} finally {
setCreating(false);
}
};
const handleAction = async (processId: string, action: "stop" | "kill" | "delete") => {
setActingProcessId(`${action}:${processId}`);
setError(null);
try {
const client = getClient();
if (action === "stop") {
await client.stopProcess(processId, { waitMs: 2_000 });
} else if (action === "kill") {
await client.killProcess(processId, { waitMs: 2_000 });
} else {
await client.deleteProcess(processId);
}
await loadProcesses("refresh");
} catch (actionError) {
setError(extractErrorMessage(actionError, `Unable to ${action} process.`));
} finally {
setActingProcessId(null);
}
};
const handleTerminalExit = useCallback(() => {
void loadProcesses("refresh");
}, [loadProcesses]);
return (
<div className="processes-container">
{/* Create form */}
<div className="processes-section">
<button
className="processes-section-toggle"
onClick={() => setShowCreateForm((prev) => !prev)}
type="button"
>
{showCreateForm ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
<span>Create Process</span>
</button>
{showCreateForm && (
<div className="process-create-form">
<div className="process-run-row">
<div className="process-run-field process-run-field-grow">
<label className="label">Command</label>
<input
className="setup-input mono"
value={command}
onChange={(event) => {
setCommand(event.target.value);
setCreateError(null);
}}
placeholder="bash"
/>
</div>
<div className="process-run-field process-run-field-grow">
<label className="label">Working Directory</label>
<input
className="setup-input mono"
value={cwd}
onChange={(event) => {
setCwd(event.target.value);
setCreateError(null);
}}
placeholder="/workspace"
/>
</div>
</div>
<div className="process-run-field">
<label className="label">Arguments</label>
<textarea
className="setup-input mono"
rows={2}
value={argsText}
onChange={(event) => {
setArgsText(event.target.value);
setCreateError(null);
}}
placeholder={"One argument per line"}
/>
</div>
<div className="process-checkbox-row">
<label className="process-checkbox">
<input
type="checkbox"
checked={interactive}
onChange={(event) => {
setInteractive(event.target.checked);
if (!event.target.checked) {
setTty(false);
}
}}
/>
<span>interactive</span>
</label>
<label className="process-checkbox">
<input
type="checkbox"
checked={tty}
onChange={(event) => {
setTty(event.target.checked);
if (event.target.checked) {
setInteractive(true);
}
}}
/>
<span>tty</span>
</label>
</div>
{createError ? <div className="banner error">{createError}</div> : null}
<button className="button primary small" onClick={() => void handleCreateProcess()} disabled={creating} style={{ alignSelf: "flex-start" }}>
{creating ? <Loader2 className="button-icon spinner-icon" /> : <Play className="button-icon" />}
{creating ? "Creating..." : "Create"}
</button>
</div>
)}
</div>
{/* Process list */}
<div className="processes-section">
<div className="processes-list-header">
<span className="processes-section-label">Processes</span>
<button className="button secondary small" onClick={() => void loadProcesses("refresh")} disabled={loading || refreshing}>
<RefreshCw className={`button-icon ${loading || refreshing ? "spinner-icon" : ""}`} size={12} />
Refresh
</button>
</div>
{error ? <div className="banner error">{error}</div> : null}
{loading ? <div className="card-meta">Loading...</div> : null}
{!loading && processes.length === 0 ? <div className="card-meta">No processes yet.</div> : null}
<div className="process-list">
{processes.map((process) => {
const isSelected = selectedProcessId === process.id;
const isStopping = actingProcessId === `stop:${process.id}`;
const isKilling = actingProcessId === `kill:${process.id}`;
const isDeleting = actingProcessId === `delete:${process.id}`;
return (
<div
key={process.id}
className={`process-list-item ${isSelected ? "selected" : ""}`}
onClick={() => {
setSelectedProcessId(process.id);
setTerminalOpen(false);
}}
>
<div className="process-list-item-main">
<span className={`process-status-dot ${process.status}`} />
<span className="process-list-item-cmd mono">{formatCommandSummary(process)}</span>
{process.interactive && process.tty && (
<span className="pill neutral" style={{ fontSize: 9 }}>tty</span>
)}
</div>
<div className="process-list-item-meta">
<span>PID {process.pid ?? "?"}</span>
<span className="process-list-item-id">{process.id.slice(0, 8)}</span>
</div>
<div className="process-list-item-actions">
{canOpenTerminal(process) ? (
<button
className="button secondary small"
onClick={(e) => {
e.stopPropagation();
setSelectedProcessId(process.id);
setTerminalOpen(true);
}}
>
<SquareTerminal className="button-icon" size={12} />
Terminal
</button>
) : null}
{process.status === "running" ? (
<>
<button
className="button secondary small"
onClick={(e) => { e.stopPropagation(); void handleAction(process.id, "stop"); }}
disabled={Boolean(actingProcessId)}
>
{isStopping ? <Loader2 className="button-icon spinner-icon" size={12} /> : null}
Stop
</button>
<button
className="button secondary small"
onClick={(e) => { e.stopPropagation(); void handleAction(process.id, "kill"); }}
disabled={Boolean(actingProcessId)}
>
{isKilling ? <Loader2 className="button-icon spinner-icon" size={12} /> : <Skull className="button-icon" size={12} />}
Kill
</button>
</>
) : null}
{process.status === "exited" ? (
<button
className="button secondary small"
onClick={(e) => { e.stopPropagation(); void handleAction(process.id, "delete"); }}
disabled={Boolean(actingProcessId)}
>
{isDeleting ? <Loader2 className="button-icon spinner-icon" size={12} /> : <Trash2 className="button-icon" size={12} />}
Delete
</button>
) : null}
</div>
</div>
);
})}
</div>
</div>
{/* Selected process detail */}
{selectedProcess ? (
<div className="processes-section">
<div className="processes-section-label">Detail</div>
<div className="process-detail">
<div className="process-detail-header">
<span className="process-detail-cmd mono">{formatCommandSummary(selectedProcess)}</span>
<span className={`pill ${selectedProcess.status === "running" ? "success" : "neutral"}`}>{selectedProcess.status}</span>
</div>
<div className="process-detail-meta">
<span>PID: {selectedProcess.pid ?? "?"}</span>
<span>Created: {formatDateTime(selectedProcess.createdAtMs)}</span>
{selectedProcess.exitedAtMs ? <span>Exited: {formatDateTime(selectedProcess.exitedAtMs)}</span> : null}
{selectedProcess.exitCode != null ? <span>Exit code: {selectedProcess.exitCode}</span> : null}
<span className="mono" style={{ opacity: 0.6 }}>{selectedProcess.id}</span>
</div>
{/* Terminal */}
{terminalOpen && canOpenTerminal(selectedProcess) ? (
<ProcessTerminal
client={getClient()}
processId={selectedProcess.id}
style={{ marginTop: 4 }}
onExit={handleTerminalExit}
/>
) : canOpenTerminal(selectedProcess) ? (
<button
className="button secondary small"
onClick={() => setTerminalOpen(true)}
style={{ marginTop: 8 }}
>
<SquareTerminal className="button-icon" size={12} />
Open Terminal
</button>
) : selectedProcess.interactive && selectedProcess.tty ? (
<div className="process-terminal-empty">
Terminal available while process is running.
</div>
) : null}
{/* Logs */}
<div className="process-detail-logs">
<div className="process-detail-logs-header">
<span className="label">Logs</span>
<button className="button secondary small" onClick={() => void loadSelectedLogs(selectedProcess)} disabled={logsLoading}>
{logsLoading ? <Loader2 className="button-icon spinner-icon" size={12} /> : <RefreshCw className="button-icon" size={12} />}
Refresh
</button>
</div>
{logsError ? <div className="banner error">{logsError}</div> : null}
<pre className="process-log-block">{logsText || (logsLoading ? "Loading..." : "(no output)")}</pre>
</div>
</div>
</div>
) : null}
</div>
);
};
export default ProcessesTab;

View file

@ -1,126 +0,0 @@
import { ChevronDown, ChevronRight, Clipboard } from "lucide-react";
import { useState } from "react";
import type { RequestLog } from "../../types/requestLog";
import { formatJson } from "../../utils/format";
const RequestLogTab = ({
requestLog,
copiedLogId,
onClear,
onCopy
}: {
requestLog: RequestLog[];
copiedLogId: number | null;
onClear: () => void;
onCopy: (entry: RequestLog) => void;
}) => {
const [expanded, setExpanded] = useState<Record<number, boolean>>({});
const toggleExpanded = (id: number) => {
setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
};
return (
<>
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
<span className="card-meta">{requestLog.length} requests</span>
<button className="button ghost small" onClick={onClear}>
Clear
</button>
</div>
{requestLog.length === 0 ? (
<div className="card-meta">No requests logged yet.</div>
) : (
<div className="event-list">
{requestLog.map((entry) => {
const isExpanded = expanded[entry.id] ?? false;
const hasDetails = entry.headers || entry.body || entry.responseBody;
return (
<div key={entry.id} className={`event-item ${isExpanded ? "expanded" : "collapsed"}`}>
<button
className="event-summary"
type="button"
onClick={() => hasDetails && toggleExpanded(entry.id)}
title={hasDetails ? (isExpanded ? "Collapse" : "Expand") : undefined}
style={{ cursor: hasDetails ? "pointer" : "default", gridTemplateColumns: "1fr auto auto auto" }}
>
<div className="event-summary-main">
<div className="event-title-row">
<span className="log-method">{entry.method}</span>
<span className="log-url text-truncate" style={{ flex: 1 }}>{entry.url}</span>
</div>
<div className="event-id">
{entry.time}
{entry.error && ` - ${entry.error}`}
</div>
</div>
<span className={`log-status ${entry.status && entry.status < 400 ? "ok" : "error"}`}>
{entry.status || "ERR"}
</span>
<span
className="copy-button"
onClick={(e) => {
e.stopPropagation();
onCopy(entry);
}}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
onCopy(entry);
}
}}
>
<Clipboard size={14} />
{copiedLogId === entry.id ? "Copied" : "curl"}
</span>
{hasDetails && (
<span className="event-chevron">
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</span>
)}
</button>
{isExpanded && (
<div className="event-payload" style={{ padding: "8px 12px" }}>
{entry.headers && Object.keys(entry.headers).length > 0 && (
<div style={{ marginBottom: 8 }}>
<div className="part-title">Request Headers</div>
<pre className="code-block">{Object.entries(entry.headers).map(([k, v]) => `${k}: ${v}`).join("\n")}</pre>
</div>
)}
{entry.body && (
<div style={{ marginBottom: 8 }}>
<div className="part-title">Request Body</div>
<pre className="code-block">{formatJsonSafe(entry.body)}</pre>
</div>
)}
{entry.responseBody && (
<div>
<div className="part-title">Response Body</div>
<pre className="code-block">{formatJsonSafe(entry.responseBody)}</pre>
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
</>
);
};
const formatJsonSafe = (text: string): string => {
try {
const parsed = JSON.parse(text);
return formatJson(parsed);
} catch {
return text;
}
};
export default RequestLogTab;

View file

@ -1,412 +0,0 @@
import { ChevronDown, ChevronRight, FolderOpen, Loader2, Plus, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import type { SandboxAgent } from "sandbox-agent";
import { formatJson } from "../../utils/format";
type SkillEntry = {
name: string;
config: { sources: Array<{ source: string; type: string; ref?: string | null; subpath?: string | null; skills?: string[] | null }> };
};
const SKILLS_DIRECTORY_STORAGE_KEY = "sandbox-agent-inspector-skills-directory";
const SkillsTab = ({
getClient,
}: {
getClient: () => SandboxAgent;
}) => {
const officialSkills = [
{
name: "Sandbox Agent SDK",
skillId: "sandbox-agent",
source: "rivet-dev/skills",
summary: "Skills bundle for fast Sandbox Agent SDK setup and consistent workflows.",
},
{
name: "Rivet",
skillId: "rivet",
source: "rivet-dev/skills",
summary: "Open-source platform for building, deploying, and scaling AI agents.",
features: [
"Session Persistence",
"Resumable Sessions",
"Multi-Agent Support",
"Realtime Events",
"Tool Call Visibility",
],
},
];
const [directory, setDirectory] = useState(() => {
if (typeof window === "undefined") return "/";
try {
return window.localStorage.getItem(SKILLS_DIRECTORY_STORAGE_KEY) ?? "/";
} catch {
return "/";
}
});
const [entries, setEntries] = useState<SkillEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [copiedId, setCopiedId] = useState<string | null>(null);
const [showSdkSkills, setShowSdkSkills] = useState(false);
const [collapsedSkills, setCollapsedSkills] = useState<Record<string, boolean>>({});
// Add form state
const [editing, setEditing] = useState(false);
const [editName, setEditName] = useState("");
const [editSource, setEditSource] = useState("");
const [editType, setEditType] = useState("github");
const [editRef, setEditRef] = useState("");
const [editSubpath, setEditSubpath] = useState("");
const [editSkills, setEditSkills] = useState("");
const [editError, setEditError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const loadAll = useCallback(async (dir: string) => {
setLoading(true);
setError(null);
try {
const configPath = `${dir === "/" ? "" : dir}/.sandbox-agent/config/skills.json`;
const bytes = await getClient().readFsFile({ path: configPath });
const text = new TextDecoder().decode(bytes);
if (!text.trim()) {
setEntries([]);
return;
}
const map = JSON.parse(text) as Record<string, SkillEntry["config"]>;
setEntries(
Object.entries(map).map(([name, config]) => ({ name, config })),
);
} catch {
// File doesn't exist yet or is empty — that's fine
setEntries([]);
} finally {
setLoading(false);
}
}, [getClient]);
useEffect(() => {
loadAll(directory);
}, [directory, loadAll]);
useEffect(() => {
try {
window.localStorage.setItem(SKILLS_DIRECTORY_STORAGE_KEY, directory);
} catch {
// Ignore storage failures.
}
}, [directory]);
const startAdd = () => {
setEditing(true);
setEditName("");
setEditSource("rivet-dev/skills");
setEditType("github");
setEditRef("");
setEditSubpath("");
setEditSkills("sandbox-agent");
setEditError(null);
};
const cancelEdit = () => {
setEditing(false);
setEditName("");
setEditSource("");
setEditType("github");
setEditRef("");
setEditSubpath("");
setEditSkills("");
setEditError(null);
};
const save = async () => {
const name = editName.trim();
if (!name) {
setEditError("Name is required");
return;
}
const source = editSource.trim();
if (!source) {
setEditError("Source is required");
return;
}
const skillEntry: SkillEntry["config"]["sources"][0] = {
source,
type: editType,
};
if (editRef.trim()) skillEntry.ref = editRef.trim();
if (editSubpath.trim()) skillEntry.subpath = editSubpath.trim();
const skillsList = editSkills.trim()
? editSkills.split(",").map((s) => s.trim()).filter(Boolean)
: null;
if (skillsList && skillsList.length > 0) skillEntry.skills = skillsList;
const config = { sources: [skillEntry] };
setSaving(true);
setEditError(null);
try {
await getClient().setSkillsConfig(
{ directory, skillName: name },
config,
);
cancelEdit();
await loadAll(directory);
} catch (err) {
setEditError(err instanceof Error ? err.message : "Failed to save");
} finally {
setSaving(false);
}
};
const remove = async (name: string) => {
try {
await getClient().deleteSkillsConfig({ directory, skillName: name });
await loadAll(directory);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete");
}
};
const fallbackCopy = (text: string) => {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
};
const copyText = async (id: string, text: string) => {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
} else {
fallbackCopy(text);
}
setCopiedId(id);
window.setTimeout(() => {
setCopiedId((current) => (current === id ? null : current));
}, 1800);
} catch {
setError("Failed to copy snippet");
}
};
const applySkillPreset = (skill: typeof officialSkills[0]) => {
setEditing(true);
setEditName(skill.skillId);
setEditSource(skill.source);
setEditType("github");
setEditRef("");
setEditSubpath("");
setEditSkills(skill.skillId);
setEditError(null);
setShowSdkSkills(false);
};
const copySkillToInput = async (skillId: string) => {
const skill = officialSkills.find((s) => s.skillId === skillId);
if (skill) {
applySkillPreset(skill);
await copyText(`skill-input-${skillId}`, skillId);
}
};
return (
<>
<div className="inline-row" style={{ marginBottom: 12, justifyContent: "space-between" }}>
<span className="card-meta">Skills Configuration</span>
<div className="inline-row" style={{ gap: 6 }}>
<button
className="button secondary small"
onClick={() => setShowSdkSkills((prev) => !prev)}
title="Toggle official skills list"
>
{showSdkSkills ? <ChevronDown className="button-icon" style={{ width: 12, height: 12 }} /> : <ChevronRight className="button-icon" style={{ width: 12, height: 12 }} />}
Official Skills
</button>
{!editing && (
<button className="button secondary small" onClick={startAdd}>
<Plus className="button-icon" style={{ width: 12, height: 12 }} />
Add
</button>
)}
</div>
</div>
{showSdkSkills && (
<div className="card" style={{ marginBottom: 12 }}>
<div className="card-meta" style={{ marginBottom: 8 }}>
Pick a skill to auto-fill the form.
</div>
{officialSkills.map((skill) => (
<div
key={skill.name}
style={{
border: "1px solid var(--border)",
borderRadius: 6,
padding: "8px 10px",
background: "var(--surface-2)",
marginBottom: 6,
}}
>
<div className="inline-row" style={{ justifyContent: "space-between", gap: 8, marginBottom: 4 }}>
<div style={{ fontWeight: 500, fontSize: 12 }}>{skill.name}</div>
<button className="button ghost small" onClick={() => void copySkillToInput(skill.skillId)}>
{copiedId === `skill-input-${skill.skillId}` ? "Filled" : "Use"}
</button>
</div>
<div className="card-meta" style={{ fontSize: 10, marginBottom: skill.features ? 6 : 0 }}>{skill.summary}</div>
{skill.features && (
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
{skill.features.map((feature) => (
<span key={feature} className="pill accent" style={{ fontSize: 9 }}>
{feature}
</span>
))}
</div>
)}
</div>
))}
</div>
)}
<div className="inline-row" style={{ marginBottom: 12, gap: 6 }}>
<FolderOpen size={14} className="muted" style={{ flexShrink: 0 }} />
<input
className="setup-input mono"
value={directory}
onChange={(e) => setDirectory(e.target.value)}
placeholder="/"
style={{ flex: 1, fontSize: 11 }}
/>
</div>
{error && <div className="banner error">{error}</div>}
{loading && <div className="card-meta">Loading...</div>}
{editing && (
<div className="card" style={{ marginBottom: 12 }}>
<div className="card-header">
<span className="card-title">Add Skill Source</span>
</div>
<div style={{ marginTop: 8 }}>
<input
className="setup-input"
value={editName}
onChange={(e) => { setEditName(e.target.value); setEditError(null); }}
placeholder="skill-name"
style={{ marginBottom: 6, width: "100%", boxSizing: "border-box" }}
/>
<div className="inline-row" style={{ marginBottom: 6, gap: 4 }}>
<select
className="setup-select"
value={editType}
onChange={(e) => setEditType(e.target.value)}
style={{ width: 90 }}
>
<option value="github">github</option>
<option value="local">local</option>
<option value="git">git</option>
</select>
<input
className="setup-input mono"
value={editSource}
onChange={(e) => { setEditSource(e.target.value); setEditError(null); }}
placeholder={editType === "github" ? "owner/repo" : editType === "local" ? "/path/to/skill" : "https://..."}
style={{ flex: 1 }}
/>
</div>
<input
className="setup-input"
value={editSkills}
onChange={(e) => setEditSkills(e.target.value)}
placeholder="Skills filter (comma-separated, optional)"
style={{ marginBottom: 6, width: "100%", boxSizing: "border-box" }}
/>
{editType !== "local" && (
<div className="inline-row" style={{ gap: 4 }}>
<input
className="setup-input mono"
value={editRef}
onChange={(e) => setEditRef(e.target.value)}
placeholder="Branch/tag (optional)"
style={{ flex: 1 }}
/>
<input
className="setup-input mono"
value={editSubpath}
onChange={(e) => setEditSubpath(e.target.value)}
placeholder="Subpath (optional)"
style={{ flex: 1 }}
/>
</div>
)}
{editError && <div className="banner error" style={{ marginTop: 4 }}>{editError}</div>}
</div>
<div className="card-actions">
<button className="button primary small" onClick={save} disabled={saving}>
{saving ? <Loader2 className="button-icon spinner-icon" /> : null}
Save
</button>
<button className="button ghost small" onClick={cancelEdit}>
Cancel
</button>
</div>
</div>
)}
{entries.length === 0 && !editing && !loading && (
<div className="card-meta">
No skills configured in this directory.
</div>
)}
{entries.map((entry) => {
const isCollapsed = collapsedSkills[entry.name] ?? true;
return (
<div key={entry.name} className="card" style={{ marginBottom: 8 }}>
<div className="card-header">
<div className="inline-row" style={{ gap: 6 }}>
<button
className="button ghost small"
onClick={() => setCollapsedSkills((prev) => ({ ...prev, [entry.name]: !(prev[entry.name] ?? true) }))}
title={isCollapsed ? "Expand" : "Collapse"}
style={{ padding: "2px 4px" }}
>
{isCollapsed ? <ChevronRight size={12} /> : <ChevronDown size={12} />}
</button>
<span className="card-title">{entry.name}</span>
</div>
<div className="card-header-pills">
<span className="pill accent">
{entry.config.sources.length} source{entry.config.sources.length !== 1 ? "s" : ""}
</span>
<button
className="button ghost small"
onClick={() => remove(entry.name)}
title="Remove"
style={{ padding: "2px 4px" }}
>
<Trash2 size={12} />
</button>
</div>
</div>
{!isCollapsed && (
<pre className="code-block" style={{ marginTop: 4, fontSize: 10 }}>
{formatJson(entry.config)}
</pre>
)}
</div>
);
})}
</>
);
};
export default SkillsTab;

View file

@ -1,48 +0,0 @@
export const askForLocalNetworkAccess = async (): Promise<boolean> => {
try {
const status = await navigator.permissions.query({
// @ts-expect-error - local-network-access is not in standard types
name: "local-network-access"
});
if (status.state === "granted") {
return true;
}
if (status.state === "denied") {
return false;
}
// If promptable, return true - browser will prompt on first request
return true;
} catch {
// Permissions API not supported or permission not recognized - try anyway
return true;
}
};
export const isHttpsToHttpConnection = (pageUrl: string, targetUrl: string): boolean => {
try {
const page = new URL(pageUrl);
const target = new URL(targetUrl);
return page.protocol === "https:" && target.protocol === "http:";
} catch {
return false;
}
};
export const isLocalNetworkTarget = (targetUrl: string): boolean => {
try {
const url = new URL(targetUrl);
const hostname = url.hostname.toLowerCase();
return (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "::1" ||
hostname.endsWith(".local") ||
// Private IPv4 ranges
/^10\./.test(hostname) ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname) ||
/^192\.168\./.test(hostname)
);
} catch {
return false;
}
};

View file

@ -1,9 +0,0 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View file

@ -1,45 +0,0 @@
export type FeatureCoverageView = {
unstable_methods?: boolean;
planMode?: boolean;
permissions?: boolean;
questions?: boolean;
toolCalls?: boolean;
toolResults?: boolean;
textMessages?: boolean;
images?: boolean;
fileAttachments?: boolean;
sessionLifecycle?: boolean;
errorEvents?: boolean;
reasoning?: boolean;
status?: boolean;
commandExecution?: boolean;
fileChanges?: boolean;
mcpTools?: boolean;
streamingDeltas?: boolean;
itemStarted?: boolean;
variants?: boolean;
sharedProcess?: boolean;
};
export const emptyFeatureCoverage: FeatureCoverageView = {
unstable_methods: false,
planMode: false,
permissions: false,
questions: false,
toolCalls: false,
toolResults: false,
textMessages: false,
images: false,
fileAttachments: false,
sessionLifecycle: false,
errorEvents: false,
reasoning: false,
status: false,
commandExecution: false,
fileChanges: false,
mcpTools: false,
streamingDeltas: false,
itemStarted: false,
variants: false,
sharedProcess: false
};

View file

@ -1,12 +0,0 @@
export type RequestLog = {
id: number;
method: string;
url: string;
headers?: Record<string, string>;
body?: string;
status?: number;
responseBody?: string;
time: string;
curl: string;
error?: string;
};

View file

@ -1,24 +0,0 @@
export const formatJson = (value: unknown) => {
if (value === null || value === undefined) return "";
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
};
export const formatTime = (value: string) => {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleTimeString();
};
export const escapeSingleQuotes = (value: string) => value.replace(/'/g, `'\\''`);
export const formatShortId = (value: string, head = 8, tail = 4) => {
if (!value) return "";
if (value.length <= head + tail + 1) return value;
return `${value.slice(0, head)}...${value.slice(-tail)}`;
};

View file

@ -1,15 +0,0 @@
import { escapeSingleQuotes } from "./format";
export const buildCurl = (method: string, url: string, body?: string, token?: string) => {
const headers: string[] = [];
if (token) {
headers.push(`-H 'Authorization: Bearer ${escapeSingleQuotes(token)}'`);
}
if (body) {
headers.push(`-H 'Content-Type: application/json'`);
}
const data = body ? `-d '${escapeSingleQuotes(body)}'` : "";
return `curl -X ${method} ${headers.join(" ")} ${data} '${escapeSingleQuotes(url)}'`
.replace(/\s+/g, " ")
.trim();
};

View file

@ -1 +0,0 @@
/// <reference types="vite/client" />

View file

@ -1,17 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true
},
"include": ["src"]
}

View file

@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View file

@ -1,17 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig(({ command }) => ({
base: command === "build" ? "/ui/" : "/",
plugins: [react()],
server: {
port: 5173,
proxy: {
"/v1": {
target: "http://localhost:2468",
changeOrigin: true,
ws: true,
},
},
},
}));

View file

@ -1,3 +0,0 @@
.astro
dist
node_modules

View file

@ -1,5 +0,0 @@
:80 {
root * /srv
file_server
try_files {path} /index.html
}

View file

@ -1,26 +0,0 @@
FROM node:22-alpine AS build
WORKDIR /app
RUN npm install -g pnpm@9
# Copy website package
COPY frontend/packages/website/package.json ./
# Install dependencies
RUN pnpm install
# Copy website source
COPY frontend/packages/website/ .
# Build
RUN pnpm build
FROM caddy:alpine
COPY --from=build /app/dist /srv
RUN cat > /etc/caddy/Caddyfile <<'EOF'
:80 {
root * /srv
file_server
try_files {path} /index.html
}
EOF
EXPOSE 80

View file

@ -1,14 +0,0 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://sandbox-agent.dev',
output: 'static',
integrations: [
react(),
tailwind(),
sitemap()
]
});

View file

@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox Agent</title>
</head>
<body>
</body>
</html>

View file

@ -1,30 +0,0 @@
{
"name": "@sandbox-agent/website",
"private": true,
"version": "0.0.1",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^4.2.0",
"@astrojs/sitemap": "^3.2.0",
"@astrojs/tailwind": "^6.0.0",
"astro": "^5.1.0",
"framer-motion": "^12.0.0",
"lucide-react": "^0.469.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^3.4.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0"
}
}

View file

@ -1 +0,0 @@
<svg width="128" height="128" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="126" height="126" rx="44" fill="#0F0F0F"/><rect x="18.25" y="18.25" width="91.5" height="91.5" rx="25.75" stroke="#F0F0F0" stroke-width="8.5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.694 43.098c0-.622-.505-1.126-1.127-1.126h-8.444a5.114 5.114 0 0 0-5.112 5.111v33.824a5.114 5.114 0 0 0 5.112 5.112h8.444c.622 0 1.127-.505 1.127-1.127V43.098Zm24.424 27.869c-1.238-2.222-4.047-4.026-6.27-4.026H62.923c-.684 0-.93.555-.549 1.239l7.703 13.822c1.239 2.223 4.048 4.026 6.27 4.026h12.927c.683 0 .93-.555.548-1.239l-7.703-13.822Zm.538-18.718c0-5.672-4.605-10.277-10.277-10.277H63.31a1.21 1.21 0 0 0-1.209 1.209v18.137c0 .667.542 1.209 1.21 1.209h9.068c5.672 0 10.277-4.605 10.277-10.278Z" fill="#F0F0F0"/></svg>

Before

Width:  |  Height:  |  Size: 818 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

View file

@ -1 +0,0 @@
<svg viewBox="0 0 281 124" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M236.014 0C260.431 9.71106e-05 280.602 17.4115 280.603 44.7432C280.602 73.5337 260.065 94.1657 233.52 94.166C224.158 94.166 215.639 92.4222 208.63 88.4902C202.886 85.2698 198.203 80.6054 194.919 74.3379L188.115 121.822L187.946 123.016H174.214L174.448 121.423L191.772 2.49414H205.372L203.937 11.3369C212.143 3.86078 223.2 0.000153635 236.014 0ZM47.082 0.154297C56.4435 0.154297 65.0012 1.8991 72.0488 5.84863C77.8222 9.08305 82.5323 13.7713 85.8271 20.085L88.1201 3.69238L88.2861 2.49316H101.863L89.1611 90.6328L88.9873 91.8262H75.4092L76.7227 82.8555C68.5854 90.4564 57.3981 94.3231 44.5889 94.3232C20.1709 94.3232 0.000167223 76.9087 0 49.5771C0.000149745 20.7854 20.54 0.154871 47.082 0.154297ZM116.234 90.6357L116.061 91.8271H102.485L115.351 3.68555L115.521 2.49414H129.083L116.234 90.6357ZM140.673 90.6357L140.499 91.8271H126.924L139.789 3.68555L139.96 2.49414H153.521L140.673 90.6357ZM177.958 2.49414L165.108 90.6357L164.935 91.8271H151.36L164.225 3.68555L164.396 2.49414H177.958ZM48.4854 11.9844C27.8638 11.985 14.0133 28.3799 14.0127 48.9521C14.0127 57.7907 16.8094 66.1771 22.3145 72.334C27.7973 78.4657 36.0631 82.4932 47.2402 82.4932C67.8534 82.4925 81.7122 65.9487 81.7129 45.3682C81.7129 35.4076 78.2493 27.0792 72.4131 21.2441C66.5794 15.4088 58.2871 11.9844 48.4854 11.9844ZM233.362 11.8291C212.749 11.8297 198.89 28.3716 198.89 48.9521C198.89 58.9123 202.356 67.2403 208.189 73.0742C214.023 78.9107 222.315 82.3358 232.116 82.3359C252.738 82.3355 266.589 65.9407 266.59 45.3682C266.59 36.5296 263.795 28.1424 258.29 21.9863C252.807 15.8551 244.542 11.8291 233.362 11.8291Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.17 -->
<svg width="1200" height="1200" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<g id="g314">
<path id="path147" fill="#d97757" stroke="none" d="M 233.959793 800.214905 L 468.644287 668.536987 L 472.590637 657.100647 L 468.644287 650.738403 L 457.208069 650.738403 L 417.986633 648.322144 L 283.892639 644.69812 L 167.597321 639.865845 L 54.926208 633.825623 L 26.577238 627.785339 L 3.3e-05 592.751709 L 2.73832 575.27533 L 26.577238 559.248352 L 60.724873 562.228149 L 136.187973 567.382629 L 249.422867 575.194763 L 331.570496 580.026978 L 453.261841 592.671082 L 472.590637 592.671082 L 475.328857 584.859009 L 468.724915 580.026978 L 463.570557 575.194763 L 346.389313 495.785217 L 219.543671 411.865906 L 153.100723 363.543762 L 117.181267 339.060425 L 99.060455 316.107361 L 91.248367 266.01355 L 123.865784 230.093994 L 167.677887 233.073853 L 178.872513 236.053772 L 223.248367 270.201477 L 318.040283 343.570496 L 441.825592 434.738342 L 459.946411 449.798706 L 467.194672 444.64447 L 468.080597 441.020203 L 459.946411 427.409485 L 392.617493 305.718323 L 320.778564 181.932983 L 288.80542 130.630859 L 280.348999 99.865845 C 277.369171 87.221436 275.194641 76.590698 275.194641 63.624268 L 312.322174 13.20813 L 332.8591 6.604126 L 382.389313 13.20813 L 403.248352 31.328979 L 434.013519 101.71814 L 483.865753 212.537048 L 561.181274 363.221497 L 583.812134 407.919434 L 595.892639 449.315491 L 600.40271 461.959839 L 608.214783 461.959839 L 608.214783 454.711609 L 614.577271 369.825623 L 626.335632 265.61084 L 637.771851 131.516846 L 641.718201 93.745117 L 660.402832 48.483276 L 697.530334 24.000122 L 726.52356 37.852417 L 750.362549 72 L 747.060486 94.067139 L 732.886047 186.201416 L 705.100708 330.52356 L 686.979919 427.167847 L 697.530334 427.167847 L 709.61084 415.087341 L 758.496704 350.174561 L 840.644348 247.490051 L 876.885925 206.738342 L 919.167847 161.71814 L 946.308838 140.29541 L 997.61084 140.29541 L 1035.38269 196.429626 L 1018.469849 254.416199 L 965.637634 321.422852 L 921.825562 378.201538 L 859.006714 462.765259 L 819.785278 530.41626 L 823.409424 535.812073 L 832.75177 534.92627 L 974.657776 504.724915 L 1051.328979 490.872559 L 1142.818848 475.167786 L 1184.214844 494.496582 L 1188.724854 514.147644 L 1172.456421 554.335693 L 1074.604126 578.496765 L 959.838989 601.449829 L 788.939636 641.879272 L 786.845764 643.409485 L 789.261841 646.389343 L 866.255127 653.637634 L 899.194702 655.409424 L 979.812134 655.409424 L 1129.932861 666.604187 L 1169.154419 692.537109 L 1192.671265 724.268677 L 1188.724854 748.429688 L 1128.322144 779.194641 L 1046.818848 759.865845 L 856.590759 714.604126 L 791.355774 698.335754 L 782.335693 698.335754 L 782.335693 703.731567 L 836.69812 756.885986 L 936.322205 846.845581 L 1061.073975 962.81897 L 1067.436279 991.490112 L 1051.409424 1014.120911 L 1034.496704 1011.704712 L 924.885986 929.234924 L 882.604126 892.107544 L 786.845764 811.48999 L 780.483276 811.48999 L 780.483276 819.946289 L 802.550415 852.241699 L 919.087341 1027.409424 L 925.127625 1081.127686 L 916.671204 1098.604126 L 886.469849 1109.154419 L 853.288696 1103.114136 L 785.073914 1007.355835 L 714.684631 899.516785 L 657.906067 802.872498 L 650.979858 806.81897 L 617.476624 1167.704834 L 601.771851 1186.147705 L 565.530212 1200 L 535.328857 1177.046997 L 519.302124 1139.919556 L 535.328857 1066.550537 L 554.657776 970.792053 L 570.362488 894.68457 L 584.536926 800.134277 L 592.993347 768.724976 L 592.429626 766.630859 L 585.503479 767.516968 L 514.22821 865.369263 L 405.825531 1011.865906 L 320.053711 1103.677979 L 299.516815 1111.812256 L 263.919525 1093.369263 L 267.221497 1060.429688 L 287.114136 1031.114136 L 405.825531 880.107361 L 477.422913 786.52356 L 523.651062 732.483276 L 523.328918 724.671265 L 520.590698 724.671265 L 205.288605 929.395935 L 149.154434 936.644409 L 124.993355 914.01355 L 127.973183 876.885986 L 139.409409 864.80542 L 234.201385 799.570435 L 233.879227 799.8927 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

View file

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="90 125 270 70" fill="none">
<path fill="currentColor" d="m126.103 130-15.679 15.678v-6.703h-6.335v15.31h6.335v-8.607l4.48 4.48 15.678-15.679-4.479-4.479Zm-4.592 13.726h21.117v6.335h-21.117v-6.335ZM96.7 165.899h9.244L94 153.955l4.48-4.48 12.041 12.041-4.383 4.383h9.04v6.335H96.701v-6.335Zm16.967 17.172 12.597-12.597v9.679h6.335v-19.006h-6.335v9.136l-4.384-4.384-12.692 12.692 4.479 4.48Z"/>
<path fill="currentColor" d="m126.263 154.541 14.185 14.185 4.48-4.479-14.185-14.186-4.48 4.48Z"/>
<path fill="currentColor" fill-rule="evenodd" d="M168.882 176.693h9.245c5.23.08 9.161-.212 11.956-1.414 2.834-1.243 4.831-3.308 5.988-6.194 1.198-2.887 1.797-7.056 1.797-12.508 0-5.052-.499-9.243-1.497-12.089-.998-2.847-2.695-4.952-5.091-6.315-2.355-1.403-5.709-2.325-10.061-2.766-1.996-.2-3.932-.3-5.809-.3a152.116 152.116 0 0 0-4.093-.01c-.784.005-1.598.009-2.435.01v41.586Zm18.266-7.187c-1.437.922-3.433 1.463-5.989 1.623-.758.08-1.577.1-2.455.06-.838-.04-1.897-.1-3.174-.18v-29.887c1.677 0 3.513.1 5.51.3 2.675.321 4.731.963 6.168 1.925 1.437.962 2.455 2.445 3.054 4.45.599 1.964.899 4.73.899 8.298 0 3.769-.32 6.716-.959 8.84-.598 2.085-1.617 3.608-3.054 4.571Zm16.454 6.675c1.518.561 3.654.841 6.408.841 2.436 0 4.292-.3 5.57-.902 1.278-.641 2.455-1.804 3.533-3.487h.479v4.091h5.869v-21.29c0-2.927-.379-5.172-1.137-6.735-.759-1.604-2.057-2.726-3.893-3.368-1.797-.681-4.352-1.022-7.666-1.022-3.274 0-5.809.301-7.606.902-1.756.561-2.994 1.543-3.713 2.947-.718 1.363-1.2 3.332-1.2 5.893h6.65c0-1.684.399-2.826 1.198-3.428.838-.601 2.395-.902 4.671-.902 1.717 0 3.055.181 4.013.541.958.361 1.637.942 2.036 1.744s.599 1.945.599 3.428v.842h-5.091c-3.872 0-6.867.341-8.983 1.022-2.116.642-3.613 1.784-4.492 3.428-.878 1.603-1.317 3.869-1.317 6.795 0 2.486.299 4.41.898 5.773.639 1.363 1.697 2.325 3.174 2.887Zm12.517-5.593c-1.118.601-2.735.902-4.851.902-2.076 0-3.493-.321-4.252-.962-.759-.642-1.138-1.824-1.138-3.548 0-1.403.22-2.466.659-3.187.439-.762 1.158-1.303 2.156-1.624 1.038-.321 2.495-.501 4.372-.541l6.288-.06c-.08 2.606-.359 4.61-.839 6.013-.479 1.403-1.277 2.406-2.395 3.007Zm71.836 6.434c-3.753 0-6.647-.481-8.684-1.443-2.036-.962-3.473-2.606-4.311-4.931-.839-2.325-1.258-5.632-1.258-9.922 0-4.33.419-7.657 1.258-9.983.838-2.325 2.275-3.968 4.311-4.931 2.037-1.002 4.931-1.503 8.684-1.503 3.753 0 6.628.501 8.624 1.503 2.036.963 3.474 2.606 4.312 4.931.878 2.326 1.318 5.653 1.318 9.983 0 4.29-.44 7.597-1.318 9.922-.838 2.285-2.276 3.929-4.312 4.931-2.036.962-4.911 1.443-8.624 1.443Zm0-5.532c2.076 0 3.653-.321 4.731-.962 1.118-.642 1.917-1.724 2.396-3.247.479-1.524.719-3.709.719-6.555 0-2.887-.24-5.092-.719-6.615-.479-1.564-1.278-2.666-2.396-3.307-1.078-.682-2.655-1.023-4.731-1.023s-3.673.341-4.791 1.023c-1.118.641-1.916 1.743-2.395 3.307-.479 1.523-.719 3.728-.719 6.615 0 2.846.24 5.031.719 6.555.479 1.523 1.277 2.605 2.395 3.247 1.118.641 2.715.962 4.791.962Z" clip-rule="evenodd"/>
<path fill="currentColor" d="M304.407 144.606h6.408v4.935h.479c.998-2.045 2.116-3.428 3.353-4.15 1.278-.721 3.115-1.082 5.51-1.082 2.875 0 5.091.381 6.648 1.143 1.557.721 2.675 1.944 3.353 3.668.679 1.724 1.019 4.189 1.019 7.396v 20.208h-6.408v-20.328c0-1.644-.18-2.926-.539-3.849-.36-.922-.979-1.563-1.857-1.924s-2.136-.541-3.773-.541c-2.036 0-3.613.301-4.731.902-1.078.601-1.857 1.624-2.336 3.067-.479 1.443-.718 3.488-.718 6.134v16.539h-6.408v-32.118Z"/>
<path fill="currentColor" fill-rule="evenodd" d="M344.149 177.022c-2.755 0-4.891-.28-6.408-.841-1.477-.562-2.535-1.524-3.174-2.887-.599-1.363-.899-3.287-.899-5.773 0-2.926.44-5.192 1.318-6.795.878-1.644 2.376-2.786 4.492-3.428 2.116-.681 5.11-1.022 8.983-1.022h5.09v-.842c0-1.483-.199-2.626-.599-3.428-.399-.802-1.078-1.383-2.036-1.744-.958-.36-2.295-.541-4.012-.541-2.276 0-3.833.301-4.672.902-.798.602-1.197 1.744-1.197 3.428h-6.517c0-2.804.348-4.53 1.067-5.893.719-1.404 1.956-2.386 3.713-2.947 1.797-.601 4.332-.902 7.606-.902 3.314 0 5.869.341 7.665 1.022 1.837.642 3.135 1.764 3.893 3.368.759 1.563 1.138 3.808 1.138 6.735v 21.29h-5.869v-4.091h-.479c-1.078 1.683-2.256 2.846-3.534 3.487-1.277.602-3.134.902-5.569.902Zm1.258-5.532c2.116 0 3.733-.301 4.85-.902 1.118-.601 1.917-1.604 2.396-3.007.479-1.403.759-3.407.838-6.013l-6.288.06c-1.876.04-3.334.22-4.372.541-.998.321-1.716.862-2.156 1.624-.439.721-.658 1.784-.658 3.187 0 1.724.379 2.906 1.137 3.548.759.641 2.176.962 4.253.962Z" clip-rule="evenodd"/>
<path fill="currentColor" d="M253.349 144.54h-6.169l-6.058 26.414h-2.205l-5.816-26.414h-6.058l5.147 26.192a7.27 7.27 0 0 0 7.134 5.869h1.168l-1.309 4.111c-.065.168-.125.336-.185.503-.526 1.46-1.018 2.825-3.867 2.825h-3.969v5.211l7.355.076c2.218-.038 5.005-1.652 7.077-8.615l7.755-36.172Zm15.285 32.264c-2.741 0-4.818-.363-6.229-1.089-1.37-.725-2.298-1.814-2.781-3.265-.484-1.492-.726-3.528-.726-6.108v-16.449h-3.744l1.09-5.322h2.896v-6.773h5.866v6.773h7.31v5.322h-7.31V166.1c0 1.33.121 2.358.363 3.084.282.686.826 1.21 1.632 1.573.807.362 2.016.544 3.629.544h1.686v5.503h-3.682Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,6 +0,0 @@
<svg width="676" height="232" viewBox="0 0 676 232" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M545.35 202V30H608.499C627.501 30 642.162 34.0952 652.482 42.2857C662.802 50.3124 667.962 61.3695 667.962 75.4571C667.962 85.7771 665.259 94.0495 659.853 100.274C654.611 106.335 647.485 110.594 638.476 113.051C645.356 114.198 651.499 116.328 656.905 119.44C662.31 122.552 666.569 126.893 669.682 132.463C672.958 137.869 674.596 144.667 674.596 152.857C674.596 168.091 669.108 180.131 658.133 188.977C647.322 197.659 631.514 202 610.71 202H545.35ZM573.607 178.166H610.956C622.259 178.166 630.859 175.954 636.756 171.531C642.653 166.945 645.602 160.638 645.602 152.611C645.602 144.257 642.571 137.787 636.51 133.2C630.449 128.613 621.931 126.32 610.956 126.32H573.607V178.166ZM573.607 102.977H609.236C619.064 102.977 626.6 100.766 631.842 96.3429C637.084 91.92 639.705 85.8591 639.705 78.16C639.705 70.461 637.084 64.4819 631.842 60.2229C626.6 55.8 619.064 53.5886 609.236 53.5886H573.607V102.977Z" fill="currentColor"/>
<path d="M405.588 202V179.851L472.534 122.86C477.843 118.381 482.571 113.984 486.719 109.67C490.867 105.357 494.102 100.877 496.425 96.2316C498.748 91.4202 499.909 86.277 499.909 80.8019C499.909 70.8472 496.84 63.2153 490.701 57.9061C484.728 52.597 476.764 49.9424 466.81 49.9424C456.855 49.9424 448.808 53.0947 442.67 59.3993C436.531 65.704 433.461 74.2484 433.461 85.0326V88.019H405.837V83.5394C405.837 72.4234 408.326 62.6346 413.303 54.1731C418.281 45.5457 425.332 38.7434 434.457 33.766C443.748 28.7887 454.532 26.3 466.81 26.3C479.917 26.3 490.95 28.5398 499.909 33.0195C509.034 37.4991 515.919 43.7207 520.565 51.6845C525.376 59.6482 527.782 68.9392 527.782 79.5576C527.782 88.1849 526.206 95.8998 523.054 102.702C519.901 109.339 515.505 115.643 509.864 121.616C504.389 127.423 498.084 133.313 490.95 139.286L444.66 177.86H529.026V 202H405.588Z" fill="currentColor"/>
<path d="M274 202V30H386.292V55.0629H302.257V102.731H371.549V127.057H302.257V176.937H389.24V 202H274Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M188.212 157.998C186.672 157.998 185.71 159.665 186.48 160.998L202.585 188.894C203.476 190.437 202.056 192.287 200.335 191.826L151.491 178.737C149.357 178.165 147.163 179.432 146.592 181.566L133.504 230.411C133.042 232.132 130.731 232.436 129.84 230.893L113.732 202.992C112.962 201.659 111.037 201.659 110.268 202.992L94.1595 230.893C93.2686 232.436 90.9568 232.132 90.4956 230.411L77.4075 181.566C76.8357 179.432 74.6423 178.165 72.5085 178.737L23.664 191.826C21.9429 192.287 20.5234 190.437 21.4143 188.894L37.5192 160.998C38.289 159.665 37.3267 157.998 35.7871 157.998L3.57893 157.998C1.79713 157.998 0.904821 155.844 2.16476 154.584L37.9218 118.827C39.484 117.265 39.484 114.733 37.9218 113.171L2.16478 77.4133C0.904844 76.1533 1.7972 73.999 3.57902 73.9991L35.7837 73.9995C37.3233 73.9995 38.2856 72.3328 37.5158 70.9995L21.4143 43.11C20.5234 41.5669 21.9429 39.717 23.664 40.1781L72.5085 53.2665C74.6423 53.8383 76.8357 52.572 77.4075 50.4381L90.4956 1.59292C90.9568 -0.128187 93.2686 -0.432531 94.1595 1.11058L110.267 29.0111C111.037 30.3445 112.962 30.3445 113.732 29.0111L129.84 1.11058C130.73 -0.432532 133.042 -0.128189 133.503 1.59292L146.592 50.4381C147.163 52.572 149.357 53.8383 151.491 53.2665L200.335 40.1781C202.056 39.717 203.476 41.5669 202.585 43.11L186.483 70.9995C185.713 72.3328 186.676 73.9995 188.215 73.9995L220.421 73.9991C222.203 73.999 223.095 76.1533 221.835 77.4133L186.078 113.171C184.516 114.733 184.516 117.265 186.078 118.827L221.835 154.584C223.095 155.844 222.203 157.998 220.421 157.998L188.212 157.998ZM175.919 81.3306C177.366 79.8837 175.963 77.4549 173.987 77.9845L130.491 89.6396C128.357 90.2114 126.164 88.9451 125.592 86.8112L113.931 43.293C113.402 41.3166 110.597 41.3166 110.068 43.293L98.4069 86.8112C97.8351 88.9451 95.6418 90.2114 93.5079 89.6396L50.0136 77.9849C48.0371 77.4553 46.6348 79.8841 48.0817 81.331L79.9216 113.171C81.4837 114.733 81.4837 117.266 79.9216 118.828L48.0742 150.675C46.6273 152.122 48.0296 154.55 50.0061 154.021L93.5079 142.364C95.6418 141.792 97.8351 143.059 98.4069 145.192L110.068 188.711C110.597 190.687 113.402 190.687 113.931 188.711L125.592 145.192C126.164 143.059 128.357 141.792 130.491 142.364L173.994 154.021C175.971 154.551 177.373 152.122 175.926 150.675L144.079 118.828C142.516 117.266 142.516 114.733 144.079 113.171L175.919 81.3306Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#10A37F" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>OpenAI icon</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v 2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v 2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1 +0,0 @@
<svg width='32' height='40' viewBox='0 0 32 40' fill='none' xmlns='http://www.w3.org/2000/svg'><g clip-path='url(#clip0_1311_94973)'><path d='M24 32H8V16H24V32Z' fill='#4B4646'/><path d='M24 8H8V32H24V8ZM32 40H0V0H32V40Z' fill='#F1ECEC'/></g><defs><clipPath id='clip0_1311_94973'><rect width='32' height='40' fill='white'/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 347 B

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
<!-- P shape: outer boundary clockwise, inner hole counter-clockwise -->
<path fill="#fff" fill-rule="evenodd" d="
M165.29 165.29
H517.36
V400
H400
V517.36
H282.65
V634.72
H165.29
Z
M282.65 282.65
V400
H400
V282.65
Z
"/>
<!-- i dot -->
<path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 473 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,3 +0,0 @@
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M24 22.525H0l12-21.05 12 21.05z"/>
</svg>

Before

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

View file

@ -1 +0,0 @@
<svg width="128" height="128" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="126" height="126" rx="44" fill="#0F0F0F"/><rect x="18.25" y="18.25" width="91.5" height="91.5" rx="25.75" stroke="#F0F0F0" stroke-width="8.5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.694 43.098c0-.622-.505-1.126-1.127-1.126h-8.444a5.114 5.114 0 0 0-5.112 5.111v33.824a5.114 5.114 0 0 0 5.112 5.112h8.444c.622 0 1.127-.505 1.127-1.127V43.098Zm24.424 27.869c-1.238-2.222-4.047-4.026-6.27-4.026H62.923c-.684 0-.93.555-.549 1.239l7.703 13.822c1.239 2.223 4.048 4.026 6.27 4.026h12.927c.683 0 .93-.555.548-1.239l-7.703-13.822Zm.538-18.718c0-5.672-4.605-10.277-10.277-10.277H63.31a1.21 1.21 0 0 0-1.209 1.209v18.137c0 .667.542 1.209 1.21 1.209h9.068c5.672 0 10.277-4.605 10.277-10.278Z" fill="#F0F0F0"/></svg>

Before

Width:  |  Height:  |  Size: 818 B

View file

@ -1,9 +0,0 @@
<svg width="204" height="68" viewBox="0 0 204 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="62" height="62" rx="17.55" stroke="white" stroke-width="6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.9979 19.7671C28.9979 19.3436 28.6541 19 28.2308 19H22.4809C20.5597 19 19 20.5597 19 22.4807V45.5125C19 47.4336 20.5597 48.9935 22.4809 48.9935H28.2308C28.6541 48.9935 28.9979 48.6496 28.9979 48.2263V19.7671ZM45.6293 38.7439C44.7861 37.231 42.8732 36.0028 41.3604 36.0028H32.5577C32.0922 36.0028 31.9249 36.3807 32.1843 36.8462L37.4298 46.2586C38.273 47.7717 40.1858 49 41.6987 49H50.5012C50.9667 49 51.1342 48.6221 50.8745 48.1563L45.6293 38.7439ZM45.9963 25.9983C45.9963 22.1359 42.8604 19 38.9977 19H32.8227C32.3682 19 31.9994 19.3688 31.9994 19.8233V32.1734C31.9994 32.6278 32.3682 32.9969 32.8227 32.9969H38.9977C42.8604 32.9969 45.9963 29.861 45.9963 25.9983Z" fill="white"/>
<path d="M197.177 18.564C197.485 18.564 197.691 18.7698 197.691 19.0784V 25.8685C197.691 26.1257 197.845 26.28 198.103 26.28H202.372C202.681 26.28 202.886 26.4858 202.886 26.7944V31.2697C202.886 31.5783 202.681 31.7841 202.372 31.7841H198.103C197.845 31.7841 197.691 31.9384 197.691 32.1956V51.4856C197.691 51.7942 197.485 52 197.177 52H191.415C191.107 52 190.901 51.7942 190.901 51.4856V32.1956C190.901 31.9384 190.747 31.7841 190.489 31.7841H185.808C185.5 31.7841 185.294 31.5783 185.294 31.2697V 26.7944C185.294 26.4858 185.5 26.28 185.808 26.28H190.489C190.747 26.28 190.901 26.1257 190.901 25.8685V19.0784C190.901 18.7698 191.107 18.564 191.415 18.564H197.177Z" fill="white"/>
<path d="M172.602 52.6173C165.143 52.6173 159.021 46.4959 159.021 38.8828C159.021 31.3211 164.628 25.457 172.036 25.457C178.311 25.457 183.558 30.0866 184.021 35.6421C184.073 35.8993 183.918 36.105 183.713 36.2594L168.281 45.2614C168.023 45.4157 167.972 45.6729 168.229 45.8786C169.567 47.0103 171.213 47.2675 172.602 47.2675C175.791 47.2675 177.386 45.7758 178.723 43.5638C178.877 43.3066 179.083 43.2038 179.34 43.2552L183.713 44.0268C184.021 44.0782 184.227 44.2326 184.176 44.4383C183.25 48.1934 179.186 52.6173 172.602 52.6173ZM165.503 40.1174L165.606 40.426C165.709 40.7346 165.966 40.7861 166.172 40.6318L176.717 34.1503C176.923 33.996 177.025 33.8417 176.871 33.5845C175.945 32.0413 174.042 31.0639 171.676 31.3211C168.229 31.7326 164.114 35.8993 165.503 40.1174Z" fill="white"/>
<path d="M132.755 26.8973C132.601 26.5372 132.806 26.28 133.166 26.28H139.288C139.545 26.28 139.751 26.3829 139.854 26.6915L145.718 42.2264C145.821 42.5865 146.129 42.5865 146.232 42.2264L152.148 26.6915C152.251 26.3829 152.456 26.28 152.714 26.28H158.835C159.195 26.28 159.401 26.5372 159.247 26.8973L149.113 51.5885C149.01 51.8971 148.804 52 148.547 52H143.403C143.146 52 142.94 51.8971 142.837 51.5885L132.755 26.8973Z" fill="white"/>
<path d="M123.506 52C123.198 52 122.992 51.7942 122.992 51.4856V 26.7944C122.992 26.4858 123.198 26.28 123.506 26.28H129.267C129.576 26.28 129.782 26.4858 129.782 26.7944V51.4856C129.782 51.7942 129.576 52 129.267 52H123.506ZM122.375 19.7986C122.375 17.5352 124.175 15.7348 126.387 15.7348C128.65 15.7348 130.399 17.5352 130.399 19.7986C130.399 22.0105 128.599 23.8109 126.387 23.8109C124.175 23.8109 122.375 22.0105 122.375 19.7986Z" fill="white"/>
<path d="M105.23 15.992C112.895 15.992 118.296 20.8274 118.296 28.4405C118.296 33.1215 116.393 36.568 112.74 38.5742C112.483 38.7285 112.483 38.9342 112.74 39.14C116.855 41.9692 118.604 47.2675 118.759 51.4856C118.759 51.7942 118.553 52 118.244 52H112.997C112.689 52 112.483 51.8457 112.483 51.4856C112.277 47.6276 109.602 41.3519 102.864 40.426C102.349 40.426 101.784 40.3746 101.218 40.3746C100.961 40.3231 100.806 40.4774 100.806 40.7346V51.4856C100.806 51.7942 100.6 52 100.292 52H94.5305C94.2219 52 94.0161 51.7942 94.0161 51.4856V16.5064C94.0161 16.1978 94.2219 15.992 94.5305 15.992H105.23ZM100.806 33.996C100.806 34.2532 100.961 34.4075 101.218 34.4075H105.076C114.283 34.4075 114.283 22.2162 105.539 22.2162H101.218C100.961 22.2162 100.806 22.3706 100.806 22.6278V33.996Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://sandbox-agent.dev/sitemap-index.xml

View file

@ -1,132 +0,0 @@
'use client';
import { useState } from 'react';
import { ChevronDown } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
const faqs = [
{
question: 'Does this replace the Vercel AI SDK?',
answer:
"No, they're complementary. AI SDK is for building chat interfaces and calling LLMs. This SDK is for controlling autonomous coding agents that write code and run commands. Use AI SDK for your UI, use this when you need a coding agent to actually code.",
},
{
question: 'Which coding agents are supported?',
answer:
'Claude Code, Codex, OpenCode, Amp, and Pi. The SDK normalizes their APIs so you can swap between them without changing your code.',
},
{
question: 'How is session data persisted?',
answer:
"This SDK does not handle persisting session data. Events stream in a universal JSON schema that you can persist anywhere. Consider using Postgres or <a href='https://rivet.gg' target='_blank' rel='noopener noreferrer' class='text-orange-400 hover:underline'>Rivet Actors</a> for data persistence.",
},
{
question: 'Can I run this locally or does it require a sandbox provider?',
answer:
'Both. Run locally for development, deploy to E2B, Daytona, or Vercel Sandboxes for production.',
},
{
question: 'Does it support [platform]?',
answer:
"The server is a single Rust binary that runs anywhere with a curl install. If your platform can run Linux binaries (Docker, VMs, etc.), it works. See the deployment guides for E2B, Daytona, and Vercel Sandboxes.",
},
{
question: 'Can I use this with my personal API keys?',
answer:
"Yes. Use <code>sandbox-agent credentials extract-env</code> to extract API keys from your local agent configs (Claude Code, Codex, OpenCode, Amp, Pi) and pass them to the sandbox environment.",
},
{
question: 'Why Rust and not [language]?',
answer:
"Rust gives us a single static binary, fast startup, and predictable memory usage. That makes it easy to run inside sandboxes or in CI without shipping a large runtime, such as Node.js.",
},
{
question: "Why can't I just run coding agents locally?",
answer:
"You can for development. But in production, you need isolation. Coding agents execute arbitrary code — that can't happen on your servers. Sandboxes provide the isolation; this SDK provides the HTTP API to control coding agents remotely.",
},
{
question: "How is this different from the agent's official SDK?",
answer:
"Official SDKs assume local execution. They spawn processes and expect interactive terminals. This SDK runs a server inside a sandbox that you connect to over HTTP — designed for remote control from the start.",
},
{
question: 'Why not just SSH into the sandbox?',
answer:
"Coding agents expect interactive terminals with proper TTY handling. SSH with piped commands breaks tool confirmations, streaming output, and human-in-the-loop flows. The SDK handles all of this over a clean HTTP API.",
},
];
function FAQItem({ question, answer }: { question: string; answer: string }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="border-t border-white/10 first:border-t-0">
<button
onClick={() => setIsOpen(!isOpen)}
className="group flex w-full items-center justify-between py-5 text-left"
>
<span className="text-base font-normal text-white pr-4 group-hover:text-zinc-300 transition-colors">{question}</span>
<ChevronDown
className={`h-4 w-4 shrink-0 text-zinc-500 transition-transform duration-200 ${
isOpen ? 'rotate-180' : ''
}`}
/>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<p className="pb-5 text-sm leading-relaxed text-zinc-500" dangerouslySetInnerHTML={{ __html: answer }} />
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export function FAQ() {
return (
<section className="border-t border-white/10 py-48">
<div className="mx-auto max-w-7xl px-6">
<div className="mb-12 text-center">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="mb-2 text-2xl font-normal tracking-tight text-white md:text-4xl"
>
Frequently Asked Questions
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="mx-auto max-w-xl text-base leading-relaxed text-zinc-500"
>
Common questions about running agents in sandboxes.
</motion.p>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className="mx-auto max-w-3xl"
>
{faqs.map((faq, index) => (
<FAQItem key={index} question={faq.question} answer={faq.answer} />
))}
</motion.div>
</div>
</section>
);
}

View file

@ -1,121 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { Workflow, Server, Database, Download, Globe, Plug } from 'lucide-react';
export function FeatureGrid() {
return (
<section id="features" className="border-t border-white/10 py-48">
<div className="mx-auto max-w-7xl px-6">
<div className="mb-12">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="mb-2 text-2xl font-normal tracking-tight text-white md:text-4xl"
>
How it works.
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="max-w-xl text-base leading-relaxed text-zinc-500"
>
A server runs inside your sandbox. Your app connects over HTTP to control any coding agent.
</motion.p>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
>
{/* Universal Agent API - Span full width */}
<div className="group col-span-full flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
<div className="flex items-center gap-3">
<div className="text-zinc-500 transition-colors group-hover:text-orange-400">
<Workflow className="h-4 w-4" />
</div>
<h4 className="text-base font-normal text-white">Universal Agent API</h4>
</div>
<p className="text-zinc-500 leading-relaxed text-base max-w-2xl">
Claude Code, Codex, OpenCode, and Amp each have different APIs. We provide a single,
unified interface to control them all.
</p>
</div>
{/* Streaming Events */}
<div className="group flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
<div className="flex items-center gap-3">
<div className="text-zinc-500 transition-colors group-hover:text-green-400">
<Server className="h-4 w-4" />
</div>
<h4 className="text-base font-normal text-white">Streaming Events</h4>
</div>
<p className="text-zinc-500 text-sm leading-relaxed">
Real-time SSE stream of everything the agent does. Persist to your storage, replay sessions, audit everything.
</p>
</div>
{/* Universal Schema */}
<div className="group flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
<div className="flex items-center gap-3">
<div className="text-zinc-500 transition-colors group-hover:text-purple-400">
<Database className="h-4 w-4" />
</div>
<h4 className="text-base font-normal text-white">Universal Schema</h4>
</div>
<p className="text-zinc-500 text-sm leading-relaxed">
Standardized session schema that covers all features of all agents. Includes tool calls, permission requests, file edits, etc.
</p>
</div>
{/* Runs Inside Any Sandbox */}
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
<div className="flex items-center gap-3">
<div className="text-zinc-500 transition-colors group-hover:text-blue-400">
<Globe className="h-4 w-4" />
</div>
<h4 className="text-base font-normal text-white">Runs Inside Any Sandbox</h4>
</div>
<p className="text-zinc-500 text-sm leading-relaxed">
Lightweight static binary. One curl command to install inside E2B, Daytona, Vercel Sandboxes, or Docker.
</p>
</div>
{/* Session Management */}
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
<div className="flex items-center gap-3">
<div className="text-zinc-500 transition-colors group-hover:text-amber-400">
<Download className="h-4 w-4" />
</div>
<h4 className="text-base font-normal text-white">Session Management</h4>
</div>
<p className="text-zinc-500 text-sm leading-relaxed">
Create sessions, send messages, persist transcripts. Full session lifecycle management over HTTP.
</p>
</div>
{/* OpenCode SDK & UI Support */}
<div className="group lg:col-span-2 flex flex-col gap-4 rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
<div className="flex items-center gap-3">
<div className="text-zinc-500 transition-colors group-hover:text-pink-400">
<Plug className="h-4 w-4" />
</div>
<h4 className="text-base font-normal text-white">OpenCode Support</h4>
<span className="rounded-full border border-white/10 px-2 py-0.5 text-[10px] font-medium text-zinc-500 transition-colors group-hover:text-pink-400 group-hover:border-pink-400/30">Experimental</span>
</div>
<p className="text-zinc-500 text-sm leading-relaxed">
Connect OpenCode CLI, SDK, or web UI to control agents through familiar OpenCode tooling.
</p>
</div>
</motion.div>
</div>
</section>
);
}

View file

@ -1,168 +0,0 @@
'use client';
import { motion } from 'framer-motion';
const footer = {
products: [
{ name: 'Actors', href: 'https://rivet.dev/docs/actors' },
{ name: 'Sandbox Agent SDK', href: '/docs' },
],
developers: [
{ name: 'Documentation', href: '/docs' },
{ name: 'Changelog', href: 'https://github.com/rivet-dev/sandbox-agent/releases' },
{ name: 'Blog', href: 'https://www.rivet.dev/blog/' },
],
legal: [
{ name: 'Terms', href: 'https://rivet.dev/terms' },
{ name: 'Privacy Policy', href: 'https://rivet.dev/privacy' },
{ name: 'Acceptable Use', href: 'https://rivet.dev/acceptable-use' },
],
social: [
{
name: 'Discord',
href: 'https://discord.gg/auCecybynK',
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
),
},
{
name: 'GitHub',
href: 'https://github.com/rivet-dev/sandbox-agent',
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
),
},
{
name: 'Twitter',
href: 'https://x.com/rivet_dev',
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
),
},
],
};
export function Footer() {
return (
<footer className="border-t border-white/10 bg-black">
<div className="mx-auto max-w-6xl px-6 py-16 lg:py-20">
<div className="xl:grid xl:grid-cols-12 xl:gap-16">
{/* Logo & Social */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="space-y-6 xl:col-span-4"
>
<a href="https://rivet.dev" className="inline-block">
<img src="/rivet-logo-text-white.svg" alt="Rivet" className="h-6 w-auto opacity-90 hover:opacity-100 transition-opacity" />
</a>
<p className="text-sm leading-6 text-zinc-500">
Infrastructure for software that thinks
</p>
<div className="flex space-x-4">
{footer.social.map((item) => (
<a
key={item.name}
href={item.href}
className="text-zinc-500 hover:text-white transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<span className="sr-only">{item.name}</span>
{item.icon}
</a>
))}
</div>
</motion.div>
{/* Links */}
<div className="mt-12 grid grid-cols-2 gap-8 md:grid-cols-3 xl:col-span-8 xl:mt-0">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
>
<h3 className="text-sm font-semibold leading-6 text-white">Products</h3>
<ul role="list" className="mt-4 space-y-3">
{footer.products.map((item) => (
<li key={item.name}>
<a
href={item.href}
className="text-sm leading-6 text-zinc-500 hover:text-white transition-colors"
>
{item.name}
</a>
</li>
))}
</ul>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.15 }}
>
<h3 className="text-sm font-semibold leading-6 text-white">Developers</h3>
<ul role="list" className="mt-4 space-y-3">
{footer.developers.map((item) => (
<li key={item.name}>
<a
href={item.href}
className="text-sm leading-6 text-zinc-500 hover:text-white transition-colors"
>
{item.name}
</a>
</li>
))}
</ul>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<h3 className="text-sm font-semibold leading-6 text-white">Legal</h3>
<ul role="list" className="mt-4 space-y-3">
{footer.legal.map((item) => (
<li key={item.name}>
<a
href={item.href}
className="text-sm leading-6 text-zinc-500 hover:text-white transition-colors"
>
{item.name}
</a>
</li>
))}
</ul>
</motion.div>
</div>
</div>
{/* Bottom */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.3 }}
className="mt-12 border-t border-white/10 pt-8"
>
<p className="text-xs text-zinc-600 text-center">
&copy; {new Date().getFullYear()} Rivet Gaming, Inc. All rights reserved.
</p>
</motion.div>
</div>
</footer>
);
}

View file

@ -1,237 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { Code, Server, GitBranch } from 'lucide-react';
import { CopyButton } from './ui/CopyButton';
const sdkCodeRaw = `import { SandboxAgent } from "sandbox-agent";
const client = await SandboxAgent.start();
await client.createSession("my-session", {
agent: "claude-code",
});
await client.postMessage("my-session", {
message: "Hello, world!",
});
for await (const event of client.streamEvents("my-session")) {
console.log(event.type, event.data);
}`;
function SdkCodeHighlighted() {
return (
<pre className="overflow-x-auto p-3 font-mono text-[11px] leading-relaxed">
<code>
<span className="text-purple-400">import</span>
<span className="text-zinc-300">{" { "}</span>
<span className="text-white">SandboxAgent</span>
<span className="text-zinc-300">{" } "}</span>
<span className="text-purple-400">from</span>
<span className="text-zinc-300"> </span>
<span className="text-green-400">"sandbox-agent"</span>
<span className="text-zinc-300">;</span>
{"\n\n"}
<span className="text-purple-400">const</span>
<span className="text-zinc-300"> client = </span>
<span className="text-purple-400">await</span>
<span className="text-zinc-300"> SandboxAgent.</span>
<span className="text-blue-400">start</span>
<span className="text-zinc-300">();</span>
{"\n\n"}
<span className="text-purple-400">await</span>
<span className="text-zinc-300"> client.</span>
<span className="text-blue-400">createSession</span>
<span className="text-zinc-300">(</span>
<span className="text-green-400">"my-session"</span>
<span className="text-zinc-300">{", {"}</span>
{"\n"}
<span className="text-zinc-300">{" agent: "}</span>
<span className="text-green-400">"claude-code"</span>
<span className="text-zinc-300">,</span>
{"\n"}
<span className="text-zinc-300">{"});"}</span>
{"\n\n"}
<span className="text-purple-400">await</span>
<span className="text-zinc-300"> client.</span>
<span className="text-blue-400">postMessage</span>
<span className="text-zinc-300">(</span>
<span className="text-green-400">"my-session"</span>
<span className="text-zinc-300">{", {"}</span>
{"\n"}
<span className="text-zinc-300">{" message: "}</span>
<span className="text-green-400">"Hello, world!"</span>
<span className="text-zinc-300">,</span>
{"\n"}
<span className="text-zinc-300">{"});"}</span>
{"\n\n"}
<span className="text-purple-400">for await</span>
<span className="text-zinc-300"> (</span>
<span className="text-purple-400">const</span>
<span className="text-zinc-300"> event </span>
<span className="text-purple-400">of</span>
<span className="text-zinc-300"> client.</span>
<span className="text-blue-400">streamEvents</span>
<span className="text-zinc-300">(</span>
<span className="text-green-400">"my-session"</span>
<span className="text-zinc-300">{")) {"}</span>
{"\n"}
<span className="text-zinc-300">{" console."}</span>
<span className="text-blue-400">log</span>
<span className="text-zinc-300">(event.type, event.data);</span>
{"\n"}
<span className="text-zinc-300">{"}"}</span>
</code>
</pre>
);
}
const sandboxCommand = `curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh`;
const sourceCommands = `git clone https://github.com/rivet-dev/sandbox-agent
cd sandbox-agent
cargo run -p sandbox-agent --release`;
export function GetStarted() {
return (
<section id="get-started" className="border-t border-white/10 py-48">
<div className="mx-auto max-w-7xl px-6">
<div className="mb-12">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="mb-2 text-2xl font-normal tracking-tight text-white md:text-4xl"
>
Get Started
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="max-w-xl text-base leading-relaxed text-zinc-500"
>
Choose the installation method that works best for your use case.
</motion.p>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="grid grid-cols-1 gap-4 md:grid-cols-3"
>
{/* Option 1: SDK */}
<div className="group flex flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
<div className="mb-4 flex items-center gap-3">
<div className="text-zinc-500">
<Code className="h-4 w-4" />
</div>
<div>
<h3 className="text-base font-normal text-white">TypeScript SDK</h3>
<p className="text-xs text-zinc-500">Embed in your application</p>
</div>
</div>
<p className="mb-4 text-sm leading-relaxed text-zinc-500">
Import the TypeScript SDK directly into your Node or browser application. Full type safety and streaming support.
</p>
<div className="flex-1 flex flex-col">
<div className="overflow-hidden rounded-lg border border-white/10 bg-black/50 flex-1 flex flex-col">
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
<span className="text-[10px] font-medium text-zinc-500">example.ts</span>
<CopyButton text={sdkCodeRaw} />
</div>
<SdkCodeHighlighted />
</div>
</div>
</div>
{/* Option 2: HTTP API */}
<div className="group flex flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
<div className="mb-4 flex items-center gap-3">
<div className="text-zinc-500">
<Server className="h-4 w-4" />
</div>
<div>
<h3 className="text-base font-normal text-white">HTTP API</h3>
<p className="text-xs text-zinc-500">Run as a server</p>
</div>
</div>
<p className="mb-4 text-sm leading-relaxed text-zinc-500">
Run as an HTTP server and connect from any language. Deploy to E2B, Daytona, Vercel, or your own infrastructure.
</p>
<div className="flex-1 flex flex-col">
<div className="overflow-hidden rounded-lg border border-white/10 bg-black/50 flex-1 flex flex-col">
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
<span className="text-[10px] font-medium text-zinc-500">terminal</span>
<CopyButton text={sandboxCommand} />
</div>
<pre className="overflow-x-auto p-3 font-mono text-[11px] leading-relaxed flex-1">
<code>
<span className="text-zinc-500">$ </span>
<span className="text-zinc-300">curl -fsSL \</span>
{"\n"}
<span className="text-zinc-300">{" "}</span>
<span className="text-green-400">https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh</span>
<span className="text-zinc-300"> | </span>
<span className="text-blue-400">sh</span>
</code>
</pre>
</div>
</div>
</div>
{/* Option 3: Open Source */}
<div className="group flex flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition-colors hover:border-white/20">
<div className="mb-4 flex items-center gap-3">
<div className="text-zinc-500">
<GitBranch className="h-4 w-4" />
</div>
<div>
<h3 className="text-base font-normal text-white">Open Source</h3>
<p className="text-xs text-zinc-500">Full control</p>
</div>
</div>
<p className="mb-4 text-sm leading-relaxed text-zinc-500">
Clone the repo and build with Cargo. Customize, contribute, or embed directly in your Rust project.
</p>
<div className="flex-1 flex flex-col">
<div className="overflow-hidden rounded-lg border border-white/10 bg-black/50 flex-1 flex flex-col">
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-3 py-2">
<span className="text-[10px] font-medium text-zinc-500">terminal</span>
<CopyButton text={sourceCommands} />
</div>
<pre className="overflow-x-auto p-3 font-mono text-[11px] leading-relaxed flex-1">
<code>
<span className="text-zinc-500">$ </span>
<span className="text-blue-400">git clone</span>
<span className="text-zinc-300"> </span>
<span className="text-green-400">https://github.com/rivet-dev/sandbox-agent</span>
{"\n"}
<span className="text-zinc-500">$ </span>
<span className="text-blue-400">cd</span>
<span className="text-zinc-300"> sandbox-agent</span>
{"\n"}
<span className="text-zinc-500">$ </span>
<span className="text-blue-400">cargo run</span>
<span className="text-zinc-300"> -p sandbox-agent --release</span>
</code>
</pre>
</div>
</div>
</div>
</motion.div>
</div>
</section>
);
}

View file

@ -1,78 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
interface GitHubStarsProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
repo?: string;
}
function formatNumber(num: number): string {
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}k`;
}
return num.toString();
}
export function GitHubStars({
repo = 'rivet-dev/sandbox-agent',
className,
...props
}: GitHubStarsProps) {
const [stars, setStars] = useState<number | null>(null);
useEffect(() => {
const cacheKey = `github-stars-${repo}`;
const cachedData = sessionStorage.getItem(cacheKey);
if (cachedData) {
const { stars: cachedStars, timestamp } = JSON.parse(cachedData);
// Check if cache is less than 5 minutes old
if (Date.now() - timestamp < 5 * 60 * 1000) {
setStars(cachedStars);
return;
}
}
fetch(`https://api.github.com/repos/${repo}`)
.then((response) => {
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
})
.then((data) => {
const newStars = data.stargazers_count;
setStars(newStars);
sessionStorage.setItem(
cacheKey,
JSON.stringify({
stars: newStars,
timestamp: Date.now(),
}),
);
})
.catch((err) => {
console.error('Failed to fetch stars', err);
});
}, [repo]);
return (
<a
href={`https://github.com/${repo}`}
target="_blank"
rel="noreferrer"
className={className}
{...props}
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
<span className="hidden md:inline">
{stars ? `${formatNumber(stars)} Stars` : 'GitHub'}
</span>
</a>
);
}

View file

@ -1,288 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Terminal, Check, ArrowRight } from 'lucide-react';
const ADAPTERS = [
{ label: 'Claude Code', color: '#D97757', x: 20, y: 70, logo: '/logos/claude.svg' },
{ label: 'Codex', color: '#10A37F', x: 132, y: 70, logo: 'openai' },
{ label: 'Pi', color: '#06B6D4', x: 244, y: 70, logo: 'pi' },
{ label: 'Amp', color: '#F59E0B', x: 76, y: 155, logo: '/logos/amp.svg' },
{ label: 'OpenCode', color: '#8B5CF6', x: 188, y: 155, logo: 'opencode' },
];
function UniversalAPIDiagram() {
const [activeIndex, setActiveIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setActiveIndex((prev) => (prev + 1) % ADAPTERS.length);
}, 2000);
return () => clearInterval(interval);
}, []);
return (
<div className="relative w-full aspect-[4/3] sm:aspect-[16/9] bg-[#050505] rounded-2xl border border-white/10 overflow-hidden flex items-center justify-center shadow-2xl">
{/* Background Dots - color changes with active adapter */}
<div
className="absolute inset-0 opacity-[0.15] pointer-events-none transition-all duration-1000"
style={{
backgroundImage: `radial-gradient(circle, ${ADAPTERS[activeIndex].color} 1px, transparent 1px)`,
backgroundSize: '24px 24px',
}}
/>
<svg viewBox="0 0 800 450" className="w-full h-full relative z-10">
<defs>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="3" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<filter id="invert-white" colorInterpolationFilters="sRGB">
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" />
</filter>
</defs>
{/* YOUR APP NODE - Glass dark effect with backdrop blur */}
<foreignObject x="60" y="175" width="180" height="100">
<div
className="w-full h-full rounded-2xl border border-white/10 bg-black/40 backdrop-blur-md flex items-center justify-center"
>
<span className="text-white text-xl font-bold">Your App</span>
</div>
</foreignObject>
{/* HTTP/SSE LINE */}
<g>
<path d="M240 225 L360 225" stroke="#3B82F6" strokeWidth="2" strokeDasharray="6 4" fill="none" opacity="0.6" />
<circle r="4" fill="#3B82F6" filter="url(#glow)">
<animateMotion path="M240 225 L360 225" dur="2s" repeatCount="indefinite" />
</circle>
<circle r="4" fill="#3B82F6" filter="url(#glow)">
<animateMotion path="M360 225 L240 225" dur="2s" repeatCount="indefinite" />
</circle>
<rect x="255" y="195" width="90" height="22" rx="11" fill="#111" stroke="#333" strokeWidth="1" />
<text x="300" y="210" fill="#60A5FA" textAnchor="middle" fontSize="11" fontWeight="800" fontFamily="monospace">
HTTP / SSE
</text>
</g>
{/* SANDBOX BOUNDARY - Glass dark effect with backdrop blur */}
<foreignObject x="360" y="45" width="410" height="360">
<div className="w-full h-full rounded-3xl border border-white/10 bg-black/40 backdrop-blur-md">
<div className="text-white text-sm font-extrabold tracking-[0.2em] text-center pt-4">
SANDBOX
</div>
</div>
</foreignObject>
{/* SANDBOX AGENT SDK */}
<g transform="translate(385, 110)">
<rect width="360" height="270" rx="20" fill="rgba(0,0,0,0.4)" stroke="rgba(255,255,255,0.2)" strokeWidth="1" />
<text x="180" y="35" fill="#FFFFFF" textAnchor="middle" fontSize="18" fontWeight="800">
Sandbox Agent Server
</text>
{/* PROVIDER ADAPTERS */}
{ADAPTERS.map((p, i) => {
const isActive = i === activeIndex;
return (
<g key={i} transform={`translate(${p.x}, ${p.y})`}>
<rect
width="95"
height="58"
rx="10"
fill={isActive ? '#1A1A1E' : '#111'}
stroke={isActive ? p.color : '#333'}
strokeWidth={isActive ? 2 : 1.5}
/>
<g opacity={isActive ? 1 : 0.4}>
{p.logo === 'openai' ? (
<svg x="36.75" y="8" width="22" height="22" viewBox="0 0 24 24" fill="none">
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" fill="#ffffff" />
</svg>
) : p.logo === 'opencode' ? (
<svg x="38.5" y="8" width="17" height="22" viewBox="0 0 32 40" fill="none">
<path d="M24 32H8V16H24V32Z" fill="#4B4646"/>
<path d="M24 8H8V32H24V8ZM32 40H0V0H32V40Z" fill="#F1ECEC"/>
</svg>
) : p.logo === 'pi' ? (
<svg x="36.75" y="8" width="22" height="22" viewBox="0 0 800 800" fill="none">
<path fill="#fff" fillRule="evenodd" d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"/>
<path fill="#fff" d="M517.36 400H634.72V634.72H517.36Z"/>
</svg>
) : (
<image href={p.logo} x="36.75" y="8" width="22" height="22" filter="url(#invert-white)" />
)}
</g>
<text
x="47.5"
y="46"
fill="#FFFFFF"
textAnchor="middle"
fontSize="10"
fontWeight="600"
opacity={isActive ? 1 : 0.4}
>
{p.label}
</text>
</g>
);
})}
{/* Active Agent Label */}
<text
x="180"
y="250"
fill={ADAPTERS[activeIndex].color}
textAnchor="middle"
fontSize="12"
fontWeight="800"
fontFamily="monospace"
letterSpacing="0.1em"
>
CONNECTED TO {ADAPTERS[activeIndex].label.toUpperCase()}
</text>
</g>
</svg>
</div>
);
}
const CopyInstallButton = () => {
const [copied, setCopied] = useState(false);
const installCommand = 'npx skills add rivet-dev/skills -s sandbox-agent';
const shortCommand = 'npx skills add rivet-dev/skills';
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(installCommand);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
return (
<div className="relative group w-full sm:w-auto">
<button
onClick={handleCopy}
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-white/10 px-4 py-2 text-sm text-zinc-300 transition-colors hover:border-white/20 hover:text-white font-mono"
>
{copied ? <Check className="h-4 w-4 text-green-400" /> : <Terminal className="h-4 w-4 flex-shrink-0" />}
<span className="hidden sm:inline">{installCommand}</span>
<span className="sm:hidden">{shortCommand}</span>
</button>
<div className="absolute left-1/2 -translate-x-1/2 top-full mt-3 opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-200 ease-out text-xs text-zinc-500 whitespace-nowrap pointer-events-none font-mono">
Give this to your coding agent
</div>
</div>
);
};
export function Hero() {
const [scrollOpacity, setScrollOpacity] = useState(1);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const updateViewportMode = () => {
const mobile = window.innerWidth < 1024;
setIsMobile(mobile);
if (mobile) {
setScrollOpacity(1);
}
};
const handleScroll = () => {
if (window.innerWidth < 1024) {
setScrollOpacity(1);
return;
}
const scrollY = window.scrollY;
const windowHeight = window.innerHeight;
const fadeStart = windowHeight * 0.15;
const fadeEnd = windowHeight * 0.5;
const opacity = 1 - Math.min(1, Math.max(0, (scrollY - fadeStart) / (fadeEnd - fadeStart)));
setScrollOpacity(opacity);
};
updateViewportMode();
handleScroll();
window.addEventListener('resize', updateViewportMode);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('resize', updateViewportMode);
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<section className="relative flex min-h-screen flex-col overflow-hidden pb-24 lg:pb-0">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-b from-zinc-900/20 via-transparent to-transparent pointer-events-none" />
{/* Main content */}
<div
className="flex flex-1 flex-col justify-start pt-32 lg:justify-center lg:pt-0 lg:pb-20 px-6"
style={isMobile ? undefined : { opacity: scrollOpacity, filter: `blur(${(1 - scrollOpacity) * 8}px)` }}
>
<div className="mx-auto w-full max-w-7xl">
<div className="flex flex-col gap-12 lg:flex-row lg:items-center lg:justify-between lg:gap-16 xl:gap-24">
{/* Left side - Text content */}
<div className="max-w-xl lg:max-w-2xl">
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-6 text-3xl font-medium leading-[1.1] tracking-tight text-white md:text-5xl"
>
Run Coding Agents in Sandboxes.
<br />
<span className="text-zinc-400">Control Them Over HTTP.</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="mb-8 text-lg text-zinc-500 leading-relaxed"
>
The Sandbox Agent SDK is a server that runs inside your sandbox. Your app connects remotely to control Claude Code, Codex, OpenCode, Amp, or Pi streaming events, handling permissions, managing sessions.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="flex flex-col gap-3 sm:flex-row"
>
<a
href="/docs"
className="selection-dark inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md bg-white px-5 py-2.5 text-sm font-medium text-black transition-colors hover:bg-zinc-200"
>
Read the Docs
<ArrowRight className="h-4 w-4" />
</a>
<CopyInstallButton />
</motion.div>
</div>
{/* Right side - Diagram */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
className="flex-1 w-full max-w-2xl"
>
<UniversalAPIDiagram />
</motion.div>
</div>
</div>
</div>
</section>
);
}

View file

@ -1,46 +0,0 @@
'use client';
import { motion } from 'framer-motion';
export function Inspector() {
return (
<section className="border-t border-white/10 py-48">
<div className="mx-auto max-w-7xl px-6">
<div className="mb-12 text-center">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="mb-2 text-2xl font-normal tracking-tight text-white md:text-4xl"
>
Built-in Debugger
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="mx-auto max-w-xl text-base leading-relaxed text-zinc-500"
>
Inspect sessions, view event payloads, and troubleshoot without writing&nbsp;code.
</motion.p>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className="overflow-hidden rounded-2xl border border-white/10"
>
<img
src="/images/inspector.png"
alt="Sandbox Agent Inspector"
className="w-full"
/>
</motion.div>
</div>
</section>
);
}

View file

@ -1,35 +0,0 @@
'use client';
const integrations = [
'Daytona',
'E2B',
'AI SDK',
'Anthropic',
'OpenAI',
'Docker',
'Fly.io',
'AWS Nitro',
'Postgres',
'ClickHouse',
'Rivet',
];
export function Integrations() {
return (
<section id="integrations" className="py-24 bg-zinc-900/20 border-t border-white/5 relative overflow-hidden">
<div className="max-w-4xl mx-auto px-6 text-center">
<h2 className="text-3xl font-bold text-white mb-6">Works with your stack</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{integrations.map((item) => (
<div
key={item}
className="h-16 flex items-center justify-center rounded-xl border border-white/5 bg-zinc-900/50 text-zinc-300 font-mono text-sm hover:border-accent/40 hover:text-accent transition-all cursor-default"
>
{item}
</div>
))}
</div>
</div>
</section>
);
}

View file

@ -1,151 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { Menu, X } from 'lucide-react';
import { GitHubStars } from './GitHubStars';
function NavItem({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a
href={href}
className="px-3 py-2 text-sm font-medium text-zinc-400 transition-colors duration-200 hover:text-white"
>
{children}
</a>
);
}
export function Navigation() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div className="fixed top-0 z-50 w-full max-w-[1200px] md:left-1/2 md:top-4 md:-translate-x-1/2 md:px-8">
<div
className={`relative before:pointer-events-none before:absolute before:inset-[-1px] before:z-20 before:hidden before:rounded-2xl before:border before:content-[''] before:transition-colors before:duration-300 before:ease-in-out md:before:block ${
isScrolled ? "before:border-white/10" : "before:border-transparent"
}`}
>
{/* Background with blur */}
<div
className={`absolute inset-0 -z-[1] hidden overflow-hidden rounded-2xl transition-all duration-300 ease-in-out md:block ${
isScrolled
? "bg-black/80 backdrop-blur-lg"
: "bg-transparent backdrop-blur-none"
}`}
/>
<header
className={`bg-black/60 border-b-transparent sticky top-0 z-10 flex flex-col items-center border-b backdrop-blur-md pt-2 pb-2 md:static md:bg-transparent md:rounded-2xl md:max-w-[1200px] md:border-transparent md:backdrop-blur-none transition-all hover:opacity-100 ${
isScrolled ? "opacity-100" : "opacity-80"
}`}
>
<div className="flex w-full items-center justify-between px-3">
{/* Left side: Logo + Nav */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<a href="https://rivet.dev" className="flex items-center">
<img src="/rivet-icon.svg" alt="Rivet" className="size-8" />
</a>
<span className="text-white/20">|</span>
<a href="/" className="flex items-center">
<img src="/logos/sandboxagent.svg" alt="Sandbox Agent SDK" className="h-6 w-auto" />
</a>
</div>
{/* Desktop Nav */}
<div className="hidden md:flex items-center ml-2">
<NavItem href="/docs">Docs</NavItem>
<NavItem href="https://github.com/rivet-dev/sandbox-agent/releases">Changelog</NavItem>
</div>
</div>
{/* Right side */}
<div className="hidden md:flex flex-row items-center gap-2">
<a
href="https://discord.gg/auCecybynK"
className="inline-flex items-center justify-center whitespace-nowrap rounded-md border border-white/10 px-4 py-2 h-10 text-sm hover:border-white/20 text-white/90 hover:text-white transition-colors"
aria-label="Discord"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
</a>
<GitHubStars
repo="rivet-dev/sandbox-agent"
className="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-white/10 bg-white/5 px-4 py-2 h-10 text-sm text-white shadow-sm hover:border-white/20 transition-colors"
/>
</div>
{/* Mobile menu button */}
<button
className="md:hidden text-zinc-400 hover:text-white p-2 transition-colors"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</header>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="md:hidden border border-white/10 bg-black/95 backdrop-blur-lg rounded-2xl mt-2 mx-2 shadow-xl">
<div className="px-4 py-4 space-y-1">
<a
href="/docs"
className="block py-2.5 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors font-medium"
onClick={() => setMobileMenuOpen(false)}
>
Docs
</a>
<a
href="https://github.com/rivet-dev/sandbox-agent/releases"
className="block py-2.5 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors font-medium"
onClick={() => setMobileMenuOpen(false)}
>
Changelog
</a>
<div className="border-t border-white/10 pt-3 mt-3 space-y-1">
<a
href="https://discord.gg/auCecybynK"
className="flex items-center gap-3 py-2.5 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors"
onClick={() => setMobileMenuOpen(false)}
aria-label="Discord"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
<span className="font-medium">Discord</span>
</a>
<GitHubStars
repo="rivet-dev/sandbox-agent"
className="flex items-center gap-3 py-2.5 px-3 text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors w-full"
onClick={() => setMobileMenuOpen(false)}
/>
</div>
</div>
</div>
)}
</div>
);
}

View file

@ -1,92 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { Shield, Layers, Database, X, Check } from 'lucide-react';
const frictions = [
{
icon: Shield,
title: 'Coding Agents Need Sandboxes',
problem:
"You can't let AI execute arbitrary code on your production servers. Coding agents need isolated environments, but existing SDKs assume local execution.",
solution: 'A server that runs inside the sandbox and exposes HTTP/SSE.',
},
{
icon: Layers,
title: 'Every Coding Agent is Different',
problem:
'Claude Code, Codex, OpenCode, Amp, and Pi each have proprietary APIs, event formats, and behaviors. Swapping coding agents means rewriting your entire integration.',
solution: 'One HTTP API. Write your code once, swap coding agents with a config change.',
},
{
icon: Database,
title: 'Sessions Are Ephemeral',
problem:
'Coding agent transcripts live in the sandbox. When the process ends, you lose everything. Debugging and replay become impossible.',
solution: 'Universal event schema streams to your storage. Persist to Postgres or Rivet, replay later, audit everything.',
},
];
export function PainPoints() {
return (
<section className="border-t border-white/10 py-48">
<div className="mx-auto max-w-7xl px-6">
<div className="mb-12">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="mb-2 text-2xl font-normal tracking-tight text-white md:text-4xl"
>
Running coding agents remotely is hard.
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="max-w-2xl text-base leading-relaxed text-zinc-500"
>
The Sandbox Agent SDK is a server that runs inside your sandbox. Your app connects remotely to control Claude Code, Codex, OpenCode, Amp, or Pi streaming events, handling permissions, managing sessions.
</motion.p>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="grid grid-cols-1 gap-8 md:grid-cols-3"
>
{frictions.map((friction) => (
<div key={friction.title} className="flex flex-col border-t border-white/10 pt-6">
<div className="mb-3 text-zinc-500">
<friction.icon className="h-4 w-4" />
</div>
<h3 className="mb-4 text-base font-normal text-white">{friction.title}</h3>
<div className="mb-4">
<div className="flex items-center gap-2 mb-2">
<X className="h-3 w-3 text-zinc-600" />
<span className="text-[10px] font-medium uppercase tracking-wider text-zinc-600">Problem</span>
</div>
<p className="text-sm leading-relaxed text-zinc-500">
{friction.problem}
</p>
</div>
<div className="mt-auto border-t border-white/5 pt-4">
<div className="flex items-center gap-2 mb-2">
<Check className="h-3 w-3 text-green-400" />
<span className="text-[10px] font-medium uppercase tracking-wider text-zinc-400">Solution</span>
</div>
<p className="text-sm leading-relaxed text-zinc-300">
{friction.solution}
</p>
</div>
</div>
))}
</motion.div>
</div>
</section>
);
}

View file

@ -1,53 +0,0 @@
'use client';
import { Workflow, Database, Server } from 'lucide-react';
import { FeatureIcon } from './ui/FeatureIcon';
const problems = [
{
title: 'Universal Agent API',
desc: 'Claude Code, Codex, OpenCode, Amp, and Pi each have different APIs. We provide a single interface to control them all.',
icon: Workflow,
color: 'text-accent',
},
{
title: 'Universal Transcripts',
desc: 'Every agent has its own event format. Our universal schema normalizes them all — stream, store, and replay with ease.',
icon: Database,
color: 'text-purple-400',
},
{
title: 'Run Anywhere',
desc: 'Lightweight Rust daemon runs locally or in any environment. One command to bridge coding agents to your system.',
icon: Server,
color: 'text-green-400',
},
];
export function ProblemsSolved() {
return (
<section id="features" className="py-24 bg-zinc-950 border-y border-white/5">
<div className="max-w-7xl mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-white mb-4">Why Coding Agent SDK?</h2>
<p className="text-zinc-400 max-w-xl mx-auto">
Solving the three fundamental friction points of agentic software development.
</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
{problems.map((item, idx) => (
<div
key={idx}
className="group p-8 rounded-2xl bg-zinc-900/40 border border-white/5 hover:border-accent/30 transition-all duration-300"
>
<FeatureIcon icon={item.icon} color={item.color} />
<h3 className="text-xl font-bold text-white mb-3">{item.title}</h3>
<p className="text-zinc-400 text-sm leading-relaxed">{item.desc}</p>
</div>
))}
</div>
</div>
</section>
);
}

View file

@ -1,11 +0,0 @@
interface BadgeProps {
children: React.ReactNode;
}
export function Badge({ children }: BadgeProps) {
return (
<span className="inline-flex px-3 py-1 rounded-full bg-accent/10 border border-accent/20 text-accent text-xs font-mono font-medium">
{children}
</span>
);
}

View file

@ -1,49 +0,0 @@
import type { ReactNode } from 'react';
interface ButtonProps {
children: ReactNode;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
href?: string;
onClick?: () => void;
className?: string;
}
export function Button({
children,
variant = 'primary',
size = 'md',
href,
onClick,
className = ''
}: ButtonProps) {
const baseStyles = 'inline-flex items-center justify-center font-bold rounded-lg transition-all';
const variants = {
primary: 'bg-white text-black hover:bg-zinc-200',
secondary: 'bg-zinc-900 border border-white/10 text-white hover:bg-zinc-800',
ghost: 'text-zinc-400 hover:text-white',
};
const sizes = {
sm: 'h-9 px-4 text-sm',
md: 'h-12 px-8 text-sm',
lg: 'h-14 px-10 text-base',
};
const classes = `${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`;
if (href) {
return (
<a href={href} className={classes}>
{children}
</a>
);
}
return (
<button onClick={onClick} className={classes}>
{children}
</button>
);
}

View file

@ -1,32 +0,0 @@
'use client';
import { useState } from 'react';
import { Copy, CheckCircle2 } from 'lucide-react';
interface CopyButtonProps {
text: string;
}
export function CopyButton({ text }: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<button
onClick={handleCopy}
className="p-2 hover:bg-white/10 rounded-md transition-colors text-zinc-500 hover:text-white"
aria-label="Copy to clipboard"
>
{copied ? (
<CheckCircle2 className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4" />
)}
</button>
);
}

View file

@ -1,23 +0,0 @@
import type { LucideIcon } from 'lucide-react';
interface FeatureIconProps {
icon: LucideIcon;
color?: string;
bgColor?: string;
hoverBgColor?: string;
glowShadow?: string;
}
export function FeatureIcon({
icon: Icon,
color = 'text-accent',
bgColor = 'bg-accent/10',
hoverBgColor = 'group-hover:bg-accent/20',
glowShadow = 'group-hover:shadow-[0_0_15px_rgba(59,130,246,0.5)]'
}: FeatureIconProps) {
return (
<div className={`rounded ${bgColor} p-2 ${color} transition-all duration-500 ${hoverBgColor} ${glowShadow}`}>
<Icon className="h-4 w-4" />
</div>
);
}

View file

@ -1,84 +0,0 @@
---
interface Props {
title: string;
description?: string;
}
const { title, description = "Universal SDK for coding agents. Control Claude Code, Codex, OpenCode, Amp, and Pi with unified events and sessions." } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, 'https://sandbox-agent.dev');
const ogImageURL = new URL('/og.png', 'https://sandbox-agent.dev');
const structuredData = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Sandbox Agent SDK",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Linux, macOS, Windows",
"description": description,
"url": "https://sandbox-agent.dev",
"author": {
"@type": "Organization",
"name": "Rivet",
"url": "https://rivet.dev"
},
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"keywords": "coding agents, AI SDK, Claude Code, Codex, OpenCode, Amp, sandbox, remote code execution, developer tools"
};
---
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<meta name="keywords" content="coding agents, AI SDK, Claude Code, Codex, OpenCode, Amp, Pi, sandbox, remote code execution, developer tools, AI coding assistant, code automation" />
<meta name="author" content="Rivet" />
<meta name="robots" content="index, follow" />
<meta name="theme-color" content="#000000" />
<link rel="canonical" href={canonicalURL} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- Preconnect to font providers -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Manrope + JetBrains Mono (from Google Fonts) -->
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<title>{title}</title>
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonicalURL} />
<meta property="og:site_name" content="Sandbox Agent SDK" />
<meta property="og:image" content={ogImageURL} />
<meta property="og:image:width" content="2400" />
<meta property="og:image:height" content="1260" />
<meta property="og:image:alt" content="Sandbox Agent SDK - Run Coding Agents in Sandboxes. Control Them Over HTTP." />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@rivet_dev" />
<meta name="twitter:creator" content="@rivet_dev" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImageURL} />
<!-- Structured Data -->
<script type="application/ld+json" set:html={JSON.stringify(structuredData)} />
</head>
<body class="min-h-screen bg-background text-foreground antialiased">
<slot />
</body>
</html>
<style is:global>
@import '../styles/global.css';
</style>

View file

@ -1,24 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import { Navigation } from '../components/Navigation';
import { Hero } from '../components/Hero';
import { PainPoints } from '../components/PainPoints';
import { FeatureGrid } from '../components/FeatureGrid';
import { GetStarted } from '../components/GetStarted';
import { Inspector } from '../components/Inspector';
import { FAQ } from '../components/FAQ';
import { Footer } from '../components/Footer';
---
<Layout title="Sandbox Agent SDK - Run Coding Agents in Sandboxes. Control Them Over HTTP.">
<Navigation client:load />
<main>
<Hero client:load />
<PainPoints client:visible />
<FeatureGrid client:visible />
<GetStarted client:visible />
<Inspector client:visible />
<FAQ client:visible />
</main>
<Footer client:visible />
</Layout>

View file

@ -1,244 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--header-height: 3.5rem;
/* Theme colors (HSL for flexibility) */
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--primary: 18.5 100% 50%;
--primary-foreground: 60 9.1% 97.8%;
--muted: 34 10% 10%;
--muted-foreground: 24 5.4% 63.9%;
--border: 12 6.5% 15.1%;
--card: 0 9.09% 6.47%;
/* Shiki syntax highlighting */
--shiki-color-text: theme('colors.white');
--shiki-foreground: hsl(var(--foreground));
--shiki-token-constant: theme('colors.violet.300');
--shiki-token-string: theme('colors.violet.300');
--shiki-token-comment: theme('colors.zinc.500');
--shiki-token-keyword: theme('colors.sky.300');
--shiki-token-parameter: theme('colors.pink.300');
--shiki-token-function: theme('colors.violet.300');
--shiki-token-string-expression: theme('colors.violet.300');
--shiki-token-punctuation: theme('colors.zinc.200');
}
* {
@apply border-white/10;
}
body {
@apply bg-black text-white antialiased;
font-family: 'Manrope', system-ui, sans-serif;
font-weight: 500;
}
/* Text selection - matches rivet.dev */
::selection {
background-color: rgba(255, 79, 0, 0.3);
color: #fed7aa;
}
::-moz-selection {
background-color: rgba(255, 79, 0, 0.3);
color: #fed7aa;
}
/* Selection style for white/light backgrounds */
.selection-dark::selection {
background-color: #18181b;
color: white;
}
.selection-dark::-moz-selection {
background-color: #18181b;
color: white;
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: #3f3f46 transparent;
}
/* Webkit scrollbar */
::-webkit-scrollbar {
@apply w-2 h-2;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply bg-zinc-700 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-zinc-600;
}
::-webkit-scrollbar-corner {
@apply bg-transparent;
}
/* Code block scrollbars */
pre, code, .overflow-x-auto {
scrollbar-width: thin;
scrollbar-color: #52525b #18181b;
}
pre::-webkit-scrollbar,
code::-webkit-scrollbar,
.overflow-x-auto::-webkit-scrollbar {
@apply h-2;
}
pre::-webkit-scrollbar-track,
code::-webkit-scrollbar-track,
.overflow-x-auto::-webkit-scrollbar-track {
@apply bg-zinc-900;
}
pre::-webkit-scrollbar-thumb,
code::-webkit-scrollbar-thumb,
.overflow-x-auto::-webkit-scrollbar-thumb {
@apply bg-zinc-600 rounded-full;
}
}
@layer components {
/* Glass morphism effects */
.glass {
@apply bg-white/[0.02] backdrop-blur-md border border-white/10;
}
.glass-hover {
@apply hover:bg-white/[0.04] hover:border-white/20 transition-all;
}
.glass-strong {
@apply bg-black/95 backdrop-blur-lg border border-white/10;
}
/* Bento box card effects */
.bento-box {
transition: border-color 0.3s ease;
}
.bento-box:hover {
border-color: rgba(255, 255, 255, 0.2);
}
/* Scroll-triggered animations */
.animate-on-scroll {
opacity: 0;
transition: opacity 0.8s ease-out, transform 0.8s cubic-bezier(0.19, 1, 0.22, 1);
transition-delay: var(--scroll-delay, 0s);
will-change: opacity, transform;
}
.animate-fade-up {
transform: translateY(30px);
}
.animate-on-scroll.is-visible {
opacity: 1;
transform: translateY(0);
}
/* Delay utilities for staggered animations */
.delay-100 { --scroll-delay: 100ms; }
.delay-200 { --scroll-delay: 200ms; }
.delay-300 { --scroll-delay: 300ms; }
.delay-400 { --scroll-delay: 400ms; }
.delay-500 { --scroll-delay: 500ms; }
.delay-600 { --scroll-delay: 600ms; }
/* Top shine highlight for cards */
.shine-top {
position: relative;
}
.shine-top::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 1px;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.2), transparent);
pointer-events: none;
}
/* Glow effect for buttons and interactive elements */
.glow-accent {
box-shadow: 0 0 20px rgba(255, 69, 0, 0.3);
}
.glow-accent-hover:hover {
box-shadow: 0 0 30px rgba(255, 69, 0, 0.5);
}
/* Code highlight styling */
.code-highlight-ref {
position: relative;
transition: background-color 0.3s ease-out;
display: block;
margin: 0 -1.5rem;
padding: 0 1.5rem;
}
.code-highlight-ref.is-active {
background-color: rgba(255, 69, 0, 0.1);
}
.code-highlight-ref.is-active::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background-color: #ff4500;
}
/* Hide scrollbar */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
}
@layer utilities {
/* Gradient text */
.text-gradient-accent {
@apply bg-gradient-to-r from-orange-400 to-orange-600 bg-clip-text text-transparent;
}
/* Backdrop with blur */
.backdrop-glow {
@apply backdrop-blur-lg bg-black/80;
}
/* Better focus ring */
.focus-ring {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-black;
}
}
/* View transition disable (for smooth prefetching) */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}

View file

@ -1,64 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {
colors: {
// Primary accent (OrangeRed)
accent: '#FF4500',
// Extended color palette
background: '#000000',
'text-primary': '#FAFAFA',
'text-secondary': '#A0A0A0',
border: '#252525',
// Code syntax highlighting
'code-keyword': '#c084fc',
'code-function': '#60a5fa',
'code-string': '#4ade80',
'code-comment': '#737373',
},
fontFamily: {
sans: ['Manrope', 'system-ui', 'sans-serif'],
heading: ['Manrope', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
animation: {
'fade-in-up': 'fade-in-up 0.8s ease-out forwards',
'hero-line': 'hero-line 1s cubic-bezier(0.19, 1, 0.22, 1) forwards',
'hero-p': 'hero-p 0.8s ease-out 0.6s forwards',
'hero-cta': 'hero-p 0.8s ease-out 0.8s forwards',
'hero-visual': 'hero-p 0.8s ease-out 1s forwards',
'infinite-scroll': 'infinite-scroll 25s linear infinite',
'pulse-slow': 'pulse-slow 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
},
keyframes: {
'fade-in-up': {
from: { opacity: '0', transform: 'translateY(24px)' },
to: { opacity: '1', transform: 'translateY(0)' },
},
'hero-line': {
'0%': { opacity: '0', transform: 'translateY(100%) skewY(6deg)' },
'100%': { opacity: '1', transform: 'translateY(0) skewY(0deg)' },
},
'hero-p': {
from: { opacity: '0', transform: 'translateY(20px)' },
to: { opacity: '1', transform: 'translateY(0)' },
},
'infinite-scroll': {
from: { transform: 'translateX(0)' },
to: { transform: 'translateX(-50%)' },
},
'pulse-slow': {
'50%': { opacity: '.5' },
},
},
spacing: {
header: 'var(--header-height, 3.5rem)',
},
borderRadius: {
'4xl': '2rem',
},
},
},
plugins: [],
};

View file

@ -1,7 +0,0 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}

View file

@ -1,7 +0,0 @@
import { defineConfig } from "vite";
export default defineConfig({
server: {
port: 3000
}
});

22
pi.svg
View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
<!-- P shape: outer boundary clockwise, inner hole counter-clockwise -->
<path fill="#fff" fill-rule="evenodd" d="
M165.29 165.29
H517.36
V400
H400
V517.36
H282.65
V634.72
H165.29
Z
M282.65 282.65
V400
H400
V282.65
Z
"/>
<!-- i dot -->
<path fill="#fff" d="M517.36 400 H634.72 V634.72 H517.36 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 473 B

13473
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,53 +0,0 @@
# Delete Or Comment Out First
This is the initial, deliberate teardown list before building ACP-native v1.
## Hard delete first (in-house protocol types and converters)
- `server/packages/universal-agent-schema/Cargo.toml`
- `server/packages/universal-agent-schema/src/lib.rs`
- `server/packages/universal-agent-schema/src/agents/mod.rs`
- `server/packages/universal-agent-schema/src/agents/claude.rs`
- `server/packages/universal-agent-schema/src/agents/codex.rs`
- `server/packages/universal-agent-schema/src/agents/opencode.rs`
- `server/packages/universal-agent-schema/src/agents/amp.rs`
- `spec/universal-schema.json`
- `docs/session-transcript-schema.mdx`
- `docs/conversion.mdx`
## Hard delete next (generated schema pipeline used only for in-house normalization)
- `server/packages/extracted-agent-schemas/Cargo.toml`
- `server/packages/extracted-agent-schemas/build.rs`
- `server/packages/extracted-agent-schemas/src/lib.rs`
- `server/packages/extracted-agent-schemas/tests/schema_roundtrip.rs`
- `resources/agent-schemas/` (entire folder)
## Remove/replace immediately (v1 hard removal)
- `server/packages/sandbox-agent/src/router.rs`: remove `/v1` handlers and replace with a unified `410 v1 removed` handler.
- `server/packages/sandbox-agent/src/cli.rs`: remove/disable `api` subcommands that target `/v1`.
- `sdks/typescript/src/client.ts`: methods bound to `/v1/*` routes.
- `sdks/typescript/src/generated/openapi.ts`: current v1 OpenAPI output.
- `docs/openapi.json`: current v1 OpenAPI document.
## Compatibility surface to disable during ACP core
- `server/packages/sandbox-agent/src/opencode_compat.rs`
- `server/packages/sandbox-agent/tests/opencode-compat/`
- `docs/opencode-compatibility.mdx`
Rationale: this layer is based on current v1 session/event model. Comment it out/disable it during ACP core implementation to avoid coupling and drift.
Important: OpenCode <-> ACP support is still required, but it is explicitly reintroduced in Phase 7 after ACP v1 core transport/runtime are stable.
## Tests to remove or disable with v1
- `server/packages/sandbox-agent/tests/http/`
- `server/packages/sandbox-agent/tests/sessions/`
- `server/packages/sandbox-agent/tests/agent-flows/`
- `server/packages/sandbox-agent/tests/http_endpoints.rs`
- `server/packages/sandbox-agent/tests/sessions.rs`
- `server/packages/sandbox-agent/tests/agent_flows.rs`
Replace with ACP-native contract tests in v1.

View file

@ -1,41 +0,0 @@
# ACP Migration Research
This folder captures the v1 migration plan from the current in-house protocol to ACP-first architecture.
## Files
- `research/acp/00-delete-first.md`: delete/comment-out-first inventory for the rewrite kickoff.
- `research/acp/acp-notes.md`: ACP protocol notes extracted from `~/misc/acp-docs`.
- `research/acp/acp-over-http-findings.md`: field research from ACP Zulip thread on real ACP-over-HTTP transport patterns and recommendations.
- `research/acp/spec.md`: proposed v1 protocol/transport spec (ACP over HTTP).
- `research/acp/v1-schema-to-acp-mapping.md`: exhaustive 1:1 mapping of all current v1 endpoints/events into ACP methods, notifications, responses, and `_meta` extensions.
- `research/acp/rfds-vs-extensions.md`: simple list of which gaps should be raised as ACP RFDs vs remain product-specific extensions.
- `research/acp/migration-steps.md`: concrete implementation phases and execution checklist.
- `research/acp/friction.md`: ongoing friction/issues log for ACP migration decisions and blockers.
## Source docs read
- `~/misc/acp-docs/docs/protocol/overview.mdx`
- `~/misc/acp-docs/docs/protocol/initialization.mdx`
- `~/misc/acp-docs/docs/protocol/session-setup.mdx`
- `~/misc/acp-docs/docs/protocol/prompt-turn.mdx`
- `~/misc/acp-docs/docs/protocol/tool-calls.mdx`
- `~/misc/acp-docs/docs/protocol/file-system.mdx`
- `~/misc/acp-docs/docs/protocol/terminals.mdx`
- `~/misc/acp-docs/docs/protocol/session-modes.mdx`
- `~/misc/acp-docs/docs/protocol/session-config-options.mdx`
- `~/misc/acp-docs/docs/protocol/extensibility.mdx`
- `~/misc/acp-docs/docs/protocol/transports.mdx`
- `~/misc/acp-docs/docs/protocol/schema.mdx`
- `~/misc/acp-docs/schema/meta.json`
- `~/misc/acp-docs/schema/schema.json`
- `~/misc/acp-docs/docs/get-started/agents.mdx`
- `~/misc/acp-docs/docs/get-started/registry.mdx`
## Important context
- ACP stable transport is stdio; streamable HTTP is still draft in ACP docs.
- v1 in this repo is intentionally breaking and ACP-native.
- v1 is removed in v1 and returns HTTP 410 on `/v1/*`.
- `/opencode/*` is disabled during ACP core phases and re-enabled in the dedicated bridge phase.
- Keep `research/acp/friction.md` current as issues/ambiguities are discovered.

View file

@ -1,66 +0,0 @@
# ACP Notes (From Docs)
## Core protocol model
ACP is JSON-RPC 2.0 with bidirectional methods plus notifications.
Client to agent baseline methods:
- `initialize`
- `authenticate` (optional if agent requires auth)
- `session/new`
- `session/prompt`
- optional: `session/load`, `session/set_mode`, `session/set_config_option`
- notification: `session/cancel`
Agent to client baseline method:
- `session/request_permission`
Agent to client optional methods:
- `fs/read_text_file`, `fs/write_text_file`
- `terminal/create`, `terminal/output`, `terminal/wait_for_exit`, `terminal/kill`, `terminal/release`
Agent to client baseline notification:
- `session/update`
## Required protocol behavior
- Paths must be absolute.
- Line numbers are 1-based.
- Initialization must negotiate protocol version.
- Capabilities omitted by peer must be treated as unsupported.
## Transport state
- ACP formally defines stdio transport today.
- ACP docs mention streamable HTTP as draft/in progress.
- Custom transports are allowed if JSON-RPC lifecycle semantics are preserved.
## Session lifecycle
- `session/new` creates session, returns `sessionId`.
- `session/load` is optional and gated by `loadSession` capability.
- `session/prompt` runs one turn and returns `stopReason`.
- Streaming progress is entirely via `session/update` notifications.
- Cancellation is `session/cancel` notification and must end with `stopReason=cancelled`.
## Tool and HITL model
- Tool calls are modeled through `session/update` (`tool_call`, `tool_call_update`).
- HITL permission flow is a request/response RPC call (`session/request_permission`).
## ACP agent process relevance for this repo
From ACP docs agent list:
- Claude: ACP via agent process (`zed-industries/claude-code-acp`).
- Codex: ACP via agent process (`zed-industries/codex-acp`).
- OpenCode: ACP agent listed natively.
Gap to confirm for launch scope:
- Amp is not currently listed in ACP docs as a native ACP agent or published agent process.
- We need an explicit product decision: block Amp in v1 launch or provide/build an ACP agent process.

Some files were not shown because too many files have changed in this diff Show more