diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 476ed12..85f828d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy @@ -21,6 +23,35 @@ jobs: node-version: 20 cache: pnpm - run: pnpm install + - name: Run formatter hooks + shell: bash + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + git fetch origin "${{ github.base_ref }}" --depth=1 + diff_range="origin/${{ github.base_ref }}...HEAD" + elif [ "${{ github.event_name }}" = "push" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then + diff_range="${{ github.event.before }}...${{ github.sha }}" + else + diff_range="HEAD^...HEAD" + fi + + mapfile -t changed_files < <( + git diff --name-only --diff-filter=ACMR "$diff_range" \ + | grep -E '\.(cjs|cts|js|jsx|json|jsonc|mjs|mts|rs|ts|tsx)$' \ + || true + ) + + if [ ${#changed_files[@]} -eq 0 ]; then + echo "No formatter-managed files changed." + exit 0 + fi + + args=() + for file in "${changed_files[@]}"; do + args+=(--file "$file") + done + + pnpm exec lefthook run pre-commit --no-stage-fixed --fail-on-changes "${args[@]}" - run: npm install -g tsx - name: Run checks run: ./scripts/release/main.ts --version 0.0.0 --only-steps run-ci-checks diff --git a/.mcp.json b/.mcp.json index cc04a2b..7bae219 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,10 +1,8 @@ { "mcpServers": { "everything": { - "args": [ - "@modelcontextprotocol/server-everything" - ], + "args": ["@modelcontextprotocol/server-everything"], "command": "npx" } } -} \ No newline at end of file +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..4a8bd54 --- /dev/null +++ b/biome.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "formatter": { + "indentStyle": "space", + "lineWidth": 160 + } +} diff --git a/docs/docs.json b/docs/docs.json index 2d57276..a6c2087 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1,131 +1,119 @@ { - "$schema": "https://mintlify.com/docs.json", - "theme": "willow", - "name": "Sandbox Agent SDK", - "appearance": { - "default": "dark", - "strict": true - }, - "colors": { - "primary": "#ff4f00", - "light": "#ff4f00", - "dark": "#ff4f00" - }, - "favicon": "/favicon.svg", - "logo": { - "light": "/logo/light.svg", - "dark": "/logo/dark.svg" - }, - "integrations": { - "posthog": { - "apiKey": "phc_6kfTNEAVw7rn1LA51cO3D69FefbKupSWFaM7OUgEpEo", - "apiHost": "https://ph.rivet.gg", - "sessionRecording": true - } - }, - "navbar": { - "links": [ - { - "label": "Gigacode", - "icon": "terminal", - "href": "https://github.com/rivet-dev/sandbox-agent/tree/main/gigacode" - }, - { - "label": "Discord", - "icon": "discord", - "href": "https://discord.gg/auCecybynK" - }, - { - "type": "github", - "href": "https://github.com/rivet-dev/sandbox-agent" - } - ] - }, - "navigation": { - "tabs": [ - { - "tab": "Documentation", - "pages": [ - { - "group": "Getting started", - "pages": [ - "quickstart", - "sdk-overview", - "react-components", - { - "group": "Deploy", - "icon": "server", - "pages": [ - "deploy/local", - "deploy/computesdk", - "deploy/e2b", - "deploy/daytona", - "deploy/vercel", - "deploy/cloudflare", - "deploy/docker", - "deploy/boxlite" - ] - } - ] - }, - { - "group": "Agent", - "pages": [ - "agent-sessions", - "attachments", - "skills-config", - "mcp-config", - "custom-tools" - ] - }, - { - "group": "System", - "pages": ["file-system", "processes"] - }, - { - "group": "Orchestration", - "pages": [ - "architecture", - "session-persistence", - "observability", - "multiplayer", - "security" - ] - }, - { - "group": "Reference", - "pages": [ - "agent-capabilities", - "cli", - "inspector", - "opencode-compatibility", - { - "group": "More", - "pages": [ - "credentials", - "daemon", - "cors", - "session-restoration", - "telemetry", - { - "group": "AI", - "pages": ["ai/skill", "ai/llms-txt"] - } - ] - } - ] - } - ] - }, - { - "tab": "HTTP API", - "pages": [ - { - "group": "HTTP Reference", - "openapi": "openapi.json" - } - ] - } - ] - } + "$schema": "https://mintlify.com/docs.json", + "theme": "willow", + "name": "Sandbox Agent SDK", + "appearance": { + "default": "dark", + "strict": true + }, + "colors": { + "primary": "#ff4f00", + "light": "#ff4f00", + "dark": "#ff4f00" + }, + "favicon": "/favicon.svg", + "logo": { + "light": "/logo/light.svg", + "dark": "/logo/dark.svg" + }, + "integrations": { + "posthog": { + "apiKey": "phc_6kfTNEAVw7rn1LA51cO3D69FefbKupSWFaM7OUgEpEo", + "apiHost": "https://ph.rivet.gg", + "sessionRecording": true + } + }, + "navbar": { + "links": [ + { + "label": "Gigacode", + "icon": "terminal", + "href": "https://github.com/rivet-dev/sandbox-agent/tree/main/gigacode" + }, + { + "label": "Discord", + "icon": "discord", + "href": "https://discord.gg/auCecybynK" + }, + { + "type": "github", + "href": "https://github.com/rivet-dev/sandbox-agent" + } + ] + }, + "navigation": { + "tabs": [ + { + "tab": "Documentation", + "pages": [ + { + "group": "Getting started", + "pages": [ + "quickstart", + "sdk-overview", + "react-components", + { + "group": "Deploy", + "icon": "server", + "pages": [ + "deploy/local", + "deploy/computesdk", + "deploy/e2b", + "deploy/daytona", + "deploy/vercel", + "deploy/cloudflare", + "deploy/docker", + "deploy/boxlite" + ] + } + ] + }, + { + "group": "Agent", + "pages": ["agent-sessions", "attachments", "skills-config", "mcp-config", "custom-tools"] + }, + { + "group": "System", + "pages": ["file-system", "processes"] + }, + { + "group": "Orchestration", + "pages": ["architecture", "session-persistence", "observability", "multiplayer", "security"] + }, + { + "group": "Reference", + "pages": [ + "agent-capabilities", + "cli", + "inspector", + "opencode-compatibility", + { + "group": "More", + "pages": [ + "credentials", + "daemon", + "cors", + "session-restoration", + "telemetry", + { + "group": "AI", + "pages": ["ai/skill", "ai/llms-txt"] + } + ] + } + ] + } + ] + }, + { + "tab": "HTTP API", + "pages": [ + { + "group": "HTTP Reference", + "openapi": "openapi.json" + } + ] + } + ] + } } diff --git a/docs/openapi.json b/docs/openapi.json index 5735c51..a984b28 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -20,9 +20,7 @@ "paths": { "/v1/acp": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_acp_servers", "responses": { "200": { @@ -40,9 +38,7 @@ }, "/v1/acp/{server_id}": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_acp", "parameters": [ { @@ -92,9 +88,7 @@ } }, "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "post_v1_acp", "parameters": [ { @@ -204,9 +198,7 @@ } }, "delete": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "delete_v1_acp", "parameters": [ { @@ -228,9 +220,7 @@ }, "/v1/agents": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_agents", "parameters": [ { @@ -280,9 +270,7 @@ }, "/v1/agents/{agent}": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_agent", "parameters": [ { @@ -351,9 +339,7 @@ }, "/v1/agents/{agent}/install": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "post_v1_agent_install", "parameters": [ { @@ -412,9 +398,7 @@ }, "/v1/config/mcp": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_config_mcp", "parameters": [ { @@ -460,9 +444,7 @@ } }, "put": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "put_v1_config_mcp", "parameters": [ { @@ -501,9 +483,7 @@ } }, "delete": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "delete_v1_config_mcp", "parameters": [ { @@ -534,9 +514,7 @@ }, "/v1/config/skills": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_config_skills", "parameters": [ { @@ -582,9 +560,7 @@ } }, "put": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "put_v1_config_skills", "parameters": [ { @@ -623,9 +599,7 @@ } }, "delete": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "delete_v1_config_skills", "parameters": [ { @@ -656,9 +630,7 @@ }, "/v1/fs/entries": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_fs_entries", "parameters": [ { @@ -691,9 +663,7 @@ }, "/v1/fs/entry": { "delete": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "delete_v1_fs_entry", "parameters": [ { @@ -732,9 +702,7 @@ }, "/v1/fs/file": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_fs_file", "parameters": [ { @@ -754,9 +722,7 @@ } }, "put": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "put_v1_fs_file", "parameters": [ { @@ -796,9 +762,7 @@ }, "/v1/fs/mkdir": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "post_v1_fs_mkdir", "parameters": [ { @@ -827,9 +791,7 @@ }, "/v1/fs/move": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "post_v1_fs_move", "requestBody": { "content": { @@ -857,9 +819,7 @@ }, "/v1/fs/stat": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_fs_stat", "parameters": [ { @@ -888,9 +848,7 @@ }, "/v1/fs/upload-batch": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "post_v1_fs_upload_batch", "parameters": [ { @@ -931,9 +889,7 @@ }, "/v1/health": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "operationId": "get_v1_health", "responses": { "200": { @@ -951,9 +907,7 @@ }, "/v1/processes": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "List all managed processes.", "description": "Returns a list of all processes (running and exited) currently tracked\nby the runtime, sorted by process ID.", "operationId": "get_v1_processes", @@ -981,9 +935,7 @@ } }, "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Create a long-lived managed process.", "description": "Spawns a new process with the given command and arguments. Supports both\npipe-based and PTY (tty) modes. Returns the process descriptor on success.", "operationId": "post_v1_processes", @@ -1043,9 +995,7 @@ }, "/v1/processes/config": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Get process runtime configuration.", "description": "Returns the current runtime configuration for the process management API,\nincluding limits for concurrency, timeouts, and buffer sizes.", "operationId": "get_v1_processes_config", @@ -1073,9 +1023,7 @@ } }, "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Update process runtime configuration.", "description": "Replaces the runtime configuration for the process management API.\nValidates that all values are non-zero and clamps default timeout to max.", "operationId": "post_v1_processes_config", @@ -1125,9 +1073,7 @@ }, "/v1/processes/run": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Run a one-shot command.", "description": "Executes a command to completion and returns its stdout, stderr, exit code,\nand duration. Supports configurable timeout and output size limits.", "operationId": "post_v1_processes_run", @@ -1177,9 +1123,7 @@ }, "/v1/processes/{id}": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Get a single process by ID.", "description": "Returns the current state of a managed process including its status,\nPID, exit code, and creation/exit timestamps.", "operationId": "get_v1_process", @@ -1228,9 +1172,7 @@ } }, "delete": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Delete a process record.", "description": "Removes a stopped process from the runtime. Returns 409 if the process\nis still running; stop or kill it first.", "operationId": "delete_v1_process", @@ -1284,9 +1226,7 @@ }, "/v1/processes/{id}/input": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Write input to a process.", "description": "Sends data to a process's stdin (pipe mode) or PTY writer (tty mode).\nData can be encoded as base64, utf8, or text. Returns 413 if the decoded\npayload exceeds the configured `maxInputBytesPerRequest` limit.", "operationId": "post_v1_process_input", @@ -1367,9 +1307,7 @@ }, "/v1/processes/{id}/kill": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Send SIGKILL to a process.", "description": "Sends SIGKILL to the process and optionally waits up to `waitMs`\nmilliseconds for the process to exit before returning.", "operationId": "post_v1_process_kill", @@ -1432,9 +1370,7 @@ }, "/v1/processes/{id}/logs": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Fetch process logs.", "description": "Returns buffered log entries for a process. Supports filtering by stream\ntype, tail count, and sequence-based resumption. When `follow=true`,\nreturns an SSE stream that replays buffered entries then streams live output.", "operationId": "get_v1_process_logs", @@ -1532,9 +1468,7 @@ }, "/v1/processes/{id}/stop": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Send SIGTERM to a process.", "description": "Sends SIGTERM to the process and optionally waits up to `waitMs`\nmilliseconds for the process to exit before returning.", "operationId": "post_v1_process_stop", @@ -1597,9 +1531,7 @@ }, "/v1/processes/{id}/terminal/resize": { "post": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Resize a process terminal.", "description": "Sets the PTY window size (columns and rows) for a tty-mode process and\nsends SIGWINCH so the child process can adapt.", "operationId": "post_v1_process_terminal_resize", @@ -1680,9 +1612,7 @@ }, "/v1/processes/{id}/terminal/ws": { "get": { - "tags": [ - "v1" - ], + "tags": ["v1"], "summary": "Open an interactive WebSocket terminal session.", "description": "Upgrades the connection to a WebSocket for bidirectional PTY I/O. Accepts\n`access_token` query param for browser-based auth (WebSocket API cannot\nsend custom headers). Streams raw PTY output as binary frames and accepts\nJSON control frames for input, resize, and close.", "operationId": "get_v1_process_terminal_ws", @@ -1759,9 +1689,7 @@ "schemas": { "AcpEnvelope": { "type": "object", - "required": [ - "jsonrpc" - ], + "required": ["jsonrpc"], "properties": { "error": { "nullable": true @@ -1795,11 +1723,7 @@ }, "AcpServerInfo": { "type": "object", - "required": [ - "serverId", - "agent", - "createdAtMs" - ], + "required": ["serverId", "agent", "createdAtMs"], "properties": { "agent": { "type": "string" @@ -1815,9 +1739,7 @@ }, "AcpServerListResponse": { "type": "object", - "required": [ - "servers" - ], + "required": ["servers"], "properties": { "servers": { "type": "array", @@ -1908,12 +1830,7 @@ }, "AgentInfo": { "type": "object", - "required": [ - "id", - "installed", - "credentialsAvailable", - "capabilities" - ], + "required": ["id", "installed", "credentialsAvailable", "capabilities"], "properties": { "capabilities": { "$ref": "#/components/schemas/AgentCapabilities" @@ -1956,11 +1873,7 @@ }, "AgentInstallArtifact": { "type": "object", - "required": [ - "kind", - "path", - "source" - ], + "required": ["kind", "path", "source"], "properties": { "kind": { "type": "string" @@ -1996,10 +1909,7 @@ }, "AgentInstallResponse": { "type": "object", - "required": [ - "already_installed", - "artifacts" - ], + "required": ["already_installed", "artifacts"], "properties": { "already_installed": { "type": "boolean" @@ -2014,9 +1924,7 @@ }, "AgentListResponse": { "type": "object", - "required": [ - "agents" - ], + "required": ["agents"], "properties": { "agents": { "type": "array", @@ -2049,9 +1957,7 @@ }, "FsActionResponse": { "type": "object", - "required": [ - "path" - ], + "required": ["path"], "properties": { "path": { "type": "string" @@ -2060,9 +1966,7 @@ }, "FsDeleteQuery": { "type": "object", - "required": [ - "path" - ], + "required": ["path"], "properties": { "path": { "type": "string" @@ -2084,12 +1988,7 @@ }, "FsEntry": { "type": "object", - "required": [ - "name", - "path", - "entryType", - "size" - ], + "required": ["name", "path", "entryType", "size"], "properties": { "entryType": { "$ref": "#/components/schemas/FsEntryType" @@ -2113,17 +2012,11 @@ }, "FsEntryType": { "type": "string", - "enum": [ - "file", - "directory" - ] + "enum": ["file", "directory"] }, "FsMoveRequest": { "type": "object", - "required": [ - "from", - "to" - ], + "required": ["from", "to"], "properties": { "from": { "type": "string" @@ -2139,10 +2032,7 @@ }, "FsMoveResponse": { "type": "object", - "required": [ - "from", - "to" - ], + "required": ["from", "to"], "properties": { "from": { "type": "string" @@ -2154,9 +2044,7 @@ }, "FsPathQuery": { "type": "object", - "required": [ - "path" - ], + "required": ["path"], "properties": { "path": { "type": "string" @@ -2165,11 +2053,7 @@ }, "FsStat": { "type": "object", - "required": [ - "path", - "entryType", - "size" - ], + "required": ["path", "entryType", "size"], "properties": { "entryType": { "$ref": "#/components/schemas/FsEntryType" @@ -2199,10 +2083,7 @@ }, "FsUploadBatchResponse": { "type": "object", - "required": [ - "paths", - "truncated" - ], + "required": ["paths", "truncated"], "properties": { "paths": { "type": "array", @@ -2217,10 +2098,7 @@ }, "FsWriteResponse": { "type": "object", - "required": [ - "path", - "bytesWritten" - ], + "required": ["path", "bytesWritten"], "properties": { "bytesWritten": { "type": "integer", @@ -2234,9 +2112,7 @@ }, "HealthResponse": { "type": "object", - "required": [ - "status" - ], + "required": ["status"], "properties": { "status": { "type": "string" @@ -2245,10 +2121,7 @@ }, "McpConfigQuery": { "type": "object", - "required": [ - "directory", - "mcpName" - ], + "required": ["directory", "mcpName"], "properties": { "directory": { "type": "string" @@ -2262,10 +2135,7 @@ "oneOf": [ { "type": "object", - "required": [ - "command", - "type" - ], + "required": ["command", "type"], "properties": { "args": { "type": "array", @@ -2299,18 +2169,13 @@ }, "type": { "type": "string", - "enum": [ - "local" - ] + "enum": ["local"] } } }, { "type": "object", - "required": [ - "url", - "type" - ], + "required": ["url", "type"], "properties": { "bearerTokenEnvVar": { "type": "string", @@ -2358,9 +2223,7 @@ }, "type": { "type": "string", - "enum": [ - "remote" - ] + "enum": ["remote"] }, "url": { "type": "string" @@ -2374,11 +2237,7 @@ }, "ProblemDetails": { "type": "object", - "required": [ - "type", - "title", - "status" - ], + "required": ["type", "title", "status"], "properties": { "detail": { "type": "string", @@ -2404,14 +2263,7 @@ }, "ProcessConfig": { "type": "object", - "required": [ - "maxConcurrentProcesses", - "defaultRunTimeoutMs", - "maxRunTimeoutMs", - "maxOutputBytes", - "maxLogBytesPerProcess", - "maxInputBytesPerRequest" - ], + "required": ["maxConcurrentProcesses", "defaultRunTimeoutMs", "maxRunTimeoutMs", "maxOutputBytes", "maxLogBytesPerProcess", "maxInputBytesPerRequest"], "properties": { "defaultRunTimeoutMs": { "type": "integer", @@ -2443,9 +2295,7 @@ }, "ProcessCreateRequest": { "type": "object", - "required": [ - "command" - ], + "required": ["command"], "properties": { "args": { "type": "array", @@ -2476,15 +2326,7 @@ }, "ProcessInfo": { "type": "object", - "required": [ - "id", - "command", - "args", - "tty", - "interactive", - "status", - "createdAtMs" - ], + "required": ["id", "command", "args", "tty", "interactive", "status", "createdAtMs"], "properties": { "args": { "type": "array", @@ -2535,9 +2377,7 @@ }, "ProcessInputRequest": { "type": "object", - "required": [ - "data" - ], + "required": ["data"], "properties": { "data": { "type": "string" @@ -2550,9 +2390,7 @@ }, "ProcessInputResponse": { "type": "object", - "required": [ - "bytesWritten" - ], + "required": ["bytesWritten"], "properties": { "bytesWritten": { "type": "integer", @@ -2562,9 +2400,7 @@ }, "ProcessListResponse": { "type": "object", - "required": [ - "processes" - ], + "required": ["processes"], "properties": { "processes": { "type": "array", @@ -2576,13 +2412,7 @@ }, "ProcessLogEntry": { "type": "object", - "required": [ - "sequence", - "stream", - "timestampMs", - "data", - "encoding" - ], + "required": ["sequence", "stream", "timestampMs", "data", "encoding"], "properties": { "data": { "type": "string" @@ -2634,11 +2464,7 @@ }, "ProcessLogsResponse": { "type": "object", - "required": [ - "processId", - "stream", - "entries" - ], + "required": ["processId", "stream", "entries"], "properties": { "entries": { "type": "array", @@ -2656,18 +2482,11 @@ }, "ProcessLogsStream": { "type": "string", - "enum": [ - "stdout", - "stderr", - "combined", - "pty" - ] + "enum": ["stdout", "stderr", "combined", "pty"] }, "ProcessRunRequest": { "type": "object", - "required": [ - "command" - ], + "required": ["command"], "properties": { "args": { "type": "array", @@ -2703,14 +2522,7 @@ }, "ProcessRunResponse": { "type": "object", - "required": [ - "timedOut", - "stdout", - "stderr", - "stdoutTruncated", - "stderrTruncated", - "durationMs" - ], + "required": ["timedOut", "stdout", "stderr", "stdoutTruncated", "stderrTruncated", "durationMs"], "properties": { "durationMs": { "type": "integer", @@ -2752,17 +2564,11 @@ }, "ProcessState": { "type": "string", - "enum": [ - "running", - "exited" - ] + "enum": ["running", "exited"] }, "ProcessTerminalResizeRequest": { "type": "object", - "required": [ - "cols", - "rows" - ], + "required": ["cols", "rows"], "properties": { "cols": { "type": "integer", @@ -2778,10 +2584,7 @@ }, "ProcessTerminalResizeResponse": { "type": "object", - "required": [ - "cols", - "rows" - ], + "required": ["cols", "rows"], "properties": { "cols": { "type": "integer", @@ -2797,16 +2600,11 @@ }, "ServerStatus": { "type": "string", - "enum": [ - "running", - "stopped" - ] + "enum": ["running", "stopped"] }, "ServerStatusInfo": { "type": "object", - "required": [ - "status" - ], + "required": ["status"], "properties": { "status": { "$ref": "#/components/schemas/ServerStatus" @@ -2821,10 +2619,7 @@ }, "SkillSource": { "type": "object", - "required": [ - "type", - "source" - ], + "required": ["type", "source"], "properties": { "ref": { "type": "string", @@ -2851,9 +2646,7 @@ }, "SkillsConfig": { "type": "object", - "required": [ - "sources" - ], + "required": ["sources"], "properties": { "sources": { "type": "array", @@ -2865,10 +2658,7 @@ }, "SkillsConfigQuery": { "type": "object", - "required": [ - "directory", - "skillName" - ], + "required": ["directory", "skillName"], "properties": { "directory": { "type": "string" @@ -2886,4 +2676,4 @@ "description": "ACP proxy v1 API" } ] -} \ No newline at end of file +} diff --git a/examples/boxlite/src/index.ts b/examples/boxlite/src/index.ts index c2401be..bdcd53a 100644 --- a/examples/boxlite/src/index.ts +++ b/examples/boxlite/src/index.ts @@ -11,17 +11,14 @@ setupImage(); console.log("Creating BoxLite sandbox..."); const box = new SimpleBox({ - rootfsPath: OCI_DIR, - env, - ports: [{ hostPort: 3000, guestPort: 3000 }], - diskSizeGb: 4, + rootfsPath: OCI_DIR, + env, + ports: [{ hostPort: 3000, guestPort: 3000 }], + diskSizeGb: 4, }); console.log("Starting server..."); -const result = await box.exec( - "sh", "-c", - "nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &", -); +const result = await box.exec("sh", "-c", "nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &"); if (result.exitCode !== 0) throw new Error(`Failed to start server: ${result.stderr}`); const baseUrl = "http://localhost:3000"; @@ -36,9 +33,9 @@ console.log(" Press Ctrl+C to stop."); const keepAlive = setInterval(() => {}, 60_000); const cleanup = async () => { - clearInterval(keepAlive); - await box.stop(); - process.exit(0); + clearInterval(keepAlive); + await box.stop(); + process.exit(0); }; process.once("SIGINT", cleanup); process.once("SIGTERM", cleanup); diff --git a/examples/boxlite/src/setup-image.ts b/examples/boxlite/src/setup-image.ts index 25b157e..9c15c99 100644 --- a/examples/boxlite/src/setup-image.ts +++ b/examples/boxlite/src/setup-image.ts @@ -5,12 +5,12 @@ export const DOCKER_IMAGE = "sandbox-agent-boxlite"; export const OCI_DIR = new URL("../oci-image", import.meta.url).pathname; export function setupImage() { - console.log(`Building image "${DOCKER_IMAGE}" (cached after first run)...`); - execSync(`docker build -t ${DOCKER_IMAGE} ${new URL("..", import.meta.url).pathname}`, { stdio: "inherit" }); + console.log(`Building image "${DOCKER_IMAGE}" (cached after first run)...`); + execSync(`docker build -t ${DOCKER_IMAGE} ${new URL("..", import.meta.url).pathname}`, { stdio: "inherit" }); - if (!existsSync(`${OCI_DIR}/oci-layout`)) { - console.log("Exporting to OCI layout..."); - mkdirSync(OCI_DIR, { recursive: true }); - execSync(`docker save ${DOCKER_IMAGE} | tar -xf - -C ${OCI_DIR}`, { stdio: "inherit" }); - } + if (!existsSync(`${OCI_DIR}/oci-layout`)) { + console.log("Exporting to OCI layout..."); + mkdirSync(OCI_DIR, { recursive: true }); + execSync(`docker save ${DOCKER_IMAGE} | tar -xf - -C ${OCI_DIR}`, { stdio: "inherit" }); + } } diff --git a/examples/cloudflare/frontend/App.tsx b/examples/cloudflare/frontend/App.tsx index e80d693..499fc63 100644 --- a/examples/cloudflare/frontend/App.tsx +++ b/examples/cloudflare/frontend/App.tsx @@ -128,7 +128,7 @@ export function App() { console.error("Event stream error:", err); } }, - [log] + [log], ); const send = useCallback(async () => { @@ -162,12 +162,7 @@ export function App() {
+ +
+ + + ) : null; + if (!activeHandoff) { return ( + <> + + + + +
+
+

Create your first task

+

+ {viewModel.repos.length > 0 + ? "Start from the sidebar to create a task on the first available repo." + : "No repos are available in this workspace yet."} +

+ +
+
+
+
+ +
+ {starRepoPrompt} + + ); + } + + return ( + <> - - -
-
-

Create your first handoff

-

- {viewModel.repos.length > 0 - ? "Start from the sidebar to create a handoff on the first available repo." - : "No repos are available in this workspace yet."} -

- -
-
-
-
- + { + setActiveTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: tabId })); + }} + onSetLastAgentTabId={(tabId) => { + setLastAgentTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: tabId })); + }} + onSetOpenDiffs={(paths) => { + setOpenDiffsByHandoff((current) => ({ ...current, [activeHandoff.id]: paths })); + }} + /> +
- ); - } - - return ( - - - { - setActiveTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: tabId })); - }} - onSetLastAgentTabId={(tabId) => { - setLastAgentTabIdByHandoff((current) => ({ ...current, [activeHandoff.id]: tabId })); - }} - onSetOpenDiffs={(paths) => { - setOpenDiffsByHandoff((current) => ({ ...current, [activeHandoff.id]: paths })); - }} - /> - - + {starRepoPrompt} + ); } diff --git a/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx b/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx index 83c8904..a5a91cf 100644 --- a/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx +++ b/factory/packages/frontend/src/components/mock-layout/history-minimap.tsx @@ -4,13 +4,7 @@ import { LabelXSmall } from "baseui/typography"; import { formatMessageTimestamp, type HistoryEvent } from "./view-model"; -export const HistoryMinimap = memo(function HistoryMinimap({ - events, - onSelect, -}: { - events: HistoryEvent[]; - onSelect: (event: HistoryEvent) => void; -}) { +export const HistoryMinimap = memo(function HistoryMinimap({ events, onSelect }: { events: HistoryEvent[]; onSelect: (event: HistoryEvent) => void }) { const [css, theme] = useStyletron(); const [open, setOpen] = useState(false); const [activeEventId, setActiveEventId] = useState(events[events.length - 1]?.id ?? null); @@ -49,7 +43,7 @@ export const HistoryMinimap = memo(function HistoryMinimap({ >
- Handoff Events + Task Events {events.length}
diff --git a/factory/packages/frontend/src/components/mock-layout/message-list.tsx b/factory/packages/frontend/src/components/mock-layout/message-list.tsx index aec6a55..5e2ccbb 100644 --- a/factory/packages/frontend/src/components/mock-layout/message-list.tsx +++ b/factory/packages/frontend/src/components/mock-layout/message-list.tsx @@ -23,11 +23,7 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({ const isUser = message.sender === "client"; const isCopied = copiedMessageId === message.id; const messageTimestamp = formatMessageTimestamp(message.createdAtMs); - const displayFooter = isUser - ? messageTimestamp - : message.durationMs - ? `${messageTimestamp} • Took ${formatMessageDuration(message.durationMs)}` - : null; + const displayFooter = isUser ? messageTimestamp : message.durationMs ? `${messageTimestamp} • Took ${formatMessageDuration(message.durationMs)}` : null; return (
@@ -90,10 +86,7 @@ const TranscriptMessageBody = memo(function TranscriptMessageBody({ })} > {displayFooter ? ( - + {displayFooter} ) : null} @@ -170,12 +163,6 @@ export const MessageList = memo(function MessageList({ }), message: css({ display: "flex", - '&[data-variant="user"]': { - justifyContent: "flex-end", - }, - '&[data-variant="assistant"]': { - justifyContent: "flex-start", - }, }), messageContent: messageContentClass, messageText: css({ @@ -200,6 +187,11 @@ export const MessageList = memo(function MessageList({ return ( <> + {historyEvents.length > 0 ? : null}
- ); + return ; }} isThinking={Boolean(tab && tab.status === "running" && transcriptEntries.length > 0)} renderThinkingState={() => ( diff --git a/factory/packages/frontend/src/components/mock-layout/model-picker.tsx b/factory/packages/frontend/src/components/mock-layout/model-picker.tsx index 743023a..2e41ddf 100644 --- a/factory/packages/frontend/src/components/mock-layout/model-picker.tsx +++ b/factory/packages/frontend/src/components/mock-layout/model-picker.tsx @@ -1,7 +1,7 @@ import { memo, useState } from "react"; import { useStyletron } from "baseui"; import { StatefulPopover, PLACEMENT } from "baseui/popover"; -import { ChevronDown, Star } from "lucide-react"; +import { ChevronDown, ChevronUp, Star } from "lucide-react"; import { AgentIcon } from "./ui"; import { MODEL_GROUPS, modelLabel, providerAgent, type ModelId } from "./view-model"; @@ -23,7 +23,7 @@ const ModelPickerContent = memo(function ModelPickerContent({ const [hoveredId, setHoveredId] = useState(null); return ( -
+
{MODEL_GROUPS.map((group) => (
@@ -100,22 +103,26 @@ export const ModelPicker = memo(function ModelPicker({ onSetDefault: (id: ModelId) => void; }) { const [css, theme] = useStyletron(); + const [isOpen, setIsOpen] = useState(false); return ( setIsOpen(true)} + onClose={() => setIsOpen(false)} overrides={{ Body: { style: { - backgroundColor: "#000000", - borderTopLeftRadius: "8px", - borderTopRightRadius: "8px", - borderBottomLeftRadius: "8px", - borderBottomRightRadius: "8px", - border: `1px solid ${theme.colors.borderOpaque}`, - boxShadow: "0 8px 24px rgba(0, 0, 0, 0.6)", + backgroundColor: "rgba(32, 32, 32, 0.98)", + backdropFilter: "blur(12px)", + borderTopLeftRadius: "10px", + borderTopRightRadius: "10px", + borderBottomLeftRadius: "10px", + borderBottomRightRadius: "10px", + border: "1px solid rgba(255, 255, 255, 0.10)", + boxShadow: "0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04)", zIndex: 100, }, }, @@ -126,15 +133,7 @@ export const ModelPicker = memo(function ModelPicker({ }, }, }} - content={({ close }) => ( - - )} + content={({ close }) => } >
diff --git a/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx b/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx index 8974b35..d036d03 100644 --- a/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx +++ b/factory/packages/frontend/src/components/mock-layout/prompt-composer.tsx @@ -43,25 +43,27 @@ export const PromptComposer = memo(function PromptComposer({ backgroundColor: "rgba(255, 255, 255, 0.06)", border: `1px solid ${theme.colors.borderOpaque}`, borderRadius: "16px", - minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT}px`, + minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 36}px`, transition: "border-color 200ms ease", ":focus-within": { borderColor: "rgba(255, 255, 255, 0.3)" }, + display: "flex", + flexDirection: "column", }), input: css({ display: "block", width: "100%", - minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT}px`, - padding: "12px 58px 12px 14px", + minHeight: `${PROMPT_TEXTAREA_MIN_HEIGHT + 20}px`, + padding: "14px 58px 8px 14px", background: "transparent", border: "none", - borderRadius: "16px", + borderRadius: "16px 16px 0 0", color: theme.colors.contentPrimary, fontSize: "13px", fontFamily: "inherit", resize: "none", outline: "none", lineHeight: "1.4", - maxHeight: `${PROMPT_TEXTAREA_MAX_HEIGHT}px`, + maxHeight: `${PROMPT_TEXTAREA_MAX_HEIGHT + 40}px`, boxSizing: "border-box", overflowY: "hidden", "::placeholder": { color: theme.colors.contentSecondary }, @@ -101,7 +103,7 @@ export const PromptComposer = memo(function PromptComposer({
{fileName(attachment.filePath)}:{attachment.lineNumber} - onRemoveAttachment(attachment.id)} - /> + onRemoveAttachment(attachment.id)} />
))}
@@ -155,17 +153,21 @@ export const PromptComposer = memo(function PromptComposer({ }} placeholder={placeholder} inputRef={textareaRef} - rows={1} + rows={2} allowEmptySubmit={isRunning} submitLabel={isRunning ? "Stop" : "Send"} classNames={composerClassNames} renderSubmitContent={() => (isRunning ? : )} - /> - ( +
+ +
+ )} />
); diff --git a/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx b/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx index ae8a1c0..ccd81b0 100644 --- a/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx +++ b/factory/packages/frontend/src/components/mock-layout/right-sidebar.tsx @@ -1,16 +1,7 @@ import { memo, useCallback, useMemo, useState, type MouseEvent } from "react"; import { useStyletron } from "baseui"; import { LabelSmall } from "baseui/typography"; -import { - Archive, - ArrowUpFromLine, - ChevronRight, - FileCode, - FilePlus, - FileX, - FolderOpen, - GitPullRequest, -} from "lucide-react"; +import { Archive, ArrowUpFromLine, ChevronRight, FileCode, FilePlus, FileX, FolderOpen, GitPullRequest } from "lucide-react"; import { type ContextMenuItem, ContextMenuOverlay, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui"; import { type FileTreeNode, type Handoff, diffTabId } from "./view-model"; @@ -86,13 +77,7 @@ const FileTree = memo(function FileTree({ {node.name}
{node.isDir && !isCollapsed && node.children ? ( - + ) : null}
); @@ -366,13 +351,7 @@ export const RightSidebar = memo(function RightSidebar({ ) : (
{handoff.fileTree.length > 0 ? ( - + ) : (
No files yet diff --git a/factory/packages/frontend/src/components/mock-layout/sidebar.tsx b/factory/packages/frontend/src/components/mock-layout/sidebar.tsx index 6975ce9..2141fea 100644 --- a/factory/packages/frontend/src/components/mock-layout/sidebar.tsx +++ b/factory/packages/frontend/src/components/mock-layout/sidebar.tsx @@ -1,17 +1,26 @@ import { memo, useState } from "react"; import { useStyletron } from "baseui"; import { LabelSmall, LabelXSmall } from "baseui/typography"; -import { CloudUpload, GitPullRequestDraft, Plus } from "lucide-react"; +import { ChevronDown, ChevronUp, CloudUpload, GitPullRequestDraft, ListChecks, Plus } from "lucide-react"; import { formatRelativeAge, type Handoff, type ProjectSection } from "./view-model"; -import { - ContextMenuOverlay, - HandoffIndicator, - PanelHeaderBar, - SPanel, - ScrollBody, - useContextMenu, -} from "./ui"; +import { ContextMenuOverlay, HandoffIndicator, PanelHeaderBar, SPanel, ScrollBody, useContextMenu } from "./ui"; + +const PROJECT_COLORS = ["#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"]; + +function projectInitial(label: string): string { + const parts = label.split("/"); + const name = parts[parts.length - 1] ?? label; + return name.charAt(0).toUpperCase(); +} + +function projectIconColor(label: string): string { + let hash = 0; + for (let i = 0; i < label.length; i++) { + hash = (hash * 31 + label.charCodeAt(i)) | 0; + } + return PROJECT_COLORS[Math.abs(hash) % PROJECT_COLORS.length]!; +} export const Sidebar = memo(function Sidebar({ projects, @@ -32,13 +41,25 @@ export const Sidebar = memo(function Sidebar({ }) { const [css, theme] = useStyletron(); const contextMenu = useContextMenu(); - const [expandedProjects, setExpandedProjects] = useState>({}); + const [collapsedProjects, setCollapsedProjects] = useState>({}); return ( + - - Handoffs + + + Tasks - ) : null}
); })} diff --git a/factory/packages/frontend/src/components/mock-layout/transcript-header.tsx b/factory/packages/frontend/src/components/mock-layout/transcript-header.tsx index badc499..820cff6 100644 --- a/factory/packages/frontend/src/components/mock-layout/transcript-header.tsx +++ b/factory/packages/frontend/src/components/mock-layout/transcript-header.tsx @@ -1,7 +1,7 @@ import { memo } from "react"; import { useStyletron } from "baseui"; import { LabelSmall } from "baseui/typography"; -import { MailOpen } from "lucide-react"; +import { Clock, MailOpen } from "lucide-react"; import { PanelHeaderBar } from "./ui"; import { type AgentTab, type Handoff } from "./view-model"; @@ -46,7 +46,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({ }} className={css({ all: "unset", - fontWeight: 600, + fontWeight: 500, fontSize: "14px", color: theme.colors.contentPrimary, borderBottom: "1px solid rgba(255, 255, 255, 0.3)", @@ -58,7 +58,7 @@ export const TranscriptHeader = memo(function TranscriptHeader({ onStartEditingField("title", handoff.title)} > {handoff.title} @@ -113,6 +113,24 @@ export const TranscriptHeader = memo(function TranscriptHeader({ ) ) : null}
+
+ + 847 min used +
{activeTab ? (
@@ -1198,7 +1175,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep {formatRelativeAge(branch.updatedAt)} - {branch.handoffId ? "handoff" : "unmapped"} + {branch.handoffId ? "task" : "unmapped"} {branch.trackedInStack ? stack : null}
@@ -1291,13 +1268,11 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }} data-testid={`repo-overview-create-${branchToken}`} > - Create Handoff + Create Task ) : null} - - {branch.conflictsWithMain ? "conflict" : "ok"} - + {branch.conflictsWithMain ? "conflict" : "ok"}
@@ -1331,11 +1306,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep > - {selectedForSession ? selectedForSession.title ?? "Determining title..." : "No handoff selected"} + {selectedForSession ? selectedForSession.title ?? "Determining title..." : "No task selected"} - {selectedForSession ? ( - {selectedForSession.status} - ) : null} + {selectedForSession ? {selectedForSession.status} : null} {selectedForSession && !resolvedSessionId ? ( @@ -1364,7 +1337,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep })} > {!selectedForSession ? ( - Select a handoff from the left sidebar. + Select a task from the left sidebar. ) : ( <>
) : null} @@ -1462,14 +1435,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep key={entry.id} data-testid="session-transcript-entry" className={css({ - borderLeft: `2px solid ${ - entry.sender === "agent" ? "rgba(29, 111, 95, 0.45)" : "rgba(32, 108, 176, 0.45)" - }`, - border: `1px solid ${ - entry.sender === "agent" ? "rgba(29, 111, 95, 0.22)" : "rgba(32, 108, 176, 0.22)" - }`, - backgroundColor: - entry.sender === "agent" ? "rgba(29, 111, 95, 0.07)" : "rgba(32, 108, 176, 0.07)", + borderLeft: `2px solid ${entry.sender === "agent" ? "rgba(29, 111, 95, 0.45)" : "rgba(32, 108, 176, 0.45)"}`, + border: `1px solid ${entry.sender === "agent" ? "rgba(29, 111, 95, 0.22)" : "rgba(32, 108, 176, 0.22)"}`, + backgroundColor: entry.sender === "agent" ? "rgba(29, 111, 95, 0.07)" : "rgba(32, 108, 176, 0.07)", padding: `12px ${theme.sizing.scale400}`, })} > @@ -1535,11 +1503,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep void sendPrompt.mutateAsync(prompt); }} disabled={ - sendPrompt.isPending || - createSession.isPending || - !selectedForSession || - !activeSandbox?.sandboxId || - draft.trim().length === 0 + sendPrompt.isPending || createSession.isPending || !selectedForSession || !activeSandbox?.sandboxId || draft.trim().length === 0 } > - {repoOverviewMode ? "Repo Details" : "Handoff Details"} + {repoOverviewMode ? "Repo Details" : "Task Details"} @@ -1618,7 +1582,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep
@@ -1628,7 +1592,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep ) ) : !selectedForSession ? ( - No handoff selected. + No task selected. ) : ( <> @@ -1644,7 +1608,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep gap: theme.sizing.scale300, })} > - + @@ -1711,9 +1675,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep - {selectedForSession.statusMessage - ? selectedForSession.statusMessage - : "Open transcript in the center panel for details."} + {selectedForSession.statusMessage ? selectedForSession.statusMessage : "Open transcript in the center panel for details."} ) : null} @@ -1773,7 +1735,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }} overrides={modalOverrides} > - Create Handoff + Create Task
- Pick a repo, describe the task, and the backend will create a handoff. + Pick a repo, describe the task, and the backend will create a task.
@@ -1921,7 +1883,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }} data-testid="handoff-create-submit" > - Create Handoff + Create Task diff --git a/factory/packages/frontend/src/features/handoffs/model.test.ts b/factory/packages/frontend/src/features/handoffs/model.test.ts index 153442e..b6fa078 100644 --- a/factory/packages/frontend/src/features/handoffs/model.test.ts +++ b/factory/packages/frontend/src/features/handoffs/model.test.ts @@ -24,7 +24,7 @@ const base: HandoffRecord = { cwd: null, createdAt: 10, updatedAt: 10, - } + }, ], agentType: null, prSubmitted: false, diff --git a/factory/packages/frontend/src/features/sessions/model.test.ts b/factory/packages/frontend/src/features/sessions/model.test.ts index 0ead4d6..94b8344 100644 --- a/factory/packages/frontend/src/features/sessions/model.test.ts +++ b/factory/packages/frontend/src/features/sessions/model.test.ts @@ -4,9 +4,7 @@ import { buildTranscript, extractEventText, resolveSessionSelection } from "./mo describe("extractEventText", () => { it("extracts prompt text arrays", () => { - expect( - extractEventText({ params: { prompt: [{ type: "text", text: "hello" }] } }) - ).toBe("hello"); + expect(extractEventText({ params: { prompt: [{ type: "text", text: "hello" }] } })).toBe("hello"); }); it("falls back to method name", () => { @@ -17,9 +15,9 @@ describe("extractEventText", () => { expect( extractEventText({ result: { - text: "agent output" - } - }) + text: "agent output", + }, + }), ).toBe("agent output"); }); @@ -31,11 +29,11 @@ describe("extractEventText", () => { sessionUpdate: "agent_message_chunk", content: { type: "text", - text: "chunk" - } - } - } - }) + text: "chunk", + }, + }, + }, + }), ).toBe("chunk"); }); }); @@ -50,7 +48,7 @@ describe("buildTranscript", () => { createdAt: 1000, connectionId: "conn-1", sender: "client", - payload: { params: { prompt: [{ type: "text", text: "hello" }] } } + payload: { params: { prompt: [{ type: "text", text: "hello" }] } }, }, { id: "evt-2", @@ -59,8 +57,8 @@ describe("buildTranscript", () => { createdAt: 2000, connectionId: "conn-1", sender: "agent", - payload: { params: { text: "world" } } - } + payload: { params: { text: "world" } }, + }, ]); expect(rows).toEqual([ @@ -68,37 +66,38 @@ describe("buildTranscript", () => { id: "evt-1", sender: "client", text: "hello", - createdAt: 1000 + createdAt: 1000, }, { id: "evt-2", sender: "agent", text: "world", - createdAt: 2000 - } + createdAt: 2000, + }, ]); }); }); describe("resolveSessionSelection", () => { - const session = (id: string, status: "running" | "idle" | "error" = "running"): SandboxSessionRecord => ({ - id, - agentSessionId: `agent-${id}`, - lastConnectionId: `conn-${id}`, - createdAt: 1, - status - } as SandboxSessionRecord); + const session = (id: string, status: "running" | "idle" | "error" = "running"): SandboxSessionRecord => + ({ + id, + agentSessionId: `agent-${id}`, + lastConnectionId: `conn-${id}`, + createdAt: 1, + status, + }) as SandboxSessionRecord; it("prefers explicit selection when present in session list", () => { const resolved = resolveSessionSelection({ explicitSessionId: "session-2", handoffSessionId: "session-1", - sessions: [session("session-1"), session("session-2")] + sessions: [session("session-1"), session("session-2")], }); expect(resolved).toEqual({ sessionId: "session-2", - staleSessionId: null + staleSessionId: null, }); }); @@ -106,12 +105,12 @@ describe("resolveSessionSelection", () => { const resolved = resolveSessionSelection({ explicitSessionId: null, handoffSessionId: "session-1", - sessions: [session("session-1")] + sessions: [session("session-1")], }); expect(resolved).toEqual({ sessionId: "session-1", - staleSessionId: null + staleSessionId: null, }); }); @@ -119,12 +118,12 @@ describe("resolveSessionSelection", () => { const resolved = resolveSessionSelection({ explicitSessionId: null, handoffSessionId: "session-stale", - sessions: [session("session-fresh")] + sessions: [session("session-fresh")], }); expect(resolved).toEqual({ sessionId: "session-fresh", - staleSessionId: null + staleSessionId: null, }); }); @@ -132,12 +131,12 @@ describe("resolveSessionSelection", () => { const resolved = resolveSessionSelection({ explicitSessionId: null, handoffSessionId: "session-stale", - sessions: [] + sessions: [], }); expect(resolved).toEqual({ sessionId: null, - staleSessionId: "session-stale" + staleSessionId: "session-stale", }); }); }); diff --git a/factory/packages/frontend/src/features/sessions/model.ts b/factory/packages/frontend/src/features/sessions/model.ts index ea4d59d..dfb59e7 100644 --- a/factory/packages/frontend/src/features/sessions/model.ts +++ b/factory/packages/frontend/src/features/sessions/model.ts @@ -105,11 +105,7 @@ export function buildTranscript(events: SandboxSessionEventRecord[]): Array<{ })); } -export function resolveSessionSelection(input: { - explicitSessionId: string | null; - handoffSessionId: string | null; - sessions: SandboxSessionRecord[]; -}): { +export function resolveSessionSelection(input: { explicitSessionId: string | null; handoffSessionId: string | null; sessions: SandboxSessionRecord[] }): { sessionId: string | null; staleSessionId: string | null; } { diff --git a/factory/packages/frontend/src/lib/env.ts b/factory/packages/frontend/src/lib/env.ts index c0744ca..3c9e1b4 100644 --- a/factory/packages/frontend/src/lib/env.ts +++ b/factory/packages/frontend/src/lib/env.ts @@ -11,8 +11,7 @@ type FrontendImportMetaEnv = ImportMetaEnv & { const frontendEnv = import.meta.env as FrontendImportMetaEnv; -export const backendEndpoint = - import.meta.env.VITE_HF_BACKEND_ENDPOINT?.trim() || resolveDefaultBackendEndpoint(); +export const backendEndpoint = import.meta.env.VITE_HF_BACKEND_ENDPOINT?.trim() || resolveDefaultBackendEndpoint(); export const defaultWorkspaceId = import.meta.env.VITE_HF_WORKSPACE?.trim() || "default"; @@ -24,9 +23,7 @@ function resolveFrontendClientMode(): "mock" | "remote" { if (raw === "remote" || raw === "" || raw === undefined) { return "remote"; } - throw new Error( - `Unsupported OPENHANDOFF_FRONTEND_CLIENT_MODE value "${frontendEnv.OPENHANDOFF_FRONTEND_CLIENT_MODE}". Expected "mock" or "remote".`, - ); + throw new Error(`Unsupported OPENHANDOFF_FRONTEND_CLIENT_MODE value "${frontendEnv.OPENHANDOFF_FRONTEND_CLIENT_MODE}". Expected "mock" or "remote".`); } export const frontendClientMode = resolveFrontendClientMode(); diff --git a/factory/packages/frontend/src/main.tsx b/factory/packages/frontend/src/main.tsx index 14c69a3..45c98f4 100644 --- a/factory/packages/frontend/src/main.tsx +++ b/factory/packages/frontend/src/main.tsx @@ -29,5 +29,5 @@ createRoot(document.getElementById("root")!).render( - + , ); diff --git a/factory/packages/frontend/src/styles.css b/factory/packages/frontend/src/styles.css index 0e7c22d..8f634e6 100644 --- a/factory/packages/frontend/src/styles.css +++ b/factory/packages/frontend/src/styles.css @@ -39,7 +39,9 @@ a { } @keyframes hf-spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } button, @@ -102,7 +104,7 @@ pre { } .mock-diff-row[data-kind="remove"] { - background: rgba(248, 81, 73, 0.10); + background: rgba(248, 81, 73, 0.1); } .mock-diff-row[data-kind="hunk"] { diff --git a/factory/packages/frontend/vite.config.ts b/factory/packages/frontend/vite.config.ts index f0cfc1d..482b0fc 100644 --- a/factory/packages/frontend/vite.config.ts +++ b/factory/packages/frontend/vite.config.ts @@ -6,9 +6,7 @@ const backendProxyTarget = process.env.HF_BACKEND_HTTP?.trim() || "http://127.0. const cacheDir = process.env.HF_VITE_CACHE_DIR?.trim() || undefined; export default defineConfig({ define: { - "import.meta.env.OPENHANDOFF_FRONTEND_CLIENT_MODE": JSON.stringify( - process.env.OPENHANDOFF_FRONTEND_CLIENT_MODE?.trim() || "remote", - ), + "import.meta.env.OPENHANDOFF_FRONTEND_CLIENT_MODE": JSON.stringify(process.env.OPENHANDOFF_FRONTEND_CLIENT_MODE?.trim() || "remote"), }, plugins: [react(), frontendErrorCollectorVitePlugin()], cacheDir, diff --git a/factory/packages/shared/src/config.ts b/factory/packages/shared/src/config.ts index c0bc256..710c531 100644 --- a/factory/packages/shared/src/config.ts +++ b/factory/packages/shared/src/config.ts @@ -2,53 +2,60 @@ import { z } from "zod"; export const AgentEnumSchema = z.enum(["claude", "codex"]); -export const NotifyBackendSchema = z.enum([ - "openclaw", - "macos-osascript", - "linux-notify-send", - "terminal" -]); +export const NotifyBackendSchema = z.enum(["openclaw", "macos-osascript", "linux-notify-send", "terminal"]); export const ConfigSchema = z.object({ theme: z.string().min(1).optional(), auto_submit: z.boolean().default(false), default_agent: AgentEnumSchema.default("codex"), - model: z.object({ - provider: z.string(), - model: z.string() - }).optional(), + model: z + .object({ + provider: z.string(), + model: z.string(), + }) + .optional(), notify: z.array(NotifyBackendSchema).default(["terminal"]), - workspace: z.object({ - default: z.string().min(1).default("default") - }).default({ default: "default" }), - backend: z.object({ - host: z.string().default("127.0.0.1"), - port: z.number().int().min(1).max(65535).default(7741), - dbPath: z.string().default("~/.local/share/openhandoff/handoff.db"), - opencode_poll_interval: z.number().default(2), - github_poll_interval: z.number().default(30), - backup_interval_secs: z.number().default(3600), - backup_retention_days: z.number().default(7) - }).default({ - host: "127.0.0.1", - port: 7741, - dbPath: "~/.local/share/openhandoff/handoff.db", - opencode_poll_interval: 2, - github_poll_interval: 30, - backup_interval_secs: 3600, - backup_retention_days: 7 - }), - providers: z.object({ - local: z.object({ - rootDir: z.string().optional(), - sandboxAgentPort: z.number().int().min(1).max(65535).optional(), - }).default({}), - daytona: z.object({ - endpoint: z.string().optional(), - apiKey: z.string().optional(), - image: z.string().default("ubuntu:24.04") - }).default({ image: "ubuntu:24.04" }) - }).default({ local: {}, daytona: { image: "ubuntu:24.04" } }) + workspace: z + .object({ + default: z.string().min(1).default("default"), + }) + .default({ default: "default" }), + backend: z + .object({ + host: z.string().default("127.0.0.1"), + port: z.number().int().min(1).max(65535).default(7741), + dbPath: z.string().default("~/.local/share/openhandoff/handoff.db"), + opencode_poll_interval: z.number().default(2), + github_poll_interval: z.number().default(30), + backup_interval_secs: z.number().default(3600), + backup_retention_days: z.number().default(7), + }) + .default({ + host: "127.0.0.1", + port: 7741, + dbPath: "~/.local/share/openhandoff/handoff.db", + opencode_poll_interval: 2, + github_poll_interval: 30, + backup_interval_secs: 3600, + backup_retention_days: 7, + }), + providers: z + .object({ + local: z + .object({ + rootDir: z.string().optional(), + sandboxAgentPort: z.number().int().min(1).max(65535).optional(), + }) + .default({}), + daytona: z + .object({ + endpoint: z.string().optional(), + apiKey: z.string().optional(), + image: z.string().default("ubuntu:24.04"), + }) + .default({ image: "ubuntu:24.04" }), + }) + .default({ local: {}, daytona: { image: "ubuntu:24.04" } }), }); export type AppConfig = z.infer; diff --git a/factory/packages/shared/src/contracts.ts b/factory/packages/shared/src/contracts.ts index a1bc7ee..a020674 100644 --- a/factory/packages/shared/src/contracts.ts +++ b/factory/packages/shared/src/contracts.ts @@ -1,6 +1,10 @@ import { z } from "zod"; -export const WorkspaceIdSchema = z.string().min(1).max(64).regex(/^[a-zA-Z0-9._-]+$/); +export const WorkspaceIdSchema = z + .string() + .min(1) + .max(64) + .regex(/^[a-zA-Z0-9._-]+$/); export type WorkspaceId = z.infer; export const ProviderIdSchema = z.enum(["daytona", "local"]); @@ -36,7 +40,7 @@ export const HandoffStatusSchema = z.enum([ "kill_destroy_sandbox", "kill_finalize", "killed", - "error" + "error", ]); export type HandoffStatus = z.infer; @@ -63,7 +67,7 @@ export const CreateHandoffInputSchema = z.object({ explicitBranchName: z.string().trim().min(1).optional(), providerId: ProviderIdSchema.optional(), agentType: AgentTypeSchema.optional(), - onBranch: z.string().trim().min(1).optional() + onBranch: z.string().trim().min(1).optional(), }); export type CreateHandoffInput = z.infer; @@ -89,7 +93,7 @@ export const HandoffRecordSchema = z.object({ cwd: z.string().nullable(), createdAt: z.number().int(), updatedAt: z.number().int(), - }) + }), ), agentType: z.string().nullable(), prSubmitted: z.boolean(), @@ -103,7 +107,7 @@ export const HandoffRecordSchema = z.object({ hasUnpushed: z.string().nullable(), parentBranch: z.string().nullable(), createdAt: z.number().int(), - updatedAt: z.number().int() + updatedAt: z.number().int(), }); export type HandoffRecord = z.infer; @@ -114,13 +118,13 @@ export const HandoffSummarySchema = z.object({ branchName: z.string().min(1).nullable(), title: z.string().min(1).nullable(), status: HandoffStatusSchema, - updatedAt: z.number().int() + updatedAt: z.number().int(), }); export type HandoffSummary = z.infer; export const HandoffActionInputSchema = z.object({ workspaceId: WorkspaceIdSchema, - handoffId: z.string().min(1) + handoffId: z.string().min(1), }); export type HandoffActionInput = z.infer; @@ -128,13 +132,13 @@ export const SwitchResultSchema = z.object({ workspaceId: WorkspaceIdSchema, handoffId: z.string().min(1), providerId: ProviderIdSchema, - switchTarget: z.string().min(1) + switchTarget: z.string().min(1), }); export type SwitchResult = z.infer; export const ListHandoffsInputSchema = z.object({ workspaceId: WorkspaceIdSchema, - repoId: RepoIdSchema.optional() + repoId: RepoIdSchema.optional(), }); export type ListHandoffsInput = z.infer; @@ -157,7 +161,7 @@ export const RepoBranchRecordSchema = z.object({ reviewer: z.string().nullable(), firstSeenAt: z.number().int().nullable(), lastSeenAt: z.number().int().nullable(), - updatedAt: z.number().int() + updatedAt: z.number().int(), }); export type RepoBranchRecord = z.infer; @@ -168,17 +172,11 @@ export const RepoOverviewSchema = z.object({ baseRef: z.string().nullable(), stackAvailable: z.boolean(), fetchedAt: z.number().int(), - branches: z.array(RepoBranchRecordSchema) + branches: z.array(RepoBranchRecordSchema), }); export type RepoOverview = z.infer; -export const RepoStackActionSchema = z.enum([ - "sync_repo", - "restack_repo", - "restack_subtree", - "rebase_branch", - "reparent_branch" -]); +export const RepoStackActionSchema = z.enum(["sync_repo", "restack_repo", "restack_subtree", "rebase_branch", "reparent_branch"]); export type RepoStackAction = z.infer; export const RepoStackActionInputSchema = z.object({ @@ -186,7 +184,7 @@ export const RepoStackActionInputSchema = z.object({ repoId: RepoIdSchema, action: RepoStackActionSchema, branchName: z.string().trim().min(1).optional(), - parentBranch: z.string().trim().min(1).optional() + parentBranch: z.string().trim().min(1).optional(), }); export type RepoStackActionInput = z.infer; @@ -194,20 +192,31 @@ export const RepoStackActionResultSchema = z.object({ action: RepoStackActionSchema, executed: z.boolean(), message: z.string().min(1), - at: z.number().int() + at: z.number().int(), }); export type RepoStackActionResult = z.infer; export const WorkspaceUseInputSchema = z.object({ - workspaceId: WorkspaceIdSchema + workspaceId: WorkspaceIdSchema, }); export type WorkspaceUseInput = z.infer; +export const StarSandboxAgentRepoInputSchema = z.object({ + workspaceId: WorkspaceIdSchema, +}); +export type StarSandboxAgentRepoInput = z.infer; + +export const StarSandboxAgentRepoResultSchema = z.object({ + repo: z.string().min(1), + starredAt: z.number().int(), +}); +export type StarSandboxAgentRepoResult = z.infer; + export const HistoryQueryInputSchema = z.object({ workspaceId: WorkspaceIdSchema, limit: z.number().int().positive().max(500).optional(), branch: z.string().min(1).optional(), - handoffId: z.string().min(1).optional() + handoffId: z.string().min(1).optional(), }); export type HistoryQueryInput = z.infer; @@ -219,14 +228,14 @@ export const HistoryEventSchema = z.object({ branchName: z.string().nullable(), kind: z.string().min(1), payloadJson: z.string().min(1), - createdAt: z.number().int() + createdAt: z.number().int(), }); export type HistoryEvent = z.infer; export const PruneInputSchema = z.object({ workspaceId: WorkspaceIdSchema, dryRun: z.boolean(), - yes: z.boolean() + yes: z.boolean(), }); export type PruneInput = z.infer; @@ -234,19 +243,19 @@ export const KillInputSchema = z.object({ workspaceId: WorkspaceIdSchema, handoffId: z.string().min(1), deleteBranch: z.boolean(), - abandon: z.boolean() + abandon: z.boolean(), }); export type KillInput = z.infer; export const StatuslineInputSchema = z.object({ workspaceId: WorkspaceIdSchema, - format: z.enum(["table", "claude-code"]) + format: z.enum(["table", "claude-code"]), }); export type StatuslineInput = z.infer; export const ListInputSchema = z.object({ workspaceId: WorkspaceIdSchema, format: z.enum(["table", "json"]), - full: z.boolean() + full: z.boolean(), }); export type ListInput = z.infer; diff --git a/factory/packages/shared/src/workspace.ts b/factory/packages/shared/src/workspace.ts index cb5385e..fb8e1b7 100644 --- a/factory/packages/shared/src/workspace.ts +++ b/factory/packages/shared/src/workspace.ts @@ -1,9 +1,6 @@ import type { AppConfig } from "./config.js"; -export function resolveWorkspaceId( - flagWorkspace: string | undefined, - config: AppConfig -): string { +export function resolveWorkspaceId(flagWorkspace: string | undefined, config: AppConfig): string { if (flagWorkspace && flagWorkspace.trim().length > 0) { return flagWorkspace.trim(); } diff --git a/factory/packages/shared/test/workspace.test.ts b/factory/packages/shared/test/workspace.test.ts index 9ecb136..54224b6 100644 --- a/factory/packages/shared/test/workspace.test.ts +++ b/factory/packages/shared/test/workspace.test.ts @@ -12,11 +12,11 @@ const cfg: AppConfig = ConfigSchema.parse({ opencode_poll_interval: 2, github_poll_interval: 30, backup_interval_secs: 3600, - backup_retention_days: 7 + backup_retention_days: 7, }, providers: { - daytona: { image: "ubuntu:24.04" } - } + daytona: { image: "ubuntu:24.04" }, + }, }); describe("resolveWorkspaceId", () => { @@ -31,7 +31,7 @@ describe("resolveWorkspaceId", () => { it("falls back to literal default when config value is empty", () => { const empty = { ...cfg, - workspace: { default: "" } + workspace: { default: "" }, } as AppConfig; expect(resolveWorkspaceId(undefined, empty)).toBe("default"); diff --git a/frontend/packages/inspector/index.html b/frontend/packages/inspector/index.html index e28187b..5893717 100644 --- a/frontend/packages/inspector/index.html +++ b/frontend/packages/inspector/index.html @@ -1510,10 +1510,11 @@ } .message.assistant .message-content { - background: var(--surface); - border: 1px solid var(--border); + background: none; + border: none; color: var(--text-secondary); - border-bottom-left-radius: 4px; + border-radius: 0; + padding: 0 16px; } .message.system .avatar { diff --git a/frontend/packages/inspector/src/App.tsx b/frontend/packages/inspector/src/App.tsx index 92eb3e5..a829ae6 100644 --- a/frontend/packages/inspector/src/App.tsx +++ b/frontend/packages/inspector/src/App.tsx @@ -32,9 +32,7 @@ import SessionSidebar from "./components/SessionSidebar"; import type { RequestLog } from "./types/requestLog"; import { buildCurl } from "./utils/http"; -const flattenSelectOptions = ( - options: ConfigSelectOption[] | Array<{ group: string; name: string; options: ConfigSelectOption[] }> -): ConfigSelectOption[] => { +const flattenSelectOptions = (options: ConfigSelectOption[] | Array<{ group: string; name: string; options: ConfigSelectOption[] }>): ConfigSelectOption[] => { if (options.length === 0) return []; if ("value" in options[0]) return options as ConfigSelectOption[]; return (options as Array<{ options: ConfigSelectOption[] }>).flatMap((g) => g.options); @@ -186,9 +184,7 @@ const getPersistedSessionModels = (): Record => { const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}; return Object.fromEntries( - Object.entries(parsed).filter( - (entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string" && entry[1].length > 0 - ) + Object.entries(parsed).filter((entry): entry is [string, string] => typeof entry[0] === "string" && typeof entry[1] === "string" && entry[1].length > 0), ); } catch { return {}; @@ -230,14 +226,12 @@ const getInitialConnection = () => { } } const hasUrlParam = urlParam != null && urlParam.length > 0; - const defaultEndpoint = import.meta.env.DEV - ? DEFAULT_ENDPOINT - : (getCurrentOriginEndpoint() ?? DEFAULT_ENDPOINT); + const defaultEndpoint = import.meta.env.DEV ? DEFAULT_ENDPOINT : (getCurrentOriginEndpoint() ?? DEFAULT_ENDPOINT); return { endpoint: hasUrlParam ? urlParam : defaultEndpoint, token: tokenParam, headers, - hasUrlParam + hasUrlParam, }; }; @@ -247,7 +241,7 @@ const agentDisplayNames: Record = { opencode: "OpenCode", amp: "Amp", pi: "Pi", - cursor: "Cursor" + cursor: "Cursor", }; export default function App() { @@ -324,96 +318,94 @@ export default function App() { }); }, []); - const createClient = useCallback(async (overrideEndpoint?: string) => { - const targetEndpoint = overrideEndpoint ?? endpoint; - const fetchWithLog: typeof fetch = async (input, init) => { - const method = init?.method ?? "GET"; - const url = - typeof input === "string" - ? input - : input instanceof URL - ? input.toString() - : input.url; - const bodyText = typeof init?.body === "string" ? init.body : undefined; - const curl = buildCurl(method, url, bodyText, token); - const logId = logIdRef.current++; + const createClient = useCallback( + async (overrideEndpoint?: string) => { + const targetEndpoint = overrideEndpoint ?? endpoint; + const fetchWithLog: typeof fetch = async (input, init) => { + const method = init?.method ?? "GET"; + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const bodyText = typeof init?.body === "string" ? init.body : undefined; + const curl = buildCurl(method, url, bodyText, token); + const logId = logIdRef.current++; - const headers: Record = {}; - if (init?.headers) { - const h = new Headers(init.headers as HeadersInit); - h.forEach((v, k) => { headers[k] = v; }); - } + const headers: Record = {}; + if (init?.headers) { + const h = new Headers(init.headers as HeadersInit); + h.forEach((v, k) => { + headers[k] = v; + }); + } - const entry: RequestLog = { - id: logId, - method, - url, - headers, - body: bodyText, - time: new Date().toLocaleTimeString(), - curl - }; - let logged = false; + const entry: RequestLog = { + id: logId, + method, + url, + headers, + body: bodyText, + time: new Date().toLocaleTimeString(), + curl, + }; + let logged = false; - const fetchInit = { - ...init, - targetAddressSpace: "loopback" - }; + const fetchInit = { + ...init, + targetAddressSpace: "loopback", + }; - try { - const response = await fetch(input, fetchInit); - const acceptsStream = headers["accept"]?.includes("text/event-stream"); - if (acceptsStream) { - const ct = response.headers.get("content-type") ?? ""; - if (!ct.includes("text/event-stream")) { - throw new Error( - `Expected text/event-stream from ${method} ${url} but got ${ct || "(no content-type)"} (HTTP ${response.status})` - ); + try { + const response = await fetch(input, fetchInit); + const acceptsStream = headers["accept"]?.includes("text/event-stream"); + if (acceptsStream) { + const ct = response.headers.get("content-type") ?? ""; + if (!ct.includes("text/event-stream")) { + throw new Error(`Expected text/event-stream from ${method} ${url} but got ${ct || "(no content-type)"} (HTTP ${response.status})`); + } + logRequest({ ...entry, status: response.status, responseBody: "(SSE stream)" }); + logged = true; + return response; + } + const clone = response.clone(); + const responseBody = await clone.text().catch(() => ""); + logRequest({ ...entry, status: response.status, responseBody }); + if (!response.ok && response.status >= 500) { + const messageText = getHttpErrorMessage(response.status, response.statusText, responseBody); + window.dispatchEvent(new CustomEvent(HTTP_ERROR_EVENT, { detail: messageText })); } - logRequest({ ...entry, status: response.status, responseBody: "(SSE stream)" }); logged = true; return response; + } catch (error) { + const messageText = error instanceof Error ? error.message : "Request failed"; + if (!logged) { + logRequest({ ...entry, status: 0, error: messageText }); + } + throw error; } - const clone = response.clone(); - const responseBody = await clone.text().catch(() => ""); - logRequest({ ...entry, status: response.status, responseBody }); - if (!response.ok && response.status >= 500) { - const messageText = getHttpErrorMessage(response.status, response.statusText, responseBody); - window.dispatchEvent(new CustomEvent(HTTP_ERROR_EVENT, { detail: messageText })); - } - logged = true; - return response; - } catch (error) { - const messageText = error instanceof Error ? error.message : "Request failed"; - if (!logged) { - logRequest({ ...entry, status: 0, error: messageText }); - } - throw error; + }; + + let persist: SessionPersistDriver; + try { + persist = new IndexedDbSessionPersistDriver({ + databaseName: "sandbox-agent-inspector", + }); + } catch { + persist = new InMemorySessionPersistDriver({ + maxSessions: 512, + maxEventsPerSession: 5_000, + }); } - }; - let persist: SessionPersistDriver; - try { - persist = new IndexedDbSessionPersistDriver({ - databaseName: "sandbox-agent-inspector", + const client = await SandboxAgent.connect({ + baseUrl: targetEndpoint, + token: token || undefined, + fetch: fetchWithLog, + headers: Object.keys(extraHeaders).length > 0 ? extraHeaders : undefined, + persist, }); - } catch { - persist = new InMemorySessionPersistDriver({ - maxSessions: 512, - maxEventsPerSession: 5_000, - }); - } - - const client = await SandboxAgent.connect({ - baseUrl: targetEndpoint, - token: token || undefined, - fetch: fetchWithLog, - headers: Object.keys(extraHeaders).length > 0 ? extraHeaders : undefined, - persist, - }); - clientRef.current = client; - return client; - }, [endpoint, token, extraHeaders, logRequest]); + clientRef.current = client; + return client; + }, + [endpoint, token, extraHeaders, logRequest], + ); const getClient = useCallback((): SandboxAgent => { if (!clientRef.current) { @@ -433,22 +425,25 @@ export default function App() { setErrorToasts((prev) => prev.filter((toast) => toast.id !== toastId)); }, []); - const scheduleErrorToastDismiss = useCallback((toastId: number, delayMs: number) => { - const existingTimeoutId = toastTimeoutsRef.current.get(toastId); - if (existingTimeoutId != null) { - window.clearTimeout(existingTimeoutId); - toastTimeoutsRef.current.delete(toastId); - } + const scheduleErrorToastDismiss = useCallback( + (toastId: number, delayMs: number) => { + const existingTimeoutId = toastTimeoutsRef.current.get(toastId); + if (existingTimeoutId != null) { + window.clearTimeout(existingTimeoutId); + toastTimeoutsRef.current.delete(toastId); + } - const clampedDelayMs = Math.max(0, delayMs); - const timeoutId = window.setTimeout(() => { - dismissErrorToast(toastId); - }, clampedDelayMs); + const clampedDelayMs = Math.max(0, delayMs); + const timeoutId = window.setTimeout(() => { + dismissErrorToast(toastId); + }, clampedDelayMs); - toastTimeoutsRef.current.set(toastId, timeoutId); - toastExpiryRef.current.set(toastId, Date.now() + clampedDelayMs); - toastRemainingMsRef.current.set(toastId, clampedDelayMs); - }, [dismissErrorToast]); + toastTimeoutsRef.current.set(toastId, timeoutId); + toastExpiryRef.current.set(toastId, Date.now() + clampedDelayMs); + toastRemainingMsRef.current.set(toastId, clampedDelayMs); + }, + [dismissErrorToast], + ); const pauseErrorToastDismiss = useCallback((toastId: number) => { const expiryMs = toastExpiryRef.current.get(toastId); @@ -464,125 +459,133 @@ export default function App() { toastExpiryRef.current.delete(toastId); }, []); - const resumeErrorToastDismiss = useCallback((toastId: number) => { - if (toastTimeoutsRef.current.has(toastId)) return; - const remainingMs = toastRemainingMsRef.current.get(toastId); - if (remainingMs == null) return; - scheduleErrorToastDismiss(toastId, remainingMs); - }, [scheduleErrorToastDismiss]); + const resumeErrorToastDismiss = useCallback( + (toastId: number) => { + if (toastTimeoutsRef.current.has(toastId)) return; + const remainingMs = toastRemainingMsRef.current.get(toastId); + if (remainingMs == null) return; + scheduleErrorToastDismiss(toastId, remainingMs); + }, + [scheduleErrorToastDismiss], + ); - const pushErrorToast = useCallback((error: unknown, fallback: string) => { - const messageText = getErrorMessage(error, fallback).trim() || fallback; - const toastId = toastIdRef.current++; - setErrorToasts((prev) => { - if (prev.some((toast) => toast.message === messageText)) { - return prev; - } - return [...prev, { id: toastId, message: messageText }].slice(-MAX_ERROR_TOASTS); - }); - scheduleErrorToastDismiss(toastId, ERROR_TOAST_MS); - }, [scheduleErrorToastDismiss]); - - // Subscribe to events for the current active session - const subscribeToSession = useCallback((session: Session) => { - const generation = ++subscriptionGenerationRef.current; - const isCurrentSubscription = (): boolean => - subscriptionGenerationRef.current === generation - && activeSessionRef.current?.id === session.id - && selectedSessionIdRef.current === session.id; - - // Unsubscribe from previous - if (eventUnsubRef.current) { - eventUnsubRef.current(); - eventUnsubRef.current = null; - } - - activeSessionRef.current = session; - const cachedEvents = sessionEventsCacheRef.current.get(session.id); - if (cachedEvents && isCurrentSubscription()) { - setEvents(cachedEvents); - setHistoryLoadingSessionId((current) => (current === session.id ? null : current)); - } else if (isCurrentSubscription()) { - setHistoryLoadingSessionId(session.id); - } - - // Hydrate existing events from persistence - const hydrateEvents = async () => { - const allEvents: SessionEvent[] = []; - let cursor: string | undefined; - while (true) { - const page = await getClient().getEvents({ - sessionId: session.id, - cursor, - limit: 250, - }); - allEvents.push(...page.items); - if (!page.nextCursor) break; - cursor = page.nextCursor; - } - sessionEventsCacheRef.current.set(session.id, allEvents); - if (!isCurrentSubscription()) return; - setEvents((prev) => (areEventsEqualById(prev, allEvents) ? prev : allEvents)); - setHistoryLoadingSessionId((current) => (current === session.id ? null : current)); - }; - hydrateEvents().catch((error) => { - console.error("Failed to hydrate events:", error); - if (isCurrentSubscription()) { - setHistoryLoadingSessionId((current) => (current === session.id ? null : current)); - } - }); - - // Subscribe to new events - const unsub = session.onEvent((event) => { - if (!isCurrentSubscription()) return; - setEvents((prev) => { - if (prev.some((existing) => existing.id === event.id)) { + const pushErrorToast = useCallback( + (error: unknown, fallback: string) => { + const messageText = getErrorMessage(error, fallback).trim() || fallback; + const toastId = toastIdRef.current++; + setErrorToasts((prev) => { + if (prev.some((toast) => toast.message === messageText)) { return prev; } - const next = [...prev, event]; - sessionEventsCacheRef.current.set(session.id, next); - return next; + return [...prev, { id: toastId, message: messageText }].slice(-MAX_ERROR_TOASTS); }); - }); - eventUnsubRef.current = unsub; + scheduleErrorToastDismiss(toastId, ERROR_TOAST_MS); + }, + [scheduleErrorToastDismiss], + ); - // Subscribe to permission requests - if (permissionUnsubRef.current) { - permissionUnsubRef.current(); - permissionUnsubRef.current = null; - } - const permUnsub = session.onPermissionRequest((request: SessionPermissionRequest) => { - if (!isCurrentSubscription()) return; - pendingPermissionsRef.current.set(request.id, request); - if (request.toolCall?.toolCallId) { - permissionToolCallToIdRef.current.set(request.toolCall.toolCallId, request.id); + // Subscribe to events for the current active session + const subscribeToSession = useCallback( + (session: Session) => { + const generation = ++subscriptionGenerationRef.current; + const isCurrentSubscription = (): boolean => + subscriptionGenerationRef.current === generation && activeSessionRef.current?.id === session.id && selectedSessionIdRef.current === session.id; + + // Unsubscribe from previous + if (eventUnsubRef.current) { + eventUnsubRef.current(); + eventUnsubRef.current = null; } - setPendingPermissionIds((prev) => new Set([...prev, request.id])); - }); - permissionUnsubRef.current = permUnsub; - }, [getClient]); - const handlePermissionReply = useCallback(async (permissionId: string, reply: PermissionReply) => { - const session = activeSessionRef.current; - if (!session) return; - try { - await session.respondPermission(permissionId, reply); - const request = pendingPermissionsRef.current.get(permissionId); - const selectedOption = request?.options.find((o) => - reply === "always" ? o.kind === "allow_always" : - reply === "once" ? o.kind === "allow_once" : - o.kind === "reject_once" || o.kind === "reject_always" - ); - setResolvedPermissions((prev) => new Map([...prev, [permissionId, selectedOption?.optionId ?? reply]])); - setPendingPermissionIds((prev) => { - const next = new Set(prev); - next.delete(permissionId); - return next; + activeSessionRef.current = session; + const cachedEvents = sessionEventsCacheRef.current.get(session.id); + if (cachedEvents && isCurrentSubscription()) { + setEvents(cachedEvents); + setHistoryLoadingSessionId((current) => (current === session.id ? null : current)); + } else if (isCurrentSubscription()) { + setHistoryLoadingSessionId(session.id); + } + + // Hydrate existing events from persistence + const hydrateEvents = async () => { + const allEvents: SessionEvent[] = []; + let cursor: string | undefined; + while (true) { + const page = await getClient().getEvents({ + sessionId: session.id, + cursor, + limit: 250, + }); + allEvents.push(...page.items); + if (!page.nextCursor) break; + cursor = page.nextCursor; + } + sessionEventsCacheRef.current.set(session.id, allEvents); + if (!isCurrentSubscription()) return; + setEvents((prev) => (areEventsEqualById(prev, allEvents) ? prev : allEvents)); + setHistoryLoadingSessionId((current) => (current === session.id ? null : current)); + }; + hydrateEvents().catch((error) => { + console.error("Failed to hydrate events:", error); + if (isCurrentSubscription()) { + setHistoryLoadingSessionId((current) => (current === session.id ? null : current)); + } }); - } catch (error) { - pushErrorToast(error, "Failed to respond to permission request"); - } - }, [pushErrorToast]); + + // Subscribe to new events + const unsub = session.onEvent((event) => { + if (!isCurrentSubscription()) return; + setEvents((prev) => { + if (prev.some((existing) => existing.id === event.id)) { + return prev; + } + const next = [...prev, event]; + sessionEventsCacheRef.current.set(session.id, next); + return next; + }); + }); + eventUnsubRef.current = unsub; + + // Subscribe to permission requests + if (permissionUnsubRef.current) { + permissionUnsubRef.current(); + permissionUnsubRef.current = null; + } + const permUnsub = session.onPermissionRequest((request: SessionPermissionRequest) => { + if (!isCurrentSubscription()) return; + pendingPermissionsRef.current.set(request.id, request); + if (request.toolCall?.toolCallId) { + permissionToolCallToIdRef.current.set(request.toolCall.toolCallId, request.id); + } + setPendingPermissionIds((prev) => new Set([...prev, request.id])); + }); + permissionUnsubRef.current = permUnsub; + }, + [getClient], + ); + + const handlePermissionReply = useCallback( + async (permissionId: string, reply: PermissionReply) => { + const session = activeSessionRef.current; + if (!session) return; + try { + await session.respondPermission(permissionId, reply); + const request = pendingPermissionsRef.current.get(permissionId); + const selectedOption = request?.options.find((o) => + reply === "always" ? o.kind === "allow_always" : reply === "once" ? o.kind === "allow_once" : o.kind === "reject_once" || o.kind === "reject_always", + ); + setResolvedPermissions((prev) => new Map([...prev, [permissionId, selectedOption?.optionId ?? reply]])); + setPendingPermissionIds((prev) => { + const next = new Set(prev); + next.delete(permissionId); + return next; + }); + } catch (error) { + pushErrorToast(error, "Failed to respond to permission request"); + } + }, + [pushErrorToast], + ); const connectToDaemon = async (reportError: boolean, overrideEndpoint?: string) => { setConnecting(true); @@ -689,19 +692,20 @@ export default function App() { } }; - const loadAgentConfig = useCallback(async (targetAgentId: string) => { - console.log("[loadAgentConfig] Loading config for agent:", targetAgentId); - try { - const info = await getClient().getAgent(targetAgentId, { config: true }); - console.log("[loadAgentConfig] Got agent info:", info); - setAgents((prev) => - prev.map((a) => (a.id === targetAgentId ? { ...a, configOptions: info.configOptions, configError: info.configError } : a)) - ); - } catch (error) { - console.error("[loadAgentConfig] Failed to load config:", error); - // Config loading is best-effort; the menu still works without it. - } - }, [getClient]); + const loadAgentConfig = useCallback( + async (targetAgentId: string) => { + console.log("[loadAgentConfig] Loading config for agent:", targetAgentId); + try { + const info = await getClient().getAgent(targetAgentId, { config: true }); + console.log("[loadAgentConfig] Got agent info:", info); + setAgents((prev) => prev.map((a) => (a.id === targetAgentId ? { ...a, configOptions: info.configOptions, configError: info.configError } : a))); + } catch (error) { + console.error("[loadAgentConfig] Failed to load config:", error); + // Config loading is best-effort; the menu still works without it. + } + }, + [getClient], + ); const fetchSessions = async () => { setSessionsLoading(true); @@ -743,13 +747,7 @@ export default function App() { // If the server already considers the session gone, still archive in local UI. console.warn("Destroy session returned an error while archiving:", error); } - setSessions((prev) => - prev.map((session) => - session.sessionId === targetSessionId - ? { ...session, archived: true, ended: true } - : session - ) - ); + setSessions((prev) => prev.map((session) => (session.sessionId === targetSessionId ? { ...session, archived: true, ended: true } : session))); setSessionModelById((prev) => { if (!(targetSessionId in prev)) return prev; const next = { ...prev }; @@ -764,11 +762,7 @@ export default function App() { const unarchiveSession = async (targetSessionId: string) => { unarchiveSessionId(targetSessionId); - setSessions((prev) => - prev.map((session) => - session.sessionId === targetSessionId ? { ...session, archived: false } : session - ) - ); + setSessions((prev) => prev.map((session) => (session.sessionId === targetSessionId ? { ...session, archived: false } : session))); await fetchSessions(); }; @@ -883,7 +877,7 @@ export default function App() { try { const agentInfo = agents.find((agent) => agent.id === nextAgentId); const modelOption = ((agentInfo?.configOptions ?? []) as ConfigOption[]).find( - (opt) => opt.category === "model" && opt.type === "select" && typeof opt.id === "string" + (opt) => opt.category === "model" && opt.type === "select" && typeof opt.id === "string", ); if (modelOption && config.model !== modelOption.currentValue) { await session.rawSend("session/set_config_option", { @@ -951,9 +945,12 @@ export default function App() { }; if (navigator.clipboard && window.isSecureContext) { - navigator.clipboard.writeText(text).then(onSuccess).catch(() => { - fallbackCopy(text, onSuccess); - }); + navigator.clipboard + .writeText(text) + .then(onSuccess) + .catch(() => { + fallbackCopy(text, onSuccess); + }); } else { fallbackCopy(text, onSuccess); } @@ -1222,18 +1219,15 @@ export default function App() { } if (event.sender === "agent" && method === "session/request_permission") { - const params = payload.params as { - options?: Array<{ optionId: string; name: string; kind: string }>; - toolCall?: { title?: string; toolCallId?: string; description?: string }; - } | undefined; + const params = payload.params as + | { + options?: Array<{ optionId: string; name: string; kind: string }>; + toolCall?: { title?: string; toolCallId?: string; description?: string }; + } + | undefined; const toolCallId = params?.toolCall?.toolCallId; - const sdkPermissionId = toolCallId - ? permissionToolCallToIdRef.current.get(toolCallId) - : undefined; - const permissionId = sdkPermissionId - ?? (typeof payload.id === "number" || typeof payload.id === "string" - ? String(payload.id) - : event.id); + const sdkPermissionId = toolCallId ? permissionToolCallToIdRef.current.get(toolCallId) : undefined; + const permissionId = sdkPermissionId ?? (typeof payload.id === "number" || typeof payload.id === "string" ? String(payload.id) : event.id); const options = (params?.options ?? []).map((o) => ({ optionId: o.optionId, name: o.name, @@ -1306,12 +1300,7 @@ export default function App() { const shouldIgnoreCreateNoise = (value: unknown): boolean => { if (Date.now() > createNoiseIgnoreUntilRef.current) return false; const message = getErrorMessage(value, "").trim().toLowerCase(); - return ( - message.length === 0 || - message === "request failed" || - message.includes("request failed") || - message.includes("unhandled promise rejection") - ); + return message.length === 0 || message === "request failed" || message.includes("request failed") || message.includes("unhandled promise rejection"); }; const handleWindowError = (event: ErrorEvent) => { @@ -1427,18 +1416,22 @@ export default function App() { const requestedSessionId = sessionId; resumeInFlightSessionIdRef.current = requestedSessionId; - getClient().resumeSession(requestedSessionId).then((session) => { - if (selectedSessionIdRef.current !== requestedSessionId) return; - subscribeToSession(session); - }).catch((error) => { - if (selectedSessionIdRef.current !== requestedSessionId) return; - setSessionError(getErrorMessage(error, "Unable to resume session")); - setHistoryLoadingSessionId((current) => (current === requestedSessionId ? null : current)); - }).finally(() => { - if (resumeInFlightSessionIdRef.current === requestedSessionId) { - resumeInFlightSessionIdRef.current = null; - } - }); + getClient() + .resumeSession(requestedSessionId) + .then((session) => { + if (selectedSessionIdRef.current !== requestedSessionId) return; + subscribeToSession(session); + }) + .catch((error) => { + if (selectedSessionIdRef.current !== requestedSessionId) return; + setSessionError(getErrorMessage(error, "Unable to resume session")); + setHistoryLoadingSessionId((current) => (current === requestedSessionId ? null : current)); + }) + .finally(() => { + if (resumeInFlightSessionIdRef.current === requestedSessionId) { + resumeInFlightSessionIdRef.current = null; + } + }); }, [connected, sessionId, sessions, getClient, subscribeToSession]); useEffect(() => { @@ -1458,9 +1451,7 @@ export default function App() { // If actively sending a prompt, show thinking if (sendingSessionId === sessionId) return true; // Check for in-progress tool calls - const hasInProgressTool = transcriptEntries.some( - (e) => e.kind === "tool" && e.toolStatus === "in_progress" - ); + const hasInProgressTool = transcriptEntries.some((e) => e.kind === "tool" && e.toolStatus === "in_progress"); if (hasInProgressTool) return true; // Check if last message was from user with no subsequent agent activity const lastUserMessageIndex = [...transcriptEntries].reverse().findIndex((e) => e.kind === "message" && e.role === "user"); @@ -1468,14 +1459,10 @@ export default function App() { // If user message is the very last entry, we're waiting for response if (lastUserMessageIndex === 0) return true; // Check if there's any agent response after the user message - const entriesAfterUser = transcriptEntries.slice(-(lastUserMessageIndex)); - const hasAgentResponse = entriesAfterUser.some( - (e) => e.kind === "message" && e.role === "assistant" - ); + const entriesAfterUser = transcriptEntries.slice(-lastUserMessageIndex); + const hasAgentResponse = entriesAfterUser.some((e) => e.kind === "message" && e.role === "assistant"); // If no assistant message after user, but there are completed tools, not thinking - const hasCompletedTools = entriesAfterUser.some( - (e) => e.kind === "tool" && (e.toolStatus === "completed" || e.toolStatus === "failed") - ); + const hasCompletedTools = entriesAfterUser.some((e) => e.kind === "tool" && (e.toolStatus === "completed" || e.toolStatus === "failed")); if (!hasAgentResponse && !hasCompletedTools) return true; return false; }, [sessionId, sessionEnded, transcriptEntries, sendingSessionId]); @@ -1560,20 +1547,21 @@ export default function App() { const update = params?.update as Record | undefined; if (update?.sessionUpdate !== "config_option_update") continue; - const category = (update.category as string | undefined) - ?? ((update.option as Record | undefined)?.category as string | undefined); + const category = (update.category as string | undefined) ?? ((update.option as Record | undefined)?.category as string | undefined); if (category && category !== "model") continue; - const optionId = (update.optionId as string | undefined) - ?? (update.configOptionId as string | undefined) - ?? ((update.option as Record | undefined)?.id as string | undefined); + const optionId = + (update.optionId as string | undefined) ?? + (update.configOptionId as string | undefined) ?? + ((update.option as Record | undefined)?.id as string | undefined); const seemsModelOption = !optionId || optionId.toLowerCase().includes("model"); if (!seemsModelOption) continue; - const candidate = (update.value as string | undefined) - ?? (update.currentValue as string | undefined) - ?? (update.selectedValue as string | undefined) - ?? (update.modelId as string | undefined); + const candidate = + (update.value as string | undefined) ?? + (update.currentValue as string | undefined) ?? + (update.selectedValue as string | undefined) ?? + (update.modelId as string | undefined); if (candidate) { latestModelId = candidate; } @@ -1594,9 +1582,7 @@ export default function App() { const optionId = params?.optionId as string | undefined; const seemsModelOption = category === "model" || (typeof optionId === "string" && optionId.toLowerCase().includes("model")); if (!seemsModelOption) continue; - const candidate = (params?.value as string | undefined) - ?? (params?.currentValue as string | undefined) - ?? (params?.modelId as string | undefined); + const candidate = (params?.value as string | undefined) ?? (params?.currentValue as string | undefined) ?? (params?.modelId as string | undefined); if (candidate) { latestModelId = candidate; } @@ -1608,18 +1594,14 @@ export default function App() { const modelPillLabel = useMemo(() => { const sessionModelId = - currentSessionModelId - ?? (sessionId ? sessionModelById[sessionId] : undefined) - ?? (sessionId ? defaultModelByAgent[agentId] : undefined); + currentSessionModelId ?? (sessionId ? sessionModelById[sessionId] : undefined) ?? (sessionId ? defaultModelByAgent[agentId] : undefined); if (!sessionModelId) return null; return sessionModelId; }, [agentId, currentSessionModelId, defaultModelByAgent, sessionId, sessionModelById]); useEffect(() => { if (!sessionId || !currentSessionModelId) return; - setSessionModelById((prev) => - prev[sessionId] === currentSessionModelId ? prev : { ...prev, [sessionId]: currentSessionModelId } - ); + setSessionModelById((prev) => (prev[sessionId] === currentSessionModelId ? prev : { ...prev, [sessionId]: currentSessionModelId })); }, [currentSessionModelId, sessionId]); useEffect(() => { @@ -1649,12 +1631,7 @@ export default function App() { onFocus={() => pauseErrorToastDismiss(toast.id)} onBlur={() => resumeErrorToastDismiss(toast.id)} > -
@@ -1690,7 +1667,7 @@ export default function App() {
- Sandbox Agent + Sandbox Agent {endpoint}
@@ -1699,11 +1676,15 @@ export default function App() { Docs - + + + Discord - + + + Issues

- Having trouble connecting? See the CORS documentation. + Having trouble connecting? See the{" "} + + CORS documentation + + .

diff --git a/frontend/packages/inspector/src/components/SessionCreateMenu.tsx b/frontend/packages/inspector/src/components/SessionCreateMenu.tsx index e952890..221a42b 100644 --- a/frontend/packages/inspector/src/components/SessionCreateMenu.tsx +++ b/frontend/packages/inspector/src/components/SessionCreateMenu.tsx @@ -18,7 +18,7 @@ const agentLabels: Record = { opencode: "OpenCode", amp: "Amp", pi: "Pi", - cursor: "Cursor" + cursor: "Cursor", }; const agentLogos: Record = { @@ -39,7 +39,7 @@ const SessionCreateMenu = ({ onCreateSession, onSelectAgent, open, - onClose + onClose, }: { agents: AgentInfo[]; agentsLoading: boolean; @@ -157,54 +157,45 @@ const SessionCreateMenu = ({
{agentsLoading &&
Loading agents...
} {agentsError &&
{agentsError}
} - {!agentsLoading && !agentsError && agents.length === 0 && ( -
No agents available.
- )} - {!agentsLoading && !agentsError && (() => { - const codingAgents = agents.filter((a) => a.id !== "mock"); - const mockAgent = agents.find((a) => a.id === "mock"); - return ( - <> - {codingAgents.map((agent) => ( - - ))} - {mockAgent && ( - <> -
- - - )} - - ); - })()} + ))} + {mockAgent && ( + <> +
+ + + )} + + ); + })()}
); } @@ -237,12 +228,7 @@ const SessionCreateMenu = ({ autoFocus /> ) : ( - handleModelSelectChange(e.target.value)} title="Model"> {activeModels.map((m) => (