- 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
The container still exited immediately after the su-based fix. can
interfere with TTY/stdin handling for interactive shells. Switch to
, which is root-only, skips PAM, and preserves file descriptors so
bash stays interactive.
Also improve container failure diagnostics:
- now combines stdout and stderr
- This helps surface the real reason when a container exits with code 0
Quality gates:
- pytest tests/unit: 219 passed
- ruff: clean on changed files
- mypy: clean on changed files
compute_image_tag hashed only the manifest JSON, so cached images built
before the entrypoint fix were reused even though compile_entrypoint() now
produces a different entrypoint. This caused containers to keep using the
old (broken) entrypoint that exited immediately.
- Include a compiler_version token in the hash input so manifest compiler
changes invalidate previously built images
- This forces a fresh image build for new instances after any change to
compile_dockerfile, compile_entrypoint, or compile_compose
Quality gates:
- pytest tests/unit: 219 passed
- ruff: clean on changed files
- mypy: clean on changed files
The generated entrypoint used a brittle invocation that could
spawn a non-interactive bash and exit immediately, causing containers to
stop with exit code 0 right after startup.
- Switch to so the
container user gets a login shell and stdin/tty are preserved
- Update the unit test assertion for the new drop-privileges command
Quality gates:
- pytest tests/unit: 219 passed
- ruff: clean on changed files
- mypy: clean on changed files
Git clone with an explicit destination puts the working copy directly into
that directory; it does not create a repo-named subdirectory. The previous
change assumed the opposite, so workspaces ended up at
/data/working-copies/{workspace_id}/ with the repo contents mixed in,
causing a 500 when the expected repo-named subdirectory was missing.
- Build the target path as /data/working-copies/{workspace_id}/{repo_name}/
and pass it directly to GitService.clone
- Remove stale directory detection and fallback logic that is no longer
needed
- Keep diagnostic logging around git clone failures
Quality gates:
- pytest tests/unit: 219 passed
- mypy: clean on changed files
The workspace creation endpoint returns 500 but the actual error is not
visible. Add explicit error logging when GitService.clone fails and info
logging when git creates a directory name different from the one derived
from the remote URL.
Quality gates:
- pytest tests/unit: 219 passed
- mypy: clean on changed files
Working copies were stored as /data/working-copies/{repo_id}/{workspace_name}/,
so git clone was forced into a user-named directory. That meant the container
mount basename was the workspace name (e.g. main) instead of the repo name.
- Generate the workspace UUID before cloning and clone into
/data/working-copies/{workspace_id}/ so git creates {repo_name}/ naturally
- Set workspace.path to /data/working-copies/{workspace_id}/{repo_name}/
- Update _migrate_clone_into_workspace() to use the same layout
- _get_repository_mount_name() now prefers workspace.path basename and only
falls back to remote URL / repo.name for legacy repo-only instances
- Update unit tests to assert workspace path basename is used for mounts
Quality gates:
- pytest tests/unit: 219 passed
- ruff: clean on changed files
- mypy: clean on changed files
The pi-agent manifest still declared an explicit repo mount with
{{WORKSPACE_NAME}}, making the mount target dependent on tool config. The
instance service now synthesizes the repo mount, so the manifest no longer
needs the explicit mount.
- Add Alembic migration 2026_06_15_090500 to remove the source_type: repo
mount from the built-in pi-agent manifest
- Add _get_repository_mount_name() helper to derive the workspace directory
name from the repository remote URL (matching git clone behavior) and
fall back to the user-provided repository name
- Use the helper for WORKSPACE_NAME/REPO_NAME in manifest, legacy dockerfile,
and legacy compose template paths
- Update unit tests for the new migration and helper
Quality gates:
- pytest tests/unit: 218 passed
- ruff: clean on changed files
- mypy: clean on changed files
- alembic heads: single head
WORKSPACE_NAME was computed from os.path.basename(repo_path), so when a
workspace path ended in a directory like 'main', the container mount target
became /home/user/main instead of /home/user/{repo-name}.
- Use GitRepository.name for WORKSPACE_NAME/REPO_NAME in manifest and
legacy dockerfile flows
- Add unit test verifying prepare_manifest_instance uses repo.name even
when the workspace path basename differs
Quality gates:
- pytest tests/unit: 213 passed
- ruff: clean on changed files
- mypy: clean on changed files
Older cached images still contain a literal /home/user/{{WORKSPACE_NAME}}
directory baked in by the previous Dockerfile generation. Even though new
images no longer create it, existing images leave the placeholder folder
alongside the real repo-named mount.
- Add entrypoint cleanup that removes /{{WORKSPACE_NAME}} if it
exists before creating the real workspace target and /workspace symlink
- Update unit tests to assert the stale placeholder removal
Quality gates:
- pytest tests/unit: 212 passed
- ruff: clean on changed files
- mypy: clean on changed files
When a manifest mount target uses ~/{{WORKSPACE_NAME}}, the Dockerfile was
building a literal directory named {{WORKSPACE_NAME}} into the image and
creating a broken /workspace symlink. The runtime mount then created the
correct repo-named folder alongside the placeholder folder.
- Only create static mount target directories in the Dockerfile; skip any
target containing {{WORKSPACE_NAME}}
- Only create the /workspace compatibility symlink at image-build time when
the workspace name is known; otherwise let the entrypoint create it from
the WORKSPACE_NAME environment variable
- Update unit tests to cover both build-time workspace names and runtime
placeholders
Quality gates:
- pytest tests/unit: 211 passed
- ruff: clean on changed files
- mypy: clean on changed files