Commit Graph

83 Commits

Author SHA1 Message Date
Alex Blank 51a399c775 feat: open all sessions in new tabs; sidebar terminal links
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
2026-06-02 15:54:03 +02:00
Alex Blank c754984df8 feat: high-performance web terminal with asyncio-native I/O
Complete rewrite of the terminal pipeline for VS Code Server-level
responsiveness. Key improvements:

Backend:
- Replace blocking select.select(0.1) with asyncio.add_reader() for
  event-driven PTY reading (eliminates ~110ms polling latency)
- Add output batching (2ms window) to reduce WebSocket frame overhead
- Add flow control: client acks processed bytes, server pauses PTY reads
  at 64KB threshold, resumes at 32KB
- Add 5s ack timeout fallback to prevent stuck sessions

Frontend:
- Switch WebSocket to binary mode (binaryType = 'arraybuffer')
- Eliminate Blob -> arrayBuffer async conversion overhead
- Add flow control ack messages (every 4096 bytes or 100ms)
- Add xterm-addon-webgl with graceful DOM fallback
- Add performance tuning (scrollback=10000, fastScrollSensitivity)

SDD artifacts:
- openspec/explorations/terminal-responsiveness.md
- openspec/proposals/terminal-responsiveness.md
- openspec/specs/terminal-responsiveness.md
- openspec/designs/terminal-responsiveness.md
- openspec/tasks/terminal-responsiveness.md

Quality gates: pytest (19 passed, 1 skipped), tsc --noEmit clean
2026-06-02 14:40:32 +02:00
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 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 f728011b2a fix: prevent cumulative terminal shrinkage in fullscreen mode
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)
2026-05-29 10:53:02 +02:00
Alex Blank 8926152fca fix: unified fullscreen terminal header
- Add showControls prop to TerminalComponent to optionally hide internal header
- Add reset() method to TerminalRef for external reset control
- TerminalPage now renders a unified fullscreen header bar combining:
  - Session tabs (TerminalSessionTabs)
  - Terminal controls (status dot, A-, A+, Reset, Exit Fullscreen)
- Unified header is always visible in fullscreen (no hover-to-reveal)
- TerminalComponent internal header hidden when in fullscreen mode
- Remove old CSS that hid session tabs with opacity:0 until hover

Quality gates: pytest 188 passed, frontend typecheck clean

Fixes: terminal-fullscreen-unified-header
2026-05-29 10:34:31 +02:00
Alex Blank e20d94d6ba chore: remove unnecessary debug logging
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
2026-05-28 23:05:48 +02:00
alex c7c4cb45a7 fix(terminal): verify container exists before creating terminal session
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
2026-05-28 21:18:47 +02:00
alex a3d01dd0a5 fix(terminal): loading state, focus handling, debug logging
- 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
2026-05-28 20:14:54 +02:00
alex 62d1bdc462 feat: multi-session terminal frontend UI + tests (PR 3)
- Add TerminalSessionTabs component with status dots, rename, close, max-5 limit
- Add 7 component tests for tab rendering, selection, close, rename
- TerminalComponent: sessionId prop, forwardRef with fit() method
- TerminalPage: multi-session orchestration, tab switching, auto-create default
- Fullscreen mode: Alt+Shift+F toggle, auto-hide tabs, Esc exit
- Keyboard shortcuts: Alt+Shift+N/W/ArrowLeft/ArrowRight/R
- Add CSS for tabs, fullscreen, mobile responsive
- Update useTerminalSessions hook for session CRUD
- terminal_manager.py: lookup by internal session_id fallback

Quality gates: tsc --noEmit clean, vitest (7/7 new tests passed), pytest (182 passed)
2026-05-28 13:35:45 +02:00
alex 22474cdba5 style: fix all ruff and eslint errors across codebase
Backend (ruff):
- Fix 106 errors: move imports to top of file (E402)
- Remove unused imports (F401)
- Add missing imports for undefined names (F821)
- Remove unused variables (F841)
- Fix test_models.py broken RefreshToken test
- Fix test_projects_api.py missing TestClient import

Frontend (eslint):
- Remove unused imports/variables across 10 files
- Fix explicit any types in client.ts and sessions.ts
- Clean up empty block statements in terminal.tsx

Quality gates: ruff (pass), eslint (pass), tsc --noEmit (pass),
pytest (98 passed, 4 pre-existing failures)
2026-05-28 10:15:59 +02:00
alex 0c839e8c6f fix: terminal 4004 infinite reconnect loop for pi-agent tool type
- 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)
2026-05-28 09:33:35 +02:00
Alex Blank d6ea5fb1fd chore: clean up debugging logs and console prints
Frontend:
- Remove 18 console.log/warn/error statements from terminal.tsx
- Remove console.warn from icon.tsx

