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).
Move the following completed changes from openspec/changes/ to
openspec/changes/archive/2026-06-12-completed-changes-archive/:
- multi-session-terminal-ux
- reorganize-long-files
- working-copies
- workspace-first-ui
Update parent and archive .pi-map*.md indexes to reflect the move and
remove the transient active-changes-archive grouping.
openspec/changes/ now contains only the archive/ 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
- Fill empty apply-pr2.md with backend API + frontend client apply report
- Mark all 12 tasks as completed in tasks.md
- Update .openspec.yaml status from exploring to completed
The implementation was already merged to dev across PR 1, PR 2, and PR 3.
This commit only synchronizes the OpenSpec change metadata.
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
Use named window targets (session-{id}, instance-{id}) instead of _blank:
- use-instance-actions.ts: window.open(..., 'session-{id}') for programmatic opens
- session-card.tsx: <a target='session-{id}'> for direct link opens (mobile + desktop)
- workspace-instance-chips.tsx: <a target='instance-{id}'> for chip links
- instance-list.tsx: <a target='instance-{id}'> for instance open links
Browser behavior: if a tab with that target name exists, it navigates/focuses
that tab instead of opening a new one.
Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
RepositoryCreateDialog was only rendered inside the isMobile block.
Desktop 'Add Repository' clicks set state but the dialog never appeared.
Add conditional rendering in the desktop section.
Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass
- ProjectCard.tsx: add onAddRepository prop, show 'Add Repository' button in expanded view
- ProjectCard.tsx: add showBackButton/onBack props for mobile detail reuse
- ProjectsPage.tsx (desktop): wire onAddRepository to open RepositoryCreateDialog
- ProjectsPage.tsx (mobile): reuse ProjectCard for detail view instead of inline duplication
- pages/projects.css: add .project-add-repo style
Both platforms now use the same ProjectCard component and RepositoryCreateDialog
for adding repositories to projects.
Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass