Compare commits

...

9 Commits

Author SHA1 Message Date
Developer 671540ded9 feat: add Headquarter favicon
- Add a geometric evergreen Headquarter mark for browser tabs\n- Register the SVG favicon and matching browser theme color\n\nOpenSpec: add-project-favicon\nQuality gates: npm run build
2026-07-15 13:06:40 +00:00
Developer ad26fd9f35 merge: dialog scroll containment 2026-07-15 10:20:39 +00:00
Developer 415aecc0dd fix: constrain dialog content to the viewport
- Add dynamic viewport bounds and scrollable body regions to dialogs and modals\n- Make tool-launch popups use the shared scrollable body pattern\n- Keep mobile sheets, action sheets, and notification popups scroll-contained\n- Restore the ProjectsPage test setup required for the frontend suite\n\nOpenSpec: fix-dialog-scroll-containment\nQuality gates: npm run typecheck, npm run lint, npm test (88 passed), npm run build
2026-07-15 10:20:20 +00:00
Developer 5e06a2a226 Merge fix/terminal-paste-partial-writes into dev 2026-07-14 10:34:33 +00:00
Developer 49180e4c6d fix: fully drain terminal paste writes to PTY
- Make the host PTY master non-blocking and wait for write readiness.
- Retry partial writes so a large bracketed paste always delivers its closing
  marker instead of leaving pi in paste mode.
- Add regression coverage for partial PTY writes and resolve diagnostics.

Quality gates: ruff, mypy, focused pytest (3 passed)
2026-07-14 10:34:33 +00:00
Developer 7a5538b53f Merge fix/web-terminal-bracketed-paste into dev 2026-07-14 10:02:30 +00:00
Developer 41d24beade fix: forward browser terminal pastes as bracketed input
- Track application bracketed-paste mode from terminal output.
- Capture browser and mobile clipboard pastes only while that mode is enabled,
  normalizing line endings and sending a single BPM-framed input event.
- Remove the synchronous output decoding and console diagnostics that could
  stall terminal rendering under output load.
- Keep the terminal's zero-scrollback configuration and resolve existing
  no-case-declarations lint blockers in terminal keyboard shortcuts.

Quality gates: npm run typecheck, npm run lint
2026-07-14 10:02:30 +00:00
Developer d1777b88ad Merge fix/terminal-paste-bpm into dev 2026-07-13 13:42:56 +00:00
Developer 691bbaa87b fix: restore web terminal multiline paste by reverting scrollback to 10000
- Revert xterm.js scrollback from 0 to 10000; empirical correlation with
  bracketed-paste mode failing (each line submitted as separate command).
- Add temporary browser-console diagnostics to confirm whether xterm receives
  pi's \e[?2004h enable sequence and whether outbound paste is BPM-wrapped.
- Keep CSS scrollbar hiding and wheel-sensitivity 0 so the original scroll-jank
  fix remains effective.

