- 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
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.
- 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)
- 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
- 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
- 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
- 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)
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
- 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 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
- 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
- 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
- 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
- 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 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