Commit Graph

327 Commits

Author SHA1 Message Date
alex 8837031fd2 fix: ESC key, .config, workspace permissions, terminal race condition 2026-06-01 23:16:06 +02:00
alex a0cfbbc2d2 fix: container mount permissions, terminal shift, ESC capture
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)
2026-06-01 22:36:31 +02:00
alex 280a6ff2fa feat: floating action button for starting tools globally
- 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)
2026-06-01 22:09:49 +02:00
alex 78e808bc54 feat: sessions page workspace-first flow + workspace instance chips
- Sessions page: replaced CreateSessionForm with workspace selector + ToolStarter
  - Fetches workspaces, shows dropdown, then renders ToolStarter for selected workspace
  - Removed old project/repo/tool-type/config-profile/clone-mode flow
- Workspace cards: new WorkspaceInstanceChips component fetches and displays
  running instances per workspace with status-colored chips and open links
- Styles: .instance-chip variants (running/starting/error), .tool-starter-header

Quality gates: tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
2026-06-01 21:53:48 +02:00
alex c1445976d7 feat: new ToolStarter component — unified workspace-first tool starting
- New ToolStarter component: workspace context, fetches real tool types,
  auto-fetches config profiles per tool type, shows SSH key status
- Backend: add repo_ssh_key_id to workspace list responses
- WorkspacesPage: uses ToolStarter in modal instead of StartToolModal
- WorkspaceDetailPage ToolsTab: uses ToolStarter in modal
- Removed old inline StartToolModal from workspace-detail.tsx
- Styles: .tool-starter-context, .context-row, .ssh-key-status

Quality gates: ruff clean, tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
2026-06-01 21:27:21 +02:00
alex ff8aa2a4f5 refactor: unify tool-starting logic with shared useStartTool hook
- New useStartTool hook: calls working createInstance+startInstance API
- WorkspacesPage: uses shared hook instead of inline createInstance logic
- WorkspaceDetailPage ToolsTab: uses shared hook instead of broken createWorkspaceInstance
- Both pages now use identical StartToolModal + identical start logic

Quality gates: tsc --noEmit clean
2026-06-01 20:58:25 +02:00
alex 398436ecb5 refactor: unify StartToolModal — remove inline duplicate from workspace detail
- 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
2026-06-01 20:45:19 +02:00
alex 06a4a27880 fix: StartToolModal hardcoded tool types caused UUID parse error
- 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
2026-06-01 20:36:31 +02:00
alex f34c733706 fix: remove trailing slashes causing FastAPI redirect → mixed-content
- workspaceUrl: no trailing slash on /{workspaceId} (backend route has none)
- deleteWorkspace: /workspaces/{id}?force= (was /{id}/?force= with slash before ?)

Quality gates: tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
2026-06-01 20:20:23 +02:00
alex 95efa5d029 fix: workspace delete mixed-content error via top-level endpoint
- Add top-level DELETE /workspaces/{workspace_id} endpoint (avoids nested path)
- Frontend deleteWorkspace now uses /workspaces/{id}/?force=... (no project/repo needed)
- Update useWorkspaceActions, ProjectsPage, WorkspacesPage to match new signature

Quality gates: ruff clean, tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
2026-06-01 20:03:24 +02:00
alex d70b8e2363 fix: branches endpoint with SSH auth for remote fallback + better error messages
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
2026-06-01 19:21:02 +02:00
alex ee3c5af7a4 fix: handle missing/corrupt repos when fetching branches + clearer manual fallback
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
2026-06-01 19:00:59 +02:00
alex b02cd978c3 feat: unified git repo hook + fix branch dropdown in workspace creation
- New useGitRepo hook: centralizes all git operations (branches, status,
  history, commit, push, pull, fetch, checkout, create/delete branch, merge)
  for a given project+repo. Auto-refreshes after mutating ops.
- Fix WorkspaceCreateForm branch dropdown:
  - Always renders <select> (never input fallback)
  - Uses useGitRepo for branch fetching with loading/error states
  - Shows 'Loading branches...' while fetching
  - Shows 'Enter branch name manually...' if API fails
  - '+ Create new branch...' option with text input reveal
  - Auto-selects default branch on load
- ProjectsPage uses updated form props (defaultProjectId/defaultRepoId)