Backend:
- Downgrade routine logger.info to logger.debug in tool_instances.py, terminal.py,
  terminal_session.py, terminal_manager.py, auth.py, docker_build.py, clone.py,
  config_profiles.py, user_config.py
- Keep important lifecycle events as logger.info:
  * Instance creation, start, running state
  * Docker build success/failure
  * Terminal session creation and reset
  * Auth success and user creation
  * Readiness probe success
  * Tunnel creation/stop
2026-05-27 22:16:59 +02:00
Alex Blank adda76a2ff fix: remove viewport-based font size calculation causing jump on mobile
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
2026-05-26 21:19:47 +02:00
Alex Blank 47962ed476 fix: set terminal default to 8px and minimum to 4px
- 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
2026-05-26 21:08:26 +02:00
Alex Blank 6a0c9bd669 fix: reduce terminal font size minimum and defaults
- 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
2026-05-26 21:05:04 +02:00
Alex Blank 76fbf0a755 fix: increase terminal lineHeight to 1.2 for mobile font metrics
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
2026-05-26 19:35:43 +02:00
Alex Blank 187193fa6e fix: terminal block character rendering by setting lineHeight to 1.0 and removing container padding 2026-05-26 19:09:06 +02:00
Alex Blank cd9c9539a2 fix: improve terminal rendering by removing CSS overrides and increasing minimum font size
- 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
2026-05-26 16:20:22 +02:00
Alex Blank 555517c144 fix: font size buttons only worked once due to stale callback
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.
2026-05-26 13:51:26 +02:00
Alex Blank fc1554140f feat: significantly reduce terminal font sizes
- 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
2026-05-26 13:42:36 +02:00
Fusion adaedb70ef feat: implement mobile app usability improvements
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
2026-05-25 11:20:25 +02:00
Fusion 9c57a94e9f Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-24 23:21:28 +02:00
alex 6c32e5266c fix: add terminal WebSocket diagnostics and unmount protection
- Add comprehensive connection/close/error/heartbeat logging
- Fix unmount to prevent reconnection attempts (code 1000)
- Add isUnmountingRef guard
- TypeScript compiles cleanly

Refs: intermittent terminal WebSocket failures
2026-05-24 21:20:16 +00:00
Fusion 9e88acaa36 fix: remove 0-dimension check blocking terminal fit and add debug logging
- 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
2026-05-24 23:15:15 +02:00
Fusion 21285498ae fix: mobile terminal black screen - replace grid with flexbox layout
- Root cause: CSS Grid 1fr row got 0 height inside flex parent
- Fix: Replace grid layout with flexbox column for mobile terminal wrapper
- Header and keys strip use flex-shrink: 0
- Content area uses flex: 1 to fill remaining space
- Remove debug logging
2026-05-24 23:00:32 +02:00
Fusion 9751b65dce debug: add more logging to trace mobile terminal black screen 2026-05-24 22:56:02 +02:00
Fusion 612217ad89 fix: mobile terminal black screen - grid cell had 0 height
- Root cause: .mobile-terminal-wrapper used height: 100vh inside flex parent
- Fix: Use flex: 1 instead so grid properly allocates 1fr height to content
- Add .shell.mobile-terminal-shell CSS to ensure full viewport coverage
- Remove debug logging
2026-05-24 22:51:31 +02:00
Fusion 22f736ce20 debug: add console logging to trace mobile terminal black screen issue 2026-05-24 22:46:48 +02:00
Fusion 76fbd74d20 fix: ensure terminal opens before fit and add dimension guards
- 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
2026-05-24 21:21:34 +02:00
Fusion 51b5d723ac fix: terminal clear on reset and data loss for text starting with {
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 '{'
2026-05-24 21:15:04 +02:00
Fusion 33b482ce91 fix: terminal reconnect typing and reset functionality
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
2026-05-24 20:57:33 +02:00
Fusion c82e628e6f fix: send stty resize on every resize with hidden command output
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
2026-05-24 20:24:41 +02:00
Fusion 5c998f5bf9 fix: revert all terminal resize fixes that caused layout issues
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
2026-05-24 20:13:10 +02:00
Fusion 3dbb6321fc fix: force explicit resize and add delayed refit after WebSocket connect 2026-05-24 19:59:35 +02:00
Fusion 3092038e40 debug: add console logging for terminal resize debugging 2026-05-24 19:44:21 +02:00
Fusion 738e01bb7c fix: add window resize fallback and delay refresh to next frame
- 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
2026-05-24 19:31:15 +02:00
Fusion 85f04447ea fix: force xterm.js canvas redraw on resize via internal renderer 2026-05-24 18:51:15 +02:00
Fusion d931f3071d Revert "fix: add window resize listener and refresh on font size change"
This reverts commit 4a356fe88e.
2026-05-24 18:42:14 +02:00
Fusion 4a356fe88e fix: add window resize listener and refresh on font size change
- 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
2026-05-24 18:18:41 +02:00