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
- Add final get_container_status check in start_tool_instance before
writing status=running; mark as error and return logs if container stopped
- Treat restarting as error in HealthMonitor when DB status was already
running, so crash loops are surfaced instead of preserved
- Disable auto-restart (restart: unless-stopped -> restart: no) for tool
instances in manifest compiler, legacy dockerfile path, and built-in seeds
Quality gates:
- pytest tests/unit: 210 passed
- ruff: clean on changed files
- mypy: clean on changed files
The compose file was forcing the container to run as uid 1001, so the
entrypoint could not create /workspace even with sudo configured.
- Remove Dockerfile USER directive so containers start as root
- Make compile_compose use user: 0:0 when the manifest declares a user
- Make the entrypoint drop to the container user via after setup,
preserving environment variables and command arguments
- Update unit tests to assert root startup and privilege drop
Quality gates:
- pytest tests/unit: 210 passed
- ruff: clean on changed files
- mypy: clean on changed files
The previous ordering checked SUDO before checking if the process was
already running as root. When Docker starts the container with a
non-root user, SUDO may be empty, but the real fix is that the
entrypoint should try root first (e.g. when the image is started as
root) and only then fall back to sudo.
- Reorder symlink creation logic: root first, then sudo, then best-effort
- Update unit test to assert root is checked before sudo
Quality gates:
- pytest tests/unit: 208 passed
- ruff: clean on changed files
- mypy: clean on changed files
The previous commit moved the pi-agent repo mount from /workspace to
/home/user/{repo_name}. This exposed a permission bug: the Dockerfile
creates /workspace as a root-owned symlink in the image, and the
non-root entrypoint could not replace it because / is owned by root.
- Update compile_entrypoint to recreate /workspace via sudo when running
as the container user, or directly when running as root
- Add unit test covering sudo/root symlink creation
- Update OpenSpec change docs with the additional root cause
Quality gates:
- pytest tests/unit: 208 passed
- ruff: clean on changed files
- mypy: clean on changed files
- alembic heads: single head