Use canonical profile files and writable Git working copies so editor and container changes share one source. Require confirmation before destructive Git refreshes and overlay profile files without composite snapshots.
- Use profile-scoped canonical Git clone sources with locked refreshes
- Mount shared Git configuration read-only and isolate profile content
- Add API and desktop/mobile actions for live Git mount refresh
Quality gates: frontend build and backend py_compile passed.
Skipped: backend pytest/Ruff unavailable; Docker/manual checks not approved.
- Add a geometric evergreen Headquarter mark for browser tabs\n- Register the SVG favicon and matching browser theme color\n\nOpenSpec: add-project-favicon\nQuality gates: npm run build
- Add dynamic viewport bounds and scrollable body regions to dialogs and modals\n- Make tool-launch popups use the shared scrollable body pattern\n- Keep mobile sheets, action sheets, and notification popups scroll-contained\n- Restore the ProjectsPage test setup required for the frontend suite\n\nOpenSpec: fix-dialog-scroll-containment\nQuality gates: npm run typecheck, npm run lint, npm test (88 passed), npm run build
Recreate the terminal when responsive classification changes so mobile scrollback and touch listeners are installed.\n\nOpenSpec: fix-mobile-terminal-scrolling\nQuality gates: npm run typecheck, npm run lint, npm test (89 passed), npm run build
- Retain xterm normal-buffer history on mobile while preserving desktop zero-scrollback behavior\n- Repair ProjectsPage tests for session context and current project list markup\n- Add focused terminal scrollback coverage\n\nOpenSpec: fix-mobile-terminal-scrolling\nQuality gates: npm run typecheck, npm run lint, npm test (89 passed), npm run build
- Make the host PTY master non-blocking and wait for write readiness.
- Retry partial writes so a large bracketed paste always delivers its closing
marker instead of leaving pi in paste mode.
- Add regression coverage for partial PTY writes and resolve diagnostics.
Quality gates: ruff, mypy, focused pytest (3 passed)
- Track application bracketed-paste mode from terminal output.
- Capture browser and mobile clipboard pastes only while that mode is enabled,
normalizing line endings and sending a single BPM-framed input event.
- Remove the synchronous output decoding and console diagnostics that could
stall terminal rendering under output load.
- Keep the terminal's zero-scrollback configuration and resolve existing
no-case-declarations lint blockers in terminal keyboard shortcuts.
Quality gates: npm run typecheck, npm run lint
- Revert xterm.js scrollback from 0 to 10000; empirical correlation with
bracketed-paste mode failing (each line submitted as separate command).
- Add temporary browser-console diagnostics to confirm whether xterm receives
pi's \e[?2004h enable sequence and whether outbound paste is BPM-wrapped.
- Keep CSS scrollbar hiding and wheel-sensitivity 0 so the original scroll-jank
fix remains effective.
Typecheck: passed (apps/web)
The web terminal only hosts full-screen TUI tools (pi-agent, opencode),
which repaint in place in the normal buffer and do not use the alternate
screen or mouse tracking. With scrollback enabled, every repaint
accumulated as history, so xterm's viewport scrollbar appeared and the
mouse-wheel scrolled through stale frames instead of interacting with the
app; the scrollbar column also perturbed FitAddon's column count.
- terminal.tsx: set scrollback:0 and scrollSensitivity/fastScrollSensitivity:0
so only the live viewport is kept (no bar, no stale-frame wheel jank).
- utilities.css: hide .xterm-viewport scrollbar (scrollbar-width:none +
::-webkit-scrollbar display:none) as belt-and-suspenders.
Wheel no longer scrolls stale frames; in-app scrolling uses the tool's own
keys. Headquarter-only change; no tool is touched.
The WebSocket control-frame heuristic only recognized resize/ack/reset,
so the frontend's heartbeat reply {"type":"pong"} fell through to
write_input() and was typed into the shell / pi every ~30s. That garbage
corrupted the foreground app: stray text in the input line, rerenders,
and scroll-position resets (visible on resize/scroll redraws).
Treat any text frame that parses to a JSON object carrying a "type" field
as control traffic that must NEVER reach the PTY: handle known types and
ignore unknown ones. Keystrokes, bracketed-paste content, and plain text
are still forwarded as raw input.
Multiline pastes into the web terminal (especially into pi) were split
into one prompt per line because bracketed-paste markers were not
reaching the foreground app intact.
- Put the host PTY into raw mode (tty.setraw) after openpty() so it acts
as a pass-through pipe. The default canonical line discipline was
line-buffering input, splitting multiline pastes at newlines, and
mangling bracketed-paste markers before docker exec / pi could see
them. The in-container PTY (docker exec -t) provides real discipline.
- Route the mobile Paste button through xterm.js (term.paste) instead of
sending raw clipboard text to the WebSocket, so content is wrapped in
bracketed-paste markers when the app has enabled BPM.
- Treat a text frame as a control message only when it is a JSON object
with a known type (resize/ack/reset); otherwise forward as raw input
so JSON-shaped pastes are no longer silently dropped.
Quality gates: ruff, mypy (changed files), pytest unit (227 passed),
tsc, eslint
The /workspace compatibility symlink was removed from the manifest
compiler/entrypoint in 3e59a25. Tool definitions that still set
runtime.working_dir to /workspace therefore start in an empty directory
instead of /home/user/{repo_name}.
- manifest-editor.tsx: default working_dir to empty instead of /workspace;
update startup-script placeholder to reference /home/alex/.
- Add Alembic migration 2026_06_19_113000 that clears the stale
runtime.working_dir = /workspace from the built-in pi-agent manifest.
- Add migration import test.
Quality gates: pytest tests/api tests/services/test_terminal_manager_multi.py tests/unit (248 passed), ruff check (clean), npx tsc --noEmit (clean), eslint (clean).
The container-user resolver introduced in 9f72093 accessed
'tool_type.manifest', which triggers a SQLAlchemy lazy load inside the
async WebSocket coroutine and raises MissingGreenlet. Fetch the manifest
explicitly with db_session.get() instead, matching the pattern used in
instance_service.py.
- Replace relationship access with explicit async loads in
_resolve_container_user().
- Add unit tests covering manifest, base-definition, legacy, and missing
manifest cases.
- Update project map artifacts.
Quality gates: pytest tests/api tests/services/test_terminal_manager_multi.py tests/unit (247 passed), ruff check (clean).
- Remove compose-level user: 0:0 override from manifest_compiler.py so the
entrypoint can start as root, fix mount ownership, and drop privileges to
the container user internally.
- Add get_manifest_container_user() helper to resolve the manifest-declared
container user (with uid:gid fallback).
- Pass container user through TerminalSession, TerminalManager, and the
terminal WebSocket handler so docker exec is invoked with --user <user>.
- Update and add unit tests for the manifest compiler and terminal session.
- Record the additional root-user fix in the fix-pi-container-mount-permissions
OpenSpec change/tasks.
Quality gates: pytest tests/unit/ (226 passed), pytest tests/services/test_terminal_manager_multi.py (7 passed), ruff check on changed files (clean), mypy on changed files (clean)