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.
- Change mobile terminal CSS to use touch-action: none and
overscroll-behavior: none so the custom touch handler owns swipes
- Archive completed/partial OpenSpec specs to
openspec/changes/archive/2026-06-14-completed-specs-archive/
- Regenerate project maps
Quality gates: npm run typecheck, npm run lint (apps/web)
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
- 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.
- 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
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).
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.
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
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
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