# Sandbox port URLs
+```
+
+### Docker Compose
+- File: `docker/docker-compose.local.yaml`
+- Project: `ii-agent-local`
+- Services: postgres (5433), redis (6379), minio (9000/9001), frontend (1420), backend (8000)
+- Backend mounts Docker socket for spawning sandbox containers
+
+---
+
+## Common Pitfalls
+
+1. **Transaction rollback**: If a multi-table UPDATE script errors on one table, ALL changes roll back (even previously "successful" ones within the same transaction)
+2. **JSONB vs varchar**: Always check column types before writing UPDATE statements with casts
+3. **app_kind determines rendering**: Agent sessions that only have chat_messages appear empty — must be classified as `app_kind='chat'`
+4. **E2B sandbox data is unrecoverable**: Any files/images that existed only in E2B sandboxes are permanently lost
+5. **Frontend axios baseURL**: Set to `VITE_API_URL` — all relative paths resolve against this
+6. **MinIO bucket auto-creation**: Must create `ii-agent` bucket manually on first setup
+7. **Alembic migrations**: Run at startup unless `II_AGENT_SKIP_MIGRATIONS=true`
+8. **Frontend URL rewriting**: `rewriteLocalhostUrl()` must be applied to ALL sandbox URLs displayed to users, not just `vscodeUrl`
diff --git a/docs/rebase-analysis/01-path-mapping.md b/docs/rebase-analysis/01-path-mapping.md
new file mode 100644
index 000000000..eb4276611
--- /dev/null
+++ b/docs/rebase-analysis/01-path-mapping.md
@@ -0,0 +1,130 @@
+# Path Mapping: develop → origin/main (DDD Restructure)
+
+## Package-Level Restructuring
+
+### src/ii_agent/ (Backend - MASSIVE restructure in #851)
+
+| Old Path (develop/topic) | New Path (origin/main) | Notes |
+|---|---|---|
+| `src/ii_agent/server/` | **REMOVED** - split into domain modules | Server monolith decomposed |
+| `src/ii_agent/server/api/` | Domain-specific `api/router.py` per module | e.g., `chat/api/`, `files/router.py` |
+| `src/ii_agent/server/app.py` | `src/ii_agent/app/` | App lifecycle extracted |
+| `src/ii_agent/server/socket/` | `src/ii_agent/realtime/` | WebSocket/SocketIO handlers |
+| `src/ii_agent/server/socket/command/query_handler.py` | `src/ii_agent/realtime/handlers/query.py` | |
+| `src/ii_agent/server/socket/command/awake_sandbox_handler.py` | `src/ii_agent/realtime/handlers/awake_sandbox.py` | |
+| `src/ii_agent/server/socket/command/sandbox_status_handler.py` | `src/ii_agent/realtime/handlers/sandbox_status.py` | |
+| `src/ii_agent/server/socket/chat_session.py` | `src/ii_agent/realtime/chat_session.py` | |
+| `src/ii_agent/server/socket/socketio.py` | `src/ii_agent/realtime/manager.py` | |
+| `src/ii_agent/server/chat/` | `src/ii_agent/chat/` | Chat domain extracted |
+| `src/ii_agent/server/chat/service.py` | `src/ii_agent/chat/application/chat_service.py` | |
+| `src/ii_agent/server/chat/context_manager.py` | `src/ii_agent/chat/application/context_service.py` | |
+| `src/ii_agent/server/chat/llm/anthropic/provider.py` | `src/ii_agent/chat/llm/anthropic/provider.py` | Similar path, different root |
+| `src/ii_agent/server/chat/llm/openai.py` | `src/ii_agent/chat/llm/openai.py` | |
+| `src/ii_agent/server/chat/router.py` | `src/ii_agent/chat/api/router.py` | |
+| `src/ii_agent/server/chat/tools/file_search.py` | `src/ii_agent/chat/application/tool_service.py` | Likely merged |
+| `src/ii_agent/server/api/files.py` | `src/ii_agent/files/router.py` | Files domain extracted |
+| `src/ii_agent/server/api/auth.py` | `src/ii_agent/auth/` | Auth domain extracted |
+| `src/ii_agent/server/api/sessions.py` | `src/ii_agent/sessions/` | Sessions domain extracted |
+| `src/ii_agent/server/services/agent_service.py` | `src/ii_agent/agents/` (application layer) | Agent domain extracted |
+| `src/ii_agent/server/services/file_service.py` | `src/ii_agent/files/service.py` | |
+| `src/ii_agent/server/services/sandbox_service.py` | `src/ii_agent/agents/sandboxes/service.py` | |
+| `src/ii_agent/server/llm_settings/` | `src/ii_agent/settings/llm/` | Settings domain |
+| `src/ii_agent/server/llm_settings/models.py` | `src/ii_agent/settings/llm/models.py` | |
+| `src/ii_agent/server/llm_settings/service.py` | `src/ii_agent/settings/llm/service.py` | |
+| `src/ii_agent/server/messages/` | `src/ii_agent/agents/hooks/` | Hooks pattern |
+| `src/ii_agent/server/models/messages.py` | Various domain schemas | Split per domain |
+| `src/ii_agent/server/slides/` | `src/ii_agent/content/` | Content domain |
+| `src/ii_agent/server/vectordb/` | **Needs investigation** | |
+| `src/ii_agent/controller/` | `src/ii_agent/agents/` | Agent runtime |
+| `src/ii_agent/controller/agent_controller.py` | `src/ii_agent/agents/agent.py` | Core agent loop |
+| `src/ii_agent/controller/state.py` | `src/ii_agent/agents/` area | State mgmt |
+| `src/ii_agent/controller/tool_manager.py` | `src/ii_agent/agents/factory/tool_manager.py` | |
+| `src/ii_agent/adapters/` | **REMOVED** | Absorbed into domain modules |
+| `src/ii_agent/adapters/sandbox_adapter.py` | `src/ii_agent/agents/sandboxes/` | |
+| `src/ii_agent/llm/` | `src/ii_agent/agents/models/` | LLM providers |
+| `src/ii_agent/llm/anthropic.py` | `src/ii_agent/agents/models/anthropic/claude.py` | |
+| `src/ii_agent/llm/openai.py` | `src/ii_agent/agents/models/openai/completions.py` | |
+| `src/ii_agent/prompts/` | `src/ii_agent/agents/prompts/` | |
+| `src/ii_agent/prompts/agent_prompts.py` | `src/ii_agent/agents/prompts/agent_prompts.py` | |
+| `src/ii_agent/prompts/system_prompt.py` | `src/ii_agent/agents/prompts/system_prompt.py` | |
+| `src/ii_agent/sandbox/ii_sandbox.py` | `src/ii_agent/agents/sandboxes/` | |
+| `src/ii_agent/storage/` | `src/ii_agent/core/storage/` | |
+| `src/ii_agent/storage/base.py` | `src/ii_agent/core/storage/providers/base.py` | |
+| `src/ii_agent/storage/factory.py` | `src/ii_agent/core/storage/` | |
+| `src/ii_agent/storage/gcs.py` | `src/ii_agent/core/storage/providers/gcs.py` | |
+| `src/ii_agent/storage/local.py` | `src/ii_agent/core/storage/providers/local.py` | **EXISTS in main!** |
+| `src/ii_agent/sub_agent/` | `src/ii_agent/agents/` | Merged into agents |
+| `src/ii_agent/core/config/ii_agent_config.py` | `src/ii_agent/core/config/settings.py` | Renamed |
+| `src/ii_agent/core/config/llm_config.py` | `src/ii_agent/core/config/llm_config.py` | Same path |
+| `src/ii_agent/core/event.py` | `src/ii_agent/realtime/events/` | Event system |
+| `src/ii_agent/core/client_host.py` | **NEW - no equivalent** | Topic-branch-only |
+| `src/ii_agent/db/manager.py` | `src/ii_agent/core/db/` | |
+| `src/ii_agent/utils/constants.py` | `src/ii_agent/core/` area | |
+| `src/ii_agent/cron/` | `src/ii_agent/workers/cron/` | |
+
+### src/ii_tool/ → src/ii_server/ (Tool Server renamed)
+
+| Old Path (develop/topic) | New Path (origin/main) | Notes |
+|---|---|---|
+| `src/ii_tool/` | `src/ii_server/` | Package renamed |
+| `src/ii_tool/browser/` | `src/ii_server/browser/` ? OR `src/ii_agent/agents/tools/browser/` | Split |
+| `src/ii_tool/integrations/` | Absorbed into `src/ii_agent/` domains | |
+| `src/ii_tool/integrations/image_generation/` | `src/ii_agent/content/media/` | |
+| `src/ii_tool/integrations/storage/` | `src/ii_agent/core/storage/` | |
+| `src/ii_tool/integrations/video_generation/` | `src/ii_agent/content/media/` | |
+| `src/ii_tool/interfaces/sandbox.py` | `src/ii_server/interfaces/sandbox.py` | |
+| `src/ii_tool/tools/dev/register_port.py` | `src/ii_agent/agents/tools/sandbox/register_port.py` | |
+| `src/ii_tool/tools/file_system/utils.py` | `src/ii_server/tools/` area | |
+| `src/ii_tool/tools/mcp_tool.py` | `src/ii_server/mcp/` | |
+| `src/ii_tool/tools/shell/shell_init.py` | `src/ii_server/tools/shell/` | |
+| `src/ii_tool/utils.py` | `src/ii_server/utils.py` | |
+
+### src/ii_sandbox_server/ → REMOVED (absorbed into ii_agent)
+
+| Old Path (develop/topic) | New Path (origin/main) | Notes |
+|---|---|---|
+| `src/ii_sandbox_server/` | **REMOVED entirely** | Absorbed into `src/ii_agent/agents/sandboxes/` |
+| `src/ii_sandbox_server/sandboxes/base.py` | `src/ii_agent/agents/sandboxes/base.py` | |
+| `src/ii_sandbox_server/sandboxes/e2b.py` | `src/ii_agent/agents/sandboxes/e2b.py` | |
+| `src/ii_sandbox_server/sandboxes/docker.py` | **DOES NOT EXIST in main** | Topic-branch-only |
+| `src/ii_sandbox_server/sandboxes/port_manager.py` | **DOES NOT EXIST in main** | Topic-branch-only |
+| `src/ii_sandbox_server/sandboxes/sandbox_factory.py` | **DOES NOT EXIST in main** | |
+| `src/ii_sandbox_server/lifecycle/sandbox_controller.py` | `src/ii_agent/agents/sandboxes/service.py` | Likely merged |
+| `src/ii_sandbox_server/client/client.py` | **Absorbed** | |
+| `src/ii_sandbox_server/config.py` | `src/ii_agent/core/config/sandbox.py` | |
+| `src/ii_sandbox_server/db/manager.py` | `src/ii_agent/core/db/` | |
+| `src/ii_sandbox_server/main.py` | **No separate process** | Integrated |
+| `src/ii_sandbox_server/models/payload.py` | `src/ii_agent/agents/sandboxes/models.py` | |
+
+### Tests → src/tests/
+
+| Old Path (develop/topic) | New Path (origin/main) | Notes |
+|---|---|---|
+| `tests/` | `src/tests/` | Moved into src |
+| `tests/conftest.py` | `src/tests/conftest.py` | |
+| `tests/sandbox/` | `src/tests/unit/engine/` (sandbox tests) | |
+| `tests/storage/` | `src/tests/unit/` area | |
+| `tests/llm/` | `src/tests/unit/` area | |
+| `tests/test_ii_tool/` | `src/tests/unit/` area | |
+| `tests/tools/` | `src/tests/unit/` area | |
+
+### Docker/Config (mostly same paths)
+
+| Old Path | New Path | Notes |
+|---|---|---|
+| `docker/docker-compose.stack.yaml` | Same | Modified in both |
+| `docker/docker-compose.local-only.yaml` | **NEW** | Topic-branch-only |
+| `docker/docker-compose.local.yaml` | **NEW** | Topic-branch-only |
+| `docker/.stack.env.local.example` | `docker/.stack.env.example` | Main has different example |
+| `docker/backend/Dockerfile` | Same | Modified in both |
+| `scripts/run_stack.sh` | `scripts/run_stack.sh` | Topic branch deleted, replaced with stack_control.sh |
+| `scripts/stack_control.sh` | **NEW** | Topic-branch-only |
+
+## Key Observations
+
+1. **Main has a LocalStorage provider already**: `src/ii_agent/core/storage/providers/local.py` exists in main
+2. **Sandbox server absorbed**: The entire `ii_sandbox_server` package no longer exists separately
+3. **Tool server renamed**: `ii_tool` → `ii_server`
+4. **Shell/sandbox execution refactored** in #865 with new architecture
+5. **DDD structure**: Domain-Driven Design with proper bounded contexts
+6. **Tests relocated**: All tests now under `src/tests/`
diff --git a/docs/rebase-analysis/02-baseline-changes.md b/docs/rebase-analysis/02-baseline-changes.md
new file mode 100644
index 000000000..441382038
--- /dev/null
+++ b/docs/rebase-analysis/02-baseline-changes.md
@@ -0,0 +1,140 @@
+# Baseline Changes Analysis: develop → origin/main
+
+## Executive Summary
+
+153 commits, 2,500 files changed, +501,149/-75,606 lines.
+This represents a **massive architectural overhaul** from a monolithic server design to a Domain-Driven Design (DDD) structure.
+
+## Major Architectural Changes
+
+### 1. DDD Restructure (#851) — 1,483 files changed
+The single largest commit. Completely reorganized `src/ii_agent/` from a monolithic `server/` package into bounded domain contexts:
+
+**Old (develop):**
+```
+src/ii_agent/
+├── server/ # Monolithic server
+│ ├── api/ # All HTTP endpoints
+│ ├── chat/ # Chat service
+│ ├── socket/ # WebSocket handlers
+│ ├── services/ # Business logic
+│ ├── models/ # Data models
+│ └── slides/ # Slide processing
+├── controller/ # Agent controller
+├── llm/ # LLM providers
+├── prompts/ # System prompts
+├── storage/ # Storage backends
+├── sandbox/ # Sandbox abstraction
+├── sub_agent/ # Sub-agent tools
+└── adapters/ # Adapter layer
+```
+
+**New (main):**
+```
+src/ii_agent/
+├── agents/ # Agent runtime (replaces controller/, llm/, prompts/, sub_agent/, adapters/)
+│ ├── models/ # LLM providers (replaces llm/)
+│ ├── prompts/ # System prompts
+│ ├── sandboxes/ # Sandbox management (replaces sandbox/, sandbox_server)
+│ ├── tools/ # Agent-side tools
+│ ├── factory/ # Agent/tool creation
+│ ├── hooks/ # Agent hooks (replaces messages/)
+│ ├── skills/ # Agent skills
+│ └── sessions/ # Session management
+├── app/ # FastAPI app lifecycle (replaces server/app.py)
+├── auth/ # Authentication domain (replaces server/api/auth.py)
+├── billing/ # Billing domain
+├── chat/ # Chat domain (replaces server/chat/)
+│ ├── api/ # Chat HTTP endpoints
+│ ├── application/ # Chat business logic
+│ └── llm/ # Chat LLM providers
+├── content/ # Content domain (replaces server/slides/)
+│ └── media/ # Media generation (replaces ii_tool/integrations/)
+├── core/ # Shared infrastructure
+│ ├── config/ # All configuration (settings.py replaces ii_agent_config.py)
+│ ├── db/ # Database (replaces db/)
+│ ├── storage/ # Storage providers (replaces storage/)
+│ │ └── providers/ # gcs.py, local.py, minio.py
+│ └── secrets/ # Secret management
+├── credits/ # Credits domain
+├── files/ # File management domain (replaces server/api/files.py)
+├── integrations/ # External integrations
+├── projects/ # Projects domain
+├── realtime/ # WebSocket/SocketIO (replaces server/socket/)
+│ ├── handlers/ # Socket command handlers
+│ └── events/ # Event system
+├── sessions/ # Sessions domain (replaces server/api/sessions.py)
+├── settings/ # Settings domain (replaces server/llm_settings/)
+│ ├── llm/ # LLM settings
+│ └── mcp/ # MCP settings
+├── tasks/ # Background tasks
+├── users/ # User domain
+└── workers/ # Background workers (replaces cron/)
+```
+
+### 2. Package Renames
+- `src/ii_tool/` → `src/ii_server/` (tool server renamed)
+- `src/ii_sandbox_server/` → **REMOVED** (absorbed into `src/ii_agent/agents/sandboxes/`)
+- `tests/` → `src/tests/` (tests moved into src)
+
+### 3. Shell and Sandbox Execution Refactor (#865)
+- New `src/ii_agent/agents/sandboxes/shell.py` — shell abstraction
+- E2B-specific shell: `e2b_shell.py`
+- Live terminal service: `live_terminal_service.py`
+- Sandbox router: `router.py`
+- Shell tools restructured: `src/ii_agent/agents/tools/shell/`
+
+### 4. Workspace Manager Removal (#825)
+- `workspace_manager.py` completely removed
+- Connector tools restructured
+
+### 5. A2A and MCP SSE Removal (#842)
+- Agent-to-Agent protocol removed
+- MCP SSE transport removed
+- Simplification of integration layer
+
+### 6. Dev Tool → Skill Migration (#848)
+- Development tools migrated from imperative tools to declarative skills
+- `ii-app` skill created under `settings/skills/builtin/ii-app/`
+- Template processor for project scaffolding
+
+### 7. Pricing/UUID Consolidation (#862)
+- `uuid.UUID` types enforced across all API contracts
+- Pricing consolidated into billing domain
+- Chat API contracts refactored
+
+### 8. Media Path Refactor (#860)
+- Media generation moved to `content/media/`
+- Unified file asset handling
+
+### 9. Code Viewer with Watcher (#855)
+- File tree, code viewer components added
+- Sandbox file explorer capability
+
+## Features Already Present in Main That Topic Branch Also Implemented
+
+| Feature | Main Implementation | Topic Branch Implementation | Status |
+|---|---|---|---|
+| **Local Storage Provider** | `core/storage/providers/local.py` | `storage/local.py` + `ii_tool/integrations/storage/local.py` | **MAIN HAS IT** |
+| **Storage Config with local** | `core/config/storage.py` (supports gcs/local/minio) | Modified `storage/` and config | **MAIN HAS IT** |
+| **Docker enum in SandboxProviderType** | `agents/sandboxes/types.py` has `DOCKER = "docker"` | Added to sandbox factory | **MAIN HAS IT (enum only)** |
+| **Sandbox Settings with docker** | `core/config/sandbox.py` has `docker` in Literal | Added docker config | **MAIN HAS IT (config only)** |
+| **Sandbox Service with Docker reference** | `agents/sandboxes/service.py` references Docker | Built docker factory | **MAIN STUBS IT** |
+
+## Features NOT in Main That Topic Branch Provides
+
+| Feature | Description | Required Integration Point |
+|---|---|---|
+| **DockerSandbox Implementation** | Full Docker container lifecycle (974 lines) | `src/ii_agent/agents/sandboxes/docker.py` |
+| **PortPoolManager** | Port 30000-30999 allocation for Docker containers | New file in `agents/sandboxes/` |
+| **Orphan Container Cleanup** | Background cleanup loop for abandoned containers | Extend `agents/sandboxes/service.py` |
+| **docker-compose.local-only.yaml** | Air-gapped Docker Compose stack | `docker/` |
+| **docker-compose.local.yaml** | Hybrid compose file | `docker/` |
+| **stack_control.sh** | Stack management script | `scripts/` |
+| **Tool Execution Timeouts** | Timeout enforcement for tool calls | Agent runtime |
+| **Mid-Tool Interruption** | Cancel running tools mid-execution | Agent runtime |
+| **Agent-Human-Agent Handoff** | noVNC browser handoff mechanism | Agent + realtime |
+| **Dynamic Token Budget** | Extended token budget for Claude 4.5 | Config/constants |
+| **Various Bug Fixes** | WebSocket, image handling, slides, etc. | Various domains |
+| **Comprehensive Test Suite** | 80+ test files | `src/tests/` |
+| **Documentation** | Architecture, feature analysis, user guide | `docs/` |
diff --git a/docs/rebase-analysis/03-three-way-assessment.md b/docs/rebase-analysis/03-three-way-assessment.md
new file mode 100644
index 000000000..5a8c3ff0c
--- /dev/null
+++ b/docs/rebase-analysis/03-three-way-assessment.md
@@ -0,0 +1,219 @@
+# Three-Way Diff Analysis & Change Assessment
+
+## Methodology
+For each topic branch change, we assess:
+1. **What changed** in the topic branch (from develop)
+2. **What changed** in main (from develop) for the same area
+3. **Whether the topic change still makes sense** given the new baseline
+
+## Tier 0: Configuration & Constants (Foundation)
+
+### TOKEN_BUDGET_EXTENDED = 800,000 (ii_agent_config.py / llm_config.py)
+- **Topic**: Added `TOKEN_BUDGET_EXTENDED = 800_000` for Claude 4.5
+- **Main**: `ii_agent_config.py` → `core/config/settings.py` — completely restructured with pydantic-settings
+- **Assessment**: Check if main already has extended token budget. If not, add to `core/config/settings.py`
+- **Verdict**: **NEEDS PORTING** — check if already addressed in main's config
+
+### Default storage provider change (gcs → local)
+- **Topic**: Changed default from `"gcs"` to `"local"` in storage config
+- **Main**: `core/config/storage.py` already supports `local` but defaults to `"gcs"`
+- **Assessment**: For local-only mode, this should be set in env vars, not hardcoded
+- **Verdict**: **DROP** — main handles this correctly via env config
+
+### Sandbox config additions (provider_type, docker_image, docker_network, etc.)
+- **Topic**: Added multiple sandbox config options: `provider_type`, `docker_image`, `docker_network`, `local_mode`, `orphan_cleanup_*`, `backend_url`
+- **Main**: `core/config/sandbox.py` already has `SandboxSettings` with pydantic-settings, supports `docker` provider enum
+- **Assessment**: Port Docker-specific settings (docker_image, docker_network, port range) into existing `SandboxSettings`
+- **Verdict**: **NEEDS PORTING** — extend `SandboxSettings` with Docker-specific fields
+
+### expose_port() — external parameter
+- **Topic**: Added `external` parameter to `expose_port()` method in sandbox base
+- **Main**: `agents/sandboxes/base.py` does not have this parameter
+- **Assessment**: This is needed for local Docker mode where port mapping differs
+- **Verdict**: **NEEDS PORTING** — add to new base class
+
+## Tier 1: Infrastructure Components
+
+### PortPoolManager (port_manager.py — 480 lines, NEW)
+- **Topic**: Created `src/ii_sandbox_server/sandboxes/port_manager.py`
+- **Main**: No equivalent exists. Port management not implemented.
+- **Assessment**: Core infrastructure for Docker sandbox. Needs new location: `src/ii_agent/agents/sandboxes/port_manager.py`
+- **Verdict**: **PORT DIRECTLY** — new file, no conflicts
+
+### LocalStorage (backend side — storage/local.py)
+- **Topic**: Created `src/ii_agent/storage/local.py` with path traversal protection, .meta sidecar files, URL download
+- **Main**: Already has `src/ii_agent/core/storage/providers/local.py` with `LocalProvider` class
+- **Assessment**: Main's LocalProvider uses pathlib, topic branch uses os.path. Main's implementation is cleaner but may be missing some features (e.g., .meta sidecar, content-type tracking). Need to compare feature sets.
+- **Verdict**: **MERGE/EXTEND** — preserve main's implementation, add any missing features
+
+### LocalStorage (tool-server side — ii_tool/integrations/storage/local.py)
+- **Topic**: Created `src/ii_tool/integrations/storage/local.py` — duplicate of backend local storage
+- **Main**: `ii_tool` no longer exists; integrations absorbed into `ii_agent` domains
+- **Assessment**: The tool-server storage is now handled by main's unified storage. This file is irrelevant.
+- **Verdict**: **DROP** — main has unified storage
+
+### Storage Factory (storage/factory.py)
+- **Topic**: Modified to route to LocalStorage based on config
+- **Main**: Storage factory is likely in `core/storage/` — already supports local routing
+- **Assessment**: Main already handles local storage factory routing
+- **Verdict**: **DROP** — main covers this
+
+## Tier 2: Docker Sandbox Implementation
+
+### DockerSandbox (docker.py — 974 lines, NEW)
+- **Topic**: Created `src/ii_sandbox_server/sandboxes/docker.py` — full Docker container lifecycle
+- **Main**: `agents/sandboxes/service.py` has `SandboxProviderType.DOCKER` enum but raises `SandboxCreationError("Unsupported provider: docker")`
+- **Assessment**: Core feature. Must be ported to `src/ii_agent/agents/sandboxes/docker.py`, implementing the new `Sandbox` base class API from main
+- **Verdict**: **NEEDS MAJOR REWORK** — rewrite to implement main's `Sandbox` ABC with Shell, LiveTerminal, and file explorer APIs
+
+### sandbox_factory.py
+- **Topic**: Created factory for e2b/docker sandbox creation
+- **Main**: Factory logic is in `agents/sandboxes/service.py._create_provider()`. Just add Docker branch.
+- **Assessment**: Add Docker provider creation to existing `_create_provider` and `_connect_provider`
+- **Verdict**: **MERGE INTO service.py** — simple addition
+
+## Tier 3: Orchestration
+
+### Sandbox Controller Orphan Cleanup (~120 lines)
+- **Topic**: Added to `src/ii_sandbox_server/lifecycle/sandbox_controller.py`
+- **Main**: `ii_sandbox_server` no longer exists. Sandbox service is in `agents/sandboxes/service.py`
+- **Assessment**: Port orphan cleanup as a method/background task in `SandboxService` or as a worker in `workers/cron/`
+- **Verdict**: **NEEDS PORTING** — adapt to main's architecture, likely in workers/cron/
+
+### client/client.py changes
+- **Topic**: Modified sandbox client for Docker support
+- **Main**: Client/server split removed — sandbox is in-process now
+- **Assessment**: The client abstraction is gone. Docker sandbox is called directly.
+- **Verdict**: **DROP** — architecture changed
+
+## Tier 4: API/Integration Layer
+
+### File upload endpoints (server/api/files.py)
+- **Topic**: Added `PUT /files/upload/{path}`, `GET /files/{path}` with token auth
+- **Main**: `files/router.py` handles file endpoints. Completely restructured.
+- **Assessment**: Check if main's file router supports the upload/serve endpoints needed for local mode
+- **Verdict**: **CHECK AND PORT** — may need to add local file serving endpoint
+
+### Backend server/app.py changes
+- **Topic**: Various startup modifications for local mode
+- **Main**: `app/__init__.py`, `app/lifespan.py` — completely different
+- **Assessment**: Local mode startup needs to be adapted to new app lifecycle
+- **Verdict**: **NEEDS REWORK** — adapt to new lifespan hooks
+
+### chat/context_manager.py, chat/service.py, chat/router.py changes
+- **Topic**: Various fixes for chat in local mode
+- **Main**: Complete restructure — `chat/application/chat_service.py`, `chat/api/router.py`
+- **Assessment**: The specific fixes need to be evaluated against new code
+- **Verdict**: **NEEDS INDIVIDUAL EVALUATION** in new codebase
+
+### WebSocket handlers (socket/ → realtime/)
+- **Topic**: Modified query_handler, awake_sandbox_handler, sandbox_status_handler, socketio
+- **Main**: All renamed and restructured under `realtime/handlers/`
+- **Assessment**: Changes need individual evaluation. The event system is completely different.
+- **Verdict**: **NEEDS REWORK** — adapt changes to new event system
+
+### LLM provider changes (llm/anthropic.py, llm/openai.py)
+- **Topic**: Streaming timeout fixes, safety net improvements
+- **Main**: `agents/models/anthropic/claude.py`, `agents/models/openai/completions.py` — rewritten
+- **Assessment**: Check if streaming timeout issues exist in main's implementations
+- **Verdict**: **CHECK AND PORT** — may already be fixed differently
+
+### Sub-agent changes (sub_agent/ → agents/)
+- **Topic**: Added interrupt events, task_agent_tool, design_document_agent modifications
+- **Main**: Sub-agents restructured. `agents/factory/agent.py` builds sub-agents differently
+- **Assessment**: Interrupt events may map to main's cancellation system
+- **Verdict**: **NEEDS EVALUATION** — check if interrupts are handled by Redis cancel
+
+## Tier 5: Frontend
+
+### Frontend component changes
+- **Topic**: Modified 16 frontend files for sandbox status, agent UI, websocket
+- **Main**: Modified same 16 files with various refactors
+- **Assessment**: Frontend mostly kept same paths. Need three-way merge for each file.
+- **Verdict**: **NEEDS THREE-WAY MERGE** — file by file
+
+### Frontend test files (NEW)
+- **Topic**: Created `frontend/src/lib/__tests__/utils.test.ts` and `agent-sandbox-status.test.ts`
+- **Main**: These specific test files don't exist in main
+- **Assessment**: Tests are additive but may need updating for changed APIs
+- **Verdict**: **PORT AND UPDATE** — update test imports/APIs
+
+## Tier 6: Docker/Compose/Scripts
+
+### docker-compose.local-only.yaml (NEW)
+- **Topic**: Complete air-gapped compose file, 194 lines
+- **Main**: Main has docker-compose.stack.yaml (updated) and docker-compose.dev.yaml (new)
+- **Assessment**: Local-only compose needs updating for new service structure (no more sandbox-server/tool-server as separate services)
+- **Verdict**: **NEEDS MAJOR REWORK** — adapt to main's compose structure
+
+### docker-compose.local.yaml (NEW)
+- **Topic**: Hybrid compose overlay
+- **Main**: No equivalent
+- **Assessment**: Same as above — needs adapting
+- **Verdict**: **NEEDS REWORK** — adapt to main's structure
+
+### stack_control.sh (NEW)
+- **Topic**: Created comprehensive stack management script
+- **Main**: `scripts/run_stack.sh` exists but is simpler
+- **Assessment**: Standalone script, mostly portable. Update compose file references.
+- **Verdict**: **PORT AND UPDATE** — update paths/references
+
+### docker/backend/Dockerfile changes
+- **Topic**: Modified for local mode build args
+- **Main**: Modified for new package structure
+- **Assessment**: Need three-way merge
+- **Verdict**: **NEEDS THREE-WAY MERGE**
+
+### e2b.Dockerfile changes
+- **Topic**: Updated sandbox image
+- **Main**: Also updated sandbox image
+- **Assessment**: Three-way merge
+- **Verdict**: **NEEDS THREE-WAY MERGE**
+
+## Tier 7: Tests
+
+### Comprehensive test suite (~80 files)
+- **Topic**: Created under `tests/` — sandbox, storage, LLM, tool tests
+- **Main**: Tests moved to `src/tests/` — completely different structure
+- **Assessment**: All test files need relocation to `src/tests/unit/` and import path updates
+- **Verdict**: **PORT ALL** — update paths, imports, and assertions for new APIs
+
+## Tier 8: Documentation
+
+### Existing topic branch docs
+- architecture-local-to-cloud.md — Architecture evolution guide
+- feature-branch-analysis.md — Feature specification
+- local-docker-sandbox.md — User guide
+- **Assessment**: All documentation remains relevant. Update for new paths/structure.
+- **Verdict**: **PORT AND UPDATE** — update all paths/references
+
+## Summary: Change Categories
+
+### Directly Portable (New files, no conflicts)
+1. PortPoolManager → `agents/sandboxes/port_manager.py`
+2. html_to_pdf.py (script)
+3. stack_control.sh (with path updates)
+4. admin_credits.sh (script)
+5. Documentation files (with content updates)
+6. docker/.stack.env.local.example (with updates)
+
+### Needs Major Rework (Architecture changed)
+1. DockerSandbox → rewrite for new Sandbox ABC
+2. docker-compose.local-only.yaml → adapt for new compose structure
+3. Orphan cleanup → move to workers/cron
+4. Frontend changes → three-way merge each file
+
+### Check and Port (May already be fixed in main)
+1. Image compression → main has `compress_image_for_provider`
+2. Streaming timeouts → check new LLM providers
+3. Failed tool lookup handling → check new tool system
+4. ThinkingBlock trailing fix → check new model response handling
+5. WebSocket session priority → check new realtime system
+
+### Drop (Superseded by main)
+1. LocalStorage backend (main has LocalProvider)
+2. LocalStorage tool-server (ii_tool doesn't exist)
+3. Storage factory changes (main has unified storage)
+4. Client/client.py changes (client/server split removed)
+5. Default storage=local (use env vars instead)
+6. ii_sandbox_server scaffolding (absorbed into ii_agent)
diff --git a/docs/rebase-analysis/04-rebase-plan.md b/docs/rebase-analysis/04-rebase-plan.md
new file mode 100644
index 000000000..e78726900
--- /dev/null
+++ b/docs/rebase-analysis/04-rebase-plan.md
@@ -0,0 +1,211 @@
+# Detailed Rebase Plan: feat/local-docker-sandbox onto origin/main
+
+## Strategy: Manual Cherry-Pick Rebase
+
+Instead of `git rebase`, we will:
+1. Create a new branch `rebase/local-docker-sandbox` from `origin/main`
+2. Manually port changes from the topic branch, adapted to the new architecture
+3. Commit in logical groups (leaf-to-root dependency tiers)
+4. Validate each commit builds and tests pass
+
+## Pre-Rebase Checklist
+
+- [x] Topic branch squashed to single commit (b93a325)
+- [x] Path mapping documented (01-path-mapping.md)
+- [x] Baseline changes documented (02-baseline-changes.md)
+- [x] Three-way assessment completed (03-three-way-assessment.md)
+- [ ] New branch created from origin/main
+- [ ] Rebase commits executed
+
+---
+
+## Commit Plan (7 Commits, Leaf-to-Root)
+
+### Commit 1: Configuration & Constants
+**Files to create/modify:**
+- `src/ii_agent/core/config/sandbox.py` — Add Docker-specific settings:
+ - `docker_image: str = "ii-agent-sandbox:latest"`
+ - `docker_network: str = "ii-agent-local_ii-network"`
+ - `port_range_start: int = 30000`
+ - `port_range_end: int = 30999`
+ - `orphan_cleanup_enabled: bool = True`
+ - `orphan_cleanup_interval_seconds: int = 60`
+ - `backend_url: str = "http://backend:8000"`
+ - `local_mode: bool = False`
+
+**Status:** NEW WORK — extend existing pydantic-settings class
+
+### Commit 2: Port Pool Manager (Infrastructure)
+**Files to create:**
+- `src/ii_agent/agents/sandboxes/port_manager.py` — Port from topic branch
+ - Update imports from `ii_sandbox_server` → `ii_agent.agents.sandboxes`
+ - Update config access to use `Settings.sandbox.*` instead of env vars directly
+ - Keep core logic intact (thread-safe allocation, startup scanning, background cleanup)
+
+**Tests to create:**
+- `src/tests/unit/agent/test_port_manager.py` — Port from `tests/sandbox/test_port_manager.py`
+ - Update imports
+ - Update class references
+
+**Status:** MOSTLY PORTABLE — import/config updates only
+
+### Commit 3: Docker Sandbox Provider (Core Feature)
+**Files to create:**
+- `src/ii_agent/agents/sandboxes/docker.py` — **MAJOR REWORK** required
+ - Must implement main's `Sandbox` ABC (from `agents/sandboxes/base.py`)
+ - Required methods: `get_info()`, `get_status()`, `get_provider_id()`, `upload_path`,
+ `create()`, `run_command()`, `upload()`, `download()`, `expose_port()`, `kill()`,
+ `get_file_tree()`, `get_file_content()`, `write_file()`, `delete_file()`
+ - Must support main's `Shell` abstraction (`agents/sandboxes/shell.py`)
+ - Must support `LiveTerminalHandle` for terminal streaming
+ - Must integrate with `PortPoolManager` for port allocation
+ - Class: `DockerSandbox(Sandbox)` with `PROVIDER = SandboxProviderType.DOCKER`
+
+**Files to modify:**
+- `src/ii_agent/agents/sandboxes/service.py` — Add Docker to `_create_provider()` and `_connect_provider()`
+ - Add: `from ii_agent.agents.sandboxes.docker import DockerSandbox`
+ - Add Docker case in `_create_provider()`: Return `DockerSandbox.create(...)`
+ - Add Docker case in `_connect_provider()`: Return `DockerSandbox.connect(...)`
+
+**Tests to create:**
+- `src/tests/unit/agent/test_docker_sandbox.py` — Rewrite from `tests/sandbox/test_docker_sandbox.py`
+- `src/tests/unit/agent/test_sandbox_factory.py` — Rewrite from `tests/sandbox/test_sandbox_factory.py`
+
+**Status:** MAJOR REWORK — new base class API, shell/terminal integration
+
+### Commit 4: Orphan Cleanup & Lifecycle (Orchestration)
+**Files to create/modify:**
+- `src/ii_agent/workers/cron/jobs/orphan_cleanup.py` — New file
+ - Port orphan cleanup logic from `ii_sandbox_server/lifecycle/sandbox_controller.py`
+ - Use `SandboxService` and `SandboxRepository` instead of direct DB queries
+ - Register as a cron job in main's worker system
+
+- OR integrate into `src/ii_agent/agents/sandboxes/service.py` as:
+ - `async def cleanup_orphan_sandboxes(self, grace_period_seconds: int = 300) -> int`
+ - Background task started in app lifespan
+
+**Tests:**
+- `src/tests/unit/agent/test_orphan_cleanup.py`
+
+**Status:** MODERATE REWORK — use main's DB/service patterns
+
+### Commit 5: Docker Compose & Deployment Scripts
+**Files to create:**
+- `docker/docker-compose.local.yaml` — Docker Compose overlay for local Docker sandbox mode
+ - Adapt from topic branch's local-only.yaml
+ - **Critical:** No separate sandbox-server or tool-server services (absorbed into backend)
+ - Add minio service (main uses minio for local storage instead of filesystem)
+ - Keep: postgres, redis, frontend, backend services
+ - Ensure backend has Docker socket mount for spawning sandbox containers
+ - Add sandbox Docker network configuration
+
+- `docker/.stack.env.local.example` — Local mode env example
+ - Update for new env var names (SANDBOX_PROVIDER, STORAGE_PROVIDER, etc.)
+
+- `scripts/stack_control.sh` — Port with updates
+ - Update compose file references
+ - Update service names for new architecture
+
+**Files to modify:**
+- `docker/docker-compose.stack.yaml` — Add Docker socket mount option for backend
+ - Add conditional volume mount for `/var/run/docker.sock`
+
+**Status:** MODERATE REWORK — new compose structure, no separate sandbox-server
+
+### Commit 6: Frontend Changes (Three-Way Merge)
+**Files to evaluate and selectively port:**
+- `frontend/src/typings/agent.ts` — Check if `'stopped'` maps to `CANCELLED` or `SYSTEM_INTERRUPTED` in main
+- `frontend/src/state/slice/agent.ts` — Sandbox status tracking changes
+- `frontend/src/contexts/websocket-context.tsx` — Session priority changes
+- `frontend/src/hooks/use-app-events.tsx` — Event handler updates
+- `frontend/src/hooks/use-session-manager.tsx` — Session management
+- `frontend/src/components/agent/agent-result.tsx` — Result display
+- `frontend/src/components/agent/subagent-container.tsx` — Subagent UI
+- `frontend/src/app/routes/agent.tsx` — Route changes
+
+**For each file:**
+1. Read main's version
+2. Read topic branch's version
+3. Identify topic-branch-only functional changes
+4. Apply only those changes to main's version
+5. Skip cosmetic/structural changes that conflict with main's refactoring
+
+**New tests to port:**
+- `frontend/src/lib/__tests__/utils.test.ts`
+- `frontend/src/state/__tests__/agent-sandbox-status.test.ts` — update for new types
+
+**Status:** CAREFUL THREE-WAY MERGE — per-file evaluation needed
+
+### Commit 7: Documentation & Remaining Files
+**Files to create/update:**
+- `docs/docs/architecture-local-to-cloud.md` — Update all paths for new structure
+- `docs/docs/local-docker-sandbox.md` — Update for new compose, env vars, paths
+- `docs/docs/feature-branch-analysis.md` — Update with new architecture mapping
+- `scripts/html_to_pdf.py` — Port directly (standalone script)
+- `scripts/admin_credits.sh` — Port directly (standalone script)
+- `.github/copilot-instructions.md` — Port directly
+
+**Status:** MOSTLY PORTABLE — content updates for new paths
+
+---
+
+## Changes to DROP (Superseded by Main)
+
+| Change | Reason |
+|---|---|
+| `src/ii_agent/storage/local.py` | Main has `core/storage/providers/local.py` |
+| `src/ii_agent/storage/factory.py` mods | Main has unified storage factory |
+| `src/ii_agent/storage/base.py` mods | Main has `core/storage/providers/base.py` |
+| `src/ii_agent/storage/gcs.py` mods | Main has `core/storage/providers/gcs.py` |
+| `src/ii_agent/storage/__init__.py` mods | Main has `core/storage/__init__.py` |
+| `src/ii_tool/integrations/storage/*` | `ii_tool` no longer exists |
+| `src/ii_tool/integrations/image_generation/*` | Moved to `content/media/` |
+| `src/ii_tool/integrations/video_generation/*` | Moved to `content/media/` |
+| `src/ii_sandbox_server/*` (scaffolding) | Absorbed into `ii_agent/agents/sandboxes/` |
+| `src/ii_agent/server/*` modifications | Server monolith decomposed into domains |
+| Image compression in agent_controller | Main has `compress_image_for_provider` |
+| `requests` → `httpx` migration | Main already uses httpx |
+| Default storage=local | Use env vars |
+| `client/client.py` changes | No more client/server split |
+| `scripts/run_stack.sh` replacement | Bring stack_control.sh alongside, don't delete run_stack.sh |
+
+## Changes to VERIFY Before Porting
+
+| Change | Check |
+|---|---|
+| ThinkingBlock trailing fix | Does main's `agents/agent.py` handle this? |
+| Failed tool lookup handling | Does main's tool system handle missing tools? |
+| WebSocket session priority | Does main's realtime system handle priority? |
+| Streaming timeout fixes | Does main's anthropic provider have timeouts? |
+| Subagent interrupt events | Does main's cancellation cover this? |
+
+---
+
+## Execution Order
+
+1. **Create branch** `rebase/local-docker-sandbox` from `origin/main`
+2. **Commit 1**: Config changes (smallest, foundation)
+3. **Commit 2**: Port manager (leaf dependency, self-contained)
+4. **Commit 3**: Docker sandbox (depends on 1 & 2)
+5. **Commit 4**: Orphan cleanup (depends on 3)
+6. **Commit 5**: Compose & scripts (depends on 1-4)
+7. **Commit 6**: Frontend (can be parallel with 5, done after for testing)
+8. **Commit 7**: Documentation (last, references everything)
+
+## Validation After Each Commit
+
+1. `python -c "import ii_agent"` — basic import check
+2. `pytest src/tests/ -x --tb=short` — run existing tests
+3. `pytest src/tests/unit/agent/test_port_manager.py` (after commit 2)
+4. `pytest src/tests/unit/agent/test_docker_sandbox.py` (after commit 3)
+5. Full test suite after commit 7
+
+## Risk Assessment
+
+| Risk | Severity | Mitigation |
+|---|---|---|
+| Docker sandbox doesn't implement full Sandbox ABC | HIGH | Implement all abstract methods, stub if needed |
+| Shell abstraction incompatible with Docker exec | MEDIUM | Implement DockerShell similar to E2BShell |
+| Compose file doesn't match new service structure | MEDIUM | Test with `docker compose config` |
+| Frontend event changes break UI | LOW | Test manually after merge |
+| Test import paths broken | LOW | Systematic find-and-replace |
diff --git a/docs/rebase-analysis/05-post-rebase-audit.md b/docs/rebase-analysis/05-post-rebase-audit.md
new file mode 100644
index 000000000..cfbe7682b
--- /dev/null
+++ b/docs/rebase-analysis/05-post-rebase-audit.md
@@ -0,0 +1,239 @@
+# Post-Rebase Audit: `rebase/local-docker-sandbox`
+
+## Executive Summary
+
+The 7-commit rebase onto `origin/main` successfully ported the core Docker sandbox functionality. **39 files** were changed (from 155 in the original topic branch). The 116 unported files were analyzed — most are correctly unported (old module structure that was rewritten by DDD restructure #851 on main). However, the audit identified:
+
+- **3 critical architectural issues** in the ported code
+- **4 high-priority issues** needing attention
+- **3 missing features** that should be ported
+- **2 regressions** to fix before merge
+- **Several nice-to-have improvements** from the original branch that were not Docker-specific
+
+---
+
+## Part 1: Completeness — What Was Missed
+
+### 1.1 Correctly Unported (No Action Needed)
+
+| Category | Files | Reason |
+|----------|-------|--------|
+| `src/ii_sandbox_server/` | 8 | Absorbed into `agents/sandboxes/` on main |
+| `src/ii_tool/` (most files) | ~12 | Now `ii_server/` on main |
+| `src/ii_agent/server/` | 26 | DDD restructure rewrote all |
+| `src/ii_agent/controller/`, `llm/`, `sub_agent/`, `storage/` | ~20 | Completely rewritten on main |
+| Old `tests/` structure | 40+ | Moved to `src/tests/` |
+| `uv.lock` | 1 | Auto-generated |
+| `frontend/pnpm-lock.yaml` | 1 | Auto-generated (but see §2.2) |
+
+### 1.2 Features That SHOULD Be Ported
+
+#### A. VNC Services in Sandbox Image (BLOCKING for human-in-the-loop)
+**Original files:** `e2b.Dockerfile`, `docker/sandbox/start-services.sh`
+**What's missing:**
+- `e2b.Dockerfile`: Missing `x11vnc` and `novnc` package installs
+- `start-services.sh`: Missing Xvfb display setup, x11vnc server startup, noVNC websockify startup, health checks for VNC processes, `/workspace` ownership fix (`chown -R pn:pn`)
+- The sandbox code allocates `NOVNC_PORT = 6080` but nothing actually starts on that port
+
+**Impact:** Human-in-the-loop sandbox access (browser VNC) will not work.
+
+#### B. Client Host URL Rewriting (BLOCKING for remote access)
+**Original file:** `src/ii_agent/core/client_host.py`
+**What's missing:** A `ContextVar` that stores the connecting browser's hostname. `DockerSandbox.expose_port()` returns hardcoded `http://localhost:{port}` — this breaks when the browser is on a different machine than the Docker host.
+
+**Impact:** Docker sandbox URLs won't work from any machine other than localhost.
+
+#### C. `docker` Python Package Dependency (BLOCKING for fresh installs)
+**Original file:** `pyproject.toml`
+**What's missing:** `docker>=7.0.0` is not in `pyproject.toml` dependencies. It happens to be installed in the current environment (`7.1.0`) but `uv sync` on a fresh clone will not install it.
+
+**Impact:** `import docker` in `docker.py` will fail on fresh installs.
+
+### 1.3 Nice-to-Have Features Not Ported (Non-Docker-Specific)
+
+These were co-developed on the topic branch but are general improvements:
+
+| Feature | Original Files | Status on Main |
+|---------|---------------|----------------|
+| DALL-E 3 image generation client | `ii_tool/integrations/image_generation/openai_dalle.py` + factory | Missing — generic video gen framework exists but no DALL-E 3 |
+| Sora video generation | `ii_tool/integrations/video_generation/` (5 files) | Missing — can be added later |
+| Browser tab limit (MAX_TABS=50) | `ii_tool/browser/browser.py` | Missing — resource exhaustion protection |
+| Shell session limit (MAX_SHELL_SESSIONS=10) | `ii_tool/tools/shell/shell_init.py` | Missing — tmux session leak protection |
+| Tool server local file serving | `ii_tool/integrations/app/main.py` `/storage/` endpoint | Missing — needed for local-mode file access |
+| MCP tool image bridging | `ii_tool/tools/mcp_tool.py` `_process_image_inputs()` | Missing — external MCP servers can't read sandbox files |
+| Dynamic token budget | `core/config/llm_config.py` `get_max_context_tokens()` | Missing — uses static config on main |
+
+### 1.4 Already Exists on Main (Verified)
+
+| Feature | Status |
+|---------|--------|
+| Image compression (5MB Anthropic limit) | ✅ `chat/application/file_processor.py` |
+| ThinkingBlock sanitization | ✅ `chat/llm/anthropic/provider.py` + tests |
+| Failed tool lookup error handling | ✅ Error `ToolResult` on unknown tool |
+| Frontend sessionId priority (URL > Redux) | ✅ `websocket-context.tsx` |
+| Orphan cleanup (no HTTP endpoint needed) | ✅ Uses Docker API directly |
+
+---
+
+## Part 2: Regressions
+
+### 2.1 pnpm-lock.yaml Not Updated for vitest
+**File:** `frontend/package.json` lists `"vitest": "^3.2.1"` in devDependencies and has test scripts.
+**Problem:** `frontend/pnpm-lock.yaml` has 0 occurrences of "vitest" — it was never regenerated.
+**Impact:** `pnpm install --frozen-lockfile` in CI will fail. Frontend tests ("vitest run") will fail.
+**Fix:** Run `cd frontend && pnpm install` to regenerate lockfile.
+
+### 2.2 Backend `/auth/dev/login` Endpoint Does Not Exist
+**File:** `frontend/src/app/routes/login.tsx` adds DevLoginButton that calls `/auth/dev/login`.
+**Problem:** No backend endpoint exists at that path. The button is safely hidden (returns null when endpoint returns non-200), but the feature is dead code.
+**Impact:** Local-mode dev login doesn't work. Not blocking (button hidden gracefully), but a missing feature.
+
+---
+
+## Part 3: Architectural Issues
+
+### 3.1 CRITICAL
+
+#### A. Exception Hierarchy Violation
+**File:** `src/ii_agent/agents/sandboxes/exceptions.py`
+**Problem:** `SandboxException` inherits from `Exception` instead of `IIAgentError`.
+**Impact:** Global error handler (`ii_agent_error_handler`) won't catch sandbox exceptions. Error responses bypass schema validation. HTTP status codes may be wrong.
+**Fix:**
+```python
+from ii_agent.core.exceptions import IIAgentError
+
+class SandboxException(IIAgentError):
+ pass
+```
+
+#### B. PortPoolManager Uses threading.Lock (Blocks Event Loop)
+**File:** `src/ii_agent/agents/sandboxes/port_manager.py`
+**Problem:** `self._port_lock = threading.Lock()` — when `DockerSandbox.create()` awaits `allocate_ports()`, the blocking lock freezes the entire asyncio event loop.
+**Impact:** Under concurrent sandbox creation, the server becomes unresponsive.
+**Fix:** Convert to `asyncio.Lock` or use `asyncio.to_thread()` wrapper.
+
+#### C. Orphan Cleanup Bypasses Service Layer
+**File:** `src/ii_agent/agents/sandboxes/orphan_cleanup.py`
+**Problem:** Creates `DockerSandbox` directly and calls `kill()` instead of going through `SandboxService`. Also uses `get_db_session_local()` directly instead of DI.
+**Impact:** DB state sync issues if `SandboxService.pause_sandbox()` is called concurrently. Pattern violation.
+**Fix:** Use `SandboxService` for sandbox lifecycle operations.
+
+### 3.2 HIGH PRIORITY
+
+#### D. Docker Client Singleton Race Condition
+**File:** `src/ii_agent/agents/sandboxes/docker.py` (lines ~151-154)
+**Problem:** `_get_docker_client()` uses a `None` check without locking — two concurrent calls can create two clients.
+**Fix:** Use double-checked locking or `asyncio.Lock`.
+
+#### E. Port Constants Hardcoded
+**File:** `src/ii_agent/agents/sandboxes/docker.py` (lines 58-72)
+**Problem:** `MCP_SERVER_PORT = 6060`, `CODE_SERVER_PORT = 9000`, `NOVNC_PORT = 6080` are module constants instead of settings.
+**Fix:** Move to `SandboxSettings` with configurable defaults.
+
+#### F. scan_existing_containers() Never Called at Startup
+**File:** `src/ii_agent/agents/sandboxes/port_manager.py`
+**Problem:** `PortPoolManager.scan_existing_containers()` exists (~70 lines) but is never called during lifespan startup. If the server restarts, previously allocated ports won't be tracked.
+**Fix:** Add call to `app/lifespan.py` startup sequence.
+
+#### G. DANGEROUS_PATTERNS Regex Defined But Unused
+**File:** `src/ii_agent/agents/sandboxes/docker.py` (lines 75-80)
+**Problem:** Security regex for strict command validation exists but is never called.
+**Fix:** Either integrate into `run_command()` or remove dead code.
+
+### 3.3 MEDIUM
+
+| Issue | File | Description |
+|-------|------|-------------|
+| Resource cleanup lacks exception safety | docker.py `kill()` | Port release can leak if container removal fails |
+| Global task tracking race | orphan_cleanup.py | `start_orphan_cleanup()` could create duplicate tasks |
+| Logging inconsistency | port_manager.py | Uses stdlib logging; main may use structlog |
+
+---
+
+## Part 4: Frontend Analysis
+
+### 4.1 Verified Clean ✅
+
+| Item | Status |
+|------|--------|
+| `isDesignModeAvailable` uses `isSandboxLink()` | ✅ Correctly migrated |
+| `isE2bLink` → `isSandboxLink` migration complete | ✅ No stale references in production code |
+| `sandboxStatus` state initialized and cleared | ✅ Proper Redux lifecycle |
+| `rewriteLocalhostUrl()` edge cases | ✅ Handles null, same-host, portless URLs |
+| Model entries (claude-opus-4-6, claude-sonnet-4-6) | ✅ Follow existing pattern |
+| DevLoginButton security | ✅ Hidden by default, backend-gated |
+| Sub-agent STOPPED status | ✅ Consistent with backend RunStatus enum |
+
+### 4.2 Issues
+
+| Issue | Severity | Description |
+|-------|----------|-------------|
+| vitest not in lockfile | ⚠️ Regression | `pnpm install` needed |
+| DevLoginButton dead code | ℹ️ Info | Backend endpoint missing |
+
+---
+
+## Part 5: Test Coverage Assessment
+
+### 5.1 Existing Tests
+
+| Test File | Lines | Coverage |
+|-----------|-------|----------|
+| `test_docker_sandbox.py` | 446 | Path validation (20+ cases), create/kill, port mapping |
+| `test_port_manager.py` | 837 | Allocation, deallocation, range bounds |
+| `test_orphan_cleanup.py` | 122 | Grace period, cleanup loop |
+| `utils.test.ts` | ~100 | rewriteLocalhostUrl, isSandboxLink, isE2bLink |
+| `agent-sandbox-status.test.ts` | ~80 | sandboxStatus reducer |
+
+### 5.2 Missing Test Coverage
+
+| Gap | Impact |
+|-----|--------|
+| No async lock contention test | Won't catch event loop blocking |
+| No port exhaustion test | Error path untested |
+| No scan_existing_containers integration test | Startup recovery untested |
+| No end-to-end create→verify→kill test | Integration gaps |
+| orphan_cleanup tests don't verify DB state | State sync untested |
+
+---
+
+## Part 6: Recommendations
+
+### Before Merge (Mandatory)
+
+1. **Fix exception hierarchy** — `SandboxException(IIAgentError)` (15 min)
+2. **Add `docker>=7.0.0`** to `pyproject.toml` dependencies (5 min)
+3. **Regenerate `pnpm-lock.yaml`** with vitest (5 min)
+4. **Convert PortPoolManager to asyncio.Lock** (1-2 hr)
+
+### Before Docker Sandbox is Production-Ready
+
+5. **Add VNC services** to `e2b.Dockerfile` and `start-services.sh`
+6. **Implement client host URL rewriting** for remote access
+7. **Add `scan_existing_containers()` to lifespan startup**
+8. **Implement `/auth/dev/login`** backend endpoint
+9. **Add exception safety** to `kill()` cleanup
+10. **Wire orphan cleanup through SandboxService**
+
+### Future Improvements (Separate PRs)
+
+11. Port browser tab limit (MAX_TABS=50)
+12. Port shell session limit (MAX_SHELL_SESSIONS=10)
+13. Port tool server local file serving
+14. Port DALL-E 3 / Sora clients (if needed)
+15. Port MCP tool image bridging
+16. Move hardcoded port constants to SandboxSettings
+
+---
+
+## Appendix: File Classification Summary
+
+| Classification | Count | Description |
+|---------------|-------|-------------|
+| ALREADY_HANDLED | ~12 | Ported to new locations |
+| MAIN_REWROTE | ~55 | Old modules completely rewritten by main |
+| SHOULD_CHECK | ~30 | Investigated — most are main-equivalent or nice-to-have |
+| COSMETIC | ~6 | Typo fixes, debug logs, import fixes |
+| MISSED | 7 | VNC packages, VNC startup, client_host, docker dep, lockfile, DALL-E 3, Sora |
+
+Of the 7 MISSED items: 3 are Docker-blocking (VNC, client_host, docker dep), 2 are regressions (lockfile, dead DevLogin), 2 are separate features (DALL-E 3, Sora).
diff --git a/docs/rebase-analysis/06-full-feature-audit.md b/docs/rebase-analysis/06-full-feature-audit.md
new file mode 100644
index 000000000..c5713d25b
--- /dev/null
+++ b/docs/rebase-analysis/06-full-feature-audit.md
@@ -0,0 +1,315 @@
+# Full Feature Audit: `rebase/local-docker-sandbox` vs `origin/main`
+
+**Date:** 2026-04-02
+**Branch:** `rebase/local-docker-sandbox` (7 commits on `fdbc0a5`/`origin/main`)
+**Scope:** 39 files changed, +5,778 / −33 lines
+
+---
+
+## 1. Changed Files Inventory
+
+### Backend — Core Docker Sandbox (NEW files)
+
+| File | Lines | Purpose |
+|------|-------|---------|
+| `src/ii_agent/agents/sandboxes/docker.py` | 962 | Full `DockerSandbox` provider — all 26 abstract methods + 3 extras |
+| `src/ii_agent/agents/sandboxes/port_manager.py` | 583 | `PortPoolManager` — port allocation, container scanning, thread safety |
+| `src/ii_agent/agents/sandboxes/orphan_cleanup.py` | 168 | Background loop to remove orphaned Docker containers |
+
+### Backend — Integration Points (MODIFIED files)
+
+| File | Change | Assessment |
+|------|--------|------------|
+| `agents/sandboxes/__init__.py` | +2 lines: export `DockerSandbox` | ✅ Correct |
+| `agents/sandboxes/base.py` | `expose_port` gains `external` kwarg | ✅ Backward-compatible (default=True) |
+| `agents/sandboxes/e2b.py` | Signature update only | ✅ Minimal, correct |
+| `agents/sandboxes/service.py` | +12 lines: Docker provider in `_create_provider`/`_connect_provider` | ✅ Correct pattern |
+| `core/config/sandbox.py` | +42 lines: Docker config fields | ✅ All have defaults, non-breaking |
+| `app/lifespan.py` | +26 lines: port scan + orphan cleanup at startup/shutdown | ✅ Guarded by `local_mode` flag |
+| `auth/router.py` | +38 lines: `/dev/login` endpoint | ✅ Guarded by `local_mode` flag |
+
+### Frontend (MODIFIED files)
+
+| File | Change | Assessment |
+|------|--------|------------|
+| `lib/utils.ts` | `isSandboxLink()` replaces hardcoded E2B check; `rewriteLocalhostUrl()` for LAN access | ✅ Correct, backward-compatible |
+| `lib/__tests__/utils.test.ts` | New test file for `isSandboxLink` + `rewriteLocalhostUrl` | ✅ Good |
+| `state/slice/agent.ts` | New `sandboxStatus` state + selector | ✅ Additive |
+| `state/__tests__/agent-sandbox-status.test.ts` | Tests for new state | ✅ Good |
+| `hooks/use-app-events.tsx` | Dispatches `setSandboxStatus`, rewrites localhost URLs | ✅ Correct |
+| `hooks/use-navigation-leave-session.tsx` | Resets `sandboxStatus` on leave | ✅ Correct |
+| `components/agent/agent-result.tsx` | Uses `sandboxStatus === 'paused'` instead of `isE2bLink()` for awake screen; moves null-check after awake screen | ✅ Better UX for Docker |
+| `components/agent/agent-task.tsx` | Stops auto-promoting tasks when agent is stopped | ✅ UX fix |
+| `components/agent/subagent-container.tsx` | Adds `stopped` status | ✅ Additive |
+| `components/share-agent-content.tsx` | `isSandboxLink` for vscodeUrl; normalizes `chat` agent_type | ✅ Correct |
+| `typings/agent.ts` | Adds `'stopped'` to `AgentContext.status` union | ✅ Additive |
+| `constants/models.tsx` | Adds `claude-opus-4-6` and `claude-sonnet-4-6` | ✅ (Unrelated to sandbox, useful) |
+| `app/routes/agent.tsx` | Redirects `chat` type sessions to `/chat` | ✅ UX fix |
+| `app/routes/login.tsx` | `DevLoginButton` component | ✅ Guarded by backend availability check |
+| `package.json` | Adds `vitest` + test scripts | ✅ Good |
+
+### Infrastructure & Docs
+
+| File | Assessment |
+|------|------------|
+| `docker/docker-compose.local.yaml` | ✅ Full local stack (postgres, redis, minio, backend, frontend) |
+| `docker/.stack.env.local.example` | ✅ Template for local env |
+| `scripts/stack_control.sh` | ✅ Stack management (start, stop, rebuild, logs) |
+| `scripts/html_to_pdf.py` | ✅ Utility script |
+| `.github/copilot-instructions.md` | ✅ Agent instructions |
+| `docs/docs/*.md` (6 files) | ✅ Comprehensive documentation |
+
+### Tests (NEW files)
+
+| File | Tests | Assessment |
+|------|-------|------------|
+| `test_docker_sandbox.py` | 100+ | ✅ Thorough coverage |
+| `test_port_manager.py` | 48 | ✅ Exhaustive |
+| `test_orphan_cleanup.py` | 24+ | ✅ Good |
+
+---
+
+## 2. Feature Porting Assessment
+
+### ✅ Fully Ported Features
+
+| Feature | Original Location | New Location | Status |
+|---------|-------------------|--------------|--------|
+| Docker container sandbox lifecycle | `ii_sandbox_server/sandboxes/docker.py` | `agents/sandboxes/docker.py` | Complete — integrated directly as `Sandbox` subclass |
+| Port pool management | `ii_sandbox_server/sandboxes/port_manager.py` | `agents/sandboxes/port_manager.py` | Complete — enhanced with thread safety, container scanning |
+| Orphan container cleanup | `ii_sandbox_server/lifecycle/sandbox_controller.py` | `agents/sandboxes/orphan_cleanup.py` | Complete — extracted to dedicated module |
+| SandboxService Docker routing | `server/services/sandbox_service.py` | `agents/sandboxes/service.py` | Complete — `_create_provider`/`_connect_provider` dispatch |
+| Config: Docker-specific settings | `ii_sandbox_server/config.py` | `core/config/sandbox.py` | Complete — `docker_image`, `docker_network`, `port_range_*`, `local_mode`, etc. |
+| Dev login (no-OAuth local mode) | `server/api/auth.py` | `auth/router.py` | Complete — `/dev/login` endpoint |
+| Frontend: sandbox URL detection | `lib/utils.ts` | `lib/utils.ts` | Complete — `isSandboxLink()` handles both E2B and Docker |
+| Frontend: localhost URL rewriting | (new) | `lib/utils.ts` | Complete — LAN access support |
+| Frontend: sandbox status tracking | (new) | `state/slice/agent.ts` | Complete — `sandboxStatus` state |
+| Frontend: stopped agent UX | (new) | Multiple components | Complete — task display, subagent container |
+| Frontend: chat routing fix | (new) | `routes/agent.tsx`, `share-agent-content.tsx` | Complete |
+| Lifespan: Docker startup/shutdown | `sandbox_controller.py` | `app/lifespan.py` | Complete — container scan + orphan cleanup |
+| Docker compose: full local stack | `docker-compose.local-only.yaml` | `docker/docker-compose.local.yaml` | Complete |
+
+### ✅ Correctly NOT Ported (obsolete/replaced by main)
+
+| Original Feature | Why Not Ported |
+|------------------|---------------|
+| `ii_sandbox_server/` (entire package) | **Eliminated by architecture change.** Main's `SandboxService` + provider pattern replaces the separate sandbox server. Docker operations now happen in-process via Docker SDK instead of through HTTP to a separate server. This is a **design improvement**. |
+| `ii_sandbox_server/client/client.py` | HTTP client to sandbox server — unnecessary when Docker SDK calls are in-process. |
+| `ii_sandbox_server/lifecycle/queue.py` | Redis queue scheduler for sandbox operations — replaced by direct async calls in the service layer. |
+| `ii_sandbox_server/db/manager.py` | Separate sandbox DB — replaced by `AgentSandbox` model in main's unified DB. |
+| `src/ii_agent/adapters/sandbox_adapter.py` | Adapter between old `IISandbox` and `ii_tool.SandboxInterface` — both gone on main. |
+| `src/ii_agent/sandbox/ii_sandbox.py` | Old sandbox client — replaced by `Sandbox` abstract class + `DockerSandbox`. |
+| `src/ii_agent/server/*` (60+ files) | Entire old server package restructured into domain modules on main. |
+| `src/ii_agent/controller/*` | Old controller pattern — replaced by agent runtime + handler pattern. |
+| `src/ii_tool/*` changes | Tool changes were for old `SandboxInterface` bridge — main's tools call `Sandbox` directly. |
+| `start_sandbox_server.sh` | No longer needed — no separate sandbox server process. |
+| `scripts/run_stack.sh` | Replaced by `scripts/stack_control.sh`. |
+
+---
+
+## 3. Gap Analysis: Missing Features
+
+### Gap 1: Shell (PTY) Backend — SIGNIFICANT
+
+**Status:** Missing
+**Impact:** Medium-High
+
+E2BSandbox exposes a `shell` property returning `E2BShell` — a full persistent terminal backend implementing the `Shell` abstract class (18 abstract methods). `SandboxService` uses this for `create_shell_session`, `run_shell_command`, `kill_shell_command`, `list_shell_sessions`, etc.
+
+**DockerSandbox has no `shell` property.** It has `run_command()` (synchronous exec) and `create_live_terminal()` (WebSocket terminal), but no `Shell` subclass for persistent PTY session management.
+
+**Consequence:** Shell-based tools (`persistent_shell`) will raise `ShellOperationError("Persistent shell sessions are not supported by sandbox ...")` for Docker sandboxes.
+
+**Remediation options:**
+1. **DockerShell implementation** — Create `docker_shell.py` implementing `Shell` using Docker exec + tmux/screen for session persistence (similar to how `E2BShell` uses E2B's PTY API). The Docker sandbox already has `create_live_terminal()` which creates terminals; a `DockerShell` could build on `exec_run` with tmux session management.
+2. **Alternative design:** Use the existing `create_live_terminal()` WebSocket approach as the primary interactive shell, with `run_command()` as the fallback for non-interactive use. Most agent tool calls use `run_command()` already.
+
+**Assessment:** This gap is real but **mitigated** because:
+- Most agent tool execution uses `run_command()` (synchronous exec), not persistent shells
+- The persistent shell feature is primarily UI-facing (terminal tabs in the frontend)
+- `run_command()` works correctly for all tool-driven command execution
+
+### Gap 2: Sandbox Pause/Resume — PARTIAL
+
+**Status:** Partially implemented
+**Impact:** Low
+
+`DockerSandbox.pause()` calls `container.pause()` (Docker native pause). However:
+- Docker pause freezes processes in-place (SIGSTOP) — different from E2B's snapshot-and-destroy model
+- No explicit `resume()` / `unpause()` method (Docker API has `container.unpause()`)
+- The `awake_sandbox` Socket.IO handler calls `init_sandbox()` which reconnects via `connect()` — this works for Docker since the container is still alive when paused
+
+**Assessment:** Functionally adequate. Docker's pause/unpause is simpler and more reliable than E2B's snapshot model. A minor enhancement would be to add an explicit `unpause()` path in `connect()`.
+
+### Gap 3: Extended Timeout / Auto-Pause — COSMETIC
+
+**Status:** Config exists but unused for Docker
+**Impact:** Low
+
+`SandboxSettings.extended_timeout_seconds` and `auto_pause` are E2B-specific. Docker sandbox timeout is managed by `set_timeout()` which kills the container. No auto-pause-on-inactivity logic exists for Docker.
+
+**Assessment:** Docker containers persist until explicitly killed or timeout expires. This is actually better for local use — no unexpected pauses. Not a real gap.
+
+### Gap 4: Sandbox Explorer Integration — UNTESTED
+
+**Status:** Implemented but untested for Docker
+**Impact:** Low
+
+`explorer.py` provides `WorkspaceExplorerService` which calls `sandbox.list_files_with_contents()` and `sandbox.watch_dir()`. `DockerSandbox` implements both, but:
+- `watch_dir()` raises `NotImplementedError` — it's stubbed
+- `list_files_with_contents()` delegates to `list_files_recursive()` + `read_file_content()`
+
+**Assessment:** `watch_dir()` needs implementation for live workspace explorer. This is a pre-existing limitation (it was also missing in the old branch).
+
+---
+
+## 4. Database Migration Path
+
+### Current State
+
+| Aspect | Existing DB | Target (New Baseline) |
+|--------|-------------|----------------------|
+| Tables | 21 | 40 |
+| Alembic head | `f7g8h9i0j1k2` | `20260330_000000` chain |
+| ID types | `VARCHAR` (string UUIDs) | `UUID` (native) |
+| Session columns | `sandbox_id`, `llm_setting_id`, `status`, `agent_state_path`, `state_storage_url`, `deleted_at`, `prompt_tokens`, `completion_tokens`, `summary_message_id`, `cost` | `model_setting_id`, `app_kind`, `api_version`, `session_metadata`, `is_deleted` |
+| User columns | `credits`, `bonus_credits` | `language` + credit tables |
+| Table renames | `llm_settings` | `model_settings` |
+| | `events` | `application_events` / `agent_event_logs` |
+| | `file_uploads` | `user_assets` / `session_assets` |
+| | `provider_containers` | `chat_provider_containers` |
+
+### Key Schema Differences
+
+1. **ID type change:** All PKs and FKs changed from `VARCHAR` to `UUID(as_uuid=True)`. The existing data uses string-formatted UUIDs, so the values are compatible — but the column types must be `ALTER`ed.
+
+2. **Table renames:**
+ - `llm_settings` → `model_settings`
+ - `events` → split into `application_events` + `agent_event_logs`
+ - `file_uploads` → `user_assets` / `session_assets`
+ - `provider_containers` → `chat_provider_containers`
+ - `provider_files` → `chat_provider_files`
+ - `provider_vector_stores` → `chat_provider_vector_stores`
+ - `agent_run_tasks` → `agent_run_messages` (with structural changes)
+
+3. **Session table restructure:**
+ - Removed: `sandbox_id`, `agent_state_path`, `state_storage_url`, `prompt_tokens`, `completion_tokens`, `summary_message_id`, `cost`
+ - Renamed: `llm_setting_id` → `model_setting_id`, `deleted_at` → `is_deleted`
+ - Added: `app_kind`, `api_version`, `session_metadata`
+
+4. **New tables (19):** `agent_event_logs`, `agent_run_messages`, `agent_sandboxes`, `apple_credentials`, `chat_provider_*`, `chat_summaries`, `composio_profiles`, `credit_balances`, `credit_transactions`, `media_templates`, `model_settings`, `project_custom_domains`, `project_databases`, `run_tasks`, `session_assets`, `session_pins`, `session_summaries`, `skills`, `slide_versions`, `storybook*`, `task_logs`, `user_assets`
+
+5. **Tables to remove:** `session_metrics` (not in target)
+
+### Migration Strategy
+
+The schema differences are extensive enough that an incremental Alembic migration would be fragile. Recommended approach:
+
+#### Option A: Data-Preserving Fresh Start (RECOMMENDED)
+
+1. **Export critical data** from existing DB:
+ ```bash
+ # Export sessions, messages, and user
+ docker exec ii-agent-local-postgres-1 pg_dump -U iiagent -d iiagentdev \
+ --data-only -t users -t sessions -t chat_messages -t session_wishlists \
+ -t agent_run_tasks > /tmp/old_data.sql
+ ```
+
+2. **Reset DB with new schema:**
+ ```bash
+ docker exec ii-agent-local-postgres-1 psql -U iiagent -c "DROP DATABASE iiagentdev;"
+ docker exec ii-agent-local-postgres-1 psql -U iiagent -c "CREATE DATABASE iiagentdev;"
+ ```
+
+3. **Run Alembic migrations** (the app does this on startup):
+ ```bash
+ # Or let the app do it:
+ II_AGENT_SKIP_MIGRATIONS=false ./scripts/start.sh
+ ```
+
+4. **Transform and import data** via a migration script that:
+ - Converts `VARCHAR` IDs to `UUID` type
+ - Maps `users.id` (VARCHAR) → `users.id` (UUID)
+ - Maps `sessions.llm_setting_id` → `sessions.model_setting_id`
+ - Maps `sessions.deleted_at IS NOT NULL` → `sessions.is_deleted = true`
+ - Sets `sessions.app_kind = 'agent'` (or `'chat'` based on `agent_type`)
+ - Drops columns that no longer exist (`sandbox_id`, `agent_state_path`, etc.)
+ - Creates `agent_sandboxes` records from `sessions.sandbox_id` where non-null
+ - Imports `chat_messages` with UUID conversion on `session_id`
+
+#### Option B: In-Place Alembic Migration
+
+Write a custom Alembic migration that:
+1. Renames tables (`llm_settings` → `model_settings`, etc.)
+2. `ALTER COLUMN` to change `VARCHAR` → `UUID USING id::uuid`
+3. Adds new columns with defaults
+4. Drops deprecated columns
+5. Creates new tables
+6. Updates `alembic_version` to the new head
+
+This is more complex but avoids data round-tripping. The main risk is the `VARCHAR` → `UUID` type change on columns with foreign key constraints (requires dropping and re-creating FKs).
+
+### Recommended Migration Script Outline
+
+```python
+"""migrate_existing_data.py — Run after new schema is in place."""
+
+import asyncio
+import uuid
+from sqlalchemy import text
+from ii_agent.core.db.base import get_engine
+
+OLD_DB_URL = "postgresql://iiagent:...@localhost:5432/iiagentdev_old"
+NEW_DB_URL = "postgresql://iiagent:...@localhost:5432/iiagentdev"
+
+async def migrate():
+ # 1. Read from old DB
+ # 2. Transform records
+ # 3. Insert into new DB
+
+ # Users: VARCHAR id → UUID
+ # Sessions: rename columns, set defaults for new fields
+ # ChatMessages: keep content/role/usage, convert session_id
+ # AgentRunTasks → agent_run_messages: structural transform
+ pass
+```
+
+### Data Preservation Summary
+
+| Table | Records | Preservable? | Notes |
+|-------|---------|--------------|-------|
+| `users` | 1 | ✅ Yes | ID type conversion needed. `credits`/`bonus_credits` → `credit_balances` table |
+| `sessions` | 22 active | ✅ Yes | Column mapping needed (see above). Active sessions will continue. |
+| `chat_messages` | 317 | ✅ Yes | `session_id` VARCHAR→UUID. Schema mostly compatible. |
+| `agent_run_tasks` | 270 | ⚠️ Partial | Structure differs from `agent_run_messages`. Core fields preservable. |
+| `session_wishlists` | ? | ✅ Yes | Direct migration, ID conversion only |
+| `llm_settings` | ? | ✅ Yes | Rename to `model_settings`, ID conversion |
+| `mcp_settings` | ? | ✅ Yes | ID conversion only |
+| `slide_contents` | ? | ✅ Yes | ID conversion |
+| `slide_templates` | ? | ✅ Yes | ID conversion (seeded data may be re-created) |
+| `session_metrics` | ? | ❌ No | Table removed in new schema |
+| `connectors` | ? | ✅ Yes | Likely empty, ID conversion |
+
+---
+
+## 5. Summary & Recommendations
+
+### Porting Quality: EXCELLENT
+
+The rebase correctly identified that the old `ii_sandbox_server` intermediary pattern was eliminated by main's direct-provider architecture, and rebuilt the Docker sandbox as a first-class `Sandbox` subclass. All 26 abstract methods are implemented. The integration with `SandboxService`, lifespan, and config is clean and follows main's established patterns.
+
+### Action Items
+
+| Priority | Item | Effort |
+|----------|------|--------|
+| **P1** | Write data migration script for existing sessions | Medium |
+| **P2** | Implement `DockerShell` for persistent PTY sessions | Medium |
+| **P3** | Implement `watch_dir()` for workspace explorer | Low |
+| **P4** | Add `unpause()` call path in `connect()` for paused Docker containers | Low |
+
+### Risk Assessment
+
+- **No regressions to E2B:** All E2B changes are signature-only (`external` kwarg with default). Zero functional impact.
+- **No regressions to main features:** All changes are additive or guarded by `local_mode` flag.
+- **Frontend changes are backward-compatible:** `isSandboxLink()` is a superset of `isE2bLink()`. New state fields have empty defaults.
+- **Database migration is feasible** but requires a dedicated script due to the VARCHAR→UUID type change and column restructuring.
diff --git a/e2b.Dockerfile b/e2b.Dockerfile
index be04871bf..12fe4283d 100644
--- a/e2b.Dockerfile
+++ b/e2b.Dockerfile
@@ -57,6 +57,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
unzip \
libmagic1 \
xvfb \
+ x11vnc \
+ novnc \
+ websockify \
+ fluxbox \
pandoc \
weasyprint \
libpq-dev \
@@ -82,6 +86,16 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
# Optimization: Combine all curl installs and npm installs into fewer layers
RUN curl -fsSL https://code-server.dev/install.sh | sh
+# GitHub CLI (gh) — required by the Copilot A2A backend (`gh copilot agent`)
+RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
+ --mount=type=cache,target=/var/lib/apt,sharing=locked \
+ curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
+ -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
+ > /etc/apt/sources.list.d/github-cli.list && \
+ apt-get update && apt-get install -y gh && \
+ rm -rf /var/lib/apt/lists/*
+
# Optimization: Use npm cache mount and install playwright package and system deps as root
RUN --mount=type=cache,target=/root/.npm \
npm install -g agent-browser @intelligent-internet/codex @ast-grep/cli @anthropic-ai/claude-code
@@ -144,6 +158,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \
COPY src/ii_server /app/ii_sandbox/src/ii_server
COPY src/ii_agent_tools /app/ii_sandbox/src/ii_agent_tools
+# Copy the A2A adapter subtree + minimal parent __init__.py files so
+# `python -m ii_agent.integrations.a2a.adapter_server` resolves inside the sandbox.
+COPY src/ii_agent/__init__.py /app/ii_sandbox/src/ii_agent/__init__.py
+COPY src/ii_agent/integrations/__init__.py /app/ii_sandbox/src/ii_agent/integrations/__init__.py
+COPY src/ii_agent/integrations/a2a /app/ii_sandbox/src/ii_agent/integrations/a2a
+
# Optimization: Copy from cached location in codex-builder
COPY --from=codex-builder /sse-http-server /usr/local/bin/sse-http-server
@@ -185,10 +205,21 @@ ENV PATH="/home/user/.bun/bin:/app/ii_sandbox/.venv/bin:$PATH"
USER user
-# Install Playwright browser binaries
+# Install Playwright browser binaries and create system symlinks
RUN playwright install chromium
+USER root
+RUN CHROME_BIN=$(find /home/user/.cache/ms-playwright -name chrome -path '*/chrome-linux/*' | head -1) && \
+ ln -sf "$CHROME_BIN" /usr/local/bin/chromium-browser && \
+ ln -sf "$CHROME_BIN" /usr/local/bin/chromium && \
+ ln -sf "$CHROME_BIN" /usr/local/bin/google-chrome
+USER user
WORKDIR /home/user
+# A2A adapter port — served by ii_agent.integrations.a2a.adapter_server
+# (launched by start-services.sh; default 18100 is in the control-plane range 18000-18999)
+ENV SANDBOX_ADAPTER_PORT=18100
+EXPOSE 18100
+
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["bash", "/app/start-services.sh"]
diff --git a/frontend/package.json b/frontend/package.json
index cbb3d71a3..8968e730b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,7 +15,9 @@
"tauri": "tauri",
"prepare": "husky",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
- "format": "prettier --write ."
+ "format": "prettier --write .",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
@@ -128,6 +130,7 @@
"typescript": "^5.8.3",
"typescript-eslint": "^8.31.1",
"vite": "^6.3.4",
- "vite-plugin-svgr": "^4.3.0"
+ "vite-plugin-svgr": "^4.3.0",
+ "vitest": "^3.2.1"
}
}
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 0bf002b7f..acf4a603b 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -327,6 +327,9 @@ importers:
vite-plugin-svgr:
specifier: ^4.3.0
version: 4.3.0(rollup@4.46.2)(typescript@5.9.2)(vite@6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))
+ vitest:
+ specifier: ^3.2.1
+ version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)
packages:
@@ -1315,56 +1318,67 @@ packages:
resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.46.2':
resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.46.2':
resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.46.2':
resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.46.2':
resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.46.2':
resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.46.2':
resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.46.2':
resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.46.2':
resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.46.2':
resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.46.2':
resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.46.2':
resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==}
@@ -1615,24 +1629,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.12':
resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.12':
resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.12':
resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.12':
resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==}
@@ -1704,30 +1722,35 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.7.1':
resolution: {integrity: sha512-/HXY0t4FHkpFzjeYS5c16mlA6z0kzn5uKLWptTLTdFSnYpr8FCnOP4Sdkvm2TDQPF2ERxXtNCd+WR/jQugbGnA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.7.1':
resolution: {integrity: sha512-GeW5lVI2GhhnaYckiDzstG2j2Jwlud5d2XefRGwlOK+C/bVGLT1le8MNPYK8wgRlpeK8fG1WnJJYD6Ke7YQ8bg==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.7.1':
resolution: {integrity: sha512-DprxKQkPxIPYwUgg+cscpv2lcIUhn2nxEPlk0UeaiV9vATxCXyytxr1gLcj3xgjGyNPlM0MlJyYaPy1JmRg1cA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.7.1':
resolution: {integrity: sha512-KLlq3kOK7OUyDR757c0zQjPULpGZpLhNB0lZmZpHXvoOUcqZoCXJHh4dT/mryWZJp5ilrem5l8o9ngrDo0X1AA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.7.1':
resolution: {integrity: sha512-dH7KUjKkSypCeWPiainHyXoES3obS+JIZVoSwSZfKq2gWgs48FY3oT0hQNYrWveE+VR4VoR3b/F3CPGbgFvksA==}
@@ -1782,6 +1805,9 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
'@types/d3-array@3.2.2':
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
@@ -1878,6 +1904,9 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
'@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
@@ -2013,6 +2042,35 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+ '@vitest/expect@3.2.4':
+ resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
+
+ '@vitest/mocker@3.2.4':
+ resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@3.2.4':
+ resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
+
+ '@vitest/runner@3.2.4':
+ resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
+
+ '@vitest/snapshot@3.2.4':
+ resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
+
+ '@vitest/spy@3.2.4':
+ resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
+
+ '@vitest/utils@3.2.4':
+ resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
+
'@xterm/addon-fit@0.10.0':
resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
peerDependencies:
@@ -2108,6 +2166,10 @@ packages:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
async-function@1.0.0:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
@@ -2154,6 +2216,10 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ cac@6.7.14:
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+ engines: {node: '>=8'}
+
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -2184,6 +2250,10 @@ packages:
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+ chai@5.3.3:
+ resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
+ engines: {node: '>=18'}
+
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -2204,6 +2274,10 @@ packages:
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+ check-error@2.1.3:
+ resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
+ engines: {node: '>= 16'}
+
chevrotain-allstar@0.3.1:
resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==}
peerDependencies:
@@ -2518,6 +2592,10 @@ packages:
decode-named-character-reference@1.2.0:
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
+ deep-eql@5.0.2:
+ resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
+ engines: {node: '>=6'}
+
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -2629,6 +2707,9 @@ packages:
resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==}
engines: {node: '>= 0.4'}
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
@@ -2718,6 +2799,9 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -2733,6 +2817,10 @@ packages:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
+ expect-type@1.3.0:
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+ engines: {node: '>=12.0.0'}
+
exsolve@1.0.7:
resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
@@ -3229,6 +3317,9 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ js-tokens@9.0.1:
+ resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
@@ -3327,24 +3418,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-musl@1.30.1:
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-gnu@1.30.1:
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.30.1:
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-win32-arm64-msvc@1.30.1:
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
@@ -3415,6 +3510,9 @@ packages:
lottie-web@5.13.0:
resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==}
+ loupe@3.2.1:
+ resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+
lower-case@2.0.2:
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
@@ -3865,6 +3963,10 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+ pathval@2.0.1:
+ resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
+ engines: {node: '>= 14.16'}
+
performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
@@ -4278,6 +4380,9 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
@@ -4321,6 +4426,9 @@ packages:
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
stackblur-canvas@2.7.0:
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
engines: {node: '>=0.1.14'}
@@ -4328,6 +4436,9 @@ packages:
state-local@1.0.7:
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
stop-iteration-iterator@1.1.0:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
@@ -4382,6 +4493,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
+ strip-literal@3.1.0:
+ resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
+
style-to-js@1.1.17:
resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==}
@@ -4433,6 +4547,12 @@ packages:
text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@0.3.2:
+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
+
tinyexec@1.0.1:
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
@@ -4440,6 +4560,18 @@ packages:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
+ tinypool@1.1.1:
+ resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+
+ tinyrainbow@2.0.0:
+ resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
+ engines: {node: '>=14.0.0'}
+
+ tinyspy@4.0.4:
+ resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
+ engines: {node: '>=14.0.0'}
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -4604,6 +4736,11 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ vite-node@3.2.4:
+ resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+
vite-plugin-svgr@4.3.0:
resolution: {integrity: sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==}
peerDependencies:
@@ -4649,6 +4786,34 @@ packages:
yaml:
optional: true
+ vitest@3.2.4:
+ resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/debug': ^4.1.12
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ '@vitest/browser': 3.2.4
+ '@vitest/ui': 3.2.4
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/debug':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
@@ -4710,6 +4875,11 @@ packages:
engines: {node: '>= 8'}
hasBin: true
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -6153,6 +6323,11 @@ snapshots:
dependencies:
'@babel/types': 7.28.2
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
'@types/d3-array@3.2.2': {}
'@types/d3-axis@3.0.6':
@@ -6274,6 +6449,8 @@ snapshots:
dependencies:
'@types/ms': 2.1.0
+ '@types/deep-eql@4.0.2': {}
+
'@types/estree-jsx@1.0.5':
dependencies:
'@types/estree': 1.0.8
@@ -6447,6 +6624,48 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@vitest/expect@3.2.4':
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/spy': 3.2.4
+ '@vitest/utils': 3.2.4
+ chai: 5.3.3
+ tinyrainbow: 2.0.0
+
+ '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))':
+ dependencies:
+ '@vitest/spy': 3.2.4
+ estree-walker: 3.0.3
+ magic-string: 0.30.17
+ optionalDependencies:
+ vite: 6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)
+
+ '@vitest/pretty-format@3.2.4':
+ dependencies:
+ tinyrainbow: 2.0.0
+
+ '@vitest/runner@3.2.4':
+ dependencies:
+ '@vitest/utils': 3.2.4
+ pathe: 2.0.3
+ strip-literal: 3.1.0
+
+ '@vitest/snapshot@3.2.4':
+ dependencies:
+ '@vitest/pretty-format': 3.2.4
+ magic-string: 0.30.17
+ pathe: 2.0.3
+
+ '@vitest/spy@3.2.4':
+ dependencies:
+ tinyspy: 4.0.4
+
+ '@vitest/utils@3.2.4':
+ dependencies:
+ '@vitest/pretty-format': 3.2.4
+ loupe: 3.2.1
+ tinyrainbow: 2.0.0
+
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
dependencies:
'@xterm/xterm': 5.5.0
@@ -6583,6 +6802,8 @@ snapshots:
get-intrinsic: 1.3.0
is-array-buffer: 3.0.5
+ assertion-error@2.0.1: {}
+
async-function@1.0.0: {}
asynckit@0.4.0: {}
@@ -6630,6 +6851,8 @@ snapshots:
buffer-from@1.1.2: {}
+ cac@6.7.14: {}
+
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -6667,6 +6890,14 @@ snapshots:
ccount@2.0.1: {}
+ chai@5.3.3:
+ dependencies:
+ assertion-error: 2.0.1
+ check-error: 2.1.3
+ deep-eql: 5.0.2
+ loupe: 3.2.1
+ pathval: 2.0.1
+
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@@ -6682,6 +6913,8 @@ snapshots:
character-reference-invalid@2.0.1: {}
+ check-error@2.1.3: {}
+
chevrotain-allstar@0.3.1(chevrotain@11.0.3):
dependencies:
chevrotain: 11.0.3
@@ -7024,6 +7257,8 @@ snapshots:
dependencies:
character-entities: 2.0.2
+ deep-eql@5.0.2: {}
+
deep-is@0.1.4: {}
define-data-property@1.1.4:
@@ -7200,6 +7435,8 @@ snapshots:
iterator.prototype: 1.1.5
safe-array-concat: 1.1.3
+ es-module-lexer@1.7.0: {}
+
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
@@ -7353,6 +7590,10 @@ snapshots:
estree-walker@2.0.2: {}
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
esutils@2.0.3: {}
eventemitter3@5.0.1: {}
@@ -7371,6 +7612,8 @@ snapshots:
signal-exit: 4.1.0
strip-final-newline: 3.0.0
+ expect-type@1.3.0: {}
+
exsolve@1.0.7: {}
extend@3.0.2: {}
@@ -7908,6 +8151,8 @@ snapshots:
js-tokens@4.0.0: {}
+ js-tokens@9.0.1: {}
+
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
@@ -8095,6 +8340,8 @@ snapshots:
lottie-web@5.13.0: {}
+ loupe@3.2.1: {}
+
lower-case@2.0.2:
dependencies:
tslib: 2.8.1
@@ -8781,6 +9028,8 @@ snapshots:
pathe@2.0.3: {}
+ pathval@2.0.1: {}
+
performance-now@2.1.0:
optional: true
@@ -9276,6 +9525,8 @@ snapshots:
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
+ siginfo@2.0.0: {}
+
signal-exit@4.1.0: {}
slice-ansi@5.0.0:
@@ -9327,11 +9578,15 @@ snapshots:
space-separated-tokens@2.0.2: {}
+ stackback@0.0.2: {}
+
stackblur-canvas@2.7.0:
optional: true
state-local@1.0.7: {}
+ std-env@3.10.0: {}
+
stop-iteration-iterator@1.1.0:
dependencies:
es-errors: 1.3.0
@@ -9432,6 +9687,10 @@ snapshots:
strip-json-comments@3.1.1: {}
+ strip-literal@3.1.0:
+ dependencies:
+ js-tokens: 9.0.1
+
style-to-js@1.1.17:
dependencies:
style-to-object: 1.0.9
@@ -9484,6 +9743,10 @@ snapshots:
utrie: 1.0.2
optional: true
+ tinybench@2.9.0: {}
+
+ tinyexec@0.3.2: {}
+
tinyexec@1.0.1: {}
tinyglobby@0.2.14:
@@ -9491,6 +9754,12 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
+ tinypool@1.1.1: {}
+
+ tinyrainbow@2.0.0: {}
+
+ tinyspy@4.0.4: {}
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -9690,6 +9959,27 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
+ vite-node@3.2.4(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.4.1
+ es-module-lexer: 1.7.0
+ pathe: 2.0.3
+ vite: 6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)
+ transitivePeerDependencies:
+ - '@types/node'
+ - jiti
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
vite-plugin-svgr@4.3.0(rollup@4.46.2)(typescript@5.9.2)(vite@6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)):
dependencies:
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
@@ -9717,6 +10007,48 @@ snapshots:
terser: 5.43.1
yaml: 2.8.1
+ vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1):
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/expect': 3.2.4
+ '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1))
+ '@vitest/pretty-format': 3.2.4
+ '@vitest/runner': 3.2.4
+ '@vitest/snapshot': 3.2.4
+ '@vitest/spy': 3.2.4
+ '@vitest/utils': 3.2.4
+ chai: 5.3.3
+ debug: 4.4.1
+ expect-type: 1.3.0
+ magic-string: 0.30.17
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.14
+ tinypool: 1.1.1
+ tinyrainbow: 2.0.0
+ vite: 6.3.5(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)
+ vite-node: 3.2.4(@types/node@22.17.2)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.1)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/debug': 4.1.12
+ '@types/node': 22.17.2
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
void-elements@3.1.0: {}
vscode-jsonrpc@8.2.0: {}
@@ -9794,6 +10126,11 @@ snapshots:
dependencies:
isexe: 2.0.0
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
word-wrap@1.2.5: {}
wrap-ansi@9.0.0:
diff --git a/frontend/src/app/routes/agent.tsx b/frontend/src/app/routes/agent.tsx
index cc236a2e2..a5caf7c34 100644
--- a/frontend/src/app/routes/agent.tsx
+++ b/frontend/src/app/routes/agent.tsx
@@ -13,6 +13,7 @@ import AgentTasks from '@/components/agent/agent-task'
import ChatBox from '@/components/agent/chat-box'
import AgentHeader from '@/components/header'
import RightSidebar from '@/components/right-sidebar'
+import { rewriteLocalhostUrl } from '@/lib/utils'
import { sessionService } from '@/services/session.service'
import {
selectActiveTab,
@@ -91,7 +92,7 @@ function AgentPageContent() {
)
// PiP preview URL (mobile takes priority over fullstack)
- const pipUrl = mobileWebPreviewUrl || previewUrl
+ const pipUrl = rewriteLocalhostUrl(mobileWebPreviewUrl || previewUrl)
const showPiP =
!isMobile &&
activeTab !== TAB.RESULT &&
@@ -160,6 +161,11 @@ function AgentPageContent() {
fetchSession()
}, 5000)
} else {
+ // Redirect chat sessions to the chat page
+ if (data.agent_type === 'chat') {
+ navigate(`/chat?id=${sessionId}`, { replace: true })
+ return
+ }
dispatch(setSelectedFeature(data.agent_type ?? null))
dispatch(setProjectId(data.project_id ?? null))
setSessionData(data)
diff --git a/frontend/src/app/routes/dashboard.tsx b/frontend/src/app/routes/dashboard.tsx
index 01cefd65a..4901a122b 100644
--- a/frontend/src/app/routes/dashboard.tsx
+++ b/frontend/src/app/routes/dashboard.tsx
@@ -45,9 +45,11 @@ import {
import { wishlistService } from '@/services/wishlist.service'
import { sessionService } from '@/services/session.service'
import { ISession } from '@/typings/agent'
-import { deleteSession } from '@/state/slice/sessions'
+import { deleteSession, selectActiveSessionId } from '@/state/slice/sessions'
import { clearSessionState } from '@/state/slice/session-state'
import { removePin } from '@/state/slice/pins'
+import { setRunStatus } from '@/state/slice/agent'
+import { setLoading } from '@/state'
enum TAB {
ALL = 'all',
@@ -74,6 +76,7 @@ export function DashboardPage() {
const currentPage = useAppSelector(selectSessionsPage)
const limit = useAppSelector(selectSessionsLimit)
const favoriteSessionIds = useAppSelector(selectFavoriteSessionIds)
+ const activeSessionId = useAppSelector(selectActiveSessionId)
const handleBack = () => {
navigate(-1)
@@ -117,6 +120,10 @@ export function DashboardPage() {
await dispatch(deleteSession(deleteSessionId)).unwrap()
dispatch(clearSessionState(deleteSessionId))
dispatch(removePin(deleteSessionId))
+ if (deleteSessionId === activeSessionId) {
+ dispatch(setRunStatus(null))
+ dispatch(setLoading(false))
+ }
setIsDeleteDialogOpen(false)
setDeleteSessionId(null)
} catch (error) {
diff --git a/frontend/src/app/routes/login.tsx b/frontend/src/app/routes/login.tsx
index 8b278afef..427ad861a 100644
--- a/frontend/src/app/routes/login.tsx
+++ b/frontend/src/app/routes/login.tsx
@@ -1,5 +1,5 @@
import { useGoogleLogin } from '@react-oauth/google'
-import { useCallback, useEffect, useMemo, useRef } from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Link, useNavigate } from 'react-router'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
@@ -344,6 +344,10 @@ export function LoginPage() {
/>
{t('auth.continueWithII')}
+
{t('auth.privacyNotice')}{' '}
@@ -359,4 +363,53 @@ export function LoginPage() {
)
}
+/**
+ * Dev login button - only shows if DEV_AUTH_ENABLED is set on backend
+ */
+function DevLoginButton({
+ apiBaseUrl,
+ onSuccess
+}: {
+ apiBaseUrl: string
+ onSuccess: (payload: IiAuthPayload | null | undefined) => Promise
+}) {
+ const [isAvailable, setIsAvailable] = useState(null)
+
+ useEffect(() => {
+ // Check if dev login is available
+ fetch(`${apiBaseUrl}/auth/dev/login`)
+ .then((res) => {
+ setIsAvailable(res.ok)
+ })
+ .catch(() => setIsAvailable(false))
+ }, [apiBaseUrl])
+
+ const handleDevLogin = async () => {
+ try {
+ const res = await fetch(`${apiBaseUrl}/auth/dev/login`)
+ if (!res.ok) {
+ throw new Error('Dev login failed')
+ }
+ const data = await res.json()
+ await onSuccess(data)
+ } catch (error) {
+ console.error('Dev login failed:', error)
+ }
+ }
+
+ if (isAvailable !== true) {
+ return null
+ }
+
+ return (
+
+ )
+}
+
export const Component = LoginPage
diff --git a/frontend/src/components/agent/agent-result.tsx b/frontend/src/components/agent/agent-result.tsx
index 55317f22b..6549281cd 100644
--- a/frontend/src/components/agent/agent-result.tsx
+++ b/frontend/src/components/agent/agent-result.tsx
@@ -7,6 +7,7 @@ import {
selectIsLoading,
selectIsSandboxIframeAwake,
selectMessages,
+ selectSandboxStatus,
useAppSelector
} from '@/state'
import { CommandType, TAB, TOOL } from '@/typings/agent'
@@ -15,7 +16,7 @@ import MobileResult from './mobile-result'
import { Icon } from '../ui/icon'
import AwakeMeUpScreen from './awake-me-up-screen'
import { useLocation, useParams } from 'react-router'
-import { cn, isE2bLink } from '@/lib/utils'
+import { cn, isSandboxLink, rewriteLocalhostUrl } from '@/lib/utils'
import { DesignModeWrapper } from '@/components/design-mode'
import { useTranslation } from 'react-i18next'
import {
@@ -45,6 +46,7 @@ const AgentResult = ({ className }: AgentResultProps) => {
const activeTab = useAppSelector(selectActiveTab)
const isSandboxIframeAwake = useAppSelector(selectIsSandboxIframeAwake)
+ const sandboxStatus = useAppSelector(selectSandboxStatus)
const messages = useAppSelector(selectMessages)
const isRunning = useAppSelector(selectIsLoading)
const isShareMode = useMemo(
@@ -89,7 +91,7 @@ const AgentResult = ({ className }: AgentResultProps) => {
mobileAppResult as { web_preview_url?: string }
).web_preview_url
if (webPreviewUrl) {
- return webPreviewUrl
+ return rewriteLocalhostUrl(webPreviewUrl)
}
}
@@ -106,7 +108,7 @@ const AgentResult = ({ className }: AgentResultProps) => {
if (result && typeof result === 'object') {
const previewUrl = (result as { preview_url?: string }).preview_url
if (previewUrl) {
- return previewUrl
+ return rewriteLocalhostUrl(previewUrl)
}
}
return ''
@@ -256,12 +258,12 @@ const AgentResult = ({ className }: AgentResultProps) => {
const shouldShowAwakeScreen = useMemo(() => {
return (
- isE2bLink(resultUrl) &&
+ sandboxStatus === 'paused' &&
!isSandboxIframeAwake &&
!isRunning &&
!isShareMode
)
- }, [resultUrl, isSandboxIframeAwake, isRunning, isShareMode])
+ }, [sandboxStatus, isSandboxIframeAwake, isRunning, isShareMode])
// Extract slide data from SlideWrite and SlideEdit messages
const slideContent = useMemo(() => {
@@ -323,7 +325,7 @@ const AgentResult = ({ className }: AgentResultProps) => {
// Check if design mode should be available (only for e2b sandbox websites)
const isDesignModeAvailable = useMemo(() => {
if (!resultUrl) return false
- if (!isE2bLink(resultUrl)) return false
+ if (!isSandboxLink(resultUrl)) return false
if (detectUrlType(resultUrl) !== 'website') return false
if (isShareMode) return false
return true
@@ -338,8 +340,6 @@ const AgentResult = ({ className }: AgentResultProps) => {
)
}
- if (!resultUrl && !mobileAppUrl) return null
-
if (shouldShowAwakeScreen)
return (
{
/>
)
+ if (!resultUrl && !mobileAppUrl) return null
+
if (hasMobileAppTools && activeTab === TAB.RESULT) {
return (
{
const activeTab = useAppSelector(selectActiveTab)
const vscodeUrl = useAppSelector(selectVscodeUrl)
+ const vncUrl = useAppSelector(selectVncUrl)
const isShareMode = useMemo(
() => location.pathname.includes('/share/'),
@@ -44,6 +46,15 @@ const AgentTabs = ({ sessionId, projectId, agentType }: AgentTabsProps) => {
window.open(vscodeUrl, '_blank')
}
+ const handleOpenVNC = () => {
+ if (!vncUrl) {
+ toast.error(t('agentTab.errors.vncUrlMissing', 'noVNC URL not available'))
+ return
+ }
+
+ window.open(vncUrl, '_blank')
+ }
+
const shouldShowProjectTab = useMemo(() => {
if (isShareMode) {
return false
@@ -114,6 +125,15 @@ const AgentTabs = ({ sessionId, projectId, agentType }: AgentTabsProps) => {
{t('agentTab.openInVSCode')}
)}
+ {vncUrl && !isShareMode && (
+
+ )}
{agentType === AGENT_TYPE.MOBILE_APP ? (
{
const { t } = useTranslation()
const messages = useAppSelector(selectMessages)
+ const isStopped = useAppSelector(selectIsStopped)
const dispatch = useAppDispatch()
const [plans, setPlans] = useState([])
@@ -28,6 +29,9 @@ const AgentTasks = ({ className }: AgentTasksProps) => {
}, [messages])
useEffect(() => {
+ // Don't auto-promote tasks if the agent is stopped
+ if (isStopped) return
+
if (Array.isArray(plans)) {
// Check if there are no in_progress tasks
const hasInProgress = plans.some(
@@ -50,11 +54,11 @@ const AgentTasks = ({ className }: AgentTasksProps) => {
}
}
}
- }, [plans, dispatch])
+ }, [plans, dispatch, isStopped])
const inProgressPlans = useMemo(
- () => countBy(plans, 'status').in_progress || 0,
- [plans]
+ () => isStopped ? 0 : (countBy(plans, 'status').in_progress || 0),
+ [plans, isStopped]
)
const completedPlans = useMemo(
@@ -69,7 +73,7 @@ const AgentTasks = ({ className }: AgentTasksProps) => {
className={`flex flex-col items-center justify-center w-full ${className}`}
>
- {t('agent.tasks.inProgress')}
+ {isStopped ? t('agent.tasks.stopped', 'Stopped') : t('agent.tasks.inProgress')}
diff --git a/frontend/src/components/agent/subagent-container.tsx b/frontend/src/components/agent/subagent-container.tsx
index f88149ba2..27f107240 100644
--- a/frontend/src/components/agent/subagent-container.tsx
+++ b/frontend/src/components/agent/subagent-container.tsx
@@ -7,12 +7,14 @@ import {
CheckCircle2,
XCircle,
Loader2,
- Clock
+ Clock,
+ StopCircle
} from 'lucide-react'
import { useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { AgentContext, Message } from '@/typings/agent'
import { formatDuration } from '@/lib/utils'
+import { useAppSelector, selectIsStopped, selectIsLoading } from '@/state'
interface SubagentContainerProps {
agentContext: AgentContext
@@ -23,7 +25,8 @@ interface SubagentContainerProps {
enum SubAgentStatus {
RUNNING = 'running',
COMPLETED = 'completed',
- FAILED = 'failed'
+ FAILED = 'failed',
+ STOPPED = 'stopped'
}
const SubagentContainer = ({
@@ -33,6 +36,8 @@ const SubagentContainer = ({
}: SubagentContainerProps) => {
const { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(true)
+ const isStopped = useAppSelector(selectIsStopped)
+ const isLoading = useAppSelector(selectIsLoading)
// Calculate execution time
const executionTime = useMemo(() => {
@@ -51,6 +56,7 @@ const SubagentContainer = ({
}, [messages])
// Determine actual status - explicit failed status takes precedence over endTime
+ // Also check global isStopped/isLoading state to determine subagent status
const actualStatus = useMemo(() => {
if (agentContext.status === SubAgentStatus.FAILED) {
return SubAgentStatus.FAILED
@@ -58,14 +64,25 @@ const SubagentContainer = ({
if (agentContext.endTime) {
return SubAgentStatus.COMPLETED
}
- return agentContext.status || SubAgentStatus.RUNNING
- }, [agentContext.status, agentContext.endTime])
+ const contextStatus = agentContext.status || SubAgentStatus.RUNNING
+ // If global agent is stopped and this subagent was still running, show as stopped
+ if (isStopped && contextStatus === SubAgentStatus.RUNNING) {
+ return SubAgentStatus.STOPPED
+ }
+ // If main agent is done (not loading, not stopped) and subagent is still "running",
+ // it means the subagent completed but wasn't marked - show as completed
+ if (!isLoading && !isStopped && contextStatus === SubAgentStatus.RUNNING) {
+ return SubAgentStatus.COMPLETED
+ }
+ return contextStatus
+ }, [agentContext.status, agentContext.endTime, isStopped, isLoading])
const statusLabel = useMemo(() => {
const keyMap: Record
= {
[SubAgentStatus.RUNNING]: 'agent.subagent.status.running',
[SubAgentStatus.COMPLETED]: 'agent.subagent.status.completed',
- [SubAgentStatus.FAILED]: 'agent.subagent.status.failed'
+ [SubAgentStatus.FAILED]: 'agent.subagent.status.failed',
+ [SubAgentStatus.STOPPED]: 'agent.subagent.status.stopped'
}
return t(keyMap[actualStatus] || 'agent.subagent.status.running')
}, [actualStatus, t])
@@ -77,6 +94,8 @@ const SubagentContainer = ({
return
case SubAgentStatus.FAILED:
return
+ case SubAgentStatus.STOPPED:
+ return
case SubAgentStatus.RUNNING:
return
default:
@@ -152,6 +171,7 @@ const SubagentContainer = ({
${actualStatus === SubAgentStatus.COMPLETED ? 'bg-green-500/20 text-green-400' : ''}
${actualStatus === SubAgentStatus.RUNNING ? 'bg-blue-500/20 text-blue-400' : ''}
${actualStatus === SubAgentStatus.FAILED ? 'bg-red-500/20 text-red-400' : ''}
+ ${actualStatus === SubAgentStatus.STOPPED ? 'bg-yellow-500/20 text-yellow-400' : ''}
`}
>
{statusLabel}
diff --git a/frontend/src/components/chat-header-mobile.tsx b/frontend/src/components/chat-header-mobile.tsx
index 27aff14cc..2cf4ce074 100644
--- a/frontend/src/components/chat-header-mobile.tsx
+++ b/frontend/src/components/chat-header-mobile.tsx
@@ -14,6 +14,7 @@ import {
} from '@/state'
import { deleteSession } from '@/state/slice/sessions'
import { clearSessionState } from '@/state/slice/session-state'
+import { setRunStatus } from '@/state/slice/agent'
import { type ISession } from '@/typings/agent'
import HeaderDropdownMenu from '@/components/header-dropdown-menu'
import ShareConversation from '@/components/agent/share-conversation'
@@ -74,6 +75,7 @@ const ChatHeaderMobile = ({
try {
await dispatch(deleteSession(sessionId)).unwrap()
dispatch(clearSessionState(sessionId))
+ dispatch(setRunStatus(null))
setIsDeleteDialogOpen(false)
navigate('/')
} catch (error) {
diff --git a/frontend/src/components/chat-header.tsx b/frontend/src/components/chat-header.tsx
index 921b2c581..9abac8bbe 100644
--- a/frontend/src/components/chat-header.tsx
+++ b/frontend/src/components/chat-header.tsx
@@ -28,6 +28,7 @@ import { useSearchParams } from 'react-router'
import { useNavigate } from 'react-router'
import { deleteSession } from '@/state/slice/sessions'
import { clearSessionState } from '@/state/slice/session-state'
+import { setRunStatus } from '@/state/slice/agent'
import ShareConversation from '@/components/agent/share-conversation'
import {
AlertDialog,
@@ -126,6 +127,10 @@ const ChatHeader = ({
try {
await dispatch(deleteSession(sessionId)).unwrap()
dispatch(clearSessionState(sessionId))
+ resetSessionState()
+ resetConversationState()
+ setSessionId(null)
+ dispatch(setRunStatus(null))
setIsDeleteDialogOpen(false)
navigate('/')
} catch (error) {
diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx
index ec9b3e736..00396c0d8 100644
--- a/frontend/src/components/header.tsx
+++ b/frontend/src/components/header.tsx
@@ -20,6 +20,7 @@ import {
} from '@/state'
import { deleteSession } from '@/state/slice/sessions'
import { clearSessionState } from '@/state/slice/session-state'
+import { setRunStatus } from '@/state/slice/agent'
import { ISession } from '@/typings'
import {
AlertDialog,
@@ -90,6 +91,7 @@ const AgentHeader = ({ sessionData, isChatPage }: AgentHeaderProps) => {
await dispatch(deleteSession(sessionId)).unwrap()
// Clear cached session state to free up localStorage
dispatch(clearSessionState(sessionId))
+ dispatch(setRunStatus(null))
setIsDeleteDialogOpen(false)
// Navigate to home page after deletion
navigate('/')
diff --git a/frontend/src/components/project-list.tsx b/frontend/src/components/project-list.tsx
index 6464211fc..d5afc292e 100644
--- a/frontend/src/components/project-list.tsx
+++ b/frontend/src/components/project-list.tsx
@@ -45,6 +45,9 @@ import { hasSessionDisplayTitle } from '@/utils/session-title'
interface ProjectListProps {
workspaceInfo?: string
isLoading: boolean
+ loadingMore: boolean
+ hasMore: boolean
+ onLoadMore: () => void
handleResetState: () => void
handleNewProject: () => void
}
@@ -52,6 +55,9 @@ interface ProjectListProps {
const ProjectList = ({
workspaceInfo,
isLoading,
+ loadingMore,
+ hasMore,
+ onLoadMore,
handleResetState,
handleNewProject
}: ProjectListProps) => {
@@ -322,6 +328,25 @@ const ProjectList = ({
{t('sidebar.seeMore')}
)}
+ {loadingMore && (
+
+ {t('common.loadingMore')}
+
+ )}
+ {!loadingMore && hasMore && showAllProjects && (
+
+ )}
{
e.preventDefault()
e.stopPropagation()
+ setIsDropdownOpen(false)
setIsDeleteDialogOpen(true)
}
@@ -105,6 +106,10 @@ const SessionItem = ({
await dispatch(deleteSession(session.id)).unwrap()
dispatch(clearSessionState(session.id))
dispatch(removePin(session.id))
+ if (isActive) {
+ dispatch(setRunStatus(null))
+ dispatch(setLoading(false))
+ }
setIsDeleteDialogOpen(false)
} catch (error) {
console.error('Failed to delete session:', error)
diff --git a/frontend/src/components/share-agent-content.tsx b/frontend/src/components/share-agent-content.tsx
index b36a59d5d..e872bac26 100644
--- a/frontend/src/components/share-agent-content.tsx
+++ b/frontend/src/components/share-agent-content.tsx
@@ -28,7 +28,7 @@ import {
import { BUILD_STEP, ISession, TAB } from '@/typings/agent'
import AgentResult from '@/components/agent/agent-result'
import AgentPopoverDone from '@/components/agent/agent-popover-done'
-import { isE2bLink } from '@/lib/utils'
+import { isSandboxLink } from '@/lib/utils'
import { SidebarProvider } from '@/components/ui/sidebar'
import AgentTabMobile, {
type ChatOption as MobileChatOption
@@ -76,7 +76,9 @@ export function ShareAgentContent() {
fetchSession()
}, 5000)
} else {
- dispatch(setSelectedFeature(data.agent_type ?? null))
+ // Normalize chat sessions to 'general' to prevent invalid agent_type
+ const agentType = data.agent_type === 'chat' ? 'general' : (data.agent_type ?? null)
+ dispatch(setSelectedFeature(agentType))
setSessionData(data)
setSessionError(null) // Clear any previous errors
}
@@ -234,7 +236,7 @@ export function ShareAgentContent() {
- {vscodeUrl && isE2bLink(vscodeUrl) && (
+ {vscodeUrl && isSandboxLink(vscodeUrl) && (