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
Backend (git_repositories.py):
- get_repository_branches now checks for .git subdirectory (not just dir existence)
- If local repo is corrupt/missing but has remote_url, falls back to git ls-remote
to list branches from the remote
- Returns 404 with actionable message instead of 400 with raw git stderr
- Pre-existing test failure in test_git_repository_clone_preflight.py unchanged
Frontend (workspace-create-form.tsx):
- When branch API fails, auto-switches to manual text input (no dropdown selection needed)
- Shows hint text: 'Couldn't load branches — type one manually'
- useGitRepo hook auto-fetches branches when projectId/repoId change
Quality gates: ruff clean, tsc --noEmit clean, 93 passed + 1 pre-existing failure
- Rewrite WorkspaceCreateForm as unified component used in both pages
- Standalone mode (WorkspacesPage): shows project/repo/branch selectors
- Contextual mode (ProjectsPage): accepts defaultProjectId/defaultRepoId,
skips project/repo selectors, shows only name + branch dropdown
- Branch dropdown fetched from repo via listRepositoryBranches API
- Auto-selects first/only option for project, repo, and branch
- '+ Create new branch...' option reveals text input for custom branch
- Falls back to free-text branch input if branch API fails
- Removes duplicated inline creation logic from WorkspacesPage
- TypeScript + eslint clean
- Fetch branches from selected repo via listRepositoryBranches API
- Branch dropdown with default branch pre-selected
- '+ Create new branch...' option reveals text input for custom branch
- Auto-select first option when only one available:
- Project: auto-selects when only 1 project
- Repo: auto-selects when only 1 repo
- Branch: auto-selects when only 1 branch, otherwise defaults to remote default
- Falls back to free-text branch input if branch API fails
- TypeScript + eslint clean
- Replace awkward first-workspace-guessing logic with inline project/repo selector
- New WorkspaceCreateInline component with cascading dropdowns:
- Select project → loads repositories for that project
- Select repository → enter workspace name + branch
- Submit creates workspace via top-level POST /workspaces/
- Add createWorkspaceTopLevel() API client for flat endpoint
- Works even with zero existing workspaces (shows create button in empty state)
- Add CSS grid layout for inline create form
- TypeScript + eslint clean
- Add all_workspaces_router with GET /workspaces/ (no project/repo required)
- Include project_id in workspace responses
- Frontend: useWorkspaces() calls listAllWorkspaces when no args
- Frontend: WorkspacesPage uses top-level list, derives project/repo from workspace for mutations
- Fixes 422 from invalid UUID path params
FastAPI auto-redirects /workspaces to /workspaces/ with 307.
Behind Traefik (HTTP internal), the 307 becomes http://,
triggering Mixed Content in the browser. Adding trailing
slashes avoids the redirect entirely.
Fixes NameError: ToolInstance not defined at runtime because
type annotations are evaluated at class definition time.
Deferring annotation evaluation with __future__ annotations
keeps TYPE_CHECKING imports from causing runtime crashes.
Also includes ruff formatting cleanup on workspace-related files.
- Add workspace_id to CreateInstanceRequest (optional, replaces clone_mode)
- create_instance: resolve workspace, validate repo ownership, use workspace.path
- create_instance: store workspace_id on ToolInstance record
- start_instance: use workspace.path when workspace_id is set (manifest + legacy flows)
- Skip SSH key mount for clone mode when workspace is used
- Backward compatible: clone_mode still works when workspace_id is absent
The API container used a named Docker volume (repo_data:/data/repos) for
storing repositories. When creating tool instances with direct mount mode,
the API told Docker to bind-mount /data/repos/<repo>:/workspace into the
tool container. But the Docker daemon resolves bind-mount paths on the HOST
filesystem, not inside the API container. Since the host had no /data/repos
(the repos only existed inside the named volume), tool containers mounted
empty directories.
Changed both compose files to use a host bind mount (/data/repos:/data/repos)
instead of a named volume. This ensures:
- The API container and tool containers both see the same /data/repos path
- Bind mounts from /data/repos into tool containers work correctly
For existing installations: repos previously stored in the repo_data named
volume should be copied to /data/repos on the host before restarting the
stack.
Quality gates: compose file syntax valid
- apps/api/src/services/tunnel.py: add 2-second sleep after discovering the
tunnel URL to allow Cloudflare DNS edge propagation before returning
- apps/web/src/hooks/use-instance-actions.ts: show alert() with the backend
error message when recreate tunnel fails, instead of silently swallowing
errors
Quality gates: ruff clean, tsc clean
Docker container names are case-sensitive for 'docker inspect' but case-
insensitive for Docker DNS. Compose templates may render container names
with mixed case (e.g. code-server-Headquarter-abc123), causing exact-name
docker inspect to fail while DNS resolution in tunnels works fine.
- apps/api/src/services/docker.py: get_container_id now tries exact match
first, then falls back to case-insensitive exact match via 'docker ps'
- apps/api/src/api/tool_instances.py: recreate_tunnel_endpoint uses
get_container_id instead of its own docker inspect call
Quality gates: ruff clean
docker ps --filter name= uses substring matching, so searching for
code-server-headquarter-abc123 also matches tunnel-code-server-headquarter-abc123.
This caused start_instance to store the tunnel container's ID instead of the
tool container's ID, breaking tunnel connectivity and all container operations.
Switched both helpers to docker inspect, which does exact name matching.
Quality gates: ruff clean
Adds INFO-level logging to trace exactly what happens during tunnel
recreation: container lookup, network membership, target IP/URL,
tunnel creation result, health check, and direct curl probe from API.
This will help diagnose why recreated tunnels return 502 while
original tunnels work.
Quality gates: ruff clean
Old instances may have auto-generated Docker Compose container names
that don't match instance.name.lower(), causing DNS resolution failures
for the tunnel. Also, old instances may not be on the backend network.
- apps/api/src/services/docker.py: add get_container_ip_on_network() and
is_container_on_network() helpers
- apps/api/src/services/tunnel.py: start_tunnel() and recreate_tunnel() now
accept an optional target_url parameter to override the default name-based URL
- apps/api/src/api/tool_instances.py: recreate_tunnel_endpoint now:
1. Looks up the tool container (by stored container_id or name)
2. Ensures it's connected to the backend network
3. Gets the container's IP on that network
4. Passes the IP as the explicit tunnel target
This guarantees the tunnel can reach the tool container regardless of
naming or network state.
Quality gates: ruff clean
The previous endpoint blocked recreation if the tunnel was 'healthy'
or returned an 'error_response', making the Recreate Tunnel button
ineffective in many cases.
- apps/api/src/api/tool_instances.py: removed the health-check guards
from recreate_tunnel_endpoint. It now unconditionally stops the old
tunnel and creates a new one, then commits the new URL to the DB.
- Frontend useInstanceActions already calls onRefresh() after success,
so the UI updates with the new tunnel URL automatically.
Quality gates: ruff clean
The post-creation 'docker network connect' was failing silently for
unknown reasons (race condition, container state, or Docker internals).
Instead of fighting with this, we now inject the backend network directly
into the compose file before 'docker compose up'. Docker Compose then
attaches the container to the network atomically during creation.
- apps/api/src/api/tool_instances.py: new _ensure_backend_network_in_compose()
adds 'networks: [backend_name]' to the service and declares the network
as external at the top level
- apps/api/src/api/tool_instances.py: call _ensure_backend_network_in_compose()
in both start_instance and restart_instance, right after
_ensure_container_name_in_compose()
- apps/api/src/api/tool_instances.py: removed connect_container_to_network
import and call entirely
- apps/api/src/api/tool_instances.py: import get_backend_network_name from
docker module for use in the new helper
Quality gates: ruff clean
Docker Compose prefixes network names with the project directory name
(e.g. 'headquarter_backend' instead of 'backend'). The previous code
hardcoded 'backend', causing 'network not found' errors.
- apps/api/src/services/docker.py: add get_backend_network_name() that
inspects the API container (hq-api) to find the actual network name
- apps/api/src/services/docker.py: connect_container_to_network() now
auto-detects the network name when not explicitly provided
- apps/api/src/services/tunnel.py: import and use get_backend_network_name()
- apps/api/src/api/tool_instances.py: remove explicit 'backend' arg from
connect_container_to_network() call
Quality gates: ruff clean
The host-network tunnel approach had issues because localhost inside
the tunnel container wasn't reaching the host-published ports correctly.
This reverts to running cloudflared as a Docker container on the
'backend' network, where Docker DNS resolves container names reliably.
The tunnel connects to http://{container_name}:{container_port}.
- apps/api/src/services/tunnel.py: use --network backend instead of host
- apps/api/src/api/tool_instances.py: pass container_port (default_port)
instead of published_port (host port) to tunnel functions
Quality gates: ruff clean
- Remove --rm from docker run so failed containers persist for inspection
- Add --no-autoupdate flag to prevent cloudflared from exiting on auto-update
- Capture both stdout and stderr from docker logs
- Check container exit code during wait loop; fail fast with logs if container exits early
- Include exit code in timeout error message for easier debugging
Replace the subprocess-based tunnel implementation with Docker containers
running on the host network. This eliminates all container name resolution
bugs that caused tunnel 502 errors.
New design:
- Each tunnel is a docker run --network host cloudflare/cloudflared container
- cloudflared connects to localhost:{published_port} (Docker port forwarding)
- No dependency on container names, backend network DNS, or binding diagnostics
- Tunnels named predictably: tunnel-{instance_name}
- Start/stop/recreate use container names instead of PIDs
Files changed:
- NEW: apps/api/src/services/tunnel.py — clean tunnel module (start/stop/recreate/health)
- apps/api/src/services/docker.py — removed 250 lines of old tunnel code
- apps/api/src/api/tool_instances.py — use new tunnel module, store container_name
- apps/api/src/services/health_monitor.py — updated import
- apps/web/src/components/session-card.tsx — Recreate Tunnel button always visible
Quality gates: ruff clean, 13 tests passed (health_monitor + notifications)
Show the Recreate Tunnel button on all active web-enabled session cards
(instead of only when tunnel_status is unreachable) so users can manually
trigger tunnel recreation at any time. Also adds it to the mobile action sheet.
Quality gates: eslint clean, tsc clean
- Dual-mode touch scroll:
- Normal mode: scroll .xterm-viewport directly when scrollHeight > clientHeight
- Alternate screen (tmux/vim): send SGR 1006 mouse-wheel protocol data
using cursor position so tmux knows which pane to scroll
- Add touch-action: none to .terminal-container to prevent browser gestures
- Lock both html and body overflow when terminal page is open on mobile
- Remove synthetic WheelEvent approach (xterm.js SmoothScrollableElement
doesn't reliably handle synthetic events)
- Attach capture-phase touch listeners to .terminal-container (parent of xterm)
- On vertical swipe: e.preventDefault() blocks page scroll, then directly
adjust .xterm-viewport.scrollTop by the swipe delta
- This bypasses term.scrollLines() API and directly manipulates the DOM
element that xterm.js watches via its internal scroll handler
- Remove all CSS touch-action overrides — container handles it in JS
- Add full mobile viewport CSS: overflow-y scroll, -webkit-overflow-scrolling
touch, overscroll-behavior-y contain, translate3d hardware accel,
scroll-behavior smooth, touch-action pan-y
- After term.open(), find .xterm-viewport and add passive touch listeners
that call stopPropagation() (not preventDefault) — this lets the browser
handle native touch scrolling while preventing xterm.js internal handlers
from interfering
- Based on xterm.js known issue #5489 and SCROLLING_FIX.md approach
- xterm.js has zero touch event handlers (verified: only 1 'touch' ref in
entire library), so it wasn't intercepting anything
- Our touch-action: none + preventDefault() combo was blocking the browser
from scrolling the .xterm-viewport natively
- Removed all custom touch event handlers from terminal.tsx
- Removed touch-action: none from .terminal-container
- Added touch-action: pan-y to .xterm-viewport so browser allows vertical pan
- Body scroll lock (terminal-page-open) prevents page from scrolling
- Attach touch listeners to document with capture:true instead of container
- Check if touch target is inside terminal container before handling
- This runs before xterm.js internal handlers, giving us full control
- Add touch-action: none to terminal container to prevent browser gestures
- Lower threshold to 3px, 20px per line for responsive scrolling
- Add body.terminal-page-open { overflow: hidden } to prevent page scroll
- TerminalPage adds/removes 'terminal-page-open' class on body when mounted
- Re-add capture-phase touch listeners in terminal.tsx with low 3px threshold
- Call e.preventDefault() immediately when vertical gesture is detected,
before browser compositor commits to page scroll
- Remove CSS touch-action overrides on xterm viewport (now handled in JS)
- Scroll forwarded via term.scrollLines() with 24px per line sensitivity
- Remove all custom touch event interception code from terminal.tsx
- After term.open(), find the internal .xterm-viewport element and set
touchAction=pan-y and overscrollBehavior=contain via inline styles
- Add CSS targeting .xterm-viewport on mobile with touch-action: pan-y,
-webkit-overflow-scrolling: touch, and overflow-y: auto
- Let the browser handle vertical touch panning natively instead of
trying to intercept and manually forward events
- Attach touch listeners to container wrapper in CAPTURE phase so they run
before xterm.js internals stop propagation
- Add e.stopPropagation() in touchmove after handling scroll to prevent
xterm.js from conflicting with our scroll
- Add wheel event fallback for mobile browsers that synthesize wheel from touch
- Remove touch-action: none CSS which was blocking native xterm viewport scroll
- Attach touch listeners to term.element (xterm root) instead of wrapper
- Fix scroll direction: swipe up now scrolls up (shows older buffer)
- Remove RAF indirection; scroll applied synchronously in touchmove
- Accumulate delta between events for smoother scrolling
- Lower threshold to 6px and px-per-line to 16 for better responsiveness
- Add touch-action: none to terminal container on mobile
- Intercepts touch events on the terminal container when isMobile=true
- Detects vertical swipe gestures (dominant over horizontal movement)
- Translates swipe distance to xterm.js scrollLines() calls
- Uses requestAnimationFrame for smooth scroll updates
- Threshold of 10px before scroll kicks in; 30px per line
- Touch listeners cleaned up on component unmount
Root cause: _ensure_web_bind_address injected --host 0.0.0.0 for code-server,
which only sets the bind host, not the port. code-server then listens on its
default port (8080) instead of the tool type's default_port (8443). Cloudflared
connects to port 8443 and gets connection refused, resulting in a 502.
Changes:
- _ensure_web_bind_address now accepts default_port and builds
--bind-addr 0.0.0.0:{port} for code-server
- Same fix for jupyter-notebook with explicit --port flag
- Existing broken --host commands are now detected and replaced
- New migration fixes tool_types templates and instance compose files on disk
- Test fixture updated to use correct --bind-addr 0.0.0.0:8443
- Inject explicit container_name into compose files at start/restart time
via _ensure_container_name_in_compose() to prevent Docker Compose from
generating UUID-based auto names that break backend network resolution.
- Use instance.name.lower() directly instead of get_container_name() lookups
which were unreliable with auto-generated names.
- Apply compose sanitization, bind-address fix, and container-name injection
on restart_instance as well so restarts pick up template fixes.
- Add --force-recreate to docker compose up to ensure container_name changes
take effect immediately.
- Fix notification lifecycle tests to match current behavior (success severity,
health_changed event for ownership test).
Quality gates: ruff clean, pytest (7 notification lifecycle tests passed)
- Uses navigator.wakeLock.request('screen') to keep device awake
- Re-acquires wake lock when tab becomes visible again
- Releases wake lock on component unmount
- Silently ignored on unsupported browsers or if denied
- Add SpecialKeysStrip and SpecialKeysPanel to mobile terminal page
- Store sendData and focusInput refs via onTerminalReady callback
- Pass activeModifier/onModifierChange to TerminalComponent on mobile
- Add virtual keyboard padding to prevent keyboard from covering terminal
- Special keys bar sits at bottom of viewport, panel opens as overlay
- Replace inline header+tabs layout with position:absolute overlay
- Overlay contains: back button, session name, status dot, A-/A+ font size, exit
- Session tabs live inside the overlay below the toolbar
- Auto-hides after 3s; clicking terminal content hides it immediately
- Pull handle at top edge appears when overlay is hidden to restore it
- Terminal content always fills full viewport; overlay never resizes container
- Pass showControls=false to TerminalComponent on mobile to avoid double headers
Problem: linuxserver/code-server already binds to 0.0.0.0 by default.
Adding any command: override (--bind-addr or --host) breaks the LSIO
s6 init system with 'not found' errors.
Changes:
- _ensure_web_bind_address(): Skip LSIO images entirely (no command
override needed). If an existing override is found, remove it.
- New migration 2026_05_29_remove_lsio_command_override: Removes
--bind-addr and --host command overrides from both DB templates
and existing instance compose files on disk for LSIO images.
- Fixed migration to use correct column name (compose_path) and
check information_schema for column existence defensively.
Quality gates: ruff clean
Problem: linuxserver/code-server already binds to 0.0.0.0 by default.
Adding any command: override (--bind-addr or --host) breaks the LSIO
s6 init system with 'not found' errors.
Changes:
- _ensure_web_bind_address(): Skip LSIO images entirely (no command
override needed). If an existing override is found, remove it.
- New migration 2026_05_29_remove_lsio_command_override: Removes
--bind-addr and --host command overrides from both DB templates
and existing instance compose files on disk for LSIO images.
Quality gates: ruff clean
Problem: The first migration already ran on the user's server with
--bind-addr (broken). Alembic won't re-run the fixed migration.
Changes:
- _ensure_web_bind_address(): Now detects existing --bind-addr commands
and replaces them with --host 0.0.0.0 instead of skipping
- New migration 2026_05_29_fix_code_server_bind_addr: Finds code-server
tool types with --bind-addr in compose_template and replaces with
--host 0.0.0.0
Quality gates: pytest 42 passed (2 pre-existing unrelated failures)
FastAPI matches routes in declaration order. The DELETE /notifications
endpoint (bulk clear) was registered AFTER DELETE /notifications/{id},
so the path parameter route intercepted all requests to the bulk route,
causing a 422 UUID validation error instead of hitting clear_all.
Moved clear_all_notifications above dismiss_notification in the router.
Added regression test to verify route order.
Quality gates: pytest (22 passed)
Frontend was sending empty string for config_profile_id when no profile
was selected, causing 'not compatible' validation error. Backend now
treats any falsy value (None, empty string) as 'no profile selected'.
Notification filtering:
- lifecycle_hooks.py: only instance.error and instance.health_changed
with status=running generate notifications. All other lifecycle events
(created, started, stopped, restarted, deleted) are filtered out.
- health_monitor.py: only error and unhealthy states generate notifications.
Running/recovered state no longer creates info notifications.
- _derive_title now maps instance.health_changed to "Container ready".
Clear-all button:
- Added dismiss_all() to NotificationService
- Added DELETE /notifications endpoint for bulk dismiss
- Frontend: clearAllNotifications API, clearAll in notification context,
"Clear all" button in notification drawer alongside "Mark all as read"
- Added CSS for .notification-clear-all with danger hover state
- Updated notification-center tests
Quality gates: pytest (21 passed), vitest (11 passed)
The --bind-addr flag caused code-server to fail entirely (app not
responding on any interface). The correct override for the
coder/code-server image is --host 0.0.0.0, which overrides the
entrypoint's --host 127.0.0.1.
Changes:
- Migration: Replace --bind-addr with --host 0.0.0.0, also handle
existing broken templates by detecting --bind-addr and replacing it
- Runtime safety net: _ensure_web_bind_address uses --host 0.0.0.0
- Test fixture: Updated compose template to match
Quality gates: pytest 42 passed
Root cause: code-server (and similar web tools) default to binding to
127.0.0.1 (localhost) inside their containers. This makes them unreachable
from the Docker network and from cloudflared, which connects via the
container's Docker network name.
Changes:
- Migration: Update code-server compose_template to include
--bind-addr 0.0.0.0:8443 command override
- Migration: Update jupyter-notebook compose_template to include
--ip=0.0.0.0 flag
- Runtime safety net: _ensure_web_bind_address() auto-injects bind
address for known web tools (code-server, jupyter-notebook) when
compose doesn't already specify a command
- Diagnostics: _check_app_binding() compares internal vs external
connectivity to detect 127.0.0.1 binding issues
- Improved readiness check: 30s timeout, checks HTTP status codes,
logs curl stderr for debugging
Files:
- apps/api/alembic/versions/2026_05_29_fix_web_tool_bind_address.py
- apps/api/src/services/docker.py
- apps/api/src/api/tool_instances.py
- apps/api/tests/integration/test_tool_types_api_extended.py
Quality gates: pytest 42 passed (5 pre-existing unrelated failures)
The create-session-form was calling startInstance() without passing the
selected config profile and SSH keys. This caused the backend to receive
ssh_key_ids=[] and clear the keys that were stored during createInstance.
The .ssh directory was never mounted because instance.ssh_key_ids was
wiped during the start call.
Also includes minor formatting cleanup on the data migration.
Quality gates: pytest (18 passed)
- Add _check_app_binding() to detect if app is bound to 127.0.0.1
instead of 0.0.0.0 (common cause of tunnel 'app error 0')
- Improve curl readiness check: wait up to 30s, check HTTP status codes
(accept 2xx, 3xx, 401, 403 as 'ready')
- Log curl stderr for connection debugging
- Log binding diagnosis when external connectivity fails
Quality gates: pytest 42 passed
The API container and tool instances share the 'backend' Docker network
(connect_container_to_network at tool_instances.py:1576). cloudflared
runs INSIDE the api container, so localhost:host_port is unreachable.
The original container_name:internal_port is correct for networking.
The 'app error 0' is an application-level issue, not networking.
This reverts commit a8fbca9.
The ssh_keys mount was already removed from the Alembic seed migration, but
that migration had already been applied to the DB. This data migration
removes the mount from the actual tool_definition_manifests row so that
instance-level SSH key mounting handles keys exclusively.
Quality gates: pytest (18 passed)
- apply_mount_permissions now skips mounts with readonly=true to avoid
'Read-only file system' warnings on post-start chown/chmod
- Removed the ssh_keys mount from the pi-agent manifest definition;
instance-level SSH key mounting now handles this exclusively
- Added unit test for read-only mount skipping
Quality gates: pytest (15 passed)
Root cause: start_cloudflared_tunnel was trying to connect to
http://{container_name}:{container_port}, but:
1. The host OS cannot resolve Docker container names
2. cloudflared runs on the host, so it needs the host-mapped port
Changes:
- start_cloudflared_tunnel: changed signature to accept host_port only
- Connects cloudflared to localhost:{host_port} via Docker port mapping
- Connectivity check uses localhost:{host_port}
- recreate_tunnel updated to match new signature
- Callers in tool_instances.py pass instance.port (host port)
Quality gates: pytest 42 passed
- start_instance now deep-merges manifest with base definition before extracting user.uid/user.gid
- The user config is typically defined in the base image (ubuntu-24.04-dev), not the extending manifest
- Add debug logging to verify resolved uid/gid/home_dir
- Add logging to prepare_ssh_key_files for chown success/failure visibility
- Log current process uid when chown fails to diagnose permission issues
Quality gates: pytest 239 passed (6 pre-existing failures), tsc --noEmit clean
- Extend prepare_ssh_key_files() with optional uid/gid parameters
- Call os.chown on created files when uid/gid are provided
- Gracefully handle PermissionError if API process is not root
- In start_instance, extract container user UID/GID from manifest
- Pass container UID/GID when preparing instance-level SSH key mounts
- Legacy clone-mode SSH keys continue to use root (0,0)
- Add unit tests for prepare_ssh_key_files ownership logic
- Keep apply_ssh_permissions() as fallback for cases where host chown fails
Quality gates: pytest 239 passed (6 pre-existing failures), tsc --noEmit clean
- Replace apply_ssh_permissions internals with _exec_and_log for full visibility
- Log every docker exec command, stdout, and stderr at DEBUG level
- After chown/chmod, run ls -la and stat to verify final state
- Log verified state at INFO level so users can see exactly what happened
- Update tests to mock subprocess.run instead of _run_in_container
Quality gates: pytest 236 passed (6 pre-existing), tsc --noEmit clean
- lifecycle_hooks.publish_lifecycle_event now skips notification creation
when event_type='instance.started' and status='starting'
- Users only see notifications for terminal states:
- Failed: instance.error
- Successful: instance.health_changed with status='running'
- Updated integration tests to verify new behavior:
- test_lifecycle_started_intermediate_skips_notification
- test_lifecycle_running_creates_notification
Quality gates: pytest 42 passed, ruff clean
- Add ssh_key_id column to ConfigProfile model and migration
- Update config profile API to accept/return ssh_key_id
- Include ssh_key_id in ResolvedProfile and resolver logic
- Mount selected SSH key into container home dir at start_instance
- Frontend config profile form with SSH key selector dropdown
- Git mount URL validation defaults to profile's SSH key
Quality gates: pytest (231 passed, 6 pre-existing), tsc --noEmit clean
- Add POST /config-profiles/validate-git-url endpoint:
- Parses URL using existing parse_git_url utility
- Suggests corrected URL for browser URLs
- Runs git ls-remote --heads to verify reachability
- Lists available branches from remote
- Supports SSH key for private repos
- Returns structured response: valid, suggested_url, branches,
default_branch, error, error_code
- Update frontend GitMountEditor:
- Add Check button next to URL field with loading state
- Show validation result: valid (green), suggestion (yellow),
invalid (red)
- Suggestion includes Use this button to apply corrected URL
- Branch field becomes dropdown when URL is validated,
populated with remote branches
- Mappings section disabled until URL is validated
- Shows hint: Validate the URL first
- Quality gates: pytest (218 passed, 6 pre-existing),
tsc --noEmit (clean)
- Add notifications table with Alembic migration
- Notification model with user-scoped indexing and partial index on unread
- NotificationService singleton with create/list/count/mark-read/dismiss
- FastAPI router: GET /notifications, GET /unread, PATCH /{id}/read,
POST /mark-all-read, DELETE /{id}
- Mute categories filtering from UserConfig
- 13 unit tests for NotificationService
- 10 integration tests for API endpoints
- Updated test_models.py with new table registration
Quality gates: pytest 23 new passed, ruff clean
The previous sorting fix exposed a deeper bug: ResolvedMount always
mounted its staging directory as a single bind mount. When a config
profile mount targeted /workspace/x/y and contained a single file
z.json, the staging directory (containing only z.json) replaced the
ENTIRE /workspace/x/y directory, hiding all sibling files from git
repo mounts.
- Change apply_resolved_profile to mount each file individually:
- source: staging_dir/relative_path
- target: expanded_target/relative_path
- Sibling files from other mounts are preserved.
- Empty mounts produce no volume entries.
- Keep volume sorting (parent paths before child paths) which is
still necessary for directory mounts and ensures parent dirs exist
before file mounts inside them.
- Add 4 unit tests for file-level mount behavior.
Quality gates: pytest (218 passed, 6 pre-existing), tsc --noEmit (clean)
- Remove global Escape key listener that intercepted Escape before
xterm.js could receive it, breaking vim/tmux/etc.
- Add click-outside-to-exit for fullscreen: clicking on the padding
area around .terminal-page-content or .terminal-fullscreen-header
exits fullscreen. Clicks inside content or header are ignored.
- Add 8px padding/gap to .terminal-page.fullscreen to create a
clickable border area around the terminal.
- Keep Exit button and Alt+Shift+F as explicit exit methods.
Quality gates: tsc --noEmit (clean), pytest (208 passed, 6 pre-existing)
When switching terminal sessions in fullscreen mode, the viewport
shrank cumulatively because .terminal-wrapper uses
grid-template-rows: auto 1fr. With showControls=false, the single
child (.terminal-container) landed in the auto track instead of 1fr,
creating a feedback loop with xterm fit().
- Add .terminal-wrapper.no-controls with grid-template-rows: 1fr
so the container fills the wrapper when the header is hidden.
- Apply no-controls class in TerminalComponent when showControls=false.
- Replace setTimeout(50) with double requestAnimationFrame in
TerminalPage for more reliable fit() timing after tab switches.
Quality gates: tsc --noEmit (clean), pytest (208 passed, 6 pre-existing)
- Add instance_events and health_checks tables with Alembic migration
- InstanceEventBus: typed pub/sub singleton with wildcard support
- HealthMonitor: async background loop polling containers every 15s
- SSE endpoint GET /events/stream with auth and connection limits
- Lifecycle hooks in tool_instances.py (create/start/stop/restart/delete)
- Structured JSON logging with correlation IDs
- 15 new unit tests (EventBus, HealthMonitor, MonitoringModels)
Quality gates: pytest 15 new passed, ruff clean
- Add expand_container_path() helper that resolves ~/ and $HOME/ prefixes
- Add get_manifest_home_dir() to compute /home/{user.name} or /root from manifest
- Set ENV HOME=... and ENV USER=... in generated Dockerfile for runtime compatibility
- Pass home_dir through instance creation and startup pipeline
- Expand mount targets in apply_resolved_profile() for regular profile mounts
- Expand mapping targets in _resolve_git_mount_mappings() for git mounts
- Expand working_directory and volume targets in _modify_compose_file()
- Update _prepare_manifest_instance to return home_dir alongside image tag
- Fetch tool_type early in start_instance to determine home_dir before profile application
Quality gates: pytest 188 passed, frontend typecheck clean
Addresses: home-path-expansion
- Add mappings array support to git_mount entries
- Clone repository once per git_mount entry, mount multiple subdirectories
- Normalize legacy source_path+target_path to mappings on read
- Update _merge_git_mounts to dedup by (remote_url, branch) and concatenate mappings
- Add _normalize_git_mount, _clone_git_repo, _resolve_git_mount_mappings helpers
- Update GitMountItem Pydantic model with GitMountMapping and model_validator
- Update frontend GitMountEditor component with mappings UI
- Auto-convert legacy git mount entries to mappings format on load
- Add 15 backend unit tests for normalization, resolution, and glob expansion
- Update existing config profile resolver tests for new merge behavior
Quality gates: pytest 167 passed, frontend typecheck clean
Addresses: config-profile-multi-repo-mounts
Frontend:
- Remove 4 console.log statements from terminal.tsx that flooded the
browser console with WebSocket traffic (open, received X bytes, sending Y,
xterm focused)
Backend:
- Downgrade Dockerfile/entrypoint compilation logs from INFO to DEBUG in
_prepare_manifest_instance
- Remove hex-dump diagnostic logging from docker_build.py (was for
troubleshooting the backslash continuation bug, now fixed)
- Downgrade Dockerfile write log from INFO to DEBUG
The manifest-based flow was building the Docker image inside start_instance,
which made the start HTTP request take 3-5 minutes (downloading ubuntu:24.04,
apt-get update, installing packages, Node.js, npm packages). The frontend
showed a spinner forever because the HTTP request was still pending.
Move the image build to create_instance (same pattern as dockerfile types):
1. create_instance now compiles Dockerfile + entrypoint and builds the image
2. start_instance sees the image already exists and skips the build
3. Start is fast — just docker compose up + health checks
This matches the UX expectation: creation has a spinner (can be slow),
start should be quick.
The compile_dockerfile function used \\\\ in Python string literals,
which produces \ (two backslashes) in the Dockerfile output. Docker's
legacy builder requires a single backslash \ for line continuation.
This caused 'unknown instruction: curl' because Docker saw the first as the continuation and the second \ as a literal character before the
newline, breaking the RUN command parsing.
Fix: change all \\ to \ in Python string literals within
compile_dockerfile, producing the correct single-backslash continuation.
Verified with hex dump from container logs:
- Before: line ended with 5c5c (two backslashes)
- After: line ends with 5c (one backslash)
Docker's legacy builder treats \r as a literal character after a backslash
continuation, breaking RUN multi-line commands and producing
'unknown instruction: curl' errors.
Add defensive CRLF→LF normalisation for both Dockerfile and build context
files before writing. Also log hex representation of first 8 lines so we
can verify exactly what bytes Docker receives.
The container build fails with 'unknown instruction: curl' on line 6, which
suggests the Dockerfile continuation characters or line endings may be
malformed. Add defensive logging to diagnose:
- Force newline='\n' in all write_text calls in build_image for consistent
Unix line endings regardless of platform
- Log compiled Dockerfile and entrypoint content at INFO/DEBUG level
- Log Dockerfile byte count when written
This will let us see exactly what Docker is receiving in the next build attempt.
create_instance had an if/else where the else branch handled both compose
and manifest types. For manifest types, compose_template is NULL (migrated
tools no longer store raw compose strings), so render_compose_template(None,...)
crashed with 'NoneType' object has no attribute 'replace'.
Add an explicit elif tool_type.definition_type == 'manifest' branch that:
1. Looks up the ToolDefinitionManifest from tool_type.manifest_id
2. Resolves base definition if referenced
3. Computes deterministic image tag
4. Generates compose via compile_compose
Legacy compose types continue to use render_compose_template in the else branch.
The ManifestEditor had a feedback loop:
1. State change → buildManifest changes → onChange notifies parent
2. Parent updates manifestData → new manifest prop
3. Loading effect sets all state from manifest (arrays get new refs even if same content)
4. New array refs → buildManifest changes → onChange fires again → loop
Fix: track the last-sent manifest via a ref and only call onChange when the
serialized built manifest actually differs. This breaks the cycle because after
the loading effect syncs state, the rebuilt manifest is identical in content
so we skip the parent notification.
Backend:
- Allow 'manifest' in tool_types definition_type validators
- Add manifest_id to ToolTypeCreate, ToolTypeUpdate, ToolTypeResponse
- Skip compose/dockerfile template validation when definition_type is manifest
- Require manifest_id when definition_type is manifest
- Clear legacy templates when switching to manifest type
Frontend:
- Load manifest data via getToolDefinition when selecting a manifest-type tool
- Create/update manifest definition via tool-definitions API when saving
- Pass manifest_id to tool-types create/update API
- Fix unused EmptyState import after configs/folders cleanup
The instance status may say 'running' but the actual Docker container
may have been removed (e.g. docker prune, host restart). The old code
created a terminal session which immediately died because docker exec
failed with 'No such container'.
- Add get_container_status check in WebSocket handler before session creation
- Return 4004 with clear message if container is missing
- This prevents spawning zombie terminal sessions
The terminal_sessions migration and drop_tool_configs migration both pointed
to add_tool_definition_manifests as their down_revision, creating two heads.
Update drop migration to depend on terminal_sessions instead, restoring a
single linear chain.
- Initialize loading=true in useTerminalSessions to prevent auto-create
from firing before initial load completes
- Remove hasAutoCreated ref from TerminalPage (no longer needed)
- Add focus() to TerminalRef, call on tab switch
- Add term.focus() after term.open() in TerminalComponent
- Add console logging for WebSocket send/receive to debug no-i/o
- Revert backend _read_loop retry logic to original break-on-error
Three fixes for multi-session terminal bugs:
1. Race-condition double creation: The auto-create effect fired twice because
loadSessions returned 0 while an earlier createSession was still in flight.
Added hasAutoCreated guard ref to ensure only one auto-create happens.
2. Page reload spawns new sessions: After server restart, list_terminal_sessions
filtered out DB-only sessions (no in-memory counterpart), so the frontend
thought no sessions existed and auto-created new ones. Reverted the filter
so DB rows are always returned. The WebSocket handler now restores the
in-memory session from the DB row on demand when connecting.
3. No input after connection: The backend _read_loop would break on any send
error, causing asyncio.wait to cancel the _write_loop. Made _read_loop
retry up to 3 times before giving up, preventing transient send errors
from killing input handling.
Quality gates: pytest (15/15 passed), tsc clean
Three related bugs fixed:
1. Frontend xterm.js crash: TerminalPage rendered ALL sessions with display:none
for inactive ones. xterm.js crashes when initialized in a hidden container
(Viewport can't read dimensions). Fix: only render the active session's
TerminalComponent using conditional rendering.
2. Backend websocket disconnect cascade: When client disconnected (due to #1),
the server tried to send 'connected' status on dead socket, caught the
WebSocketDisconnect in a generic except block, then tried to close() again
causing RuntimeError. Fix: catch WebSocketDisconnect specifically and suppress
close() errors.
3. Stale DB sessions: After server restart, DB still had old terminal session
rows but no in-memory sessions. list_terminal_sessions returned these ghosts,
causing the frontend to render dead tabs. Fix: skip DB-only sessions that
have no live in-memory counterpart.
Quality gates: pytest (15/15 passed), tsc clean, vitest (7/7 passed)
The frontend router navigates to /instances/:instanceId/terminal without
project_id or repo_id. The backend terminal REST endpoints were requiring
these path params, causing 404s.
- Simplify _get_terminal_instance to validate by instance_id only
- Update all REST routes from /projects/{pid}/repositories/{rid}/instances/{iid}/terminal/*
to /instances/{instance_id}/terminal/*
- Update frontend API client to match new paths
- Update useTerminalSessions hook to take instanceId only
- Update TerminalPage to use simplified hook
- Update tests to match new paths
Fixes: 404 on GET /projects/repositories/instances/{id}/terminal/sessions
- Add test_tool_instances_legacy.py with 8 unit tests:
- dockerfile definition type builds from template
- dockerfile build failure raises HTTP 500
- compose definition type renders template
- manifest compiler is NOT called for legacy types
- start_instance legacy/compose/dockerfile types all skip manifest flow
- start_instance manifest type correctly invokes compiler
- Mark T3.2 and T3.3 tasks complete in OpenSpec
- Add openspec/docs/tool-workshop-guide.md with user guide covering
definition types, manifest creation workflow, base definitions,
migration path, and permissions
The production database was stamped with a migration that no longer exists
in the codebase (created on another branch, applied, then removed). This
adds a no-op bridge migration so Alembic can reconcile the DB state.
- Create bridge migration 2026_05_28_add_tool_definition_manifests (no-op)
- Re-chain terminal_sessions migration to depend on the bridge
- Fixes startup failure: Can't locate revision identified by ...
- Add tool_definitions API client with types for manifests
- Add ManifestEditor component: base image selector, package editors
(apt/npm/pip/node), script editors (build/startup), mount schema
designer, runtime config, and live preview panel
- Integrate ManifestEditor into Tool Workshop as 'Manifest (Declarative)'
definition type alongside Compose and Dockerfile
- Update ToolType API types to include manifest_id and 'manifest'
definition_type
- Frontend builds clean, TypeScript typecheck passes
- Add useAutoHide hook to TerminalPage for mobile header/tab strip
- Header and tabs auto-hide after 3s, tap to reveal
- Add CSS transitions for smooth show/hide on mobile
- Fullscreen mobile mode hides header and tabs completely
- Add ToolDefinitionManifest model with base image versioning
- Add manifest compiler: Dockerfile + Compose generation from JSON manifests
- Add permission fixer: post-start chown/chmod for mount policies
- Add tool definition CRUD API with live compile preview endpoint
- Integrate manifest-based startup flow in start_instance
- Add Alembic migration with data conversion for pi-agent
- Add 48 unit tests for manifest compiler, permission fixer, docker service
- Keep backward compatibility with legacy dockerfile_template/compose_template
Migration: applied successfully. Pi-agent converted to manifest.
Quality gates: pytest (146 passed, 4 pre-existing unrelated failures)
- Add WebSocket route /ws/tool-instances/{instance_id}/terminal/{session_id}
- Preserve /terminal as default-session alias for backward compatibility
- Extract shared _handle_terminal_websocket handler for both routes
- Add REST endpoints: GET list, POST create, DELETE close, POST reset, POST rename
- Preserve legacy POST .../terminal/reset as default session alias
- Add frontend API client (apps/web/src/api/terminal.ts)
- Add useTerminalSessions React hook for session CRUD + state management
- Add integration tests for auth requirements on all new endpoints
Quality gates: pytest (8 new passed, 182 total passed, 51 pre-existing failures)
- Add TerminalSessionModel DB table with instance_id FK, name, status,
created_at, last_activity_at, closed_at columns
- Add Alembic migration for terminal_sessions table
- Refactor TerminalManager to use composite key (instance_id, session_id)
supporting up to 5 concurrent sessions per instance
- Add create_session, get_session, get_sessions_for_instance, close_session
- Preserve get_or_create_session for backward compatibility (default session)
- Fix attach_websocket to only close sockets within same session
- Add name (auto-generated 'Session N') and status tracking to TerminalSession
- Add 7 unit tests for multi-session logic
Quality gates: pytest (7 new passed, 174 total passed, 51 pre-existing failures)
- get_container_id() and get_container_name() now lowercase the
instance name before passing to docker ps --filter, because
Docker container names are lowercase internally and the filter
is case-sensitive. This caused container_id to never be captured
when instance.name contained uppercase chars (e.g. 'Headquarter'),
breaking terminal WebSocket connections.
- Also guard proc.stdout being None in start_cloudflared_tunnel().
- Add unit tests for get_container_id and get_container_name.
Quality gates: pytest (14 passed), python clean
- Add stdin_open: true and tty: true to dockerfile-based compose generation.
Without these, bash (PID 1) exits immediately, causing a container restart
loop that makes the instance invisible to docker ps and triggers 4004.
- Treat WebSocket close codes 4001/4003/4004 as permanent errors in the
frontend. Stop retrying and show the server reason to the user.
- Prevent visibilitychange handler from resetting retry attempts after a
permanent error has occurred.
- Use docker ps -a in get_container_id/get_container_name to find
stopped/exited containers for diagnostics.
Quality gates: tsc --noEmit (pass), pytest (98 passed, 4 pre-existing failures)
- Fix git mount clone to check correct path (repo-clone subdir)
- Pull updates instead of re-cloning when git mount dir exists
- Add compose file sanitization to remove invalid port 0 mappings
- Fixes startup failures for existing instances with old compose files
- Adds pi-agent to tool_types table with terminal interface
- Includes Dockerfile template for pi.dev coding agent
- Idempotent: checks for existing entry before insert
- Remove absolute path requirement from target_path validation
- Resolve relative paths against working_directory at instance startup
- Fall back to /home/user if no working_directory is configured
- Update frontend to allow relative target paths
- Update spec to document relative path support
- Update tests to allow relative paths and test path traversal rejection
- _checkout_branch now returns bool and falls back gracefully on failure
- Glob warning message includes matched file count
- Fix database model comment to reference remote_url
- Update tests for new branch checkout behavior
All 51 tests pass
- Change git mount schema from repo_id to remote_url
- Update validation rules to check URL format instead of repo existence
- Update cloning scenarios to clone directly from URL
- Update UI scenarios to show URL input instead of repo selector
- Remove references to internal/existing repositories
- Change git mount schema from repo_id to remote_url
- Remove database lookups for git mount resolution
- Clone directly from URL at instance startup
- Simplify frontend UI to text input for Git URL
- Fix route ordering in git_repositories.py to prevent 422 errors
- Update all tests to use remote_url field
Breaking change: Git mounts now use remote_url instead of repo_id
- Add createExternalRepository API function
- Update GitMountEditor with "+ Add new repository..." option
- Show form to enter repo name and remote URL
- Auto-create external repo and refresh list on success
- Update config-profiles page to pass onCreateRepository handler
- Add GET /repositories endpoint documentation
- Add POST /repositories endpoint for external repos
- Update config-profiles.md with git mount details
- Update repositories.md with external repo support
- Add POST /repositories endpoint for external repos (no project_id)
- Update GitRepositoryResponse to allow nullable project_id
- Update list_repositories to support listing all user repos
- Add _pull_repository_updates for auto-pull on container creation
- Update git mount validation to allow external repos
- Frontend: Update listRepositories to support optional projectId
- Spec updates: external repos, auto-clone, per-instance isolation
The server is missing 2026_05_27_make_project_id_nullable.py but the
merge migration referenced it. Removing the merge migration leaves a
clean single head chain.
The server has both the old (2026_05_27_make_project_id_nullable) and
new (2026_05_27_external_repos) migration files, creating two heads.
This merge migration resolves them into a single head.
- Make project_id nullable in git_repositories table (migration)
- Allow external repos not tied to any project
- Update validation to allow user-owned external repos in git mounts
- Add /projects/repositories endpoint to list all user repos
- Update frontend to fetch all user repos for git mount selector
- TypeScript and build pass
- API documentation for config profiles with git mounts endpoint details
- User guide for using git repositories in config profiles
- Document branch pinning, glob patterns, error handling, and best practices
- Update API README to link to new config-profiles documentation
The calculateFontSize() function was overriding the font size on mobile
based on viewport width (vw/25), causing the terminal to display at ~15px
while the internal state was 8px. When pressing A-, it would jump from
15px to 7px. Now it respects the actual fontSize state consistently.
Quality gates: TypeScript check passed, production build successful
- Reduce MIN_FONT_SIZE from 8 to 4 for maximum text size reduction
- Set default font size to 8px for both mobile and desktop
- Allows very small terminal text for mobile viewport optimization
Quality gates: TypeScript check passed, production build successful
- Reduce MIN_FONT_SIZE from 12 to 8 for smaller text option
- Reduce default font size from 14/12 to 10/10 for mobile/desktop
- Allows users to make terminal text significantly smaller
Quality gates: TypeScript check passed, production build successful
Mobile devices use different monospace fonts (Courier on iOS, Droid Sans Mono
on Android) with larger ascent/descent metrics than desktop fonts. With
lineHeight: 1.0, calculated cell height was smaller than actual glyph height,
causing block characters to render at ~3/4 height. Increasing to 1.2 gives
mobile fonts proper vertical space while maintaining desktop compatibility.
Quality gates: TypeScript check passed, production build successful
- Remove CSS overrides that interfere with xterm.js internal sizing
- Increase MIN_FONT_SIZE from 10 to 12 to prevent broken character rendering
- Increase default font sizes from 10/12 to 12/14 (desktop/mobile)
- Add clamping for stored font size values to prevent old tiny values
- Remove !important rules on xterm-viewport that could cause clipping
The changeFontSize callback passed to MobileTerminalWrapper was capturing
the initial handleFontSizeChange function, so subsequent clicks used stale
fontSize state. Fixed by wrapping handleFontSizeChange in a ref so the
callback always calls the latest version.
- Reduce MIN_FONT_SIZE from 10 to 6
- Reduce MAX_FONT_SIZE from 24 to 20
- Reduce default desktop font size from 14 to 10
- Reduce default mobile font size from 16 to 12
- Create shared_validators.py with validate_mount_path, validate_files, validate_env_vars, validate_volumes
- Refactor config_folders.py to use shared validators
- Refactor tool_configs.py to use shared validators
- Refactor config_profiles.py to use shared env_vars validator
- Reduce ~80 lines of duplicate validation code
- Restored original desktop CSS that was accidentally overwritten
- Added back all mobile-specific styles
- CSS file now 3945 lines (original + mobile styles)
- Build passes successfully
- Add mobile viewport detection to RepoWorkspace
- Implement bottom tab navigation (Files, Editor, Git, Terminal)
- Add repository and branch selectors for mobile
- Create mobile workspace layout with tab bar
- Add CSS styles for mobile workspace components
- Desktop layout remains unchanged
- Add mobile list view showing all config profiles
- Add mobile detail view with profile information display
- Add mobile edit/create view with full form
- Implement list→detail→edit navigation
- Fix TypeScript errors and build issues
- Add mobile viewport detection to ToolWorkshopPage
- Implement mobile list view with MobileListView component
- Implement mobile detail view with MobileDetailView component
- Implement mobile edit view with MobileEditView component
- Add MobileFAB for creating new tool types
- Fix IconName type issues in mobile components
- TypeScript check passes, build succeeds
Mobile Navigation:
- Add MobileNav component with bottom tab bar
- Show mobile nav on small screens, hide desktop sidebar
- Add session count badge to Sessions tab
- Add safe area padding for notched devices
Session Management:
- Redesign SessionCard for mobile with action menu
- Add MobileActionSheet for session actions
- Keep primary action prominent
Forms & Dialogs:
- Stack form fields vertically on mobile
- Ensure 44px minimum touch targets
- Update dialogs for 320px viewport
Responsive Layout:
- Add MobilePageHeader with back button
- Reduce page padding on mobile
- Stack multi-column grids vertically
Touch & Interaction:
- Add active states to interactive elements
- Ensure 8px spacing between touch targets
Complex Pages:
- Update Repo Workspace for mobile
- Update Tool Workshop and Config Profiles
Build: TypeScript check passes, production build succeeds
- Add startup_command field to ToolType model and API
- Execute startup command before interactive shell in terminal sessions
- Add tmux and ranger to OpenCode container spec
- Update Tool Workshop UI with startup_command input for terminal types
- Add backend tests for startup_command CRUD operations
- Sync specs: tool-terminal, tool-types-definition, opencode-web-server
- New spec: tool-terminal-startup-command
Quality gates: Frontend typecheck/lint passed. Backend tests blocked by environment (Python/Docker not available).
OpenSpec: terminal-startup-and-container-tools
- Add overflow-y: auto and -webkit-overflow-scrolling: touch to xterm-viewport
- Change touch-action from 'none' to 'pan-y' on mobile terminal wrapper and container
- This allows vertical scrolling through terminal output history while preventing zoom
- Change .mobile-terminal-content to display: flex with flex-direction: column
- Change .terminal-wrapper.mobile to use flex: 1 instead of position: absolute
- Change .terminal-container to use flex: 1 instead of height: 100%
- Ensures proper height calculation in flex layout chain
- Remove chicken-and-egg check that prevented fit() when cols/rows were 0
- Add console logging for container dimensions and fit results
- Add retry limit (50 attempts) for initial fit to prevent infinite loops
- Add includes section to profile editor with drag-and-drop reordering
- Display included profiles with scope badges (Global, Project, Tool)
- Add 'Add Include' dropdown filtered by compatibility and cycle prevention
- Add remove button per include row
- Save includes together with profile form
- Add include count badges to profile list sidebar
- Add drag icon to Icon component
Implements config-profile-includes-ui tasks 1.1-4.3
- Add early return guards in all session action handlers (start, stop, delete, recreate tunnel)
- Prevents race conditions where double-clicks or rapid clicks fire duplicate API calls
- First delete succeeds, second would 404 because instance is already deleted
- Applied to both sessions page and dashboard/home page
- Add per-item busy overlay to SessionCard component
- Remove full-screen loading overlay from sessions page
- Remove loadingAction state, use per-item busy state only
- Add handleStart to sessions page for consistency
- Add session-card CSS for busy overlay positioning
- Both home and sessions pages now use same per-item loading pattern
- Add retry logic for transient network errors in API client
- Retry up to 2 times with exponential backoff on network errors
- Reduce session polling from 10s to 30s to decrease error frequency
- Handle 502/503/504 gateway errors with retries as well
- Add position: relative to create-session-form-wrapper so overlay fills only the form
- Remove text from instance busy overlay, show only spinner
- Delete progress indicator now fills only the target card
- Open terminal before calling fitTerminal() to avoid race conditions
- Add container dimension checks before fitting
- Add guards to prevent fit/refresh with 0x0 dimensions
- Only send resize messages when dimensions are valid
- Prevent xterm.js internal errors from invalid dimension access
- Replace full-screen loading with per-instance busy state
- Add busy overlay with spinner to instance cards
- Disable action buttons while instance is busy
- Add CSS for visual dimming and overlay positioning
Fixes add-config-profiles: instance UI polish
Frontend:
- Clear xterm.js screen when receiving 'connected' status after reset
- Send resize message after clearing to ensure proper dimensions
- Fixes terminal artifacts after reset
Backend:
- Fix data loss bug: text starting with '{' but not valid JSON was silently dropped
- Now writes such text to session as regular input
- Fixes missing characters when user types '{'
Frontend:
- Fix term.onData to use wsRef.current instead of captured ws variable
- Fix fitTerminal to use wsRef.current for resize messages
- Fix sendData callback to use wsRef.current
- This fixes 'cannot type' after WebSocket reconnect
Backend:
- Add SessionRef class for mutable session reference
- Update _read_loop and _write_loop to use SessionRef
- Reset now updates session_ref.session instead of returning
- This keeps the WebSocket alive after reset instead of closing it
Instead of sending stty commands through the user's terminal session
(which causes 'inappropriate ioctl' errors), send SIGWINCH signal to
the docker exec process on the host. Docker exec should forward this
to the container process, causing the shell to re-read its terminal size.
This avoids:
- Visible stty commands in the terminal
- ioctl errors from stty
- Interference with user's shell session
Reverted docker exec back to -it (required for interactive bash).
Instead, sends stty command with \r to hide it from the terminal display:
- \r moves cursor to start of line (overwrites prompt)
- stty command executes silently (no output on success)
- \r moves cursor back to start, hiding echoed command
This sends stty on EVERY resize so the container shell always matches
frontend dimensions.
Removes -t flag from docker exec so it uses our PTY slave directly instead
of creating its own PTY inside the container. This allows TIOCSWINSZ on the
host PTY master to propagate naturally to the container shell via SIGWINCH.
Also removes all stty command injection logic since resize now works natively.
When Docker starts a container, it creates network interfaces which
triggers Chrome's ERR_NETWORK_CHANGED error, aborting the request.
The backend successfully starts the container but the frontend never
gets the response, showing 'failed to create session' even though
the session is up.
Fix: Add retry with exponential backoff for startInstance and
restartInstance when network errors occur (no HTTP response).
Retries up to 2 times with 1.5s delay between attempts.
Fixes: False 'failed to create session' errors when launching tools.
When FastAPI parses the request body and model_dump() is called,
nested MountItem models are already serialized to plain dicts.
The update handler was unconditionally calling model_dump() again,
causing AttributeError on dict objects.
Fix: Check if mount items are already dicts before calling model_dump().
Fixes: 422 error when updating profiles with mounts.
Backend:
- Remove _stty_sent guard to send stty on EVERY resize
- Use stty -echo to hide command, then delete the command line with ANSI escapes
- Change log level from info to debug
Frontend:
- Add window resize listener as fallback to ResizeObserver
- 250ms debounce to avoid excessive refits
- Proper cleanup on unmount
- Move Config Profiles from settings to top-level navigation
- Implement split-pane layout: profile list on left, editor on right
- Add project and tool type dropdowns with live data
- Keep form open after save with success feedback
- Add sticky save bar at bottom of editor
- Remove Config Profiles tab from Settings page
OpenSpec: add-config-profiles
Reverted terminal.tsx, terminal_session.py, and terminal.py to clean state
from before the resize debugging saga. Removed:
- Debug console.log statements
- Explicit term.resize() calls that broke xterm.js
- position: relative CSS overrides on .xterm
- stty -echo wrapper and asyncio.sleep delay
- Extra requestAnimationFrame refresh calls
Kept:
- Mobile terminal features (special keys, modifiers, font size)
- ResizeObserver for container resize detection
- Basic fit() and WebSocket resize messaging
- Fix React key stability in env vars, files, and mount file inputs
to prevent focus loss on every keystroke
- Improve validation error messages to explain Files vs Mounts
- Add helper text in UI clarifying relative vs absolute paths
Fixes focus loss bug and improves UX for path validation errors.
- Add ConfigProfile and ConfigProfileInclude data models with migrations
- Implement profile resolver service with ordered includes and merge rules
- Add profile CRUD API with validation, compatibility, and cycle detection
- Add instance API plumbing for profile selection on create/start/restart
- Add resolved profile preview and default resolution APIs
- Add frontend config profile API client and management UI
- Add launch/restart profile selection UI
- Add backend integration and unit tests (31 passing)
OpenSpec: add-config-profiles
Quality gates: ruff, TypeScript compile, 31 tests passing
- Add window resize listener as fallback for ResizeObserver
- Use 250ms debounce to avoid early layout reads
- Delay term.refresh() to next animation frame so renderer
can process resize before we force redraw
- Clean up window resize listener on unmount
The _stty_sent guard prevented the container shell from updating its
terminal size after the first resize. This caused visual mismatches
where xterm.js displayed at the new size but the shell still wrapped
output at the old size.
Remove the guard so stty is sent on every resize event.
- Add window resize listener to complement ResizeObserver
- Clear window resize timeout on cleanup
- Force term.refresh() after font size changes
- Send resize message after font size change
- Frontend: Add ResizeObserver with dimension tracking for accurate resize detection
- Frontend: Fix cleanup function to properly disconnect ResizeObserver
- Frontend: Use CSS grid for terminal wrapper layout
- Backend: Add duplicate dimension check to avoid unnecessary resizes
- Backend: Ensure stty command is sent correctly to container shell
Issues fixed:
1. Terminal container now has explicit width: 100% and height: 100%
2. Added term.refresh() after fit() to force redraw when dimensions change
3. Changed shell-body from min-height to height for definite sizing
4. Added .xterm-viewport width: 100% to ensure proper filling
This ensures the terminal properly fills the viewport and redraws
content when the window is resized.
When window resize fires, CSS layout hasn't settled yet. Adding
requestAnimationFrame ensures the browser has calculated new sizes
before xterm.js fit() reads the container dimensions. Reduced
debounce from 250ms to 100ms since rAF handles the layout timing.
The terminal page uses height: 100% but parent .shell-content didn't
have explicit height, so the terminal couldn't fill the viewport.
Changes:
- .shell-content: added height: 100%
- .shell-body: added flex: 1 to fill flex parent
- Mobile .shell-content: added height: 100%
This ensures the terminal wrapper can properly calculate and fill
the available viewport space.
The ResizeObserver detected size changes caused by the stty command
output appearing in the terminal, creating an infinite resize loop:
1. Resize detected -> fit() -> send resize to backend
2. Backend sends stty command through PTY
3. stty text appears in terminal output
4. ResizeObserver detects content height change
5. fit() calculates new rows -> send resize
6. Loop continues forever
Reverted to:
- Window resize event instead of ResizeObserver
- stty command only sent once on first resize
This means the container shell stays at the initial size and won't
dynamically resize when the browser window changes, but prevents
the infinite loop.
Window resize events fire before CSS layout settles, so FitAddon
was reading stale container dimensions. ResizeObserver fires after
the element actually changes size, ensuring fit() gets correct
dimensions. Reduced debounce from 250ms to 100ms for snappier response.
Previously the stty command was only sent on the first resize. Now it
is sent every time the terminal dimensions change, so resizing the
browser window or rotating the device properly updates the container
shell size. Added a check to skip when dimensions haven't changed.
Docker exec doesn't forward PTY resize to the container process,
so the container bash stays at 80x24 regardless of frontend resize.
Work around this by sending a stty command through the terminal
on first resize to set the correct dimensions inside the container.
The write loop was crashing with 'name instance_id is not defined' when
processing resize messages. This caused the connection to drop with 1006
and the frontend to reconnect in a loop. Fixed by passing instance_id
as a parameter to _write_loop. Also cleaned up debug logging.
When any of the read/write/heartbeat loops ends, we were cancelling
remaining tasks but not explicitly closing the WebSocket. This caused
the connection to be dropped with 1006 abnormal closure instead of
a clean 1000 close. The frontend then reconnected, creating a loop.
Docker exec doesn't forward PTY resize to the container process,
so the container bash stays at 80x24 regardless of frontend resize.
Work around this by sending a stty command through the terminal
on first resize to set the correct dimensions inside the container.
Sending SIGWINCH to the docker exec process was crashing/killing it,
which closed the PTY and caused WebSocket 1006 abnormal closure loops.
Reverting to the original TIOCSWINSZ-only approach.
When resizing the PTY, docker exec needs to be notified so it can
re-read the terminal size and propagate it to the container's PTY.
Without this, the container shell stays at 80x24 regardless of what
the frontend sends.
- Backend PTY starts with default 80x24 dimensions
- Previous code only sent resize during layout changes
- Now sends current terminal size immediately when WebSocket opens
- Ensures PTY is properly sized before shell starts rendering
- Simplified fit logic: just fit after open, after fonts load, and on resize
- Added console logging to debug what FitAddon calculates
- Single fitTerminal() function used everywhere
- Removed complex retry logic that wasn't working
- Wait for document.fonts.ready before fitting (ensures correct cell metrics)
- Retry fit every 100ms if rows <= 1 or cols <= 10 (layout still settling)
- Up to 30 retries (3 seconds) for layout to stabilize
- Remove redundant delayed fits, keep only header auto-hide fit at 4s
- Container-level ResizeObserver created feedback loop with fitAddon.fit()
- Removed it, kept initialization-time dimension check only
- Rely on window resize listener for viewport changes
- xterm docs require parent to have dimensions when open() is called
- Added ResizeObserver to wait for non-zero dimensions before initializing
- Added container ResizeObserver to handle resizes (header hide, keyboard)
- Fixed cleanup to properly disconnect observers and handle uninitialized ws
- Don't reconnect when server closes old connection with code 4000
- Code 4000 means new connection was established, not an error
- Prevents infinite reconnection loop between old/new connections
Refs: terminal switching between 4000 error and connected
- Measure parent dimensions and set them on container before term.open()
- Ensures FitAddon gets correct dimensions on initialization
- Prevents 1-row/1-col calculation that breaks scrolling and sizing
- Add docs/api/terminal.md with WebSocket protocol and reset endpoint
- Add docs/features/terminal.md with user guide for persistent sessions
- Add docs/features/terminal-troubleshooting.md with diagnostic steps
- Mark tasks 8.1-8.3 complete
Refs: persistent-terminal-sessions tasks 8.x
TerminalManager was trying to create an asyncio task at module import time,
but no event loop exists yet during import. This caused RuntimeError on startup.
Changes:
- _start_idle_check() now checks if event loop is running before creating task
- If no loop exists, silently skips (will be started lazily)
- Added lazy start call in get_or_create_session() when websocket connects
- Backend: Send ping every 30s from WebSocket endpoint
- Frontend: Respond to pings with pongs, detect missed pings (60s timeout)
- Update type definitions to include 'resetting' status
Refs: persistent-terminal-sessions task 6.4
TypeScript build failed because 'resetting' status was not included
in the onTerminalReady callback type definition.
Updated types in:
- TerminalComponent props
- MobileTerminalWrapper state and callback
- MobileTerminalHeader props
Replace flexbox chains with CSS Grid to give content area definite height:
- grid-template-rows: auto 1fr auto for header/content/keys
- Use 100dvh for proper mobile viewport handling
- Terminal fills content area with position: absolute
- Remove mobile-terminal-shell wrapper (redundant)
- Content area gets real height from grid, fixing FitAddon calculations
- Terminal sessions now persist across WebSocket disconnections
- Added circular output buffer (10KB) for replay on reconnect
- Added idle timeout cleanup (30 minutes)
- Added reset functionality via WebSocket message and HTTP endpoint
- Concurrent connections close old WebSocket when new one connects
- Frontend: Added reset button with confirmation dialog
- Frontend: Handle resetting status and reconnection
Refs: persistent-terminal-sessions
- Make .terminal-container position: relative with overflow: hidden
- Make xterm element absolutely positioned to fill container
- This ensures xterm.js always has concrete dimensions for fitAddon
- Remove conflicting height: 100% !important overrides
- Terminal now properly fills available space and calculates correct rows
The ResizeObserver triggered fit() which changed canvas dimensions,
triggering the observer again in an infinite loop. We already have
window resize handling and delayed fit() calls, so the observer was
redundant.
- Change .terminal-wrapper.mobile from height:100% to flex:1 for proper flex behavior
- Add explicit width/height to xterm-viewport and xterm-screen to prevent overflow
- Add delayed fit() at 4s to resize after mobile header auto-hides
- Remove min-height:100% which caused overflow issues
- Add SessionCard component with status indicators, actions, and confirmation dialogs
- Add SessionList component with grouping (active/recent) and filtering
- Refactor dashboard.tsx to use unified components
- Refactor sessions.tsx to use unified components
- Remove duplicated session rendering logic from both pages
Refs: session-list-overhaul tasks 1-4
- After changing font size and calling fit(), send resize message to WebSocket
- OpenCode now receives correct terminal dimensions after font size adjustment
- Fixes issue where OpenCode UI didn't fill available space after font resize
- Remove ResizeObserver that was causing infinite resize loop
- Add display: flex to terminal-container for proper child sizing
- Use flex: 1 on .xterm element instead of height: 100%
- Remove explicit height/width from xterm-viewport and xterm-screen
- Let flexbox handle the layout naturally
- Add ResizeObserver to watch terminal container and trigger fit() on size changes
- Add delayed second fit() call 500ms after initialization
- Remove initial setTimeout resize in favor of ResizeObserver
- Ensure resizeObserver is cleaned up on unmount
- Change from named volume instance_data to host bind mount
- Consistent with docker-compose.yml fix for clone mode
Refs: clone mode repo files not visible in containers
- Use double requestAnimationFrame before initial fitAddon.fit() to ensure DOM is settled
- Add display: flex to mobile-terminal-content for proper child sizing
- Add width: 100% to terminal-wrapper.mobile
- Ensure terminal fills parent container both horizontally and vertically
- Add width: 100% to xterm, xterm-viewport, and xterm-screen
- Add explicit canvas display: block for proper sizing
- Remove padding from terminal-container on mobile
- Add min-height: 100% to terminal-wrapper.mobile
- Ensure xterm.js internal elements fill parent container
- Change .terminal-page height from 100vh to 100% to fit within shell layout
- Add display: flex and min-height: 0 to .shell-content to allow flex children to expand
- Terminal container now properly fills available vertical space
- Reduce MIN_FONT_SIZE from 16 to 10 for better range
- Remove calculateFontSize from useEffect dependencies to prevent
terminal re-initialization when font size changes
- Font size changes now update xterm options directly without
disposing/recreating the terminal (no WebSocket reconnection)
- Add null checks and try/catch around fitAddon.fit() to prevent viewport errors
- Use requestAnimationFrame to ensure DOM is stable before fitting
- Remove isMobile condition from font size buttons in TerminalComponent
- Font size controls now visible on both mobile and desktop terminals
- Add safety check after rendering compose template to ensure REPO_PATH is mounted
- If compose template lacks volume mount, auto-add default mount to /workspace
- Add cloned repo verification to catch empty clone directories
Refs: clone mode repo not appearing in container workspace
- TerminalComponent: expose changeFontSize via onTerminalReady callback
- MobileTerminalWrapper: pass changeFontSize to header
- MobileTerminalHeader: add A- and A+ font size buttons
- CSS: collapse header height/padding/margin/border when hidden to reclaim space
Backend:
- Add created_at to get_user_sessions response
Frontend:
- Hide tunnel error badges, probe output, and 'Recreate Tunnel' button for terminal-only sessions
- Show session start time (created_at) in active sessions list
- Show repository configuration (clone_mode, branch) for each session
- Skip health check polling for terminal-only sessions
- Update Session type to include created_at field
- Create merge migration f3d2dc90ba3a to merge single_interface and clone_mode heads
- Make remove_is_builtin migration idempotent with IF EXISTS clause
Refs: alembic migration fix for dev branch
- Remove useSpecialKeys hook state, export pure utility functions instead
- MobileTerminalWrapper now owns activeModifier state
- SpecialKeysStrip and SpecialKeysPanel receive modifier via props
- TerminalComponent applies modifier to virtual keyboard input via activeModifier prop
- Modifier now works with both special keys AND virtual keyboard input
- Modifier clears after any key press (special or virtual keyboard)
When API runs in Docker with named volume instance_data:/data/instances,
generated docker-compose.yml files use bind mounts like
/data/instances/.../repo-clone:/workspace. Docker resolves bind mounts
on the host filesystem, not in named volumes, so containers see empty
directories.
By mounting /data/instances as a host bind mount, both the API and
generated tool containers access the same host path.
- Redesign useSpecialKeys hook with modifier state tracking
- Add one-shot activation for Ctrl and Alt keys
- Visual feedback: active modifiers shown with yellow highlight
- Fix focusInput to use term.focus() instead of hidden input
- Always refocus terminal after sending any special key
- Add requestAnimationFrame for reliable focus restoration
Git branch -a --format=%(refname:short) returns remote branches as
'origin/branch-name', not 'remotes/origin/branch-name'. The code was
only filtering 'remotes/' prefix, causing clone to fail with branch
names like 'origin/feat/foo'.
Now properly detects remote names using 'git remote' and strips the
remote prefix (e.g., 'origin/') from branch names.
- Add tabIndex={-1} to all special key buttons to prevent focus
- Add onFocus handler to immediately blur if focused
- Terminal focus stays intact when tapping special keys
- Move terminal hidden input to off-screen position (-9999px) to prevent
text selection/caret visibility on mobile
- Add user-select: none to prevent any selection UI
- Fix create-session-form to use new listRepositoryBranches API signature
(projectId, repoId) and access response.branches/default_branch
Resolved conflicts:
- Moved branch selection UI from inline sessions.tsx to CreateSessionForm component
- Integrated branch dropdown and new branch creation into CreateSessionForm
- Removed duplicate branch state management from sessions.tsx
All branch selection tests pass (7/7).
- Use onPointerDown with preventDefault() instead of onClick
- Add onKeepFocus callback to SpecialKeysStrip and SpecialKeysPanel
- Expose focusInput via onTerminalReady in TerminalComponent
- MobileTerminalWrapper passes focus callback to keep keyboard open
- Remove status from TerminalComponent useEffect dependencies to prevent recreation on WebSocket status changes
- Use ref for onTerminalReady callback to avoid parent re-renders triggering terminal recreation
- Wrap MobileTerminalWrapper onTerminalReady with useCallback for stable reference
- Replace free-text branch input with dropdown of available branches
- Add 'Create new branch...' option with name and base branch inputs
- Load branches from API when repository is selected in clone mode
- Pass newBranch parameter to createInstance API
- Add new_branch field to CreateInstanceRequest
- Run git checkout -b after cloning when new_branch is provided
- Store new branch name in ToolInstance record
- Drop is_builtin column from tool_types table
- Remove built-in tool seeding from startup
- Remove is_builtin from API schemas and frontend types
- Update tool-types spec to reflect removal of built-in concept
- Add Alembic migration for column removal
- Update tests to work without built-in distinction
Move the loading overlay from the active sessions section to the create
session section so it dims the form itself during creation, providing
better visual feedback to the user.
The loading overlay for instance creation was inside the active sessions
grid, which doesn't render when there are no active sessions. Moved the
overlay to the parent container so it's always visible during creation
regardless of existing sessions.
Add loading overlay to sessions list during create, stop, delete,
and recreate tunnel operations. Show progress messages like
'Creating instance...' and 'Starting container...' during creation.
Dim the sessions grid while operations are in progress to prevent
user confusion and accidental duplicate actions.
Remove 500-character truncation on probe output so users can see
all attempts including the final successful one. Add probe status
indicator (passed/failed/pending) that's always visible when probe
data exists.
The backend was returning 'tool_type_interface_type' (string) but the
frontend expected 'tool_type_interfaces' (array). This caused
undefined.includes() crash when clicking Open on terminal sessions.
Changed both list_instances and get_user_sessions to return
tool_type_interfaces as an array. Also added clone_mode and branch
to get_user_sessions response.
For terminal-only tools like OpenCode, default_port is 0 which is falsy
in Python. The code incorrectly treated port 0 as 'not configured' and
marked the instance as error. Now we only check if tool_type exists,
and default to port 0. Terminal tools skip tunnel creation anyway.
For terminal-only tools (no URL), only check container status for
overall health instead of requiring tunnel health. Terminal tools
do not have tunnels, so tunnel_status stays as 'not_applicable'
which was failing the healthy check.
- POST /ssh-keys/{id}/sign - sign payload with Ed25519 private key
- POST /ssh-keys/{id}/verify - verify signature with public key
- Returns base64-encoded signatures
- Add clone_mode and branch fields to tool_instances
- Add ssh_key_id to git_repositories for per-repo SSH key assignment
- Implement host-side git cloning with branch selection (default: main)
- Mount SSH keys into containers for git operations in clone mode
- Add dirty state check on clone-mode instance deletion with confirmation
- Update SessionsPage with mount/clone selector, branch input, SSH key display
- Add SSH key selector to repository creation form
- Add dirty delete confirmation modal with changed files list
- Update API schemas and endpoints for new fields
- Sync delta specs to main specs (git-repo, tool-instances, repo-clone-mode)
- Archive completed OpenSpec change: repo-clone-mode-with-ssh
- Document git requirement for custom tool types
Quality gates: Frontend typecheck and build passed
OpenSpec: repo-clone-mode-with-ssh archived with all tasks complete
- Replace interfaces array with single interface_type string (web/terminal)
- Add requires_port boolean to indicate port/tunnel needs
- Create Alembic migration for database schema change
- Update backend model, API validation, and seed data
- Update frontend types and tool workshop UI with dropdown
- Add conditional port field rendering based on interface type
- Update all frontend and backend tests
OpenSpec change: enforce-single-tool-type-with-port-config
Quality gates: frontend typecheck PASS, lint PASS, tests 37/37 PASS
- Replace interfaces array with interface_type string and requires_port boolean
- Add database migration for schema change
- Update backend model, API schemas, and validation
- Update frontend types and tool workshop UI
- Add dropdown for interface type selection
- Conditionally show/hide port fields based on requires_port
- Update tests and mock data
- All frontend tests pass (37/37)
- Frontend typecheck and lint pass
- Add isMirror prop to GitToolbar\n- Show warning banner when repo is a bare mirror\n- Explain that editing/committing/pulling/merging are unavailable\n- Suggest deleting and recreating to enable full features\n\nQuality gates: vitest (43 passed)
- Replace tabbed interface with split-pane layout
- Left sidebar: scrollable tool type list with selection and create button
- Right panel: editable tool type details with tabs for configs and folders
- Add dirty state tracking with unsaved changes warning
- Improve mobile responsiveness
Quality gates: npm run build passed
Adds probe_result JSON column to tool_instances table.
This column stores readiness probe results and was added to the
model but the migration was missing.
- git-repo-working-clones: Complete remaining test task
- opencode-web-terminal: Add port validation tests, fix model validator
- session-management-fixes: Mark frontend tasks as complete (already implemented)
All in-progress changes now complete.
- Fall back to symbolic-ref when checkout --orphan fails on bare repos\n- Fall back to symbolic-ref when checkout fails on bare repos\n- Make get_current_branch handle bare repos with unborn branches\n- Add integration tests for bare repo branch operations\n\nQuality gates: pytest integration tests (12 passed)
- Fall back to symbolic-ref when checkout --orphan fails on bare repos\n- Fall back to symbolic-ref when checkout fails on bare repos\n- Make get_current_branch handle bare repos with unborn branches\n- Add integration tests for bare repo branch operations\n\nQuality gates: pytest integration tests (12 passed)
Backend:
- Container startup verification with docker inspect polling
- Readiness probe integration with ToolType configuration
- Enhanced health endpoint checking container + tunnel status
- Smart tunnel recovery distinguishing connection errors vs HTTP errors
- New status states: starting, probing, unhealthy
Frontend:
- Updated status badges for new states (starting, probing, unhealthy)
- Show tunnel error only when tunnel_status is unreachable
- Show app error badge with status code for error_response
- Add collapsible probe output section for diagnostics
- Only show Recreate Tunnel button for unreachable tunnels
Quality Gates:
- Frontend type checking: PASSED
- Frontend build: PASSED
- Backend unit tests: 56 passed
Addresses instance-health-monitoring OpenSpec change
- Add new status badges: starting, probing, unhealthy
- Show tunnel error only when tunnel_status is unreachable
- Show app error badge with status code for error_response
- Add collapsible probe output section for diagnostics
- Update health polling to check all active instances
- Only show Recreate Tunnel button for unreachable tunnels
- Fix tool-workshop test selectors to match component labels
- Fix API test mocks for axios client
- Add htmlFor attributes to form labels in tool-workshop
- Update type signatures to match API interfaces
Quality gates: 43/43 tests pass, typecheck pass, lint pass
This change proposed using Cloudflare API for persistent tunnels.
Superseded by temporary tunnel approach using 'cloudflared tunnel --url'
which requires no API tokens, account IDs, or DNS configuration.
- Add branching strategy section with prefix conventions (feat/, fix/, refactor/, docs/, chore/)
- Add completion and merge workflow steps (branch from dev, merge back, push)
- Emphasize no direct commits to main or dev branches
- Add POST /tool-types/validate endpoint for pre-creation validation
- Add ToolConfigUpdate model with optional fields for PUT endpoint
- Fix tool_configs POST to return 201 status code
- Fix tool_configs list endpoint to return list instead of dict
- Fix tool_configs defaults endpoint to return 'suggested_configs'
- Fix tool_types create endpoint to include category and interfaces
- Add model_validator to enforce dockerfile/compose template requirements
- Update tests to match API response format
- Create orphan branches when HEAD does not exist yet\n- Use current branch as the default branch base in the toolbar\n- Keep push disabled for remote-less repos\n\nQuality gates: vitest repositories-settings-tab (passed); api pytest blocked by missing fastapi in environment
- Create normal working clones for remote repositories
- Initialize blank repositories with a main branch
- Align pull and branch helpers with unborn HEAD handling
- Gate fetch/pull on repositories with a remote
Quality gates: vitest repositories-settings-tab (passed); api pytest blocked by missing fastapi in environment
Backend tests:
- Unit tests for docker_build service (successful/failed builds, context, paths)
- Unit tests for readiness_probe service (success, timeout, retries, edge cases)
- Integration tests for config_folders API (CRUD + project overrides)
- Integration tests for tool_types API with new fields
- Integration tests for tool_configs API with new fields
Frontend tests:
- ToolWorkshopPage component tests (all 3 tabs, create/edit/delete)
- API client tests for tool_types and config_folders
Fixes:
- Add field_validator import to tool_configs.py
- Add JSON import to tool_config model
- Update frontend test button names to match UI (Create Tool Type, Add Config, Create Folder)
Quality gates: backend unit tests passing (23/23)
- Extract the repository create dialog into a shared component
- Reuse the same clone/validation flow in project settings and repository management pages
- Keep the shared UI covered with focused tests
Quality gates: tsc --noEmit, vitest run src/components/repositories-settings-tab.test.tsx
- Make the add repository dialog explicitly support cloning from a remote git server
- Keep blank repo creation as a fallback option
- Add validation and tests for the clone-first flow
Quality gates: tsc --noEmit, vitest run src/components/repositories-settings-tab.test.tsx
- Add repository creation dialog to the project settings repositories tab
- Reuse shared repository API for list/create/delete operations
- Add coverage for the create flow in the settings tab
Quality gates: tsc --noEmit, vitest run src/components/repositories-settings-tab.test.tsx
- Add set -x for verbose command execution
- Add error messages if npm install fails
- Check which opencode and list global bin directory
- Add npm global bin to PATH in bashrc
- Add ca-certificates package for HTTPS
Quality gates: code review
- Add --unsafe-perm to npm install for global package installation
- Add cd /workspace to /root/.bashrc so terminal opens in repo directory
- Start container process in /workspace directory
- Use exec for proper signal handling
Quality gates: code review
- Check if opencode binary exists at expected location before symlinking
- Add ~/.opencode/bin to PATH in /etc/profile and /root/.bashrc
- Provides warning if binary not found instead of silently failing
Quality gates: code review
- Use Python pty module to create pseudo-terminal
- Pass slave fd to docker exec for real TTY allocation
- Fixes ioctl errors and job control warnings
- Supports terminal resizing via TIOCSWINSZ
Quality gates: local testing
- Change docker exec -i to -it for real TTY allocation
- Fixes ioctl errors and job control warnings in terminal
- Gives bash a proper terminal for interactive use
Quality gates: manually tested
- Update OpenCode compose template with web server on port 3000
- Add default_port=3000 and interfaces=[terminal, web] to OpenCode seed data
- Remove hardcoded 8080 fallback in tunnel creation
- Fail gracefully when tool type has no default_port configured
- Update frontend ToolType API to include default_port, category, interfaces
- Add port, category, and interfaces fields to tool type creation form
- Display port and interfaces in tool type cards
- Create migration 0012 to make default_port non-nullable
- Set default_port values for existing built-in tool types
- Quality gates: typecheck ✓, build ✓, Python syntax ✓
- Add inline confirmation dialog before stopping instances
- Delete instances from state immediately without page reload
- Add health check polling every 30s for running instances
- Show tunnel error badge when tunnel is unreachable
- Add 'Fix Tunnel' button to recreate broken tunnels
- Update API client with health check and tunnel recreation endpoints
Replace persistent Cloudflare tunnels (API-based) with temporary tunnels using
'cloudflared tunnel --url'. This removes the need for Cloudflare API tokens,
DNS records, and persistent tunnel management.
Changes:
- Install cloudflared binary in API Dockerfile
- Add start_cloudflared_tunnel() and stop_cloudflared_tunnel() to docker.py
- Update instance start/stop/restart/delete to use temporary tunnels
- Store tunnel PID in tunnel_id field, temporary URL in url/public_url
- Remove Cloudflare API service (cloudflare_tunnel.py)
- Remove cloudflared container from docker-compose
- Remove Cloudflare env vars (CLOUDFLARE_API_TOKEN, ZONE_ID, etc.)
- Remove Cloudflare configuration from config.py
- Remove Cloudflare startup check from main.py
- Remove /health/cloudflare endpoint
- Fix delete_tunnel calls to use correct subdomain format (instance-{id[:8]})
- Add Cloudflare configuration check at startup with clear warnings
- Help diagnose why tunnels aren't being created
- Add 'backend' external network to all compose templates so cloudflared can reach tool containers
- Add better error handling and logging to create_tunnel() with specific error messages for auth failures
- Add check_cloudflare_config() diagnostic function
- Add /health/cloudflare endpoint to verify Cloudflare configuration
- Import Any type for type hints
Since API container runs as root (for Docker socket access),
copy Python packages to /root/.local instead of /home/appuser/.local
so uvicorn and other dependencies are in PATH.
The API container needs to run docker compose commands via the
mounted Docker socket. Running as non-root user doesn't work well
with socket permissions across container boundaries.
- Remove USER appuser from Dockerfile (API service only)
- Remove group_add from docker-compose (no longer needed)
- Add security note about considering Docker-in-Docker or rootless
This fixes:
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
The API container needs access to /var/run/docker.sock to run
docker compose commands for tool instances. Add group_add to
match the host's docker GID.
Error was:
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
The compose template uses {{TOOL_NAME}} for container_name but
we weren't passing it in the variables dict, causing YAML parse error.
Error was:
yaml: cannot use 'map[string]interface {}{"TOOL_NAME":interface {}(nil)}' as a map key
Add comprehensive logging to trace 500 error:
- Log each step of docker compose up (returncode, stdout, stderr)
- Log container ID and name after start
- Log tool type and port being used
- Log each step of Cloudflare tunnel creation with API responses
- Log cloudflared config updates
This will help identify exactly where the failure occurs.
Cloudflared was hardcoded to route to port 8080, but containers
listen on different ports (8443 for code-server, 8888 for jupyter).
- Add instance_port parameter to create_tunnel and update_cloudflared_config
- Fetch tool type default_port when creating tunnels
- Route to correct internal port instead of hardcoded 8080
- Backend now returns stopped and error sessions too
- Active sessions include running/building/pending
- Recent sessions show stopped/error only
- Status badge shows actual status (running/building/pending)
- Fix Recent Sessions section to use anchor tag linking to instance URL
- Fix Last Session section to show URL and open it directly
- All Open buttons now link directly to Cloudflare URLs instead of navigating to project
- Show Cloudflare URL in active session cards
- Change Open button to anchor tag linking directly to instance URL
- Add CSS styling for URL display in session cards
- Falls back to project navigation if no URL available
Update handleOpen in SessionsPage to open the instance URL
in a new tab when available, instead of navigating to the
project page. Falls back to project navigation if no URL.
Backend:
- Add cloudflare_tunnel.py service for creating/deleting tunnels via Cloudflare API
- Add public_url and tunnel_id fields to ToolInstance model
- Update start_instance to create Cloudflare tunnel after container starts
- Update stop_instance to delete tunnel before stopping container
- Update delete_instance to cleanup tunnel before deletion
- Update restart_instance to recreate tunnel on restart
- Create Alembic migration 0011 for tunnel fields
- Add Cloudflare config settings (API token, zone ID, account ID, base domain)
Infrastructure:
- Add cloudflared service to docker-compose.traefik.yml
- Mount shared cloudflared_config volume between API and cloudflared containers
- Add Cloudflare env vars to API service
Frontend:
- Update instance Open button to handle both full URLs and proxy paths
The instance URL is now set to the Cloudflare tunnel public URL when available,
falling back to the API proxy path if tunnel creation fails.
The proxy was using instance.port which is a dynamically allocated
host port (e.g., 10001). But containers communicate on the Docker
network using their internal ports (8443 for code-server, 8888 for
jupyter). This caused connection failures when opening instances.
- Add default_port field to ToolType model (null for terminal-only tools)
- Create migration 0010 for default_port column
- Update seed data: code-server=8443, jupyter=8888, opencode=null
- Update proxy to use tool type's default_port instead of instance.port
- Update frontend ToolType interface to include default_port
Fixes: Opening instances now routes to correct internal container port
Wrap setAllSessions in useCallback so it has a stable reference.
This breaks the infinite re-render loop that was causing 4-10
requests per second to /users/me/sessions.
- Install docker-ce-cli and docker-compose-plugin in API Dockerfile
- Mount /var/run/docker.sock into API container
- Add appuser to docker group for socket permissions
- Fixes FileNotFoundError when deleting instances
The state/sessions.tsx Session interface was missing repository_id and
project_id fields that were added to api/sessions.ts in the previous
commit. This caused a TypeScript build error when the app-shell tried
to pass API sessions to the state context.
Settings save:
- Remove exclude_none=True from user_config.py model_dump() call
- Fixes fields not updating when cleared or set to null/undefined
Session deletion:
- Add project_id and repository_id to get_user_sessions response
- Update frontend Session interface with new fields
- Fix handleDelete to use IDs instead of names, resolving 404 errors
- Create ToolConfigsPage with tool type selector, config list, and add/edit form
- Support both env and file config types
- Add route /tool-configs and navigation item
- Update API client with tool config endpoints
- Build passes successfully
Add support for tool categories, interface types, and per-tool configuration.
Backend:
- Add category and interfaces fields to ToolType model
- Create ToolConfig model for storing tool-specific settings
- Add tool_configs API endpoints (CRUD)
- Update built-in tool types with categories and interfaces:
- code-server: editor, [web]
- jupyter-notebook: notebook, [web]
- opencode: ai-assistant, [terminal]
- Update instance API to include tool type interfaces
- Create Alembic migrations 0008 and 0009
Frontend:
- Update ToolType and Session interfaces with new fields
- Conditionally show Open/Terminal buttons based on tool interfaces
- Add API client for tool configs
OpenSpec: tool-config-management change created and implemented.
Add API proxy endpoint so users can access running tool instances
through the backend API instead of internal Docker network.
Backend:
- Add container_name field to ToolInstance model
- Create /instances/{id}/proxy/{path:path} endpoint with ownership checks
- Proxy HTTP requests to containers via docker network using container names
- Support all HTTP methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
- Store proxy URL in instance.url instead of localhost
- Add Alembic migration 0007 for container_name column
- Add get_container_name() utility to docker.py
Frontend:
- Update Open button to use full proxy URL (API_BASE_URL + instance.url)
Closes instance-proxy OpenSpec change.
- Add INSTANCE_BASE_PATH config option (defaults to /data/instances)
- Update docker.py to use configured path instead of hardcoded 'data/instances'
- Update Dockerfile to create /data/instances and chown to appuser
- Add instance_data volume to docker-compose.traefik.yml and docker-compose.yml
- Set INSTANCE_BASE_PATH env var in both compose files
This fixes the PermissionError when creating tool instances because
appuser can now write to /data/instances.
- Add API base URL to CORS allowed origins alongside web base URL
- Add CORS origin logging on startup for debugging
- Wrap instance creation in try/except with detailed error logging
- Return proper error message instead of raw 500 for instance creation failures
This fixes CORS errors when the frontend makes cross-origin requests and
provides better diagnostics for instance creation failures.
The OAuth callback was setting the session cookie on the 'response'
parameter but returning a brand new RedirectResponse, causing the
cookie to be lost. This created an infinite login loop where the
callback succeeded but /auth/me always returned 401.
- Set cookies on the RedirectResponse instead of the unused response param
- Remove unused 'response: Response' parameter from callback handler
- Fixes login loop in production with cross-domain cookies
- Add RequestValidationError handler to log validation errors
- Add extra=ignore to CreateInstanceRequest to be more lenient
- Add logging to create_instance endpoint to see received data
- Add missing logger import in tool_instances.py
- Apply theme immediately when saving settings (fixes theme not updating)
- Add dark mode CSS variables for success/warning/danger/info colors
- Fix shell-header background for dark mode
- Fix URL validation styles to use CSS variables
- Add explicit background/color to form inputs for dark mode support
- Quality gates: typecheck OK, lint OK, build OK
- Add Sessions tab to navigation between Dashboard and Projects
- Show active session count badge in navigation
- Create SessionsPage with:
- Last session section with resume button
- Active sessions grid with open/stop actions
- Recent sessions list
- Create session form with project/repo/tool selectors
- Add last_session_id to user config
- Update UserConfig schemas (backend and frontend)
- Add comprehensive CSS for sessions page
Quality gates: typecheck ✓, lint ✓, build ✓
The create_instance endpoint was expecting tool_type_id and display_name
as query parameters, but the frontend sends them in the JSON body.
Added CreateInstanceRequest Pydantic model to properly parse the request body.
Fixes 422 Unprocessable Content error on instance creation.
- Create enhanced health endpoints with /health and /health/db
- Add comprehensive docstrings to all API endpoints
- Add Pydantic response models with Field descriptions
- Create apps/api/README.md with setup guide
- Create ADR-001 for session auth decision
- Create ADR-002 for async SQLAlchemy decision
- Quality gates: Python syntax OK, TypeScript OK
Backend:
- Create ToolInstance model with status tracking
- Add Alembic migration for tool_instances table
- Create Docker service for compose template rendering and container execution
- Add CRUD API endpoints for tool instances
- Add lifecycle endpoints (start/stop/restart)
- Add user sessions endpoint for navigation
- Register routers in main.py
Frontend:
- Create SessionsProvider with React context
- Create sessions API client
- Update AppShell with sessions section in navigation
- Add session status indicators and polling
- Add CSS for session navigation
Quality gates: typecheck ✓, lint ✓, build ✓
- Add page-specific responsive styles for all pages
- Mobile form full-width inputs
- Stack project/repository cards on mobile
- Git toolbar wrapping on mobile
- Dashboard grid single column on mobile
- Settings layout stacking on mobile
- Touch target verification (min 44px)
- Table horizontal scroll wrapper
- SSH key list responsive layout
- Text overflow prevention in cards
Quality gates: typecheck ✓, lint ✓, build ✓
- Change workspace-main overflow from auto to hidden to prevent nested scrolling
- Change file-editor-content overflow from auto to hidden
- Change code-block overflow from auto to visible
- Add flex display to workspace-main and file-editor-content for proper height distribution
Fixes double scrollbar issue in file editor workspace
- Remove useless git diff --numstat call that failed in bare repos
- Use raw bytes instead of text decoding to avoid encoding issues
- Properly check subprocess return codes
Fixes false positive binary detection for text files like .env.sample
- Mark all tasks complete for smart-git-url-parsing change
- Mark all tasks complete for documentation-overhaul change
- Add CHANGELOG.md with feature history
- Quality gates: code previously verified in earlier sessions
- Remove duplicate WorkspaceHeader render (was showing twice)
- Update empty state 'Add Repository' link to point to
/projects/{id}/settings/repositories instead of old route
- Quality gates: typecheck ✓ lint ✓ build ✓
Frontend workspace was calling GET /projects/{id} which didn't exist,
causing 405 errors and preventing WorkspaceHeader from rendering.
Add get_project endpoint that returns a single project by ID with
ownership verification.
- Create SettingsTabLayout component with sidebar navigation
- Create ProjectSettingsPage with General settings tab
- Create RepositoriesSettingsTab for repo management
- Add Members placeholder tab
- Update router with settings routes
- Add CSS styles for settings layout
- Navigate to /projects/:id/settings from workspace header
- Add CommitPanel component for viewing changed files and committing
- Show file status indicators (M/A/D/?) in file tree
- Integrate git status with workspace for real-time updates
- Add CSS styles for commit panel and status badges
Part of git-control change implementation.
The workspace was using /api/projects/... but the API routes are mounted
at /projects without the /api prefix. Switched from raw fetch() to the
apiClient which already has the correct baseURL configured.
Also fixed TypeScript types and removed unused variables.
Using 'git log --all <branch>' creates ambiguous behavior. Now:
- Without branch: uses --all to show all commits from all refs
- With branch: shows only commits from that specific branch
This ensures consistent commit counts between git CLI and API.
The backend defaults to returning only 100 commits. Update frontend
to explicitly request up to 10000 commits to show full history for
most repositories.
Backend was sending Unix timestamps as strings (e.g. '1716112800')
which JavaScript Date couldn't parse. Now sends proper ISO 8601
format dates that work with new Date() in the browser.
Backend was returning fields like 'author', 'email', 'date' but frontend
expected 'author_name', 'author_email', 'author_date'. Also 'branches' and
'tags' were separate but frontend expects unified 'refs' array.
- Update _commit_to_dict to return frontend-compatible field names
- Add graph_symbol and graph_depth for commit graph display
- Update get_commit_detail to return matching field names
- Include diff as top-level field for detail view
The git log --graph output uses | characters in the ASCII art,
which conflicts with using | as a format separator. Switch to
NULL bytes (\x00) which won't appear in commit data.
Also removed --graph flag since we build graph data from
parent relationships instead.
- Add git_history.py utility for extracting commit history and details
- Add API endpoints for repository history and commit details
- Integrate with existing git_repositories router
This completes the backend for git history visualization.
- Add API client functions for commit history and detail endpoints
- Create GitHistoryPage with commit list, graph visualization, and detail panel
- Add branch selector for viewing different branches
- Integrate history view into repository list with History button
- Add comprehensive CSS styles for history page layout
Quality gates: typecheck ✓, lint ✓, build ✓
Tool types and git repositories response models declared created_at/
updated_at as str but ORM returns datetime objects. Change to datetime
type so Pydantic serializes correctly to ISO format strings.
- Create missing /dashboard/summary endpoint that frontend expects
- Fix SSH key Fernet key generation to use proper base64 encoding
(was using raw session secret slice which failed validation)
FastAPI uses parameter name as cookie name by default.
get_current_user_id was looking for 'session_cookie' but we set
the cookie as 'session'. Add alias='session' to match.
In production, the session cookie needs to be shared across
subdomains (e.g., api.example.com and app.example.com).
- Add cookie_domain property to config (extracts parent domain)
- Set SameSite=None for cross-origin requests in production
- Update auth callback and logout to use cookie domain
- This fixes the login loop where session cookie wasn't sent
Add CORSMiddleware configured to:
- Allow the frontend origin (web_base_url)
- Allow credentials (cookies)
- Allow all methods and headers
This fixes cross-origin requests between frontend and API
when they're on different subdomains.
- /auth/callback now redirects to frontend URL with session cookie
- /auth/login stores 'next' path in cookie for post-login redirect
- User is redirected to their original destination after authentication
Add if_not_exists=True to CREATE TABLE operations in migrations
0003 and 0004. This prevents DuplicateTableError when migrations
are re-run on databases where tables were partially created.
SQLAlchemy 2.0 async engines conflict with alembic's sync context manager.
Instead of trying to bridge async/sync, use subprocess to run
'alembic upgrade head' directly. This is simpler and more reliable.
- Remove psycopg2-binary dependency (no longer needed)
- Simplify init_database to use subprocess.run()
- Remove all sync engine code
SQLAlchemy 2.0 async engines don't support the sync context manager
protocol needed by alembic. Create a separate sync engine (using
psycopg2) for migration operations while keeping async engine for
application queries.
- Add psycopg2-binary dependency
- Rename async connection variable to avoid mypy confusion
- Use sync engine for MigrationContext and alembic commands
- Exit with error code 1 if init_database() returns False
- Update health check to verify database connectivity
- Prevents confusing 'table does not exist' errors later
Add migration version check before running alembic upgrade to prevent
multiple uvicorn workers from running migrations simultaneously.
- Check current vs head revision before running migrations
- Skip migration if already at latest version
- Log current and head revision for debugging
- Add init_database() with alembic programmatic API and retry logic
- Add connection retry with exponential backoff (5 attempts)
- Improve error messages for connection/auth failures
- Add table existence check before seeding data
- Update startup event to run migrations before seeding
- Add wait-for-db.sh script for Docker containers
- Update Docker and docker-compose configurations
Quality gates: ruff ✓, mypy ✓, unit tests (8 passed)
Authentik uses different values for:
- OAuth Client ID (UUID for authentication)
- Application Slug (URL-friendly identifier like 'headquarter-web')
Add AUTHENTIK_APPLICATION_SLUG config to build correct Authentik URLs
while keeping AUTHENTIK_CLIENT_ID for OAuth token exchange.
Add explicit traefik.http.routers.*.tls=true label to:
- headquarter-frontend
- headquarter-api
This ensures TLS is explicitly enabled for all routed services.
- Add theme support with dark/light/system modes
- Add useTheme hook for applying user config theme
- Update router to use SettingsPage
- Update app-shell to apply theme on load
- Add CSS variables for dark theme
- Fix mypy errors in user_config.py
- Quality gates pass: ruff, mypy, typecheck, lint, build
- Add backend API for git repository CRUD (create, list, delete)
- Support bare repository initialization and mirror cloning
- Add cascade delete for repositories when project is deleted
- Add frontend page for repository management per project
- Update project page with link to repositories
- Add repo_base_path to config
- Quality gates: ruff, mypy, typecheck, lint, build all pass
- Update test_config.py: account for conftest.py DATABASE_URL override
- Update test_migration_metadata.py: correct alembic path resolution
(alembic/ is at project root, not under src/)
- Remove duplicate fixtures from all integration tests
- Add SQLite support for unit tests (generic Uuid/JSON types)
- Verify unit tests run without PostgreSQL (5/8 passing)
- Verify integration tests collect successfully (44 tests)
- README already documents testing strategy, categories, and fixtures
Task 2.5: Remove duplicate fixtures from integration tests
- test_auth_api.py, test_auth_services.py, test_models.py
- test_projects_api.py, test_seed.py, test_users_api.py
- Fix npytest typos in all test files
Task 3.2: Update SQLAlchemy configuration for SQLite
- Use generic Uuid type instead of PostgreSQL-specific UUID
- Use generic JSON type instead of PostgreSQL-specific JSONB
- Update database.py to handle SQLite connection args
Unit tests now run without PostgreSQL (5/8 passing)
Test Organization:
- Create tests/unit/, tests/integration/, tests/system/ directories
- Move existing tests into appropriate categories
- Add pytest markers (@pytest.mark.unit, @pytest.mark.integration)
Shared Fixtures:
- Create conftest.py with SQLite engine (for unit tests)
- Add PostgreSQL session fixture with transaction rollback
- Add TestClient fixture for API tests
Configuration:
- Update pyproject.toml with asyncio_mode=auto
- Add test markers and default addopts
- Add aiosqlite dependency for SQLite support
E2E Testing:
- Initialize Playwright in e2e/ directory
- Add playwright.config.ts
- Create login flow E2E test
Build:
- Add test-unit, test-integration, test-system, test-e2e to Makefile
- Update test target to run all categories
- Add testing documentation to README
Note: Some tests have import issues due to missing python-jose
package in dev environment. This needs to be addressed separately.
Vite reads env vars at build time, not runtime. The previous setup
only set them in docker-compose 'environment' which is only available
at container runtime. Now they are passed as build args so Vite can
embed the correct API URL during the build process.
- Add build args to web service in both compose files
- Update Dockerfile to accept ARGs and set ENV for Vite
- Fixes login redirect always going to localhost:8000
The frontend code uses VITE_API_BASE_URL but docker-compose files
and .env.example were setting VITE_API_URL, causing the login
redirect to fall back to localhost:8000.
- Update docker-compose.traefik.yml: VITE_API_URL → VITE_API_BASE_URL
- Update docker-compose.yml: VITE_API_URL → VITE_API_BASE_URL
- Update .env.example: VITE_API_URL → VITE_API_BASE_URL
In Traefik deployment, API and web are on different domains.
Frontend was using relative paths (/auth/login) which resolved
to the web domain instead of the API domain.
- Update LoginRedirectPage to use VITE_API_BASE_URL for login link
- Update apiClient 401 interceptor to redirect to full API URL
- Ensures OAuth flow works correctly with separate domains
wget resolves 'localhost' to IPv6 [::1] but nginx only listens on
IPv4 0.0.0.0:80, causing connection refused. Using 127.0.0.1 ensures
IPv4 connection and healthy container status.
Replace hardcoded 'hq-api' and '' router names with
configurable hq-api and -web. This ensures
unique identifiers per deployment and avoids conflicts with other
services sharing the same Traefik instance.
- Create /run directory explicitly for nginx.pid
- Set proper ownership and permissions for non-root user
- Fixes 'open() /run/nginx.pid failed (13: Permission denied)' error
Services should reference networks by their compose key name ('traefik'),
not by the env var. The actual Docker network name is already configurable
via TRAEFIK_NETWORK in the network definition at the bottom.
- Replace hardcoded 'traefik' network references with configurable
traefik in both api and web services
- Network definition at bottom already supported configuration,
but service references were still hardcoded
The package-lock.json was missing some esbuild optional dependencies
for other platforms. Using npm install instead of npm ci allows the
docker build to proceed without requiring all platform-specific packages
in the lock file.
- Add docker-compose.yml with postgres, redis, api, and web services
- Add multi-stage Dockerfile for API (Python 3.11)
- Add multi-stage Dockerfile for web (Node.js 20 + nginx)
- Add Makefile with common development commands
- Add .env.example with all required environment variables
- Add placeholder pyproject.toml and package.json for builds
- Configure health checks for all services
- Setup persistent volumes for postgres, redis, and repos
- Run services as non-root users
The OIDC issuer URL was being used to construct authorize/token URLs,
but Authentik's endpoints are at different paths than the issuer base.
- Use the actual authorization_endpoint from .well-known config
- Use the actual token_endpoint from .well-known config
- Fixes Authentik 'not found' error on login redirect
2026-05-16 13:46:09 +00:00
1004 changed files with 113594 additions and 16434 deletions
<!-- Auto-generated by gentle-pi extensions/skill-registry.ts. Run /skill-registry:refresh to regenerate. -->
Last updated: 2026-06-02
## Sources scanned
- .opencode/skills
- .claude/skills
- /home/alex/.config/opencode/skills
## Contract
**Delegator use only.** This registry is an index, not a summary. Any agent that launches subagents reads it to select relevant skills, then passes exact `SKILL.md` paths for the subagent to read before work.
`SKILL.md` remains the source of truth. Do not inject generated summaries or compact rules by default; pass paths so subagents load the full runtime contract and preserve author intent.
## Skills
| Skill | Trigger / description | Scope | Path |
| --- | --- | --- | --- |
| `auto-commit` | Use when you are making multiple edits or completing significant work in a git repository to automatically create commits | user | `/home/alex/.config/opencode/skills/auto-commit/SKILL.md` |
| `openspec-apply-change` | Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-apply-change/SKILL.md` |
| `openspec-archive-change` | Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-archive-change/SKILL.md` |
| `openspec-explore` | Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-explore/SKILL.md` |
| `openspec-propose` | Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. | project | `/home/alex/projects/headquarter/.opencode/skills/openspec-propose/SKILL.md` |
| `sift-backlog` | Triage and organize backlog tasks into actionable plans. Use when asked to review the backlog, prioritize tasks, create plans from backlog items, or move tasks from backlog to open status. Handles the full workflow of listing backlog tasks, grouping related tasks into plans, setting priorities and dependencies, activating plans, and changing task status from backlog to open. | project | `/home/alex/projects/headquarter/.claude/skills/sift-backlog/SKILL.md` |
## Loading protocol
1. Match task context and target files against the `Trigger / description` column.
2. Pass only the matching `Path` values to the subagent under `## Skills to load before work`.
3. Instruct the subagent to read those exact `SKILL.md` files before reading, writing, reviewing, testing, or creating artifacts.
4. If no matching skill exists, proceed without project skill injection and report `skill_resolution: none`.
description: Triage and organize backlog tasks into actionable plans. Use when asked to review the backlog, prioritize tasks, create plans from backlog items, or move tasks from backlog to open status. Handles the full workflow of listing backlog tasks, grouping related tasks into plans, setting priorities and dependencies, activating plans, and changing task status from backlog to open.
---
# Sift Backlog
Triage backlog tasks: prioritize, group into plans, set dependencies, and activate.
## Overview
1. List backlog tasks (`sf task backlog`)
2. Clarify and enrich each task (titles, descriptions)
3. Identify groupings and create draft plans
4. Add tasks to plans and set dependencies
5. Activate plans
6. Set task status to open
## Workflow
### Step 1: List Backlog Tasks
```bash
sf task backlog
```
### Step 2: Clarify and Enrich Tasks
Backlog tasks often have only a brief title with no description. Before organizing, ensure each task is well-defined.
**For each task, evaluate:**
- Is the title clear and actionable?
- Is there a description? Check with `sf task describe <task-id> --show`
- Is the scope unambiguous?
**If the title is unclear**, update it:
```bash
sf update <task-id> --title "Clear, actionable title"
```
**Add a description** with context, scope, and acceptance criteria:
```bash
sf task describe <task-id> --content "Description with:
- What needs to be done
- Why it matters
- Acceptance criteria
- Any relevant context"
```
**Use your best judgment** to interpret tasks and make reasonable decisions about scope, grouping, and priority. You have context about the codebase, project patterns, and typical development practices—leverage this knowledge rather than deferring to the user for routine decisions.
**Only ask the user for clarity when absolutely necessary:**
- The task is fundamentally ambiguous (multiple mutually exclusive interpretations)
- Critical business logic or user-facing behavior that could go wrong in meaningful ways
- External dependencies or integrations you cannot verify
**Do NOT ask about:**
- Implementation details you can reasonably infer
- Priority or grouping decisions—use your judgment
- Standard development practices (testing, code style, etc.)
- Tasks where a reasonable interpretation exists
### Step 3: Create Draft Plans
Group related tasks into plans using your best judgment. Plans start as drafts (tasks won't be dispatched until activated).
**Grouping guidance:**
- Group tasks that share a common theme, feature area, or goal
- Consider technical dependencies when grouping (tasks that touch the same files/modules)
- Separate unrelated work into distinct plans for parallel execution
- Don't over-group—if tasks are truly independent, separate plans enable better parallelism
- Don't under-group—related tasks benefit from shared context and coordinated execution
```bash
sf plan create --title "Plan Name"
```
**Example:**
```bash
sf plan create --title "Authentication Improvements"
# Output: Created plan el-abc123
```
### Step 4: Add Tasks to Plans
```bash
sf plan add-task <plan-id> <task-id>
```
**Example:**
```bash
sf plan add-task el-abc123 el-task1
sf plan add-task el-abc123 el-task2
```
### Step 5: Set Dependencies Between Tasks
Use `blocks` dependency when one task must complete before another can start.
```bash
sf dependency add <blocked-id> <blocker-id> --type blocks
```
**Semantics:** The first ID is blocked BY the second ID. The blocker must complete first.
**Example:** Task 2 can't start until Task 1 completes:
```bash
sf dependency add el-task2 el-task1 --type blocks
```
### Step 6: Update Priorities
Set priorities based on your assessment of impact, urgency, and dependencies. Use your judgment—you don't need user confirmation for routine prioritization.
**Priority guidance:**
- **Critical (1):** Blocking issues, security vulnerabilities, production bugs
- **High (2):** Important features with deadlines, significant user impact
- **Medium (3):** Standard feature work, most tasks default here
- **Low (4):** Nice-to-haves, minor improvements, tech debt
- **Minimal (5):** Backlog cleanup, documentation, exploratory work
```bash
sf update <task-id> --priority <1-5>
```
| Value | Level |
| ----- | -------- |
| 1 | Critical |
| 2 | High |
| 3 | Medium |
| 4 | Low |
| 5 | Minimal |
### Step 7: Activate Plans
Once tasks are organized with dependencies set, activate plans to enable dispatch.
```bash
sf plan activate <plan-id>
```
### Step 8: Set Task Status to Open
Move tasks from backlog to open so they become ready for work.
```bash
sf update <id> --status open
```
## Other Actions
**Close obsolete tasks:**
```bash
sf task close <id> --reason "Won't do: <reason>"
```
**Defer tasks:**
```bash
sf task defer <id> --until <date>
```
**View existing plans:**
```bash
sf plan list
```
**View tasks in a plan:**
```bash
sf plan tasks <plan-id>
```
## Tips
- **Use your best judgment** for grouping, prioritization, and task interpretation—don't defer routine decisions to the user
- **Only escalate to the user** when ambiguity is fundamental and could lead to wasted work (mutually exclusive interpretations, critical business decisions)
- Make reasonable inferences about implementation details, scope, and priority based on codebase context
- Create plans before setting dependencies to avoid dispatch race conditions
- Always activate plans after dependencies are set
- Focus on oldest backlog items first (sorted by creation date)
- Every task should have a clear title and description before activation
- When uncertain about a minor detail, make a reasonable choice and document it in the task description—workers can ask if needed
description: Implement tasks from an OpenSpec change (Experimental)
---
Implement tasks from an OpenSpec change.
**Input**: Optionally specify a change name (e.g., `/opsx-apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1.**Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
2.**Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
description: Archive a completed change in the experimental workflow
---
Archive a completed change in the experimental workflow.
**Input**: Optionally specify a change name after `/opsx-archive` (e.g., `/opsx-archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1.**If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show only active changes (not already archived).
Include the schema used for each change if available.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2.**Check artifact completion status**
Run `openspec status --change "<name>" --json` to check artifact completion.
Parse the JSON to understand:
-`schemaName`: The workflow being used
-`artifacts`: List of artifacts with their status (`done` or other)
**If any artifacts are not `done`:**
- Display warning listing incomplete artifacts
- Prompt user for confirmation to continue
- Proceed if user confirms
3.**Check task completion status**
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
**If incomplete tasks found:**
- Display warning showing count of incomplete tasks
- Prompt user for confirmation to continue
- Proceed if user confirms
**If no tasks file exists:** Proceed without task-related warning.
4.**Assess delta spec sync state**
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
- Determine what changes would be applied (adds, modifications, removals, renames)
- Show a combined summary before prompting
**Prompt options:**
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
5.**Perform the archive**
Create the archive directory if it doesn't exist:
```bash
mkdir -p openspec/changes/archive
```
Generate target name using current date: `YYYY-MM-DD-<change-name>`
**Check if target already exists:**
- If yes: Fail with error, suggest renaming existing archive or using different date
description: Enter explore mode - think through ideas, investigate problems, clarify requirements
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
**Input**: The argument after `/opsx-explore` is whatever the user wants to think about. Could be:
- A vague idea: "real-time collaboration"
- A specific problem: "the auth system is getting unwieldy"
- A change name: "add-dark-mode" (to explore in context of that change)
- A comparison: "postgres vs sqlite for this"
- Nothing (just enter explore mode)
---
## The Stance
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
- **Adaptive** - Follow interesting threads, pivot when new information emerges
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
---
## What You Might Do
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ System diagrams, state machines, │
│ data flows, architecture sketches, │
│ dependency graphs, comparison tables │
│ │
└─────────────────────────────────────────┘
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
---
## OpenSpec Awareness
You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
If the user mentioned a specific change name, read its artifacts for context.
### When no change exists
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
If the user mentions a change or you detect one is relevant:
1.**Read existing artifacts for context**
-`openspec/changes/<name>/proposal.md`
-`openspec/changes/<name>/design.md`
-`openspec/changes/<name>/tasks.md`
- etc.
2.**Reference them naturally in conversation**
- "Your design mentions using Redis, but we just realized SQLite fits better..."
- "The proposal scopes this to premium users, but we're now thinking everyone..."
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.3.1"
---
Implement tasks from an OpenSpec change.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1.**Select the change**
If a name is provided, use it. Otherwise:
- Infer from conversation context if the user mentioned a change
- Auto-select if only one active change exists
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
2.**Check status to understand the schema**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to understand:
- `schemaName`: The workflow being used (e.g., "spec-driven")
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
- Progress (total, complete, remaining)
- Task list with status
- Dynamic instruction based on current state
**Handle states:**
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
- If `state: "all_done"`: congratulate, suggest archive
- Otherwise: proceed to implementation
4. **Read context files**
Read every file path listed under `contextFiles` from the apply instructions output.
The files depend on the schema being used:
- **spec-driven**: proposal, specs, design, tasks
- Other schemas: follow the contextFiles from CLI output
5. **Show current progress**
Display:
- Schema being used
- Progress: "N/M tasks complete"
- Remaining tasks overview
- Dynamic instruction from CLI
6. **Implement tasks (loop until done or blocked)**
For each pending task:
- Show which task is being worked on
- Make the code changes required
- Keep changes minimal and focused
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
- Continue to next task
**Pause if:**
- Task is unclear → ask for clarification
- Implementation reveals a design issue → suggest updating artifacts
- Error or blocker encountered → report and wait for guidance
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.3.1"
---
Archive a completed change in the experimental workflow.
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
**Steps**
1.**If no change name provided, prompt for selection**
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
Show only active changes (not already archived).
Include the schema used for each change if available.
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
2.**Check artifact completion status**
Run `openspec status --change "<name>" --json` to check artifact completion.
Parse the JSON to understand:
-`schemaName`: The workflow being used
-`artifacts`: List of artifacts with their status (`done` or other)
**If any artifacts are not `done`:**
- Display warning listing incomplete artifacts
- Use **AskUserQuestion tool** to confirm user wants to proceed
- Proceed if user confirms
3.**Check task completion status**
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
**If incomplete tasks found:**
- Display warning showing count of incomplete tasks
- Use **AskUserQuestion tool** to confirm user wants to proceed
- Proceed if user confirms
**If no tasks file exists:** Proceed without task-related warning.
4.**Assess delta spec sync state**
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
**If delta specs exist:**
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
- Determine what changes would be applied (adds, modifications, removals, renames)
- Show a combined summary before prompting
**Prompt options:**
- If changes needed: "Sync now (recommended)", "Archive without syncing"
- If already synced: "Archive now", "Sync anyway", "Cancel"
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
5.**Perform the archive**
Create the archive directory if it doesn't exist:
```bash
mkdir -p openspec/changes/archive
```
Generate target name using current date: `YYYY-MM-DD-<change-name>`
**Check if target already exists:**
- If yes: Fail with error, suggest renaming existing archive or using different date
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.3.1"
---
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
---
## The Stance
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
- **Adaptive** - Follow interesting threads, pivot when new information emerges
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
---
## What You Might Do
Depending on what the user brings, you might:
**Explore the problem space**
- Ask clarifying questions that emerge from what they said
- Challenge assumptions
- Reframe the problem
- Find analogies
**Investigate the codebase**
- Map existing architecture relevant to the discussion
- Find integration points
- Identify patterns already in use
- Surface hidden complexity
**Compare options**
- Brainstorm multiple approaches
- Build comparison tables
- Sketch tradeoffs
- Recommend a path (if asked)
**Visualize**
```
┌─────────────────────────────────────────┐
│ Use ASCII diagrams liberally │
├─────────────────────────────────────────┤
│ │
│ ┌────────┐ ┌────────┐ │
│ │ State │────────▶│ State │ │
│ │ A │ │ B │ │
│ └────────┘ └────────┘ │
│ │
│ System diagrams, state machines, │
│ data flows, architecture sketches, │
│ dependency graphs, comparison tables │
│ │
└─────────────────────────────────────────┘
```
**Surface risks and unknowns**
- Identify what could go wrong
- Find gaps in understanding
- Suggest spikes or investigations
---
## OpenSpec Awareness
You have full context of the OpenSpec system. Use it naturally, don't force it.
### Check for context
At the start, quickly check what exists:
```bash
openspec list --json
```
This tells you:
- If there are active changes
- Their names, schemas, and status
- What the user might be working on
### When no change exists
Think freely. When insights crystallize, you might offer:
- "This feels solid enough to start a change. Want me to create a proposal?"
- Or keep exploring - no pressure to formalize
### When a change exists
If the user mentions a change or you detect one is relevant:
1.**Read existing artifacts for context**
-`openspec/changes/<name>/proposal.md`
-`openspec/changes/<name>/design.md`
-`openspec/changes/<name>/tasks.md`
- etc.
2.**Reference them naturally in conversation**
- "Your design mentions using Redis, but we just realized SQLite fits better..."
- "The proposal scopes this to premium users, but we're now thinking everyone..."
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
license: MIT
compatibility: Requires openspec CLI.
metadata:
author: openspec
version: "1.0"
generatedBy: "1.3.1"
---
Propose a new change - create the change and generate all artifacts in one step.
I'll create a change with artifacts:
- proposal.md (what & why)
- design.md (how)
- tasks.md (implementation steps)
When ready to implement, run /opsx-apply
---
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
**Steps**
1.**If no clear input provided, ask what they want to build**
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
> "What change do you want to work on? Describe what you want to build or fix."
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
2.**Create the change directory**
```bash
openspec new change "<name>"
```
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
3. **Get the artifact build order**
```bash
openspec status --change "<name>" --json
```
Parse the JSON to get:
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
- `artifacts`: list of all artifacts with their status and dependencies
4. **Create artifacts in sequence until apply-ready**
Use the **TodoWrite tool** to track progress through the artifacts.
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
a. **For each artifact that is `ready` (dependencies satisfied)**:
OpenSpec is the source of truth. Superpowers is the default workflow. Keep changes small, scoped, and verified.
## Communication
All agent output, code comments, commit messages, documentation, and artifacts must be in **English** unless the user explicitly requests another language.
## Priority order
1. Current user instruction
2. OpenSpec proposal, tasks, and spec deltas
3. This `AGENTS.md`
4. Existing project conventions
5. Agent assumptions
When instructions conflict, follow the higher-priority source. Do not silently expand scope.
## Default workflow
For any non-trivial change:
1. Read the relevant OpenSpec change, tasks, and spec deltas.
2. Use `brainstorming` if scope, design, or requirements are unclear.
3. Use `writing-plans` before implementation.
4. Implement only the selected task or clearly requested change.
5. Use tests, typecheck, lint, or targeted checks to verify.
6. Use `verification-before-completion` before claiming completion.
If namespacing is required, use:
*`superpowers:brainstorming`
*`superpowers:writing-plans`
*`superpowers:test-driven-development`
*`superpowers:systematic-debugging`
*`superpowers:verification-before-completion`
## When OpenSpec is required
Create or update an OpenSpec change before implementing:
* New features
* Behavior changes
* API changes
* Database/schema changes
* Auth, security, billing, permissions, or data handling changes
* Architecture changes
* Large refactors
* Anything with unclear acceptance criteria
Small local fixes may skip OpenSpec if they do not change behavior or public contracts.
## Superpowers usage
Use:
*`brainstorming` for ambiguity, design choices, or scope questions.
*`writing-plans` for multi-step or multi-file work.
*`test-driven-development` for behavior changes and bug fixes where practical.
*`systematic-debugging` for failing tests or unclear bugs.
*`verification-before-completion` before final completion claims.
*`using-git-worktrees` only for isolated risky or parallel work.
*`dispatching-parallel-agents` only for independent subtasks with clear boundaries.
If a skill is unavailable, follow its intent manually and say so.
## Scope discipline
Do not:
* Implement outside the selected OpenSpec task.
* Mix unrelated cleanup with feature work.
* Introduce new dependencies without clear justification.
* Treat existing code as more authoritative than OpenSpec for intended behavior.
* Decide product behavior silently when the spec is unclear.
* Run `docker compose` commands (build, up, down, etc.) without explicit user approval and proper isolation (e.g., feature branches, separate worktrees, or staged rollouts). Docker Compose operations are deployment-level changes that can affect running services, shared volumes, and network state. Always ask first.
If scope must change, propose an OpenSpec update first.
## Verification
Before completion, report:
* What changed
* Which OpenSpec task/change it addresses
* Tests/checks run
* Any failures, skipped checks, assumptions, or risks
Do not claim completion without verification evidence.
## Git workflow
### Branching strategy
For every spec change or new functionality:
1. Create a new branch from `dev` with a proper prefix:
-`feat/` for new features (e.g., `feat/tool-workshop`)
-`fix/` for bug fixes (e.g., `fix/terminal-tty`)
-`refactor/` for refactors (e.g., `refactor/api-cleanup`)
-`docs/` for documentation (e.g., `docs/api-guide`)
-`chore/` for maintenance (e.g., `chore/update-deps`)
2. Branch name should reference the OpenSpec change name when applicable.
3. Do not commit directly to `main` or `dev`.
### Completion and merge
When implementation is complete and verified:
1. Ensure all tests pass and quality gates are met.
2. Stage all changes with `git add -A`.
3. Create a commit with a proper conventional commit message (see below).
4. Switch to `dev`: `git checkout dev`.
5. Merge the feature branch: `git merge --no-ff <branch-name>`.
6. Push to remote: `git push origin dev`.
7. Delete the local feature branch if desired: `git branch -d <branch-name>`.
### Auto-commit on spec completion
When an OpenSpec change is fully implemented and all tasks are complete:
1. Stage all changes with `git add -A`
2. Create a commit with a proper conventional commit message
3. The commit message should:
- Use conventional commit format (`feat:`, `fix:`, `refactor:`, etc.)
- Reference the OpenSpec change name and relevant user stories
Hosted workspace and tool-orchestration platform where authenticated users create projects, connect Git repositories, and spawn self-hosted tools such as OpenCode and code-server.
A self-hosted platform for managing projects, git repositories, and development tools with OAuth2 authentication.
## Current Status
## Overview
This repository provides:
Headquarter provides a centralized workspace for development teams to:
- Manage projects and their associated git repositories
- Browse repository files and view git history
- Spawn development tools (VS Code Server, Jupyter Notebook, etc.)
- Manage SSH keys and user preferences
- React + Vite + TypeScript frontend (`apps/web`)
- FastAPI + Python backend (`apps/api`)
- Manifest-driven tool registry with built-in OpenCode and code-server definitions
All pull requests and pushes to `main` are validated by a GitHub Actions workflow (`.github/workflows/ci.yml`). The workflow runs the frontend and backend quality gates in parallel:
### Production Deployment
- **Web CI** — lint, typecheck, and test the React frontend.
- **API CI** — lint with `ruff`, typecheck with `mypy`, and run `pytest` against a PostgreSQL service container.
See [Deployment Guide](docs/deployment/) for production setup with Traefik and Authentik.
See [Development](docs/development.md) for details on running these checks locally.
## Tech Stack
### Backend
- **FastAPI** - Python web framework
- **SQLAlchemy** - ORM with async PostgreSQL support
- **Pydantic** - Data validation
- **Alembic** - Database migrations
- **python-jose** - JWT handling
### Frontend
- **React** - UI library
- **TypeScript** - Type safety
- **Vite** - Build tool
- **React Router** - Client-side routing
### Infrastructure
- **Docker** - Containerization
- **PostgreSQL** - Database
- **Traefik** - Reverse proxy (production)
- **Authentik** - Identity provider
## Documentation
- [Architecture](docs/architecture.md) — System design and MVP phases
- [Development](docs/development.md) — Local setup and day-to-day commands
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.