Use named window targets (session-{id}, instance-{id}) instead of _blank:
- use-instance-actions.ts: window.open(..., 'session-{id}') for programmatic opens
- session-card.tsx: <a target='session-{id}'> for direct link opens (mobile + desktop)
- workspace-instance-chips.tsx: <a target='instance-{id}'> for chip links
- instance-list.tsx: <a target='instance-{id}'> for instance open links
Browser behavior: if a tab with that target name exists, it navigates/focuses
that tab instead of opening a new one.
Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
RepositoryCreateDialog was only rendered inside the isMobile block.
Desktop 'Add Repository' clicks set state but the dialog never appeared.
Add conditional rendering in the desktop section.
Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
- ProjectCard.tsx: add onAddRepository prop, show 'Add Repository' button in expanded view
- ProjectCard.tsx: add showBackButton/onBack props for mobile detail reuse
- ProjectsPage.tsx (desktop): wire onAddRepository to open RepositoryCreateDialog
- ProjectsPage.tsx (mobile): reuse ProjectCard for detail view instead of inline duplication
- pages/projects.css: add .project-add-repo style
Both platforms now use the same ProjectCard component and RepositoryCreateDialog
for adding repositories to projects.
Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
- ProjectsPage.tsx: detect mobile viewport, show MobileListView / custom detail view / MobileFAB
- Mobile list: tap project to view details (name, description, repositories, workspaces)
- Mobile detail: shows repositories with New Workspace buttons, Add Repository button, Edit/Delete project actions
- Mobile FAB: opens inline project creation form
- RepositoryCreateDialog reused for mobile 'Add Repository' flow
- New CSS: .mobile-form-actions, .mobile-form-group for mobile form layouts
Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
- WorkspacesPage.tsx: detect mobile viewport, show MobileListView / MobileDetailView / MobileFAB
- Mobile list: tap workspace to view details (branch, status, path, instances, sync time)
- Mobile detail: view workspace fields with Edit and Delete actions
- Mobile FAB: opens inline WorkspaceCreateForm
- New styles/pages/workspaces.css with responsive grid and card styles
- Import workspaces.css in main.tsx
Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
Backend (health_monitor.py):
- Skip health checks for instances with no container_id
- Treat 'not_found' as error only when container was previously running
- Skip duplicate error notifications when already in error state
- Skip 'not_found' notifications for containers that never ran
Frontend (notification-item.tsx):
- Display notification.message (detailed error text)
- Add expandable Details section showing metadata (exit_code, previous_status, etc.)
- New CSS styles for message and metadata display
Quality gates: py_compile, tsc --noEmit, 80/80 tests pass
- Move APIRouter definition from instance_service.py back to tool_instances.py
(service files should not define FastAPI routers)
- Add missing prepare_manifest_instance import in tool_instances.py
- Guard repo.remote_url before clone_repository call
- Guard tool_type.compose_template before render_compose_template call
- Rename subprocess result variable to avoid shadowing SQLAlchemy Result
- Build error message as local string to avoid None/bool type issues
Quality gates: py_compile pass, LSP clean
All styles have been extracted to styles/ directory and component modules.
Build verified without styles.css.
Quality gates: tsc --noEmit passes, npm run build passes
- Extract use-repo-workspace hook for data loading
- Extract WorkspaceLayout and FileBrowser components
- Slim RepoWorkspacePage from 505 to ~80 lines
Quality gates: tsc --noEmit passes, npm run build passes
- Extract use-projects hook for state management
- Extract ProjectCard and ProjectDialog components
- Slim ProjectsPage from 433 to ~100 lines
Quality gates: tsc --noEmit passes, npm run build passes
- Extract use-ssh-keys hook for state management
- Extract SSHKeyCreateForm and SSHKeyList components
- Slim SshKeysPage from 277 to ~80 lines
Quality gates: tsc --noEmit passes, npm run build passes
- Move GeneralSettingsTab to components/features/settings/
- Re-export from page for backward compatibility
- Slim SettingsPage from 284 to ~150 lines
Quality gates: tsc --noEmit passes, npm run build passes
- Extract use-terminal-page hook for terminal state and effects
- Extract MobileTerminalView and DesktopTerminalView components
- Slim TerminalPage from 571 to 112 lines
Quality gates: tsc --noEmit passes, npm run build passes
The xterm.js WebGL addon has known rendering bugs with reverse-video
(inverse color) ANSI sequences — exactly what tmux uses for its status
bar, pane borders, and selected text. On desktop the WebGL addon loaded
successfully, causing characters to render as black-on-black and appear
to 'disappear'. On mobile WebGL typically fails to initialize, so the
terminal silently fell back to the DOM renderer which handles these
color attributes correctly.
- Remove WebGL addon loading and its cleanup logic
- Remove unused xterm-addon-webgl import and dependency
- DOM renderer is the default and correctly handles all ANSI color
attributes including reverse video
Quality gates: tsc --noEmit (pass), build (pass), bundle -100KB
Refs: xterm.js WebGL reverse-video / minimumContrastRatio issues
- Use useMobileViewport to detect mobile and position dropdown
centered with left/right margins instead of right-aligned, which
caused overflow on small screens.
- Add a semi-transparent backdrop overlay on mobile so tapping
outside the dropdown naturally closes it.
- Update mobile CSS: notification-dropdown fills screen width
with 0.75rem margins, max-height capped at 70vh for reachability.
- Remove the 360px max-width cap on mobile so the dropdown uses
available screen space properly.
Quality gates: tsc --noEmit (pass), build (pass)
The notification dropdown was trapped inside .shell-header's stacking
context (created by backdrop-filter). Even with z-index: 9999, it
remained below any element with a higher root-level z-index such as
modal overlays (1000), dialog overlays (1000), and fullscreen
terminals (1000).
- Render the dropdown via ReactDOM.createPortal into document.body
so it escapes all parent stacking contexts.
- Dynamically measure the bell button's bounding rect to position
the dropdown correctly with position: fixed.
- Update click-outside handler to also ignore clicks on the bell
button itself.
- Add window resize listener to keep dropdown aligned.
- Change .notification-dropdown from position: absolute to fixed.
Quality gates: tsc --noEmit (pass), build (pass)
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.
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.