Quality gates: tsc --noEmit clean, eslint clean
2026-06-01 18:45:53 +02:00
alex e956d7c30d feat: unify workspace creation component with branch dropdown
- 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
2026-06-01 18:18:00 +02:00
alex b8fc4e6642 feat: workspace creation with branch dropdown and auto-select
- 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
2026-06-01 17:48:27 +02:00
alex ab1843b1c3 fix: allow workspace creation from workspaces page
- 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
2026-06-01 17:32:35 +02:00
alex 88a973dc68 feat: workspace-first UI refresh - PR-3 projects page + routing cleanup
- Rewrite ProjectsPage with inline repository and workspace display
- Expandable project cards showing repos + workspace chips
- Inline workspace creation from project page (New Workspace button per repo)
- Workspace chips link to workspace detail page
- Sync/delete actions on workspace chips
- Update Project type: add ProjectWithRepos, RepositorySummary, WorkspaceSummary
- Update listProjects API to return ProjectWithRepos[]
- Update dashboard, sessions, config-profiles to use ProjectWithRepos
- Remove old /projects/:projectId route (RepoWorkspace)
- Add chevron icons to Icon component
- Projects page CSS: project-toggle, repo-block, workspace-grid, workspace-chip
- TypeScript + eslint clean

Quality gates: tsc --noEmit clean, eslint clean
2026-06-01 17:18:51 +02:00
alex 27c77af591 feat: workspace-first UI refresh - PR-2 workspace detail page
- Add workspace detail page (/workspaces/:id) with 4 tabs:
  - Files: file tree, viewer, editor, git toolbar (commit/push/pull/fetch)
  - Git: branch selector, commit history
  - Tools: instance grid, start tool modal
  - Settings: workspace info read-only
- Add workspace API clients: workspace-files, workspace-git, workspace-instances
- Add hooks: useWorkspaceFiles, useWorkspaceGit, useWorkspaceInstances
- WorkspaceCard links to detail page via router Link
- Add comprehensive CSS for workspace detail layout
- Mobile: bottom tab bar, responsive file tree/split
- TypeScript + eslint clean

Quality gates: tsc --noEmit clean, eslint clean
2026-06-01 17:04:44 +02:00
alex e7587ca9f5 feat: workspace-first UI refresh - PR-1 backend endpoints
- Add FileService for workspace-scoped file operations
- Add GitOperations service for workspace-scoped git commands
- Add workspace_files API: GET/POST /workspaces/{id}/files
- Add workspace_git API: status, branches, commit, push, pull, fetch, checkout, history
- Add workspace_instances API: list instances per workspace
- Add top-level POST /workspaces/ (accepts repo_id directly)
- Enrich GET /projects/ with nested repositories and workspaces
- Register all new routers in main.py
- 23 tests passing (17 existing + 6 new)

