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