chore: recover edinburgh workspace state
82
Cargo.toml
|
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -1 +0,0 @@
|
|||
CLAUDE.md
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
Before Width: | Height: | Size: 11 KiB |
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
};
|
||||
|
|
@ -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" };
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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)}`;
|
||||
};
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
3
frontend/packages/website/.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
|||
.astro
|
||||
dist
|
||||
node_modules
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
:80 {
|
||||
root * /srv
|
||||
file_server
|
||||
try_files {path} /index.html
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
]
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
Before Width: | Height: | Size: 1.7 MiB |
|
|
@ -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 |
|
|
@ -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 |
|
Before Width: | Height: | Size: 62 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 10 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
Before Width: | Height: | Size: 11 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 272 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -1,4 +0,0 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://sandbox-agent.dev/sitemap-index.xml
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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">
|
||||
© {new Date().getFullYear()} Rivet Gaming, Inc. All rights reserved.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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: [],
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3000
|
||||
}
|
||||
});
|
||||
22
pi.svg
|
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||