Quality gates: ruff clean
2026-06-01 16:47:09 +02:00
alex 59b125d8e2 fix: add top-level GET /workspaces endpoint and derive project/repo from workspace data
- 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
2026-06-01 00:20:19 +02:00
alex b05de96569 fix: add trailing slash to workspace API URLs
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.
2026-06-01 00:03:33 +02:00
alex a5d64d1859 fix: add from __future__ import annotations to workspace_manager.py
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.
2026-05-31 23:41:45 +02:00
alex 5bba2bbd92 feat: workspace frontend integration (PR-4)
- Add workspace_id parameter to createInstance API client
- Wire WorkspacesPage 'Start Tool' modal to createInstance + startInstance
- Pass workspace_id when creating instance from workspace page
- TypeScript + eslint clean
2026-05-31 23:30:09 +02:00
alex 986091ac56 feat: workspace frontend core (PR-3)
- Workspace types, API client, hooks (useWorkspaces, useWorkspaceActions)
- WorkspaceCard, WorkspaceCreateForm, StartToolModal components
- WorkspacesPage with list, create, sync, delete, start-tool flow
- Sidebar navigation: new 'Workspaces' entry
- Router: /workspaces route
- TypeScript + eslint clean
2026-05-31 23:27:10 +02:00
alex d2b6bba15c fix: use host bind mount for /data/repos so tool containers can access mounted repos
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
2026-05-30 15:46:02 +02:00
alex b7396d58d2 fix: add DNS propagation delay and frontend error feedback for tunnel recreation
- 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
2026-05-30 15:33:04 +02:00
alex 6cf06d2380 refactor: rewrite tunnel system with host-network cloudflared containers
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)
2026-05-30 12:30:27 +02:00
alex 401ad2e65d feat: always show Recreate Tunnel button for web instances
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
2026-05-30 12:30:27 +02:00
Alex Blank 4814ec2363 fix: mobile terminal scroll in both normal mode and tmux
- 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)
2026-05-29 20:35:28 +02:00
Alex Blank 98b9d612fa fix: container-level capture touch with direct viewport.scrollTop manipulation
- 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
2026-05-29 20:23:19 +02:00
Alex Blank 874873541d fix: xterm.js mobile touch scrolling via viewport CSS and stopPropagation
- 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
2026-05-29 20:16:12 +02:00
Alex Blank ef9ac76f06 fix: remove all touch interception, let browser scroll xterm viewport natively
- 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
2026-05-29 20:08:58 +02:00
Alex Blank ca9db195de fix: document-level capture touch listeners for mobile terminal scroll
- 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
2026-05-29 20:03:11 +02:00
Alex Blank c1e16f2163 fix: lock body scroll and re-add programmatic terminal touch scroll
- 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
2026-05-29 19:57:48 +02:00
Alex Blank 61d32fa00f fix: enable native touch scrolling on xterm.js viewport for mobile
- 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
2026-05-29 19:41:23 +02:00
Alex Blank cddb3f8ccf fix: mobile terminal scroll via capture-phase touch listeners
- 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
2026-05-29 19:35:42 +02:00
Alex Blank 87a938fe58 fix: mobile terminal touch scrolling direction and target
- 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
2026-05-29 19:29:29 +02:00
Alex Blank aa34314175 feat: touch swipe scrolling in terminal 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
2026-05-29 19:20:52 +02:00
Alex Blank 4866ad08b1 feat: request screen wake lock while terminal page is open
- 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
2026-05-29 17:39:57 +02:00
Alex Blank 97ebc19313 fix: restore special keys bar on mobile terminal
- 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
2026-05-29 17:36:08 +02:00
Alex Blank 946ac6f66a fix: remove mobile terminal pull handle, tap terminal to toggle overlay 2026-05-29 17:29:14 +02:00
Alex Blank d713bfc5f9 fix: mobile terminal overlay status bar with auto-hide
- 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
2026-05-29 17:20:15 +02:00
Alex Blank 2b5223097f feat: filter notifications to warnings/errors/ready only and add clear-all button
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)
2026-05-29 16:42:31 +02:00
Alex Blank 1e2c5a68cf fix: pass SSH keys and config profile to startInstance in create form
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)
2026-05-29 16:14:40 +02:00
alex 19242b4152 feat: notification center toast coordination (PR-4)
- EventToastBridge checks notification_toast_level and notification_mute_categories
- toast-rules.ts: event-to-category/severity mapping functions
- Settings page: notification preferences section (toast level dropdown, mute checkboxes)
- Settings API types extended with notification preference fields
- 17 frontend tests (toast-rules + bridge)
- Preference hierarchy: mute categories → toast level → show/hide

Quality gates: vitest 17 passed, tsc clean, eslint clean
2026-05-29 13:52:49 +02:00
Alex Blank ceaed9af66 Merge remote dev branch 2026-05-29 13:34:08 +02:00
Alex Blank e9364fa70f feat: instance-level SSH key selection for container mounting
- Revert mistaken ssh_key_id from ConfigProfile (model, API, resolver, frontend)
- Add ssh_key_ids JSON column to tool_instances via migration
- Update create_instance to accept and store ssh_key_ids
- Update start_instance to mount selected SSH keys to {home_dir}/.ssh
- Update list_instances to return ssh_key_ids
- Frontend CreateSessionForm: multi-select SSH key checkboxes
- Frontend instance-list: SSH key selector for start/restart actions
- Maintain separate SSH key dirs per key to avoid conflicts

Quality gates: pytest (231 passed, 6 pre-existing), tsc --noEmit clean
2026-05-29 13:30:53 +02:00
alex 2bec205a30 feat: notification center frontend core (PR-3)
- Bell icon in icon registry (Phosphor Bell)
- NotificationProvider context with polling (15s unread / 30s list)
- useNotifications() hook with optimistic updates
- NotificationCenter component: bell + badge + dropdown panel
- NotificationItem component: severity icon, title, relative time, actions
- AppShell integration: mount in header-actions, hidden on mobile
- CSS styles: dropdown, items, unread/read states, empty state
- formatRelativeTime utility (custom, no new deps)
- 25 frontend tests (9 hook + 6 item + 10 center)

Quality gates: vitest 25 passed, tsc clean, eslint clean
2026-05-29 13:17:22 +02:00
Alex Blank 57ff236f2d feat: add ssh_key_id to config profiles for container key mounting
- 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
2026-05-29 12:53:51 +02:00
Alex Blank 090edf7ef6 feat: git mount URL validation with branch detection
- 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)
2026-05-29 12:15:30 +02:00