The notification dropdown was at z-index: 100, well below fullscreen
terminals (1000), modal overlays (1000), and dialog overlays (1000).
Because .notification-center creates a stacking context with no explicit
z-index, the dropdown was trapped behind any of those overlays and
became unclickable.
- Set .notification-center z-index to 9999 so it competes above all
other overlays in the root stacking context.
- Set .notification-dropdown z-index to 9999 for consistency.
Quality gates: tsc --noEmit (pass), build (pass)
Terminal sessions are opened in a new tab via window.open with
noopener,noreferrer. In a new tab, window.history has no previous
entry, so navigate(-1) silently does nothing. Both the back arrow
and the X button in the mobile terminal overlay called navigate(-1),
which made them appear broken.
Change both buttons to navigate('/sessions') so they always exit
to a sensible page regardless of how the terminal was opened.
Quality gates: tsc --noEmit (pass), build (pass)
When a workspace is provided, auto-generated display names now use
workspace.name instead of repo.name:
'myworkspace / VS Code Server' # first
'myworkspace / VS Code Server #2' # second
Without a workspace, naming falls back to repo.name:
'myrepo / VS Code Server'
'myrepo / VS Code Server #2'
The counter is scoped to workspace+tool_type (or repo+tool_type),
so different tool types for the same workspace/repo are numbered
independently.
This replaces the old format of 'project / repo / tool #N' which
was always repo-based and included the project name even though
the sidebar already groups by project.
Quality gates: py_compile passed, ruff passed.
services/correlation.py was moved to services/shared/correlation.py,
services/event_bus.py to services/instance/event_bus.py, and
services/health_monitor.py to services/instance/health_monitor.py
but main.py was still importing from the old flat paths.
Updated main.py to import from the new subpackage paths via
__init__.py re-exports.
Quality gates: py_compile passed, ruff passed.
Fixed import paths for 43 components moved into features/ directories.
Key fixes:
- api/, types/, hooks/, state/, utils/ imports need ../../../ from features/*/
- components/ imports need ../../ from features/*/
- Cross-feature imports use relative paths (e.g., ../tool/tools-bottom-sheet)
- app-shell.tsx updated to import from features/ subdirectories
Frontend typecheck now passes except for one pre-existing error:
xterm-addon-webgl missing type declarations.
Quality gates: ruff passed on backend, py_compile passed on all backend files.
Recovers and adapts the structural refactoring from overwritten
main merge (b6f89f9) to current dev reality.
Scope:
- Schema extraction into apps/api/src/schemas/
- Docker service split into services/docker/ package
- Instance lifecycle extraction from api/tool_instances.py
- Config profile service extraction from api/config_profiles.py
- Auth dependency refactor (get_current_user)
- Frontend reorganization into features/ dirs + kebab-case naming
Exclusions (already in dev): seeding, defaults, unique constraint,
SSH key mounting, terminal backend, tunnel regex, session auto-numbering.
The .dockerignore added in 8a0d82f incorrectly excluded wait-for-db.sh,
but the Dockerfile copies it as the container entrypoint. This caused
the Docker build to fail with 'failed to compute cache key: not found'.
Quality gates: verified file exists, py_compile passed.
1. Built-in tool type seeding (apps/api/src/seeds/builtin_tool_types.py):
- Seeds code-server, jupyter-notebook, and opencode on startup.
- Adapts to current dev model: uses interface_type (single string)
instead of interfaces array, and created_by_id=None instead of
is_builtin flag.
- Called from main.py startup event.
2. Config profile default management:
- Adds default_profile_id and default_profiles properties to
UserConfig model for JSON-backed per-tool-type defaults.
- Adds GET /config-profiles/defaults, PUT /config-profiles/defaults,
and GET /config-profiles/defaults/{tool_type_id} endpoints.
- Validates that all profile IDs in default mappings belong to the
authenticated user before persisting.
3. Config profile unique constraint:
- Adds __table_args__ with UniqueConstraint(user_id, name) to
ConfigProfile model. The constraint already exists in the DB
from migration 2026_05_24_add_config_profiles.py; this just
aligns the SQLAlchemy model with the schema.
Quality gates: py_compile passed, ruff passed on all modified files.
From ae02e97 ('fix: tunnel URLs, session naming, git control bar placement'):
1. Tunnel URL regex: exclude api.trycloudflare.com from pattern.
Real tunnel subdomains are 10+ random chars. Prevents matching the
Cloudflare API endpoint instead of the actual tunnel URL.
2. Session auto-numbering: when user doesn't provide a display_name,
auto-generate 'project / repo / tool_type #N' where N increments
for each existing instance with the same project/repo/tool_type.
Prevents confusing duplicate display names in the sidebar.
These fixes were lost when main's merge was overwritten. Ported to
our clean dev codebase.
Quality gates: py_compile passed, ruff passed on tool_instances.py and tunnel.py.
Add .dockerignore to exclude __pycache__, .venv, test artifacts, and
other host-only files from Docker build context. Prevents stale .pyc
cache pollution in container images.
Add read-only bind mount for ./apps/api/src:/app/src in docker-compose.yml
so code changes on the host are reflected in the running container
without requiring image rebuild. This is a dev convenience that resolves
the persistent 'tool_configs' import error from stale container images.
Quality gates: docker-compose.yml syntax valid, .dockerignore parsed.
The production DB was already migrated to 86cec91fdb00 (merge of
0014_add_profile_resolver_fields and 2026_06_01_add_workspaces) during
earlier fixes. The clean base branch lacked these migration files,
causing startup failure: 'Can't locate revision identified by 86cec91fdb00'.
Copy the idempotent 0013/0014 migrations and the no-op merge revision
from the fix commits so the Alembic graph matches the DB state.
Quality gates: alembic heads returns single head (86cec91fdb00),
py_compile and ruff passed on all three files.
Move ToolDefinitionManifest import out of TYPE_CHECKING in tool_type.py
so SQLAlchemy can resolve the string-annotated relationship during mapper
configuration.
Add Workspace to models/__init__.py (before ToolInstance) so the
ToolInstance-Workspace relationship can be resolved.
Quality gates: py_compile passed, ruff passed, all mappers configure OK.
Sidebar SessionItem was only opening web tool URLs in new tabs.
Terminal sessions linked to the project page in the same tab.
SessionCard 'Open' buttons for terminal sessions navigated in-place.
Changes:
- app-shell.tsx: SessionItem now builds terminal URLs
(/instances/:id/terminal) and always uses target=_blank
- session-card.tsx: compute openHref for both web and terminal sessions,
render <a> links with target=_blank instead of callback buttons
- use-instance-actions.ts: handleOpen now uses window.open(..., '_blank')
for terminal sessions and project fallback
All session opening (sidebar, cards, callbacks) now consistently opens
in a new tab.
Quality gates: tsc clean
tool-starter.tsx was hardcoding ssh_key_ids=[] and only showing a read-only
SSH key status. Users couldn't select keys when starting tools from workspaces.
Changes:
- tool-starter.tsx: add checkboxes for SSH key selection with repo key
pre-selected, pass selected keys to createInstance/startInstance
- AGENTS.md: add explicit rule forbidding docker compose commands without
user approval and proper isolation
The web container must be rebuilt to pick up the frontend changes:
docker compose up -d --build web
Quality gates: tsc clean, pytest (19 passed, 1 skipped)
When a tool container stops, the docker exec PTY reaches EOF. Previously,
the event-driven reader silently returned on EOF, leaving websockets
attached to a dead session. Input writes then failed silently.
Changes:
- _on_fd_readable: detect EOF (empty read) and call _handle_eof()
- _handle_eof: stop reading, mark process dead, close all websockets
with code 4001 to force frontend reconnection
- write_input: detect write errors and trigger EOF cleanup
Quality gates: pytest (19 passed, 1 skipped)
SSH key mounting was broken because:
1. Each selected key was mounted to a separate source dir but all targeted
the same ~/.ssh path in the container, causing Docker Compose's
last-mount-wins behavior
2. All keys were named id_ed25519, so they'd overwrite each other
Changes:
- ssh_keys.py: add key_filename param to prepare_ssh_key_files for unique
key names; add write_ssh_config for combined multi-key config
- tool_instances.py: collect all selected keys into a single ~/.ssh mount
with sanitized unique filenames (id_ed25519_<name>); generate combined
SSH config with all IdentityFile entries
- tests: add os.makedirs mock for SSH permission tests
Quality gates: pytest (19 passed, 1 skipped)
The workspace mount was failing because /data/working-copies/ was not
bind-mounted into the API container. The workspace management code writes
to /data/working-copies/ inside the API container, but tool instances
mount from the host filesystem. Without a shared bind mount, the host
saw an empty directory.
- docker-compose.yml: add /data/working-copies bind mount, replace repo_data
- docker-compose.traefik.yml: same changes
- Remove repo_data named volume declaration from both files
- Log REPO_PATH, SSH_PATH, EXTRA_VOLUMES, manifest mounts, and resolved
volumes in compile_compose() to trace why mounts may be missing
- Log repo_path and generated compose content in _prepare_manifest_instance()
to verify the full compose YAML at start time
- Convert WorkspaceHasInstancesError to store plain dicts instead of
SQLAlchemy ORM objects, preventing lazy-load failures outside async
session context (MissingGreenlet)
- Update both delete endpoints (top-level and nested) to use exc.instances
directly since they're already plain dicts
- Add no-cache headers for index.html in nginx.conf so browsers always
fetch new hashed JS/CSS bundles on deploy
1. Remove Docker build from create_instance for manifest types — the build
was blocking the HTTP request for several minutes, causing frontend
timeouts and retries. Image is now built lazily on start (via the
existing _prepare_manifest_instance path in start_instance).
2. Increase MAX_CONNECTIONS_PER_USER from 5 to 20 for SSE endpoint —
aggressive reconnect loops from the frontend were exhausting the limit
and causing 429 errors unrelated to tool starting.
Quality gates: ruff clean, tsc --noEmit clean, pytest workspaces (9 passed)
Bug 1 — in-container repo mounting:
- docker-compose.yml: added /data/working-copies:/data/working-copies mount
to API container so workspace dirs are visible on host filesystem
- Dockerfile: create /data/working-copies dir in image
Bug 2 — /home/user not writable:
- workspace_manager.py: chmod 777 workspace dirs + 666 files after clone
and after sync, so any container user can write
- manifest_compiler.py: explicit mkdir + chown + chmod 755 for home dir
in generated Dockerfile
Bug 3 — terminal text shifts left on typing:
- terminal.tsx: removed manual term.refresh() after fit (caused reflow)
- Track lastSentCols/lastSentRows and only send resize when dimensions
actually changed, preventing resize feedback loops
Bug 4 — ESC key captured by terminal:
- terminal.tsx: attachCustomKeyEventHandler allows ESC to propagate to
browser when not in alternate buffer (vim/tmux), so modals/navigation
work; ESC still sent to PTY when in vim/tmux alternate screen
Quality gates: ruff clean, tsc --noEmit clean, pytest workspaces (9 passed)
- New StartToolFAB component: fixed floating button (bottom-right) opens
a modal with workspace selector + ToolStarter
- Added to AppShell: available on every page except mobile terminal
- Dashboard (home): removed old CreateSessionForm and 'Quick create' section,
replaced with FAB + 'Workspaces quick access' prompt
- Sessions page: removed inline workspace selector + ToolStarter, now
shows prompt to use the FAB
- Styles: .start-tool-fab with hover scale, shadow, mobile offset above tab bar
Quality gates: tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
- Delete inline hardcoded StartToolModal from workspace-detail.tsx
- Import shared StartToolModal that fetches real tool types from API
- Pass workspace object to ToolsTab so shared modal gets proper context
- onStart handler passes optional configProfileId through to useWorkspaceInstances.create()
Quality gates: tsc --noEmit clean
- Fetch real tool types from API instead of hardcoded string names
- Use actual tool type UUID (id) as select value
- Remove fake 'terminal' option — terminal is a feature, not a tool type
- Show display_name in dropdown, handle loading/error states
Quality gates: tsc --noEmit clean
- GitService.clone() now accepts ssh_key and sets up GIT_SSH_COMMAND env
- WorkspaceManager.create() loads repo SSH key from DB and decrypts it
- Both workspace create endpoints pass session for SSH key lookup
Quality gates: ruff clean, pytest workspaces API (9 passed, 1 skipped)
Backend (git_repositories.py):
- get_repository_branches: check for .git dir OR HEAD file (handles bare repos)
- When local repo is missing, git ls-remote fallback now uses SSH key auth
via _prepare_ssh_env() for repos with ssh_key_id
- Cleans up temp SSH key file after ls-remote
- Logs ls-remote stderr/exit code for debugging
- Returns server's detail message instead of raw axios 404 text
Frontend (use-git-repo.ts):
- extractError() helper pulls server detail/message from axios responses
- User sees 'repository not found on disk — re-clone or re-create'
instead of generic 'Request failed with status code 404'
Quality gates: ruff clean, tsc --noEmit clean, 11 passed + 1 pre-existing failure