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)
create_tool_instance fetched `project` only inside the auto-display-name
branch, so callers supplying display_name left it unbound and the
manifest/dockerfile/compose branches crashed with UnboundLocalError at
_get_repository_mount_name (in-container layout /home/user/{project_name}).
Resolve project unconditionally after repo validation with a not-found
guard (narrowing Project|None -> Project), and drop the now-redundant
fetch from the auto-name branch. Add a regression test covering the
manifest + display_name path.
Quality gates: ruff, mypy, pytest (74 unit tests passed)
- Add openSession utility with tab deduplication/focus
- Use openSession in navbar live sessions, use-instance-actions, and project tool links
- Pass onAddRepository to ProjectListItem and add dashed 'Add Repository' card
- Style add-repo card in projects.css
- Add ProjectListItem component with project header actions
- Show repositories horizontally with branch labels
- List workspaces vertically under each repo
- Show running tools per workspace from useSessions
- Link workspace names to /workspaces/:id
- Link tool names to web URL or terminal page
- Keep existing mobile view and dialogs unchanged
- Fix .project-card flex direction so expanded repos align left
- Add .config-section-actions for consistent save bars
- Replace sticky footer in Config Profiles with config-section-actions
- Replace dialog-actions in Tool Type editor with config-section-actions
- Convert manifest editor preview block to .config-section
- Add shared .config-section component class and migrate Tool Workshop
and Config Profiles editors to use it.
- Restore --space-4 padding on home hero, summary cards, and sections
with --space-3 on mobile.
- Restructure expandable project card: add 'Repositories' header with
repo count and move 'Add Repository' button into the section header.
- Left-align repository blocks so they fill from the project detail edge.
Pass 2 of the web UI spacing/typography/visual-rhythm rework.
- Add layout, spacing, typography, visual, card, and component utilities
- Add form-section, form-row, form-help, text-error, alert-success
- Unify .form-group and .form-field; add .status-badge family
- Alias legacy button classes to .btn primitives
- Refactor ToolTypeListSidebar and ConfigProfileListSidebar to use .sidebar
and var(--sidebar-width) instead of hardcoded 280px
- Refactor ToolTypeEditorPanel, ConfigProfileEditorPanel,
git-mount-editor, and manifest-editor to use utility classes
Quality gates: npm run typecheck, npm run lint, npm run build pass.
Inline style blocks in target components reduced from 198 to 11.
Restructure live session nav entries into a 3-line layout:
- workspace/repository context on top
- session display name in the middle
- tool type and project on the bottom
Tool icon now has a status-dot badge, and the tooltip lists
display name, tool, project, context, and status.
The recent redraw fix keeps all xterm instances mounted (display:none)
when switching sessions. However, sendData/focus/font-size refs and the
header connection status were still stored globally, so the last-mounted
hidden session could own keyboard input, font-size buttons, and the
status dot for the active session.
- Pass sessionId to onTerminalReady from TerminalComponent.
- Store terminal callbacks and status keyed by sessionId in use-terminal-page.
- Use activeSessionId to route special-key input, font-size changes, and header status.
- Clean up per-session refs and status when sessions are closed.
- Update MobileTerminalWrapper signature for the new callback shape.
Quality gates: npm run typecheck, npm run lint, npm test (87 passed)
Render all terminal sessions and hide inactive ones with display:none so
xterm instances are no longer unmounted/remounted when switching tabs.
Remove terminal.clear() on the connected status message: the server keeps
the session buffer, and clearing was erasing visible content when the tab
regained focus or reconnected.
- Keep TerminalComponent instances alive in both Desktop and Mobile views.
- Only refit/resize on reconnect instead of clearing.
Closes terminal redraw on focus regain.
When a config profile declares both a git_mount and a mounts entry for the
same directory (e.g. ~/.pi), the generated bind-mounts would mask each other
inside the container. Instead, copy the static profile files into the
instance-scoped git-mount source directory so the container sees both the
cloned repo contents and the static files through a single bind-mount.
- Add _stack_profile_mounts_with_git_mounts helper to merge overlapping
profile mounts into git-mount sources.
- Integrate stacking into start_tool_instance after resolving both mount
types.
- Add unit tests for exact, descendant, non-overlapping, and file cases.
- Include branch in git-mount clone dir hash so different branches of the
same repo get separate directories and no longer race.
- Resolve profile working_directory before git mounts so relative targets
are not silently skipped.
- Fall back to tool_type.home_directory for non-manifest tools.
- Fix glob target calculation to avoid duplicate directory segment.
- Add exc_info logging for git mount clone failures.
Detached containers with tty: true still exited immediately because a
plain /bin/bash invocation exits with code 0 when stdin is not connected.
- Detect when the container CMD is /bin/bash or bash and exec an
interactive login shell () after dropping privileges
- Keep the generic path for non-shell commands
- Bump compiler_version to v4 to force a fresh image build
Quality gates:
- pytest tests/unit: 219 passed
- ruff: clean on changed files
- mypy: clean on changed files
The previous compiler_version v2 already produced an image tag
(3d34c636) for the su-based fix. Images built under v2 still had the
broken su entrypoint that exited immediately. Bump to v3 so the runuser
change forces a fresh image build with the corrected privilege drop.
Quality gates:
- pytest tests/unit: 219 passed