From d2346bafb3938fcc91ba0c0e9b63c1606d3b47c0 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 10 Mar 2026 23:03:11 -0700 Subject: [PATCH] Configure lefthook formatter checks (#231) * Add lefthook formatter checks * Fix SDK mode hydration * Stabilize SDK mode integration test --- .github/workflows/ci.yaml | 31 + .mcp.json | 6 +- biome.json | 7 + docs/docs.json | 246 ++-- docs/openapi.json | 368 ++---- examples/boxlite/src/index.ts | 19 +- examples/boxlite/src/setup-image.ts | 14 +- examples/cloudflare/frontend/App.tsx | 9 +- examples/cloudflare/frontend/main.tsx | 2 +- examples/cloudflare/src/prompt-endpoint.ts | 102 +- examples/computesdk/src/computesdk.ts | 32 +- examples/computesdk/tests/computesdk.test.ts | 9 +- examples/daytona/src/daytona-with-snapshot.ts | 20 +- examples/daytona/src/index.ts | 20 +- examples/daytona/tests/daytona.test.ts | 2 +- examples/docker/src/index.ts | 32 +- examples/docker/tests/docker.test.ts | 2 +- examples/e2b/tests/e2b.test.ts | 2 +- examples/file-system/src/index.ts | 10 +- examples/mcp-custom-tool/src/index.ts | 24 +- examples/mcp/src/index.ts | 23 +- examples/permissions/src/index.ts | 24 +- examples/persist-memory/src/index.ts | 5 +- examples/persist-postgres/src/index.ts | 51 +- examples/persist-sqlite/src/index.ts | 5 +- examples/shared/src/docker.ts | 45 +- examples/shared/src/sandbox-agent-client.ts | 15 +- examples/skills-custom-tool/src/index.ts | 20 +- examples/skills/src/index.ts | 5 +- examples/vercel/tests/vercel.test.ts | 2 +- examples/vercel/tsconfig.json | 14 +- .../_scripts/generate-actor-migrations.ts | 5 +- .../packages/backend/src/actors/context.ts | 7 +- .../packages/backend/src/actors/handles.ts | 73 +- .../src/actors/handoff-status-sync/index.ts | 16 +- .../src/actors/handoff/db/drizzle.config.ts | 1 - .../db/drizzle/meta/0000_snapshot.json | 2 +- .../db/drizzle/meta/0001_snapshot.json | 2 +- .../db/drizzle/meta/0002_snapshot.json | 2 +- .../src/actors/handoff/db/migrations.ts | 64 +- .../backend/src/actors/handoff/index.ts | 118 +- .../backend/src/actors/handoff/workbench.ts | 121 +- .../src/actors/handoff/workflow/commands.ts | 72 +- .../src/actors/handoff/workflow/common.ts | 33 +- .../src/actors/handoff/workflow/index.ts | 50 +- .../src/actors/handoff/workflow/init.ts | 206 ++-- .../src/actors/handoff/workflow/push.ts | 14 +- .../src/actors/handoff/workflow/queue.ts | 2 +- .../actors/handoff/workflow/status-sync.ts | 43 +- .../src/actors/history/db/drizzle.config.ts | 1 - .../db/drizzle/meta/0000_snapshot.json | 2 +- .../history/db/drizzle/meta/_journal.json | 2 +- .../src/actors/history/db/migrations.ts | 16 +- .../backend/src/actors/history/index.ts | 16 +- factory/packages/backend/src/actors/index.ts | 4 +- factory/packages/backend/src/actors/keys.ts | 14 +- .../packages/backend/src/actors/logging.ts | 8 +- .../packages/backend/src/actors/polling.ts | 105 +- .../src/actors/project-branch-sync/index.ts | 29 +- .../src/actors/project-pr-sync/index.ts | 14 +- .../backend/src/actors/project/actions.ts | 297 ++--- .../src/actors/project/db/drizzle.config.ts | 1 - .../db/drizzle/meta/0000_snapshot.json | 2 +- .../db/drizzle/meta/0001_snapshot.json | 2 +- .../db/drizzle/meta/0002_snapshot.json | 2 +- .../src/actors/project/db/migrations.ts | 40 +- .../backend/src/actors/project/db/schema.ts | 2 +- .../backend/src/actors/project/index.ts | 2 +- .../sandbox-instance/db/drizzle.config.ts | 1 - .../db/drizzle/meta/0000_snapshot.json | 2 +- .../actors/sandbox-instance/db/migrations.ts | 24 +- .../src/actors/sandbox-instance/index.ts | 168 +-- .../src/actors/sandbox-instance/persist.ts | 44 +- .../backend/src/actors/workspace/actions.ts | 100 +- .../src/actors/workspace/db/drizzle.config.ts | 1 - .../db/drizzle/meta/0000_snapshot.json | 2 +- .../db/drizzle/meta/0001_snapshot.json | 2 +- .../src/actors/workspace/db/migrations.ts | 32 +- .../backend/src/actors/workspace/index.ts | 2 +- .../packages/backend/src/db/actor-sqlite.ts | 7 +- factory/packages/backend/src/driver.ts | 20 +- factory/packages/backend/src/index.ts | 16 +- .../src/integrations/daytona/client.ts | 4 +- .../src/integrations/git-spice/index.ts | 61 +- .../backend/src/integrations/git/index.ts | 96 +- .../backend/src/integrations/github/index.ts | 115 +- .../src/integrations/graphite/index.ts | 41 +- .../src/integrations/sandbox-agent/client.ts | 28 +- .../backend/src/notifications/backends.ts | 4 +- .../backend/src/notifications/index.ts | 6 +- .../src/notifications/state-tracker.ts | 9 +- .../backend/src/providers/daytona/index.ts | 96 +- .../packages/backend/src/providers/index.ts | 28 +- .../backend/src/providers/local/index.ts | 26 +- .../backend/src/services/create-flow.ts | 32 +- .../backend/src/services/openhandoff-paths.ts | 7 +- factory/packages/backend/src/services/tmux.ts | 11 +- .../packages/backend/test/create-flow.test.ts | 12 +- .../backend/test/daytona-provider.test.ts | 22 +- .../packages/backend/test/git-spice.test.ts | 35 +- .../backend/test/helpers/test-context.ts | 10 +- .../backend/test/helpers/test-driver.ts | 16 +- factory/packages/backend/test/keys.test.ts | 4 +- .../backend/test/malformed-uri.test.ts | 6 +- .../packages/backend/test/providers.test.ts | 8 +- .../backend/test/repo-normalize.test.ts | 20 +- .../packages/backend/test/stack-model.test.ts | 14 +- .../backend/test/workspace-isolation.test.ts | 21 +- .../packages/backend/tmp-decode-actors.mjs | 20 +- factory/packages/backend/tmp-inspect-deep.mjs | 28 +- .../packages/backend/tmp-inspect-stuck.mjs | 20 +- factory/packages/backend/vitest.config.ts | 3 +- factory/packages/cli/src/backend/manager.ts | 39 +- factory/packages/cli/src/build-id.ts | 6 +- factory/packages/cli/src/index.ts | 65 +- factory/packages/cli/src/task-editor.ts | 6 +- factory/packages/cli/src/theme.ts | 55 +- factory/packages/cli/src/tmux.ts | 70 +- factory/packages/cli/src/tui.ts | 90 +- .../packages/cli/test/backend-manager.test.ts | 34 +- factory/packages/cli/test/task-editor.test.ts | 1 - factory/packages/cli/test/theme.test.ts | 18 +- factory/packages/cli/test/tui-format.test.ts | 12 +- .../cli/test/workspace-config.test.ts | 6 +- factory/packages/cli/tsup.config.ts | 7 +- factory/packages/client/src/backend-client.ts | 225 ++-- factory/packages/client/src/keys.ts | 14 +- .../client/src/mock/workbench-client.ts | 12 +- .../client/src/remote/workbench-client.ts | 4 +- factory/packages/client/src/view-model.ts | 24 +- .../packages/client/src/workbench-client.ts | 4 +- .../packages/client/src/workbench-model.ts | 25 +- .../test/e2e/full-integration-e2e.test.ts | 145 +-- .../client/test/e2e/github-pr-e2e.test.ts | 349 +++--- .../client/test/e2e/workbench-e2e.test.ts | 319 +++-- .../test/e2e/workbench-load-e2e.test.ts | 250 ++-- factory/packages/client/test/keys.test.ts | 4 +- .../packages/client/test/view-model.test.ts | 19 +- .../packages/frontend-errors/src/client.ts | 8 +- .../packages/frontend-errors/src/router.ts | 22 +- .../packages/frontend-errors/src/script.ts | 4 +- factory/packages/frontend-errors/src/types.ts | 8 +- factory/packages/frontend-errors/src/vite.ts | 4 +- .../frontend-errors/test/router.test.ts | 2 +- .../packages/frontend-errors/vitest.config.ts | 4 +- factory/packages/frontend/src/app/router.tsx | 34 +- factory/packages/frontend/src/app/theme.ts | 14 +- .../frontend/src/components/mock-layout.tsx | 26 +- .../mock-layout/history-minimap.tsx | 8 +- .../components/mock-layout/message-list.tsx | 20 +- .../components/mock-layout/model-picker.tsx | 10 +- .../mock-layout/prompt-composer.tsx | 13 +- .../components/mock-layout/right-sidebar.tsx | 27 +- .../src/components/mock-layout/sidebar.tsx | 197 ++- .../src/components/mock-layout/ui.tsx | 10 +- .../src/components/workspace-dashboard.tsx | 123 +- .../src/features/handoffs/model.test.ts | 2 +- .../src/features/sessions/model.test.ts | 63 +- .../frontend/src/features/sessions/model.ts | 6 +- factory/packages/frontend/src/lib/env.ts | 7 +- factory/packages/frontend/src/main.tsx | 2 +- factory/packages/frontend/src/styles.css | 6 +- factory/packages/frontend/vite.config.ts | 4 +- factory/packages/shared/src/config.ts | 89 +- factory/packages/shared/src/contracts.ts | 52 +- factory/packages/shared/src/workspace.ts | 5 +- .../packages/shared/test/workspace.test.ts | 8 +- frontend/packages/inspector/src/App.tsx | 633 +++++----- .../src/components/ConnectScreen.tsx | 46 +- .../src/components/SessionCreateMenu.tsx | 97 +- .../src/components/SessionSidebar.tsx | 63 +- .../agents/FeatureCoverageBadges.tsx | 4 +- .../src/components/chat/ChatPanel.tsx | 27 +- .../components/chat/InspectorConversation.tsx | 2 +- .../src/components/debug/AgentsTab.tsx | 31 +- .../src/components/debug/DebugPanel.tsx | 38 +- .../src/components/debug/EventsTab.tsx | 44 +- .../inspector/src/components/debug/McpTab.tsx | 89 +- .../src/components/debug/ProcessRunTab.tsx | 18 +- .../src/components/debug/ProcessesTab.tsx | 159 ++- .../src/components/debug/RequestLogTab.tsx | 22 +- .../src/components/debug/SkillsTab.tsx | 119 +- .../packages/inspector/src/lib/permissions.ts | 2 +- frontend/packages/inspector/src/main.tsx | 2 +- .../packages/inspector/src/types/agents.ts | 2 +- frontend/packages/inspector/src/utils/http.ts | 4 +- frontend/packages/website/astro.config.mjs | 18 +- .../packages/website/src/components/FAQ.tsx | 43 +- .../website/src/components/FeatureGrid.tsx | 17 +- .../website/src/components/Footer.tsx | 77 +- .../website/src/components/GetStarted.tsx | 8 +- .../website/src/components/GitHubStars.tsx | 33 +- .../packages/website/src/components/Hero.tsx | 167 ++- .../website/src/components/Inspector.tsx | 10 +- .../website/src/components/Integrations.tsx | 16 +- .../website/src/components/Navigation.tsx | 36 +- .../website/src/components/PainPoints.tsx | 34 +- .../website/src/components/ProblemsSolved.tsx | 33 +- .../website/src/components/ui/Badge.tsx | 6 +- .../website/src/components/ui/Button.tsx | 29 +- .../website/src/components/ui/CopyButton.tsx | 18 +- .../website/src/components/ui/FeatureIcon.tsx | 14 +- .../packages/website/src/layouts/Layout.astro | 31 +- .../packages/website/src/pages/index.astro | 18 +- frontend/packages/website/tailwind.config.mjs | 72 +- frontend/packages/website/vite.config.ts | 4 +- lefthook.yml | 11 + package.json | 5 +- pnpm-lock.yaml | 497 ++++---- research/opencode-compat/capture-native.ts | 16 +- .../opencode-compat/capture-sandbox-agent.ts | 29 +- .../snapshots/native/all-events.json | 2 +- .../snapshots/native/message-1-response.json | 2 +- .../snapshots/native/message-2-response.json | 2 +- .../snapshots/native/messages-after-1.json | 2 +- .../snapshots/native/messages-after-2.json | 2 +- .../snapshots/native/metadata-agent.json | 2 +- .../snapshots/native/metadata-config.json | 2 +- .../snapshots/native/metadata-providers.json | 222 +--- .../snapshots/native/session-create.json | 2 +- .../snapshots/native/session-details.json | 2 +- .../snapshots/native/session-events.json | 2 +- .../snapshots/native/session-status.json | 2 +- .../snapshots/sandbox-agent/all-events.json | 2 +- .../sandbox-agent/messages-after-1.json | 2 +- .../sandbox-agent/messages-after-2.json | 2 +- .../sandbox-agent/metadata-agent.json | 2 +- .../sandbox-agent/metadata-config.json | 2 +- .../sandbox-agent/session-create.json | 2 +- .../sandbox-agent/session-details.json | 2 +- .../sandbox-agent/session-events.json | 2 +- .../sandbox-agent/session-status.json | 2 +- .../artifacts/json-schema/pi.json | 15 +- resources/agent-schemas/src/pi.ts | 17 +- .../artifacts/json-schema/ui-message.json | 141 +-- resources/vercel-ai-sdk-schemas/src/index.ts | 34 +- scripts/agent-configs/dump.ts | 95 +- scripts/agent-configs/package.json | 34 +- scripts/agent-configs/tsconfig.json | 12 +- scripts/release/build-artifacts.ts | 35 +- scripts/release/docker.ts | 68 +- scripts/release/git.ts | 122 +- scripts/release/main.ts | 974 +++++++-------- scripts/release/package.json | 46 +- scripts/release/promote-artifacts.ts | 154 ++- scripts/release/sdk.ts | 607 +++++----- scripts/release/tsconfig.json | 22 +- scripts/release/update_version.ts | 109 +- scripts/release/utils.ts | 265 ++-- scripts/sandbox-testing/package.json | 38 +- scripts/sandbox-testing/test-sandbox.ts | 1065 +++++++++-------- scripts/skill-generator/generate.js | 26 +- sdks/acp-http-client/src/index.ts | 12 +- sdks/acp-http-client/tests/smoke.test.ts | 15 +- sdks/cli-shared/src/index.ts | 134 +-- sdks/cli/tests/launcher.test.ts | 5 +- sdks/persist-indexeddb/src/index.ts | 24 +- .../tests/integration.test.ts | 15 +- sdks/persist-indexeddb/vitest.config.ts | 2 +- sdks/persist-postgres/src/index.ts | 26 +- .../tests/integration.test.ts | 15 +- sdks/persist-rivet/src/index.ts | 18 +- sdks/persist-sqlite/src/index.ts | 27 +- sdks/persist-sqlite/tests/integration.test.ts | 15 +- sdks/react/src/AgentConversation.tsx | 14 +- sdks/react/src/AgentTranscript.tsx | 48 +- sdks/react/src/ChatComposer.tsx | 10 +- sdks/react/src/ProcessTerminal.tsx | 15 +- .../scripts/patch-openapi-types.mjs | 6 +- sdks/typescript/src/client.ts | 310 ++--- sdks/typescript/src/generated/openapi.ts | 80 +- sdks/typescript/src/index.ts | 4 +- sdks/typescript/src/spawn.ts | 32 +- sdks/typescript/src/types.ts | 35 +- sdks/typescript/tests/helpers/mock-agent.ts | 10 +- sdks/typescript/tests/integration.test.ts | 77 +- .../tests/opencode-compat/events.test.ts | 49 +- .../tests/opencode-compat/helpers/spawn.ts | 18 +- .../tests/opencode-compat/models.test.ts | 4 +- .../tests/opencode-compat/permissions.test.ts | 26 +- .../tests/opencode-compat/real-agent.test.ts | 32 +- .../tests/opencode-compat/session.test.ts | 15 +- 282 files changed, 5840 insertions(+), 8399 deletions(-) create mode 100644 biome.json create mode 100644 lefthook.yml 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 b399f74..262f639 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() {
))} @@ -161,12 +157,7 @@ export const PromptComposer = memo(function PromptComposer({ 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..9a02f96 100644 --- a/factory/packages/frontend/src/components/mock-layout/sidebar.tsx +++ b/factory/packages/frontend/src/components/mock-layout/sidebar.tsx @@ -4,14 +4,7 @@ import { LabelSmall, LabelXSmall } from "baseui/typography"; import { CloudUpload, GitPullRequestDraft, 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"; export const Sidebar = memo(function Sidebar({ projects, @@ -91,106 +84,104 @@ export const Sidebar = memo(function Sidebar({ > {project.label} - - {formatRelativeAge(project.updatedAtMs)} - + {formatRelativeAge(project.updatedAtMs)}
{project.handoffs.slice(0, visibleCount).map((handoff) => { - const isActive = handoff.id === activeId; - const isDim = handoff.status === "archived"; - const isRunning = handoff.tabs.some((tab) => tab.status === "running"); - const hasUnread = handoff.tabs.some((tab) => tab.unread); - const isDraft = handoff.pullRequest == null || handoff.pullRequest.status === "draft"; - const totalAdded = handoff.fileChanges.reduce((sum, file) => sum + file.added, 0); - const totalRemoved = handoff.fileChanges.reduce((sum, file) => sum + file.removed, 0); - const hasDiffs = totalAdded > 0 || totalRemoved > 0; + const isActive = handoff.id === activeId; + const isDim = handoff.status === "archived"; + const isRunning = handoff.tabs.some((tab) => tab.status === "running"); + const hasUnread = handoff.tabs.some((tab) => tab.unread); + const isDraft = handoff.pullRequest == null || handoff.pullRequest.status === "draft"; + const totalAdded = handoff.fileChanges.reduce((sum, file) => sum + file.added, 0); + const totalRemoved = handoff.fileChanges.reduce((sum, file) => sum + file.removed, 0); + const hasDiffs = totalAdded > 0 || totalRemoved > 0; - return ( -
onSelect(handoff.id)} - onContextMenu={(event) => - contextMenu.open(event, [ - { label: "Rename handoff", onClick: () => onRenameHandoff(handoff.id) }, - { label: "Rename branch", onClick: () => onRenameBranch(handoff.id) }, - { label: "Mark as unread", onClick: () => onMarkUnread(handoff.id) }, - ]) - } - className={css({ - padding: "12px", - borderRadius: "8px", - border: isActive ? "1px solid rgba(255, 255, 255, 0.2)" : "1px solid transparent", - backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent", - cursor: "pointer", - transition: "all 200ms ease", - ":hover": { - backgroundColor: "rgba(255, 255, 255, 0.06)", - borderColor: theme.colors.borderOpaque, - }, - })} - > -
-
- -
- - {handoff.title} - - {hasDiffs ? ( -
- +{totalAdded} - -{totalRemoved} + return ( +
onSelect(handoff.id)} + onContextMenu={(event) => + contextMenu.open(event, [ + { label: "Rename handoff", onClick: () => onRenameHandoff(handoff.id) }, + { label: "Rename branch", onClick: () => onRenameBranch(handoff.id) }, + { label: "Mark as unread", onClick: () => onMarkUnread(handoff.id) }, + ]) + } + className={css({ + padding: "12px", + borderRadius: "8px", + border: isActive ? "1px solid rgba(255, 255, 255, 0.2)" : "1px solid transparent", + backgroundColor: isActive ? "rgba(255, 255, 255, 0.06)" : "transparent", + cursor: "pointer", + transition: "all 200ms ease", + ":hover": { + backgroundColor: "rgba(255, 255, 255, 0.06)", + borderColor: theme.colors.borderOpaque, + }, + })} + > +
+
+ +
+ + {handoff.title} + + {hasDiffs ? ( +
+ +{totalAdded} + -{totalRemoved} +
+ ) : null} +
+
+ + {handoff.repoName} + + {handoff.pullRequest != null ? ( + + + #{handoff.pullRequest.number} + + {handoff.pullRequest.status === "draft" ? : null} + + ) : ( + + )} + + {formatRelativeAge(handoff.updatedAtMs)} + +
- ) : null} -
-
- - {handoff.repoName} - - {handoff.pullRequest != null ? ( - - - #{handoff.pullRequest.number} - - {handoff.pullRequest.status === "draft" ? : null} - - ) : ( - - )} - - {formatRelativeAge(handoff.updatedAtMs)} - -
-
- ); + ); })} {hiddenCount > 0 ? ( diff --git a/factory/packages/frontend/src/components/mock-layout/ui.tsx b/factory/packages/frontend/src/components/mock-layout/ui.tsx index 96da758..e517410 100644 --- a/factory/packages/frontend/src/components/mock-layout/ui.tsx +++ b/factory/packages/frontend/src/components/mock-layout/ui.tsx @@ -129,7 +129,10 @@ export const HandoffIndicator = memo(function HandoffIndicator({ const ClaudeIcon = memo(function ClaudeIcon({ size = 14 }: { size?: number }) { return ( - + ); }); @@ -137,7 +140,10 @@ const ClaudeIcon = memo(function ClaudeIcon({ size = 14 }: { size?: number }) { const OpenAIIcon = memo(function OpenAIIcon({ size = 14 }: { size?: number }) { return ( - + ); }); diff --git a/factory/packages/frontend/src/components/workspace-dashboard.tsx b/factory/packages/frontend/src/components/workspace-dashboard.tsx index 420e27d..97002c8 100644 --- a/factory/packages/frontend/src/components/workspace-dashboard.tsx +++ b/factory/packages/frontend/src/components/workspace-dashboard.tsx @@ -137,7 +137,7 @@ function branchTestIdToken(value: string): string { function useSessionEvents( handoff: HandoffRecord | null, - sessionId: string | null + sessionId: string | null, ): ReturnType> { return useQuery({ queryKey: ["workspace", handoff?.workspaceId ?? "", "session", handoff?.handoffId ?? "", sessionId ?? ""], @@ -147,15 +147,10 @@ function useSessionEvents( if (!handoff?.activeSandboxId || !sessionId) { return { items: [] }; } - return backendClient.listSandboxSessionEvents( - handoff.workspaceId, - handoff.providerId, - handoff.activeSandboxId, - { - sessionId, - limit: 120, - } - ); + return backendClient.listSandboxSessionEvents(handoff.workspaceId, handoff.providerId, handoff.activeSandboxId, { + sessionId, + limit: 120, + }); }, }); } @@ -343,19 +338,11 @@ function MetaRow({ label, value, mono = false }: { label: string; value: string; > {label} {mono ? ( - + {value} ) : ( - + {value} )} @@ -483,17 +470,14 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }); }, [repos, rows]); - const selectedSummary = useMemo( - () => rows.find((row) => row.handoffId === selectedHandoffId) ?? rows[0] ?? null, - [rows, selectedHandoffId] - ); + const selectedSummary = useMemo(() => rows.find((row) => row.handoffId === selectedHandoffId) ?? rows[0] ?? null, [rows, selectedHandoffId]); const selectedForSession = repoOverviewMode ? null : (handoffDetailQuery.data ?? null); const activeSandbox = useMemo(() => { if (!selectedForSession) return null; const byActive = selectedForSession.activeSandboxId - ? selectedForSession.sandboxes.find((sandbox) => sandbox.sandboxId === selectedForSession.activeSandboxId) ?? null + ? (selectedForSession.sandboxes.find((sandbox) => sandbox.sandboxId === selectedForSession.activeSandboxId) ?? null) : null; return byActive ?? selectedForSession.sandboxes[0] ?? null; }, [selectedForSession]); @@ -539,7 +523,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep handoffSessionId: selectedForSession?.activeSessionId ?? null, sessions: sessionRows, }), - [activeSessionId, selectedForSession?.activeSessionId, sessionRows] + [activeSessionId, selectedForSession?.activeSessionId, sessionRows], ); const resolvedSessionId = sessionSelection.sessionId; const staleSessionId = sessionSelection.staleSessionId; @@ -716,17 +700,14 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep const repoOptions = useMemo(() => repos.map((repo) => createOption({ id: repo.repoId, label: repo.remoteUrl })), [repos]); const selectedRepoOption = repoOptions.find((option) => option.id === createRepoId) ?? null; - const selectedAgentOption = useMemo( - () => createOption(AGENT_OPTIONS.find((option) => option.id === newAgentType) ?? AGENT_OPTIONS[0]!), - [newAgentType] - ); + const selectedAgentOption = useMemo(() => createOption(AGENT_OPTIONS.find((option) => option.id === newAgentType) ?? AGENT_OPTIONS[0]!), [newAgentType]); const selectedFilterOption = useMemo( () => createOption(FILTER_OPTIONS.find((option) => option.id === overviewFilter) ?? FILTER_OPTIONS[0]!), - [overviewFilter] + [overviewFilter], ); const sessionOptions = useMemo( () => sessionRows.map((session) => createOption({ id: session.id, label: `${session.id} (${session.status ?? "running"})` })), - [sessionRows] + [sessionRows], ); const selectedSessionOption = sessionOptions.find((option) => option.id === resolvedSessionId) ?? null; @@ -746,11 +727,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep if (!selectedOverviewBranch) { return filteredOverviewBranches[0] ?? null; } - return ( - filteredOverviewBranches.find((row) => row.branchName === selectedOverviewBranch) ?? - filteredOverviewBranches[0] ?? - null - ); + return filteredOverviewBranches.find((row) => row.branchName === selectedOverviewBranch) ?? filteredOverviewBranches[0] ?? null; }, [filteredOverviewBranches, selectedOverviewBranch]); useEffect(() => { @@ -799,7 +776,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep }, }, }), - [theme.colors.backgroundSecondary, theme.colors.borderOpaque] + [theme.colors.backgroundSecondary, theme.colors.borderOpaque], ); return ( @@ -815,14 +792,14 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep gap: theme.sizing.scale400, })} > -
+
Workspace
) : null} - {!handoffsQuery.isLoading && repoGroups.length === 0 ? ( - No repos or handoffs yet. Add a repo to start a workspace. - ) : null} + {!handoffsQuery.isLoading && repoGroups.length === 0 ? No repos or handoffs yet. Add a repo to start a workspace. : null} {repoGroups.map((group) => (
{formatRelativeAge(branch.updatedAt)} - - {branch.handoffId ? "handoff" : "unmapped"} - + {branch.handoffId ? "handoff" : "unmapped"} {branch.trackedInStack ? stack : null}
@@ -1295,9 +1268,7 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep ) : null} - - {branch.conflictsWithMain ? "conflict" : "ok"} - + {branch.conflictsWithMain ? "conflict" : "ok"}
@@ -1331,11 +1302,9 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep > - {selectedForSession ? selectedForSession.title ?? "Determining title..." : "No handoff selected"} + {selectedForSession ? (selectedForSession.title ?? "Determining title...") : "No handoff selected"} - {selectedForSession ? ( - {selectedForSession.status} - ) : null} + {selectedForSession ? {selectedForSession.status} : null} {selectedForSession && !resolvedSessionId ? ( @@ -1441,11 +1410,11 @@ export function WorkspaceDashboard({ workspaceId, selectedHandoffId, selectedRep ? selectedForSession.statusMessage ? `Sandbox unavailable: ${selectedForSession.statusMessage}` : "This handoff is still provisioning its sandbox." - : staleSessionId - ? `Session ${staleSessionId} is unavailable. Start a new session to continue.` - : resolvedSessionId - ? "No transcript events yet. Send a prompt to start this session." - : "No active session for this handoff."} + : staleSessionId + ? `Session ${staleSessionId} is unavailable. Start a new session to continue.` + : resolvedSessionId + ? "No transcript events yet. Send a prompt to start this session." + : "No active session for this handoff."} ) : null} @@ -1462,14 +1431,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 +1499,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 } > - + )} @@ -1711,9 +1668,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} 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..899bb40 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,12 +192,12 @@ 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; @@ -207,7 +205,7 @@ 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 +217,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 +232,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/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) => (