Typecheck: passed (apps/web)
2026-07-13 13:42:56 +00:00
31 changed files with 1285 additions and 1002 deletions
+4 -13
View File
@@ -16,7 +16,7 @@ dir: .
Trust boundary: index routes, map orients, source decides.
## role
Infrastructure and configuration package for "Headquarter," a self-hosted project management platform with Git repository features, containing deployment configs, development tooling, and UI refactoring documentation.
Package .
## parent
-
## children
@@ -32,15 +32,6 @@ Infrastructure and configuration package for "Headquarter," a self-hosted projec
- .pi
index: .pi/.pi-map.index.md
map: .pi/.pi-map.md
- .pi-lens
index: .pi-lens/.pi-map.index.md
map: .pi-lens/.pi-map.md
- .pytest_cache
index: .pytest_cache/.pi-map.index.md
map: .pytest_cache/.pi-map.md
- .ruff_cache
index: .ruff_cache/.pi-map.index.md
map: .ruff_cache/.pi-map.md
- .sisyphus
index: .sisyphus/.pi-map.index.md
map: .sisyphus/.pi-map.md
@@ -62,12 +53,12 @@ Infrastructure and configuration package for "Headquarter," a self-hosted projec
- scripts
index: scripts/.pi-map.index.md
map: scripts/.pi-map.md
- tests
index: tests/.pi-map.index.md
map: tests/.pi-map.md
- tool-images
index: tool-images/.pi-map.index.md
map: tool-images/.pi-map.md
- uploads
index: uploads/.pi-map.index.md
map: uploads/.pi-map.md
## files
- .env.example
- .gitignore
+16 -16
View File
@@ -18,25 +18,25 @@ index: ./.pi-map.index.md
Trust boundary: index routes, map orients, source decides.
## role
Infrastructure and configuration package for "Headquarter," a self-hosted project management platform with Git repository features, containing deployment configs, development tooling, and UI refactoring documentation.
Package .
## files
- .env.example | Provides a template of environment variables for configuring database, Redis, sessions, domains, Authentik OAuth, frontend, and Docker settings for the application. | dep: Postgres, Redis, Authentik, Vite, Docker Compose, Traefik
- .gitignore | Specifies intentionally untracked files and directories to exclude from version control across multiple technologies and environments.
- AGENTS.md | Defines rules, workflow conventions, and verification standards for AI agents operating on the project, using OpenSpec as the source of truth and Superpowers as the default workflow. | dep: OpenSpec, Superpowers, Git
- CHANGELOG.md | Documents notable changes and version history for a project management application with Git repository features.
- Makefile | Provides common development automation commands for a Dockerized full-stack application, including service lifecycle, testing, linting, migrations, and cleanup. | dep: docker compose, alembic, pytest, playwright, ruff, mypy, npm
- README.md | Provides documentation and onboarding instructions for "Headquarter," a self-hosted platform for managing projects, git repositories, and development tools.
- docker-compose.traefik.yml | Defines a Docker Compose deployment configuration for a multi-service application (PostgreSQL, Redis, web frontend, API) behind an existing Traefik reverse proxy with TLS termination. | dep: postgres:15-alpine, redis:7-alpine, traefik, docker.sock, authentik
- docker-compose.yml | Defines a multi-container Docker Compose configuration for a full-stack application with PostgreSQL, Redis, an API service, and a web frontend. | dep: postgres:15-alpine, redis:7-alpine, apps/api, apps/web
- progress.md | Tracks progress of a backend-frontend refactoring project across completed phases, pending tasks, and remaining work.
- swap-pane | Empty package initialization file that marks a directory as a Python package.
- ui-audit-spacing-typography.md | Audit report documenting spacing, typography, and visual rhythm issues in the web UI, including missing CSS classes, inconsistent units, incomplete design tokens, and recommendations for fixes.
- ui-rework-foundations-apply.md | Documenting the implementation of Pass 1 (Foundations) of a web UI spacing/typography/visual-rhythm rework, summarizing changed files, commits, validation results, and next steps. | dep: tokens.css, global.css, utilities.css, ConfigProfileListSidebar.tsx, ConfigProfileEditorPanel.tsx, instance-list.tsx, openspec artifacts
- ui-rework-pass2-apply.md | Implementation report documenting a UI component cleanup pass that refactored inline styles into CSS utility classes and design tokens.
- .env.example | Provides example environment variable configuration for a full-stack application with database, caching, authentication, and deployment settings
- .gitignore | Specifies files and directories for Git to ignore across a project using Beads/Dolt, Python, Node, and various IDE/OS tooling. | dep: git, dolt, beads, python, node, npm, yarn, pnpm, pytest, mypy, ruff, coverage
- AGENTS.md | Defines operational guidelines and workflows for AI agents collaborating on a software project governed by OpenSpec | dep: OpenSpec, superpowers (brainstorming, writing-plans, test-driven-development, systematic-debugging, verification-before-completion, using-git-worktrees, dispatching-parallel-agents), git
- CHANGELOG.md | Documents version history and notable changes for a project management and Git repository application
- Makefile | Provides standardized development commands for managing a Docker-based full-stack application with API, web frontend, database, and testing infrastructure | dep: docker compose, alembic, pytest, ruff, mypy, playwright, npm, postgres, redis
- README.md | Documentation for a self-hosted development platform that manages projects, git repositories, and development tools with OAuth2 authentication | dep: FastAPI, SQLAlchemy, Pydantic, Alembic, python-jose, React, TypeScript, Vite, React Router, Docker, PostgreSQL, Traefik, Authentik
- docker-compose.traefik.yml | Deploys a multi-service application (PostgreSQL, Redis, web frontend, API) behind an existing Traefik reverse proxy with TLS termination | dep: docker, docker-compose, traefik, postgres, redis, node/vite, python/fastapi
- docker-compose.yml | Defines a multi-service Docker Compose stack for a web application with PostgreSQL, Redis, API backend, and web frontend services | dep: Docker, Docker Compose, PostgreSQL, Redis, Vite, asyncpg, Python/FastAPI (implied), Node.js (implied)
- progress.md | Documents the progress and remaining tasks for a backend-frontend refactoring project involving modularization, code reorganization, and verification.
- swap-pane | Provides a command to swap the position of two tmux panes within a window or between windows | dep: tmux, client, window, layout, cmd-find, cmd-parse, options
- ui-audit-spacing-typography.md | A detailed audit report identifying critical CSS styling gaps, mobile layout failures, and design system inconsistencies in a web application's UI components and stylesheets. | dep: React/TSX components, CSS stylesheets (global.css, utilities.css, tokens.css, page-specific CSS), JSX/TSX files in apps/web/src/components and apps/web/src/pages
- ui-rework-foundations-apply.md | Documents the implementation of Pass 1 (Foundations) of a web UI spacing/typography/visual-rhythm rework, including design token expansion, primitive CSS class additions, component refactors, and OpenSpec documentation. | dep: CSS design tokens, React/TSX components, OpenSpec documentation system, Git/SSH, npm build toolchain
- ui-rework-pass2-apply.md | Documents the implementation and verification of Pass 2 of a web UI spacing/typography rework, refactoring inline styles into utility classes and design tokens across six components. | dep: CSS custom properties, BEM methodology, utility-first CSS, React/TSX components, OpenSpec documentation system
## arch
Dockerized microservices architecture with Docker Compose orchestrating PostgreSQL, Redis, API, and web frontend services; uses Traefik reverse proxy with TLS termination, environment-based configuration, and Makefile-driven development workflow with documented UI design token system.
Contains 13 files.
## tags
docker, ui, application, web, css, rework, redis, frontend
docker, web, git, application, ui, rework, python, redis
## symbols
-
## workflows
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps
## role
Contains the main application entry points and executable modules for the project.
Package apps
## parent
index: ./.pi-map.index.md
map: ./.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: apps
index: apps/.pi-map.index.md
## role
Contains the main application entry points and executable modules for the project.
Package apps
## files
## arch
Modular application structure with separate deployable units, likely following microservices or multi-app monorepo pattern.
Contains 0 files.
## tags
-
## symbols
@@ -19,6 +19,8 @@ from typing import Any
logger = logging.getLogger(__name__)
_session_counters_by_instance: dict[str, int] = {}
class TerminalSession:
"""Manages a single terminal session with event-driven PTY I/O.
@@ -46,9 +48,6 @@ class TerminalSession:
# Max WebSocket frame size
MAX_FRAME_SIZE = 64 * 1024
# Session number counter per instance_id for auto-naming
_instance_counters: dict[str, int] = {}
def __init__(
self,
session_id: str,
@@ -99,11 +98,11 @@ class TerminalSession:
# Ack timeout fallback
self._ack_timeout_handle: asyncio.TimerHandle | None = None
@classmethod
def _generate_name(cls, instance_id: str) -> str:
@staticmethod
def _generate_name(instance_id: str) -> str:
"""Generate an auto-incremented session name for the instance."""
count = cls._instance_counters.get(instance_id, 0) + 1
cls._instance_counters[instance_id] = count
count = _session_counters_by_instance.get(instance_id, 0) + 1
_session_counters_by_instance[instance_id] = count
return f"Session {count}"
async def start(self, startup_command: str | None = None) -> None:
@@ -121,8 +120,12 @@ class TerminalSession:
if self.container_user:
exec_cmd.extend(["--user", self.container_user])
# Create a pseudo-terminal on the host
# Create a pseudo-terminal on the host. The master must be
# non-blocking: a browser paste can be larger than the PTY input
# buffer, and write_input() drains it asynchronously without dropping
# the closing bracketed-paste marker.
self._master_fd, slave_fd = pty.openpty()
os.set_blocking(self._master_fd, False)
# Put the host PTY into raw mode so it behaves as a pass-through
# pipe. openpty() leaves the slave in canonical mode by default,
@@ -204,6 +207,9 @@ class TerminalSession:
try:
data = os.read(self._master_fd, 4096)
except BlockingIOError:
# The readiness notification raced with another callback.
return
except (OSError, IOError) as exc:
logger.debug("PTY read error for session %s: %s", self.session_id, exc)
self._handle_eof()
@@ -343,12 +349,42 @@ class TerminalSession:
pass
logger.info("Session %s EOF handled, websockets closed", self.session_id)
async def _wait_for_write_ready(self, fd: int) -> None:
"""Wait until a non-blocking PTY master can accept more input."""
loop = asyncio.get_running_loop()
writable = loop.create_future()
def mark_writable() -> None:
if not writable.done():
writable.set_result(None)
loop.add_writer(fd, mark_writable)
try:
await writable
finally:
loop.remove_writer(fd)
async def write_input(self, data: bytes) -> None:
"""Write input to the PTY master."""
"""Write all terminal input bytes to the PTY master in order."""
if self._master_fd is None or self._closed:
return
fd = self._master_fd
remaining = memoryview(data)
try:
os.write(self._master_fd, data)
while remaining and not self._closed and self._master_fd == fd:
try:
written = os.write(fd, remaining)
except BlockingIOError:
await self._wait_for_write_ready(fd)
continue
if written == 0:
await self._wait_for_write_ready(fd)
continue
remaining = remaining[written:]
self.last_activity = time.time()
except (OSError, IOError) as exc:
logger.debug("PTY write error for session %s: %s", self.session_id, exc)
+53 -22
View File
@@ -1,9 +1,9 @@
"""Unit tests for TerminalSession docker exec invocation."""
"""Unit tests for TerminalSession PTY handling."""
import uuid
from unittest.mock import AsyncMock, patch
import pytest
import pytest # pyright: ignore[reportMissingImports]
from src.services.terminal.terminal_session import TerminalSession
@@ -19,17 +19,20 @@ async def test_start_passes_container_user_to_docker_exec() -> None:
container_user="dev",
)
with patch(
"src.services.terminal.terminal_session.pty.openpty",
return_value=(1, 2),
with (
patch(
"src.services.terminal.terminal_session.pty.openpty",
return_value=(1, 2),
),
patch("src.services.terminal.terminal_session.os.set_blocking") as set_blocking,
patch("src.services.terminal.terminal_session.tty.setraw"),
patch(
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
new=AsyncMock(),
) as mock_exec,
patch("src.services.terminal.terminal_session.os.close"),
):
with patch("src.services.terminal.terminal_session.tty.setraw"):
with patch(
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
new=AsyncMock(),
) as mock_exec:
with patch("src.services.terminal.terminal_session.os.close"):
await session.start()
await session.start()
args, _kwargs = mock_exec.call_args
assert "docker" in args
@@ -38,6 +41,7 @@ async def test_start_passes_container_user_to_docker_exec() -> None:
user_index = args.index("--user")
assert args[user_index + 1] == "dev"
assert "container-123" in args
set_blocking.assert_called_once_with(1, False)
@pytest.mark.unit
@@ -50,17 +54,44 @@ async def test_start_omits_user_when_not_configured() -> None:
container_id="container-123",
)
with patch(
"src.services.terminal.terminal_session.pty.openpty",
return_value=(1, 2),
with (
patch(
"src.services.terminal.terminal_session.pty.openpty",
return_value=(1, 2),
),
patch("src.services.terminal.terminal_session.os.set_blocking"),
patch("src.services.terminal.terminal_session.tty.setraw"),
patch(
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
new=AsyncMock(),
) as mock_exec,
patch("src.services.terminal.terminal_session.os.close"),
):
with patch("src.services.terminal.terminal_session.tty.setraw"):
with patch(
"src.services.terminal.terminal_session.asyncio.create_subprocess_exec",
new=AsyncMock(),
) as mock_exec:
with patch("src.services.terminal.terminal_session.os.close"):
await session.start()
await session.start()
args, _kwargs = mock_exec.call_args
assert "--user" not in args
@pytest.mark.unit
@pytest.mark.asyncio
async def test_write_input_retries_partial_pty_writes() -> None:
"""A large paste is fully written even when the PTY accepts it in chunks."""
session = TerminalSession(
session_id=str(uuid.uuid4()),
instance_id=uuid.uuid4(),
container_id="container-123",
)
session._master_fd = 42
with patch(
"src.services.terminal.terminal_session.os.write",
side_effect=[2, 2, 1],
) as write:
await session.write_input(b"hello")
assert [bytes(call.args[1]) for call in write.call_args_list] == [
b"hello",
b"llo",
b"o",
]
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web
## role
Frontend web application serving as the user-facing UI for the "headquarter" project, built as a React/Vite single-page application and containerized for production deployment.
Frontend web application for a terminal/code-editor interface named "Headquarter," built with React/TypeScript using Vite and served as a static single-page app.
## parent
index: apps/.pi-map.index.md
map: apps/.pi-map.md
+11 -11
View File
@@ -4,21 +4,21 @@ dir: apps/web
index: apps/web/.pi-map.index.md
## role
Frontend web application serving as the user-facing UI for the "headquarter" project, built as a React/Vite single-page application and containerized for production deployment.
Frontend web application for a terminal/code-editor interface named "Headquarter," built with React/TypeScript using Vite and served as a static single-page app.
## files
- .env.example | Provides example environment variable configuration for a Vite-based frontend application | dep: Vite
- .env.example | Template/example file defining environment variables for a Vite-based application's API and OAuth configuration | dep: Vite
- .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ES2022 module support | dep: eslint, @typescript-eslint/parser, @typescript-eslint/eslint-plugin
- Dockerfile | Multi-stage Docker build for a Node.js/Vite frontend application compiled to static assets and served by nginx with non-root user security | dep: node:20-alpine, nginx:alpine, npm, wget
- index.html | HTML entry point for a React/Vite single-page application that loads a TypeScript root component | dep: Google Fonts (Inter, IBM Plex Mono), /src/main.tsx
- nginx.conf | Configures an nginx web server to serve a single-page application with gzip compression, client-side routing support, cache control for static assets, and a health check endpoint. | dep: nginx
- package-lock.json | Auto-generated dependency lock file that records exact versions and resolved URLs of all npm packages for reproducible installs of a React-based web application named "headquarter-web" | dep: npm, react, react-dom, react-router-dom, axios, tailwindcss, prismjs, xterm, vite, vitest, typescript, eslint, @babel/core, @vitejs/plugin-react, @testing-library/react, jsdom
- package.json | Defines the configuration, dependencies, and scripts for a React-based web application called "headquarter-web" built with Vite and TypeScript. | dep: react, react-dom, react-router-dom, axios, tailwindcss, prismjs, react-simple-code-editor, xterm, xterm-addon-fit, xterm-addon-web-links, xterm-addon-webgl, @phosphor-icons/react, vite, typescript, vitest, eslint, @testing-library/react, jsdom
- tsconfig.json | Configures TypeScript compiler settings for a modern React/Vite web application with strict type checking and ES module output. | dep: TypeScript, Vite, React
- vite.config.ts | Configures Vite build tool with React plugin, dev server port, and Vitest test settings | dep: vite, @vitejs/plugin-react
- Dockerfile | Multi-stage Docker build for a Node.js/Vite frontend application compiled to static assets and served by nginx as a non-root user | dep: node:20-alpine, nginx:alpine, npm, wget
- index.html | Entry point HTML file for a React application named "Headquarter" that loads a TypeScript module and Google Fonts | dep: Google Fonts (Inter, IBM Plex Mono), main.tsx module
- nginx.conf | Configures Nginx as a web server for a single-page application with gzip compression, client-side routing support, cache control for static assets, and a health check endpoint. | dep: nginx
- package-lock.json | Records exact dependency versions for a React-based web application to ensure reproducible installs | dep: npm, React, React Router, Tailwind CSS, Vite, Vitest, TypeScript, ESLint, xterm, PrismJS, axios, phosphor-icons
- package.json | Defines a React-based web application project named "headquarter-web" with build tooling, testing, and UI dependencies for a terminal/code editor interface. | dep: react, react-dom, react-router-dom, vite, typescript, tailwindcss, axios, prismjs, xterm, @phosphor-icons/react, react-simple-code-editor, vitest, eslint
- tsconfig.json | Configures TypeScript compiler options for a modern React/Vite project with ES2020 target and strict type checking | dep: TypeScript, React, Vite
- vite.config.ts | Configures Vite build tool with React support, dev server port, and Vitest testing settings | dep: vite, @vitejs/plugin-react
## arch
Modern React SPA architecture using TypeScript with Vite build tooling, containerized via multi-stage Docker build producing static assets served by nginx, with strict type checking, ESLint code quality, and Vitest testing integration.
Containerized single-page React/TypeScript app using Vite for build/dev, Vitest for testing, ESLint for linting, and nginx for static asset delivery with client-side routing support.
## tags
react, vite, application, eslint, typescript, web, nginx, xterm
react, vite, eslint, application, typescript, nginx, configures, web
## symbols
-
## workflows
+2
View File
@@ -3,6 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#275d4b" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<title>Headquarter</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Headquarter</title>
<rect width="64" height="64" rx="15" fill="#275d4b"/>
<path fill="#fffef9" d="M17 15h8v13h14V15h8v34h-8V36H25v13h-8z"/>
<path fill="#9dcdb7" d="M25 28h14v8H25z"/>
</svg>

After

Width:  |  Height:  |  Size: 266 B

+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src
## role
Frontend web application bootstrap and core infrastructure package providing React SPA initialization, routing, and shared type definitions.
Frontend web client that bootstraps and renders a React single-page application with routing, authentication, and session-driven UI.
## parent
index: apps/web/.pi-map.index.md
map: apps/web/.pi-map.md
+6 -6
View File
@@ -4,15 +4,15 @@ dir: apps/web/src
index: apps/web/src/.pi-map.index.md
## role
Frontend web application bootstrap and core infrastructure package providing React SPA initialization, routing, and shared type definitions.
Frontend web client that bootstraps and renders a React single-page application with routing, authentication, and session-driven UI.
## files
- main.tsx | Entry point that bootstraps a React SPA with routing, authentication, and session context providers. | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css
- router.tsx | Defines the React Router configuration for a web application with nested routes, protected routes, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage
- types.ts | Defines core TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
- main.tsx | Entry point that bootstraps a React SPA with routing, authentication, and session state management | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom
- router.tsx | Defines the React Router configuration for a multi-page application with nested routes, protected authentication, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage
- types.ts | Defines TypeScript type declarations for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
## arch
Single-page application using React with context-based dependency injection for auth/session state, declarative nested routing with route guards, and centralized TypeScript domain modeling.
React SPA built with a declarative React Router configuration, protected/nested routes, and centralized TypeScript types for users, sessions, projects, repositories, and workspaces.
## tags
pages, styles, css, router, react, session, project, summary
pages, styles, css, router, react, session, dom, project
## symbols
- AppRouter
- SessionUser
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/components
## role
Provides reusable, type-safe React UI components for the web application including layout shells, data display states, iconography, code editing, syntax highlighting, authentication guards, and toast notification rules.
Package components
## parent
index: apps/web/src/.pi-map.index.md
map: apps/web/src/.pi-map.md
+13 -13
View File
@@ -4,22 +4,22 @@ dir: apps/web/src/components
index: apps/web/src/components/.pi-map.index.md
## role
Provides reusable, type-safe React UI components for the web application including layout shells, data display states, iconography, code editing, syntax highlighting, authentication guards, and toast notification rules.
Package components
## files
- app-shell.tsx | Provides the main application layout shell with navigation, session management, and responsive mobile/desktop views for a React application. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../utils/open-session, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
- code-editor.tsx | A React component that renders a syntax-highlighted code editor with line numbers using a lightweight editor library. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
- data-states.tsx | Defines reusable React components for displaying loading, error, and empty data states in a UI. | exp: LoadingState, ErrorState, EmptyState | dep: ./icon, icon
- icon.tsx | Provides a centralized, type-safe React component that maps semantic icon names to Phosphor icons with configurable size, weight, color, and accessibility properties. | exp: IconName, IconProps, Icon | dep: react, @phosphor-icons/react
- loading-overlay.tsx | Renders an accessible loading spinner overlay with optional text label that conditionally displays based on visibility prop | exp: func:LoadingOverlay({ visible, label }: LoadingOverlayProps) | dep: ./icon, icon
- protected-route.test.tsx | Tests a ProtectedRoute component that guards content based on authentication state | dep: @testing-library/react, react-router-dom, vitest, ./protected-route, ../state/auth
- protected-route.tsx | A React component that conditionally renders children or redirects to login based on authentication state | exp: ProtectedRoute | dep: react-router-dom, ../state/auth
- syntax-highlighter.tsx | A React component that renders syntax-highlighted code blocks with optional line numbers, language badge, and copy-to-clipboard functionality. | exp: SyntaxHighlighter | dep: react, ./icon, ../utils/language
- toast-rules.test.ts | Unit tests for toast notification rules that map instance events to UI categories and severity levels. | dep: vitest, ./toast-rules, ../types/events
- toast-rules.ts | Maps instance events to toast notifications with deduplication logic to prevent spam. | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast, InstanceEventPayload
- app-shell.tsx | Main application layout shell with responsive navigation, session management, and global state providers for notifications and toasts. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../utils/open-session, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
- code-editor.tsx | A React component that renders a syntax-highlighting code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
- data-states.tsx | Provides reusable UI components for rendering loading, error, and empty data states. | exp: LoadingState, ErrorState, EmptyState | dep: ./icon, Icon
- icon.tsx | Provides a centralized, type-safe React icon component that maps semantic names to Phosphor icons with configurable sizes, weights, and colors. | exp: IconName, IconProps, Icon | dep: react, @phosphor-icons/react
- loading-overlay.tsx | Renders an accessible loading overlay with optional text label when visible | exp: func:LoadingOverlay({ visible, label }: LoadingOverlayProps) | dep: ./icon, icon
- protected-route.test.tsx | Tests a ProtectedRoute component that guards content based on authentication state, showing loading or redirecting to login | dep: @testing-library/react, react-router-dom, vitest, ./protected-route
- protected-route.tsx | Conditionally renders children or redirects to login based on authentication state | exp: ProtectedRoute | dep: react-router-dom, ../state/auth
- syntax-highlighter.tsx | A React component that renders syntax-highlighted code with optional line numbers and a copy-to-clipboard button | exp: SyntaxHighlighter | dep: react, ./icon, ../utils/language, React, icon, language utils
- toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events
- toast-rules.ts | Maps instance events to toast notifications with deduplication logic | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast, InstanceEventPayload
## arch
Follows a component-based architecture with presentational/ui components, separation of concerns (display logic vs. business rules in `.ts` files), test co-location, and centralized abstractions for common patterns like icons and data states.
Contains 10 files.
## tags
toast, state, react, icon, loading, event, code, editor
toast, state, icon, react, loading, code, event, editor
## symbols
- LoadingOverlay
- mapEventToCategory
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features
## role
Reusable UI components organized by domain-specific feature areas for the web application frontend.
Feature-specific UI components organized by business domain/capability for the web
## parent
index: apps/web/src/components/.pi-map.index.md
map: apps/web/src/components/.pi-map.md
+2 -2
View File
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
index: apps/web/src/components/features/.pi-map.index.md
## role
Reusable UI components organized by domain-specific feature areas for the web application frontend.
Feature-specific UI components organized by business domain/capability for the web
## files
## arch
Feature-based component colocation pattern with domain-driven folder structure, separating shared/common components from page-specific feature implementations.
Contains 0 files.
## tags
-
## symbols
@@ -2,7 +2,7 @@
dir: apps/web/src/components/features/terminal
## role
Provides responsive, cross-platform terminal UI components with session management, virtual special keys, and WebSocket-backed xterm.js integration for web-based terminal access.
Provides the UI components for a responsive, multi-session terminal interface in the web app, including desktop and mobile layouts, session tabs, and special-key input controls.
## parent
index: apps/web/src/components/features/.pi-map.index.md
map: apps/web/src/components/features/.pi-map.md
@@ -4,19 +4,19 @@ dir: apps/web/src/components/features/terminal
index: apps/web/src/components/features/terminal/.pi-map.index.md
## role
Provides responsive, cross-platform terminal UI components with session management, virtual special keys, and WebSocket-backed xterm.js integration for web-based terminal access.
Provides the UI components for a responsive, multi-session terminal interface in the web app, including desktop and mobile layouts, session tabs, and special-key input controls.
## files
- DesktopTerminalView.tsx | Renders a desktop terminal view with session tabs, fullscreen controls, and multiple terminal instances | exp: DesktopTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../../api/terminal
- MobileTerminalView.tsx | Renders a mobile-optimized terminal interface with session tabs, toolbar controls, special keys input, and keyboard-aware layout adjustments. | exp: MobileTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../icon, ./special-keys-strip, ./special-keys-panel, ../../../hooks/use-special-keys, ../../../api/terminal, terminal, terminal-session-tabs, icon, special-keys-strip, special-keys-panel, use-special-keys, api/terminal
- special-keys-panel.tsx | Renders an overlay panel of special keyboard keys (Home, End, F1-F12, Ctrl+C, etc.) that sends terminal sequences when clicked and supports modifier key combinations. | exp: SpecialKeysPanel | dep: react, ../../../hooks/use-special-keys, React, use-special-keys hook
- special-keys-strip.tsx | Renders a strip of virtual special keys (Esc, Tab, Ctrl, Alt, arrows, etc.) for sending terminal key sequences with optional modifier support. | exp: SpecialKeysStrip | dep: react, ../../../hooks/use-special-keys, use-special-keys hook
- terminal-session-tabs.test.tsx | Tests a React terminal session tabs component for rendering, selection, closing with confirmation, renaming, session limits, and connection status display. | dep: @testing-library/react, vitest, ./terminal-session-tabs
- terminal-session-tabs.tsx | Renders an interactive tab bar for managing multiple terminal sessions with selection, renaming, closing, and creation capabilities | exp: TerminalSessionInfo, TerminalSessionTabsProps, TerminalSessionTabs | dep: react, React, useState, useRef, useCallback
- terminal.tsx | A React terminal component that provides an interactive xterm.js-based terminal with WebSocket connectivity, mobile touch support, and session management. | exp: TerminalProps, TerminalRef, TerminalComponent | dep: react, xterm, xterm-addon-fit, xterm-addon-web-links, xterm/css/xterm.css, ../../../hooks/use-special-keys, React
- DesktopTerminalView.tsx | Renders the desktop layout for a multi-session terminal UI, managing tabs, fullscreen mode, font controls, and reset confirmation. | exp: DesktopTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../../api/terminal, React, TerminalComponent, TerminalSessionTabs, TerminalSession
- MobileTerminalView.tsx | Renders a mobile-specific terminal interface with session tabs, toolbar controls, special key inputs, and multiple terminal instances. | exp: MobileTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../icon, ./special-keys-strip, ./special-keys-panel, ../../../hooks/use-special-keys, ../../../api/terminal, React, TerminalComponent, TerminalSessionTabs, Icon, SpecialKeysStrip, SpecialKeysPanel
- special-keys-panel.tsx | Renders a modal overlay panel of special keyboard keys (Home, End, F1-F12, Ctrl+C, etc.) that sends escape sequences to a terminal when clicked. | exp: SpecialKeysPanel | dep: react, ../../../hooks/use-special-keys
- special-keys-strip.tsx | Renders a touch-accessible strip of special keyboard keys (Esc, Tab, arrows, modifiers) that sends terminal escape sequences with optional modifier support. | exp: SpecialKeysStrip | dep: react, ../../../hooks/use-special-keys, React, use-special-keys hook
- terminal-session-tabs.test.tsx | Unit tests for a TerminalSessionTabs React component covering tab rendering, selection, close confirmation, inline renaming, session limit enforcement, and connection status display. | dep: @testing-library/react, vitest, ./terminal-session-tabs
- terminal-session-tabs.tsx | Renders a tabbed interface for managing multiple terminal sessions with support for switching, renaming, closing with confirmation, and creating new sessions. | exp: TerminalSessionInfo, TerminalSessionTabsProps, TerminalSessionTabs | dep: react, React
- terminal.tsx | | exp: TerminalProps, TerminalRef, TerminalComponent | dep: react, xterm, xterm-addon-fit, xterm-addon-web-links, xterm/css/xterm.css, ../../../hooks/use-special-keys
## arch
Component-based architecture with platform-specific view layers (desktop/mobile), modular special keys subsystems (strip/overlay panel), tab-based session management with React state, and xterm.js integration via WebSocket abstraction with touch/mobile adaptations.
Modular React component architecture organized by feature, with separate presentational components for desktop/mobile viewports, shared tab/session management, and helper components for terminal input handling.
## tags
terminal, special, keys, session, tabs, react, view, strip
terminal, special, session, keys, react, tabs, view, strip
## symbols
- DesktopTerminalView
- MobileTerminalView
File diff suppressed because it is too large Load Diff
@@ -578,19 +578,23 @@ export const InstanceList = ({
{showCreate && (
<div className="dialog-overlay" role="dialog" aria-modal="true">
<div className="dialog">
<h2>Launch Tool</h2>
<CreateSessionForm
projects={[]}
repositories={[]}
toolTypes={toolTypes}
fixedProjectId={projectId}
fixedRepoId={repoId}
projectName={projectName}
repoName={repoName}
onSuccess={handleCreateSuccess}
onCancel={() => setShowCreate(false)}
submitLabel="Launch"
/>
<div className="dialog-header">
<h2>Launch Tool</h2>
</div>
<div className="dialog-body">
<CreateSessionForm
projects={[]}
repositories={[]}
toolTypes={toolTypes}
fixedProjectId={projectId}
fixedRepoId={repoId}
projectName={projectName}
repoName={repoName}
onSuccess={handleCreateSuccess}
onCancel={() => setShowCreate(false)}
submitLabel="Launch"
/>
</div>
</div>
</div>
)}
@@ -62,6 +62,7 @@ export function StartToolFAB() {
</button>
</div>
<div className="modal-body">
{workspacesLoading ? (
<p className="muted">Loading workspaces...</p>
) : workspaces.length === 0 ? (
@@ -110,6 +111,7 @@ export function StartToolFAB() {
/>
</>
)}
</div>
</div>
</div>
)}
@@ -0,0 +1,47 @@
import "@testing-library/jest-dom/vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { WorkspaceToolsPanel } from "./workspace-tools-panel";
import type { Workspace } from "../../../types/workspace";
vi.mock("../../../hooks/use-workspace-instances", () => ({
useWorkspaceInstances: () => ({
instances: [],
loading: false,
refresh: vi.fn(),
}),
}));
vi.mock("../tool/tool-starter", () => ({
ToolStarter: () => <div data-testid="tool-starter" />,
}));
const workspace: Workspace = {
id: "workspace-1",
name: "Main",
repo_id: "repo-1",
repo_name: "repository",
repo_ssh_key_id: null,
project_id: "project-1",
project_name: "Project",
user_id: "user-1",
branch: "main",
path: "/workspace",
status: "ready",
last_sync_at: null,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
instance_count: 0,
};
describe("WorkspaceToolsPanel", () => {
it("places the tool launcher inside the shared scrollable dialog body", () => {
const { container } = render(<WorkspaceToolsPanel workspace={workspace} />);
fireEvent.click(screen.getByRole("button", { name: "Start Tool" }));
const dialogBody = container.querySelector(".dialog-body");
expect(dialogBody).toContainElement(screen.getByTestId("tool-starter"));
});
});
@@ -64,15 +64,19 @@ export function WorkspaceToolsPanel({ workspace }: WorkspaceToolsPanelProps) {
{showModal && (
<div className="dialog-overlay" onClick={() => setShowModal(false)}>
<div className="dialog" onClick={(e) => e.stopPropagation()}>
<h3>Start Tool</h3>
<ToolStarter
workspace={workspace}
onStarted={() => {
setShowModal(false);
void refresh();
}}
onCancel={() => setShowModal(false)}
/>
<div className="dialog-header">
<h3>Start Tool</h3>
</div>
<div className="dialog-body">
<ToolStarter
workspace={workspace}
onStarted={() => {
setShowModal(false);
void refresh();
}}
onCancel={() => setShowModal(false)}
/>
</div>
</div>
</div>
)}
+10 -7
View File
@@ -169,6 +169,10 @@ export const useTerminalPage = () => {
const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey;
if (!isAltShift) return;
const activeSessionIndex = activeSessionId
? sessions.findIndex((session) => session.id === activeSessionId)
: -1;
switch (e.key.toLowerCase()) {
case "n":
e.preventDefault();
@@ -187,17 +191,14 @@ export const useTerminalPage = () => {
break;
case "arrowleft":
e.preventDefault();
if (activeSessionId) {
const idx = sessions.findIndex((s) => s.id === activeSessionId);
if (idx > 0) setActiveSessionId(sessions[idx - 1].id);
if (activeSessionIndex > 0) {
setActiveSessionId(sessions[activeSessionIndex - 1].id);
}
break;
case "arrowright":
e.preventDefault();
if (activeSessionId) {
const idx = sessions.findIndex((s) => s.id === activeSessionId);
if (idx < sessions.length - 1)
setActiveSessionId(sessions[idx + 1].id);
if (activeSessionIndex >= 0 && activeSessionIndex < sessions.length - 1) {
setActiveSessionId(sessions[activeSessionIndex + 1].id);
}
break;
case "r":
@@ -208,6 +209,8 @@ export const useTerminalPage = () => {
e.preventDefault();
setIsFullscreen((prev) => !prev);
break;
default:
break;
}
};
+22 -27
View File
@@ -1,17 +1,26 @@
import "@testing-library/jest-dom/vitest";
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ProjectsPage } from "./ProjectsPage";
import * as projectsApi from "../api/projects";
import { SessionsProvider } from "../state/sessions";
import type { ProjectWithRepos } from "../types";
const mockProjects = [
vi.mock("../api/sessions", () => ({
getUserSessions: vi.fn().mockResolvedValue([]),
}));
const mockProjects: ProjectWithRepos[] = [
{
id: "proj-1",
name: "Alpha Project",
description: "First project",
owner_id: "user-1",
default_ssh_key_id: null,
repositories: [],
created_at: "2026-07-01T00:00:00Z",
},
{
id: "proj-2",
@@ -19,6 +28,8 @@ const mockProjects = [
description: null,
owner_id: "user-1",
default_ssh_key_id: null,
repositories: [],
created_at: "2026-07-01T00:00:00Z",
},
];
@@ -31,9 +42,7 @@ describe("ProjectsPage", () => {
it("renders loading state initially", () => {
vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {}));
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
});
@@ -41,9 +50,7 @@ describe("ProjectsPage", () => {
it("renders project list after loading", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -56,9 +63,7 @@ describe("ProjectsPage", () => {
it("renders empty state when no projects", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -69,9 +74,7 @@ describe("ProjectsPage", () => {
it("renders error state with retry button", async () => {
vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -85,9 +88,7 @@ describe("ProjectsPage", () => {
const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -118,9 +119,7 @@ describe("ProjectsPage", () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
@@ -138,16 +137,14 @@ describe("ProjectsPage", () => {
const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
});
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
const alphaCard = screen.getByText("Alpha Project").closest(".project-list-item") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
fireEvent.click(within(alphaCard).getByRole("button", { name: /edit/i }));
@@ -171,16 +168,14 @@ describe("ProjectsPage", () => {
const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined);
render(
<MemoryRouter>
<ProjectsPage />
</MemoryRouter>
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
});
const alphaCard = screen.getByText("Alpha Project").closest(".project-card") as HTMLElement | null;
const alphaCard = screen.getByText("Alpha Project").closest(".project-list-item") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
+13 -13
View File
@@ -453,12 +453,16 @@
backdrop-filter: blur(2px);
}
.dialog {
.dialog,
.modal-content,
.commit-dialog {
width: 100%;
max-width: 32rem;
max-height: calc(100vh - var(--space-8));
max-height: calc(100dvh - var(--space-8));
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
background: var(--panel);
border: 1px solid var(--border);
@@ -468,16 +472,6 @@
.modal-content {
/* deprecated alias */
width: 100%;
max-width: 32rem;
max-height: calc(100vh - var(--space-8));
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
}
.dialog-lg {
@@ -500,9 +494,13 @@
line-height: var(--line-height-tight);
}
.dialog-body {
.dialog-body,
.modal-body {
flex: 1;
min-height: 0;
overflow: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
padding: var(--space-4);
}
@@ -543,9 +541,11 @@
}
.dialog,
.modal-content {
.modal-content,
.commit-dialog {
max-width: 100%;
max-height: calc(100vh - var(--space-6));
max-height: calc(100dvh - var(--space-6));
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
}
+35 -8
View File
@@ -152,7 +152,9 @@
}
.font-mono {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-family:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
monospace;
}
.font-medium {
@@ -2326,8 +2328,10 @@ a.nav-item,
width: 100%;
max-width: 600px;
max-height: 70vh;
max-height: 70dvh;
display: flex;
flex-direction: column;
min-height: 0;
animation: slide-up 0.2s ease-out;
}
@@ -2363,8 +2367,12 @@ a.nav-item,
}
.mobile-bottom-sheet-content {
flex: 1;
min-height: 0;
padding: 8px 0;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
.mobile-bottom-sheet-item {
@@ -3238,7 +3246,10 @@ a:active,
width: 100%;
max-width: 500px;
max-height: 80vh;
max-height: 80dvh;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
animation: slideUp 0.3s ease;
padding-bottom: env(safe-area-inset-bottom, 0);
}
@@ -3600,6 +3611,7 @@ a:active,
max-width: none;
border-radius: 12px;
max-height: 70vh;
max-height: 70dvh;
}
}
@@ -3817,7 +3829,9 @@ a:active,
color: var(--muted);
cursor: pointer;
border-radius: var(--radius-md);
transition: background 0.12s ease, color 0.12s ease;
transition:
background 0.12s ease,
color 0.12s ease;
}
.delete-btn:hover {
@@ -3844,7 +3858,9 @@ a:active,
font-size: var(--font-size-sm);
font-weight: 500;
text-decoration: none;
transition: background 0.12s ease, border-color 0.12s ease;
transition:
background 0.12s ease,
border-color 0.12s ease;
}
.workspace-header-action-btn:hover {
@@ -3919,7 +3935,9 @@ a:active,
color: var(--muted);
font-size: var(--font-size-xs);
cursor: pointer;
transition: background 0.12s ease, color 0.12s ease;
transition:
background 0.12s ease,
color 0.12s ease;
}
.copy-button:hover {
@@ -4165,7 +4183,10 @@ a:active,
}
.drag-item-active {
background: var(--brand-light, color-mix(in srgb, var(--brand) 10%, transparent));
background: var(
--brand-light,
color-mix(in srgb, var(--brand) 10%, transparent)
);
border-color: var(--brand);
}
@@ -4181,7 +4202,9 @@ a:active,
padding: var(--space-4);
border-radius: var(--radius-md);
overflow: auto;
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-family:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
monospace;
font-size: var(--font-size-sm);
line-height: var(--line-height-normal);
}
@@ -4329,7 +4352,9 @@ a:active,
color: var(--muted);
cursor: pointer;
font-weight: 600;
transition: border-color 0.15s, color 0.15s;
transition:
border-color 0.15s,
color 0.15s;
}
.sidebar-create-button:hover {
@@ -4354,7 +4379,9 @@ a:active,
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.15s ease, box-shadow 0.15s ease;
transition:
transform 0.15s ease,
box-shadow 0.15s ease;
}
.mobile-fab:hover {
@@ -0,0 +1,21 @@
# Add a Headquarter Favicon
## Summary
Add a compact, recognizable favicon for Headquarter and register it in the web document head.
## Design
Use a geometric cream `H` on the product's evergreen brand field. The mark remains identifiable at small browser-tab sizes, avoids font rendering dependencies, and matches both light and dark application themes.
## Scope
- `apps/web/public/favicon.svg`
- `apps/web/index.html`
## Acceptance Criteria
- [ ] The browser uses a dedicated Headquarter favicon.
- [ ] The mark remains legible at small sizes and on light or dark browser chrome.
- [ ] The HTML declares the icon type and a matching browser theme color.
- [ ] Frontend production build passes.
@@ -0,0 +1,6 @@
# Add a Headquarter Favicon — Tasks
- [x] Create the favicon asset.
- [x] Register the favicon and browser theme color in the web entry document.
- [x] Run the frontend production build.
- [x] Update project maps for changed source files.
@@ -0,0 +1,28 @@
# Constrain and Scroll Edit Dialogs and Popups
## Summary
Ensure every edit dialog, form popup, and modal popup remains usable on a viewport that is shorter than its content. Dialog chrome must stay within the visible viewport while the content area scrolls independently.
## Problem
The shared dialog system constrains only components that follow its `dialog-header` / `dialog-body` structure. Several tool-launch and commit dialogs place form content directly inside the container or use a bespoke container, so long forms can be clipped. Existing viewport sizing also relies on `vh`, which is unreliable when mobile browser chrome changes height.
## Scope
- Shared dialog and modal CSS in `apps/web/src/styles/global.css`.
- Mobile notification dropdown sizing in `apps/web/src/styles/utilities.css`.
- Tool-launch and commit popup markup that does not currently provide a scrollable content region.
## Acceptance Criteria
- [ ] Dialogs and modal popups are bounded by the current visible viewport, including mobile dynamic viewport changes.
- [ ] Headers and footer/action bars remain visible while long form content scrolls independently.
- [ ] Tool-launch and commit popups use the shared scrollable content pattern.
- [ ] Mobile sheets, action sheets, and notification dropdowns remain scrollable without propagating scroll gestures to the page.
- [ ] Relevant frontend tests, typecheck, lint, and production build pass.
## Non-Goals
- Redesigning dialog visuals or interaction flows.
- Changing page-level scrolling outside overlays.
@@ -0,0 +1,8 @@
# Constrain and Scroll Edit Dialogs and Popups — Tasks
- [x] Audit all dialog, modal, sheet, action-sheet, and popup implementations.
- [x] Strengthen shared dialog/modal viewport and body scrolling rules.
- [x] Update bespoke tool-launch and commit popups to use scrollable content regions.
- [x] Add focused coverage for the shared scrollable dialog-body markup.
- [x] Run frontend typecheck, lint, tests (88 passed), and production build.
- [x] Update project maps for changed source files.