The desktop terminal page sometimes grew an outer scrollbar because the
terminal instance/wrapper/container chain lacked height constraints.
Without min/max-height enforcement, xterm.js's internal viewport could
expand its parent flex/grid track past the available space.
- Add overflow: hidden to .terminal-page.
- Add max-height: 100% and overflow: hidden to .terminal-instance.
- Add max-height: 100% to .terminal-wrapper.
- Add min-height: 0 to .terminal-container.
- Constrain .xterm-viewport to max-height/width 100% so it fills but
never exceeds its container.
Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed).
Refs: openspec/changes/fix-terminal-container-overflow
The Pi Agent dockerfile templates created ~/.tmux.conf with a literal
\n because the RUN command used single-quoted echo. Tmux never parsed
the malformed line, so mouse mode stayed off. Without tmux mouse mode,
mouse-wheel events in xterm.js fell back to Up/Down arrow keys and
cycled shell command history instead of scrolling the terminal buffer.
- Use printf '%s\n' to write real newlines in .tmux.conf.
- Apply the same fix to the ranger rc.conf where the same bug existed.
- Update tool-images/pi-agent.dockerfile and both affected alembic
migration dockerfile strings.
Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed),
py_compile on changed migrations.
Refs: openspec/changes/fix-tmux-mouse-config
- Wire MobileListView onItemDelete/onItemDuplicate callbacks to render
action buttons in each list row.
- Pass onItemDelete in ToolWorkshopMobileView list view.
- Add CSS for mobile-list-item-action buttons.
- Fix MobileEditView sticky bottom action bar that was hidden behind
the 64px mobile navigation bar; raise to bottom: 64px and z-index 110.
Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed)
Refs: openspec/changes/mobile-list-delete-button
- Update MobileEditView to render Save and optional Delete in a
sticky bottom action bar; header now shows Cancel + title only.
- Make ConfigProfilesMobileView open edit view on profile tap.
- Make ToolWorkshopMobileView open edit view on tool type tap.
- Wire delete into MobileEditView for existing profiles and tool
types.
- Stay on edit view after saving an existing item; create flow
returns to list as before.
- Update ToolWorkshopPage cancel to return to list.
- Add mobile-edit-actions and mobile-edit-delete CSS.
Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed)
Refs: openspec/changes/mobile-edit-default-bottom-actions
- Rewrite ConfigProfilesMobileView to match desktop functionality:
full detail view with all fields, preview action showing resolved
profile, and edit view with project/tool selects, includes,
environment variables, runtime hints, files, mounts with nested
files, and git mounts.
- Update useConfigProfiles.handleSubmit to return boolean success.
- Update ConfigProfilesPage to pass required state and callbacks.
- Add mobile-specific CSS for config profile forms, includes,
mount/file cards, and preview panels.
- Allow MobileDetailView to render extra children.
Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed)
Refs: openspec/changes/mobile-config-profiles-ui
The mobile profile form already renders its own header via
ProfileMobileView, so the desktop page title was redundant on small
viewports.
Quality gates: npm run typecheck, npm run lint
Refs: openspec/changes/mobile-tool-profile-ui
- Rework ToolWorkshopMobileView to support full desktop functionality:
definition type selection (Compose/Dockerfile/Manifest), manifest editor,
conditional port, startup command, readiness probe, required variables,
and validation feedback.
- Add ProfileMobileView and wire ProfilePage to render it on mobile.
- Update useToolWorkshop hook to return boolean success from submit.
- Add responsive CSS for mobile forms, edit views, and manifest editor.
- Update project maps.
Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed)
Refs: openspec/changes/mobile-tool-profile-ui
- Revert touch-action: none on .xterm-viewport so xterm.js can fall back
to its own viewport scrolling when the custom handler doesn't take over.
- Detect alternate screen via reference equality
(term.buffer.active === term.buffer.alternate) instead of the
string, which could report normal buffer incorrectly.
- Lower vertical-scroll activation threshold from 4px to 2px and only
prevent default once a vertical gesture is recognized.
- In normal buffer use term.scrollLines() so xterm.js handles the buffer
scroll consistently; in alternate screen continue sending SGR 1006
mouse-wheel sequences to tmux/vim.
Quality gates: typecheck, lint clean, npm test -- --run 87 passed.
- Add touch-action: none and overscroll-behavior: none to mobile terminal
page, content, wrapper, container, and xterm viewport so the browser
never treats swipes as page/address-bar scrolling or pull-to-refresh.
- Make .terminal-page.mobile fixed-position to prevent viewport layout
scroll; keep the overlay toolbar as absolute with pointer-events only
on interactive parts.
- Rework the mobile touch handler in terminal.tsx:
* Detect normal vs alternate buffer via term.buffer.active.type instead
of measuring the DOM viewport, which was unreliable in tmux/vim.
* Accumulate swipe distance and emit SGR 1006 mouse-wheel sequences in
steps, so tmux pane scrolling tracks the gesture correctly.
* Prevent default as soon as the swipe is recognized so the page does
not start a competing scroll gesture.
Quality gates: npm run typecheck, npm run lint clean, npm test -- --run 87 passed.
- The top-level GET /workspaces endpoint was returning project_name as
an empty string because the GitRepository.project relationship was not
eager-loaded.
- Select the full GitRepository entity and apply selectinload(project)
so project_name is populated for every workspace row.
Backend quality gates: python3 -m pytest 313 passed, 34 skipped.
- Move the project name below the workspace title row so it reads as a
distinct line with a project icon.
- Move the status badge into the title row next to the workspace name,
preventing it from crowding the project label.
- Add .workspace-title-row flex styles and update .workspace-project-name
to display inline-flex with a brand-colored project icon.
Quality gates: npm run typecheck, npm run lint clean,
npm test -- --run 87 passed.
- Reorder desktop sidebar so Sessions sits between spaces/tools groups:
Home, Projects, Workspaces, Sessions, Tool Workshop, Config Profiles, Settings.
- Reorder mobile bottom nav so Sessions is the center item:
Home, Spaces, Sessions, Tools, Settings.
- Workspace cards already display the owning project name; no extra change needed.
Quality gates: npm run typecheck, npm run lint clean,
npm test -- --run 87 passed.
- Combine Projects and Workspaces into a single 'Spaces' grouped mobile
nav item that opens a bottom-sheet menu.
- Add SpacesBottomSheet component with Projects/Workspaces options.
- Extend MobileListView with optional renderItem prop for rich rows.
- Redesign mobile ProjectsPage rows to show project description and
repository chips.
- Replace mobile WorkspacesPage list with compact WorkspaceCard grid,
matching desktop card content.
- Add mobile-list-* CSS and mobile-workspaces-list spacing.
Quality gates: npm run typecheck, npm run lint clean,
npm test -- --run 87 passed.
- Swap workspace card header order: project name is now the primary eyebrow,
workspace name is the bold title below it.
- Restructure workspace card into clean top/body/actions sections with more
whitespace and clearer hierarchy.
- Replace cramped meta paragraphs with an inline meta row (repo, branch,
instance count) and dedicated instance chip area.
- Use icon-only ghost buttons for sync/delete to reduce visual noise; keep
prominent Start Tool button.
- Add top divider for actions, improve hover states, and make long names
truncate gracefully.
- Update mobile workspace list subtitle to project · workspace name.
- Refresh workspaces.css with new card layout and responsive mobile rules.
Quality gates: npm run typecheck clean, npm run lint clean,
npm test -- --run 87 passed
- modify_compose_file now appends :ro when a config-profile volume entry
has readonly=true, matching the manifest compiler behavior.
- Add a guard for missing tool_type in prepare_manifest_instance.
Quality gates: python3 -m pytest (313 passed, 34 skipped)
- Switch apply_resolved_profile from per-file bind mounts to one
directory-level bind mount per ResolvedMount target.
- Stage all configured files under instance_dir/mounts/<sanitized_target>
and bind-mount that directory, so Docker no longer creates a root-owned
parent directory such as ~/.config.
- Propagate read-only mode ('ro') as the 'readonly' flag on volume entries.
- Update unit tests to expect directory-level mounts and add coverage for
readonly/writable flags.
Quality gates: python3 -m py_compile, pytest (313 passed, 34 skipped),
npm run typecheck, npm run lint.
- Remove pi_state and pi_config mounts from the pi-agent manifest.
- Add Alembic data migration to strip those mounts from existing DB rows.
- Remove opencode_home:/tmp volume and HOME=/tmp override from the opencode
built-in compose template; config/state now belongs in config profiles.
- Workspace and SSH key mounts remain unchanged.
Quality gates: python3 -m py_compile, pytest (311 passed, 34 skipped),
npm run typecheck, npm run lint
Add a reusable LoadingOverlay component that dims and disables the
container owning an in-flight action, with a spinning indicator and
label. Apply it to:
- SessionCard (when actionBusyId matches)
- InstanceList cards (per busyInstanceId with action-specific labels)
- CreateSessionForm (while submitting)
- ToolStarter (while starting)
Also add .icon-spin animation and position:relative to the relevant
containers.
Quality gates: npm run typecheck, npm run lint, npm test -- --run
(87 passed).
The bottom-right progress panel duplicated feedback already shown by
toasts. Remove it and the operation-tracking state to simplify the UI:
- Delete state/session-operations.tsx and session-progress-panel.tsx.
- Remove SessionOperationsProvider/SessionProgressPanel from AppShell.
- Remove startOperation/completeOperation calls from useInstanceActions
and ToolStarter.
- Remove SessionOperationsProvider wrapper from DashboardPage.test.tsx.
- Remove .session-progress-panel CSS rules.
- Format use-events.test.ts mock to match project lint rules.
Quality gates: npm run typecheck, npm run lint, npm test -- --run
(87 passed).
The backend sends named lifecycle events (event: instance.health_changed),
but useEvents only set es.onmessage, which only receives unnamed message
events. Add explicit addEventListener registrations for all lifecycle
event types so the progress panel receives updates and completes.
Quality gates: npm run typecheck, npm run lint, npm test -- --run
(87 passed).
The workspace-first cleanup removed clone_mode from the creation flow,
so the API now inserts NULL. Align the database with the model by
making clone_mode nullable.
Apply with: cd apps/api && alembic upgrade head
Config-profile and git mounts staged under instance_dir were created
by the API process (root), so when bind-mounted over ~/.config the
container user could not write. Recursively chown staged sources to
the resolved container uid/gid before compose up.
Quality gates: python3 -m py_compile passed; ruff/pytest skipped
(test tooling not available in this shell, helper smoke tested
with a temporary directory).
Regenerate .pi-map*.md artifacts for source changes in:
- apps/api/src/api/tool/tool_types_validation.py
- apps/api/src/schemas/tool/tool_type.py
- apps/api/tests/integration/test_tool_types_api_extended.py
- and all affected test files from backend-frontend refactoring cleanup
- Extract tool instance lifecycle endpoints (start/stop/restart/delete) from
api/tool/tool_instances.py into new api/tool/tool_lifecycle.py.
- Register tool_lifecycle_router in main.py and api/tool/__init__.py.
- Extract inline WorkspaceDetailPage components into
components/features/workspace/: detail header, tab bars, file/git/tools/settings
panels. Slim page from ~446 to ~62 lines.
- Update OpenSpec reorganize-long-files tasks to reflect completed work and
current source state; mark change completed.
- Regenerate project maps.
Quality gates: python3 -m py_compile (backend clean), npm run typecheck,
npm run lint, npm test -- --run (87 passed), pytest workspace integration
and unit tests (27 passed, 1 skipped).
- Delete dead repo-workspace code: RepoWorkspacePage, useRepoWorkspace,
WorkspaceLayout, FileBrowser, old git components (git-toolbar, file-editor,
commit-panel), and repo-workspace.css.
- Fix stale backend test imports for moved models/services.
- Add GitOperations unit tests.
- Add integration tests for workspace files, git, and instances endpoints.
- Add frontend tests for WorkspaceDetailPage and ProjectCard.
- Update OpenSpec workspace-first-ui tasks and mark change completed.
- Regenerate project maps.
Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed),
python3 -m py_compile on changed backend files, pytest backend workspace tests.
- Remove clone_mode/branch from API responses and make DB columns nullable
- Remove legacy clone-mode branches from create_tool_instance
- Add WORKSPACE_PATH compose variable alongside REPO_PATH
- Add workspace migration helpers in WorkspaceManager
Remaining: POST /workspaces/:id/instances, frontend clone_mode cleanup, tests
Add generated .pi-map.md and .pi-map.index.md files across the repository
so the project navigation maps are shared and versioned. These artifacts
are maintained by project_map_init/patch/validate and must be kept in
sync with source edits.
Note: .cache/ remains ignored (added in previous commit).
- Add SessionOperationsContext + SessionProgressPanel for global,
non-blocking lifecycle progress (create/start/stop/restart/delete/
recreate-tunnel) driven by SSE events.
- Promote SessionsContext to authoritative shared session state with
refresh, addOrUpdateSession, and removeSession helpers.
- Wire AppShell, DashboardPage, SessionsPage, useInstanceActions,
ToolStarter, and InstanceList into shared state so lists update
immediately after create/delete without manual refresh.
- Remove legacy blocking overlays from CreateSessionForm, SessionCard,
and InstanceList; keep disabled states and inline spinners only.
- Update DashboardPage tests to wrap with SessionsProvider and
SessionOperationsProvider.
- Add .cache/ to .gitignore.
Quality gates: npm run typecheck, npm run lint, npm test -- --run
(82 passed).
The session options dropdown used var(--surface) which was never
defined anywhere in the stylesheet, causing a transparent background.
Changed to var(--panel) which is defined in tokens.css.
Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
Root cause: manifest-based Dockerfile created the home directory and
chowned only the home root. Files/directories copied from /etc/skel by
useradd -m (or created later by root) remained root-owned, so apps like
ranger failed when writing to ~/.config.
Changes:
- manifest_compiler.py: recursive chown of the home directory after
useradd so /etc/skel contents are owned by the container user
- Pre-create .config, .local/share, .cache and chown them to the user
so first-run apps have writable directories immediately
- Add unit test verifying the Dockerfile emits the expected user/home
setup and config directory creation
Quality gates: py_compile all backend files pass, test file compiles,
tsc --noEmit pass, npm run build pass, 82/82 web tests pass
Note: pytest not available in this shell; backend unit test was not
executed but follows existing project conventions.
Instead of relying on display_name (which for old instances is just the
workspace name), fetch the full session info and build the title from
individual fields:
- Base: '{workspace_name} {tool_type_name}'
- With multiple terminal sessions: '{workspace_name} {tool_type_name} {session_name}'
- Fallback: 'Instance {id}' if session lookup fails
This gives meaningful titles like 'MyWorkspace code-server Session 1'
instead of just 'MyWorkspace — Terminal'.
Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
- Remove useless document.title from handleOpen (it only changed the
caller page's title, not the new tab)
- Terminal tab title: simpler format without extra '— Headquarter' suffix
- Single session: '{name} — Terminal'
- Multiple sessions: '{name} · {session_name} — Terminal'
- Sidebar live sessions: show tool type in small muted text next to
display_name so sessions aren't indistinguishable when display_name
is just the workspace name
Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
ToolStarter:
- Auto-populate Session Name as '{workspace.name} {tool_type.display_name}'
when a tool type is selected
- Track whether user has manually edited the name (nameEdited flag) to avoid
overwriting their custom input
Terminal page:
- Fetch instance display_name via getUserSessions for tab title
- Tab title format: '{display_name} {terminal_session_name} — Terminal'
instead of just '{session_name} — Terminal'
Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
ToolStarter (used by FAB and workspace detail):
- Add Session Name text input, defaulting to workspace.name
- Pass user-provided name to createInstance display_name parameter
- If left empty or only whitespace, falls back to auto-generated name
Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
Backend:
- sessions.py: include workspace_name in session response
- instance_service.py: auto-generate display names as
'Project / Workspace / Tool #N' instead of 'Workspace / Tool #N'
- instance_service.py: add rename_tool_instance() service function
- tool_instances.py: add PATCH /instances/{id} endpoint for renaming
display_name
Frontend:
- api/sessions.ts: add workspace_name to Session type, add renameInstance()
- use-instance-actions.ts: add handleRename, set document.title when opening
- session-card.tsx: click-to-edit display_name inline; always show project
context line (Project / Workspace or Repo / Tool)
- session-list.tsx: pass through onRename prop
- SessionsPage.tsx: wire handleRename to SessionCard and SessionList
- app-shell.tsx: sidebar tooltip includes workspace or repo name
- use-terminal-page.ts: set document.title based on active terminal session
Quality gates: py_compile all backend files pass, tsc --noEmit pass,
npm run build pass, 82/82 tests pass
FilesTab in WorkspaceDetailPage was returning early for directories with
no action, making folders unclickable.
Changes:
- use-workspace-files.ts: add currentPath state and navigateTo() function;
refresh() now passes currentPath to listWorkspaceFiles API
- WorkspaceDetailPage.tsx FilesTab: handleSelect now calls navigateTo()
for directories; added navigateUp() button using '..' when not at root
- Clear selected file/editor state when changing directories
Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
Root causes:
1. No dedup for monitor restarts — _last_known_state is cleared on stop,
so every restart re-sent notifications for all unhealthy instances.
2. Aggressive error classification — any Docker state other than 'running'
was treated as 'error', including transient 'created' and 'restarting'.
3. Confusing metadata — when new_status == previous_status (after restart),
notifications showed previous_status equal to current status.
Fixes:
- _check_instance: when previous is None (first check) and new_status equals
the DB status, just record the snapshot and skip _handle_state_change.
This prevents duplicate events/notifications on monitor restart.
- _derive_status: only treat 'exited' and 'dead' as error. Preserve current
status for transient Docker states ('created', 'restarting').
- _derive_status: if DB says 'running' but container is 'not_found',
return 'error' instead of preserving 'running' (fixes silent failure).
- _handle_state_change: improved unhealthy message to 'Container tunnel is
unreachable' instead of generic 'Container is now unhealthy'.
Quality gates: py_compile all backend files pass, tsc --noEmit pass,
npm run build pass, 82/82 tests pass
RepositoryCreateDialog fixes:
- Change useAdvancedUrl default from false to true so full URL is the default
- Move isSshUrl helper before the effect that references it
- Short-circuit SSH URLs client-side in debounced validation so they always
show as valid without depending on backend parseGitUrl behavior
- Keeps submit-time SSH key requirement: error shown if SSH URL without key
Tests:
- Update repositories-settings-tab tests for full-URL default mode
- Add SSH URL acceptance test with key selected (client-side short-circuit)
- Add SSH URL rejection test without key selected
Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
rel=noopener forces a fresh browsing context, breaking target name matching
and window reference tracking across browsers.
Changes:
- use-instance-actions.ts: track opened tabs in Map, call .focus() on existing
- session-card.tsx: all Open buttons now go through handleOpen callback
- app-shell.tsx: sidebar session links use target='session-{id}' + noreferrer
- workspace-instance-chips.tsx: remove noopener from chip links
- instance-list.tsx: remove noopener from Open links
- WorkspaceDetailPage.tsx: use named target + noreferrer
Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
window.open(url, name, 'noopener,noreferrer') with a non-empty features
string forces a new popup window and ignores the name for tab reuse.
Remove the third parameter so the browser focuses existing named tabs.
Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass