Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 415aecc0dd | |||
| 5e06a2a226 | |||
| 49180e4c6d | |||
| 7a5538b53f | |||
| 41d24beade | |||
| d1777b88ad | |||
| 691bbaa87b |
+4
-13
@@ -16,7 +16,7 @@ dir: .
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
-
|
-
|
||||||
## children
|
## children
|
||||||
@@ -32,15 +32,6 @@ Infrastructure and configuration package for "Headquarter," a self-hosted projec
|
|||||||
- .pi
|
- .pi
|
||||||
index: .pi/.pi-map.index.md
|
index: .pi/.pi-map.index.md
|
||||||
map: .pi/.pi-map.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
|
- .sisyphus
|
||||||
index: .sisyphus/.pi-map.index.md
|
index: .sisyphus/.pi-map.index.md
|
||||||
map: .sisyphus/.pi-map.md
|
map: .sisyphus/.pi-map.md
|
||||||
@@ -62,12 +53,12 @@ Infrastructure and configuration package for "Headquarter," a self-hosted projec
|
|||||||
- scripts
|
- scripts
|
||||||
index: scripts/.pi-map.index.md
|
index: scripts/.pi-map.index.md
|
||||||
map: scripts/.pi-map.md
|
map: scripts/.pi-map.md
|
||||||
- tests
|
|
||||||
index: tests/.pi-map.index.md
|
|
||||||
map: tests/.pi-map.md
|
|
||||||
- tool-images
|
- tool-images
|
||||||
index: tool-images/.pi-map.index.md
|
index: tool-images/.pi-map.index.md
|
||||||
map: tool-images/.pi-map.md
|
map: tool-images/.pi-map.md
|
||||||
|
- uploads
|
||||||
|
index: uploads/.pi-map.index.md
|
||||||
|
map: uploads/.pi-map.md
|
||||||
## files
|
## files
|
||||||
- .env.example
|
- .env.example
|
||||||
- .gitignore
|
- .gitignore
|
||||||
|
|||||||
+16
-16
@@ -18,25 +18,25 @@ index: ./.pi-map.index.md
|
|||||||
Trust boundary: index routes, map orients, source decides.
|
Trust boundary: index routes, map orients, source decides.
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- .env.example | Provides example environment variable configuration for a full-stack application with database, caching, authentication, and deployment settings
|
||||||
- .gitignore | Specifies intentionally untracked files and directories to exclude from version control across multiple technologies and environments.
|
- .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 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
|
- 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 notable changes and version history for a project management application with Git repository features.
|
- CHANGELOG.md | Documents version history and notable changes for a project management and Git repository application
|
||||||
- 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
|
- 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 | Provides documentation and onboarding instructions for "Headquarter," a self-hosted platform for managing projects, git repositories, and development tools.
|
- 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 | 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.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-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
|
- 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 | Tracks progress of a backend-frontend refactoring project across completed phases, pending tasks, and remaining work.
|
- progress.md | Documents the progress and remaining tasks for a backend-frontend refactoring project involving modularization, code reorganization, and verification.
|
||||||
- swap-pane | Empty package initialization file that marks a directory as a Python package.
|
- 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 | 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-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 | 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-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 | Implementation report documenting a UI component cleanup pass that refactored inline styles into CSS utility classes and design tokens.
|
- 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
|
## 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
|
## tags
|
||||||
docker, ui, application, web, css, rework, redis, frontend
|
docker, web, git, application, ui, rework, python, redis
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps
|
dir: apps
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains the main application entry points and executable modules for the project.
|
Package apps
|
||||||
## parent
|
## parent
|
||||||
index: ./.pi-map.index.md
|
index: ./.pi-map.index.md
|
||||||
map: ./.pi-map.md
|
map: ./.pi-map.md
|
||||||
|
|||||||
+2
-2
@@ -4,10 +4,10 @@ dir: apps
|
|||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## role
|
||||||
Contains the main application entry points and executable modules for the project.
|
Package apps
|
||||||
## files
|
## files
|
||||||
## arch
|
## arch
|
||||||
Modular application structure with separate deployable units, likely following microservices or multi-app monorepo pattern.
|
Contains 0 files.
|
||||||
## tags
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ from typing import Any
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_session_counters_by_instance: dict[str, int] = {}
|
||||||
|
|
||||||
|
|
||||||
class TerminalSession:
|
class TerminalSession:
|
||||||
"""Manages a single terminal session with event-driven PTY I/O.
|
"""Manages a single terminal session with event-driven PTY I/O.
|
||||||
@@ -46,9 +48,6 @@ class TerminalSession:
|
|||||||
# Max WebSocket frame size
|
# Max WebSocket frame size
|
||||||
MAX_FRAME_SIZE = 64 * 1024
|
MAX_FRAME_SIZE = 64 * 1024
|
||||||
|
|
||||||
# Session number counter per instance_id for auto-naming
|
|
||||||
_instance_counters: dict[str, int] = {}
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session_id: str,
|
session_id: str,
|
||||||
@@ -99,11 +98,11 @@ class TerminalSession:
|
|||||||
# Ack timeout fallback
|
# Ack timeout fallback
|
||||||
self._ack_timeout_handle: asyncio.TimerHandle | None = None
|
self._ack_timeout_handle: asyncio.TimerHandle | None = None
|
||||||
|
|
||||||
@classmethod
|
@staticmethod
|
||||||
def _generate_name(cls, instance_id: str) -> str:
|
def _generate_name(instance_id: str) -> str:
|
||||||
"""Generate an auto-incremented session name for the instance."""
|
"""Generate an auto-incremented session name for the instance."""
|
||||||
count = cls._instance_counters.get(instance_id, 0) + 1
|
count = _session_counters_by_instance.get(instance_id, 0) + 1
|
||||||
cls._instance_counters[instance_id] = count
|
_session_counters_by_instance[instance_id] = count
|
||||||
return f"Session {count}"
|
return f"Session {count}"
|
||||||
|
|
||||||
async def start(self, startup_command: str | None = None) -> None:
|
async def start(self, startup_command: str | None = None) -> None:
|
||||||
@@ -121,8 +120,12 @@ class TerminalSession:
|
|||||||
if self.container_user:
|
if self.container_user:
|
||||||
exec_cmd.extend(["--user", 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()
|
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
|
# Put the host PTY into raw mode so it behaves as a pass-through
|
||||||
# pipe. openpty() leaves the slave in canonical mode by default,
|
# pipe. openpty() leaves the slave in canonical mode by default,
|
||||||
@@ -204,6 +207,9 @@ class TerminalSession:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
data = os.read(self._master_fd, 4096)
|
data = os.read(self._master_fd, 4096)
|
||||||
|
except BlockingIOError:
|
||||||
|
# The readiness notification raced with another callback.
|
||||||
|
return
|
||||||
except (OSError, IOError) as exc:
|
except (OSError, IOError) as exc:
|
||||||
logger.debug("PTY read error for session %s: %s", self.session_id, exc)
|
logger.debug("PTY read error for session %s: %s", self.session_id, exc)
|
||||||
self._handle_eof()
|
self._handle_eof()
|
||||||
@@ -343,12 +349,42 @@ class TerminalSession:
|
|||||||
pass
|
pass
|
||||||
logger.info("Session %s EOF handled, websockets closed", self.session_id)
|
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:
|
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:
|
if self._master_fd is None or self._closed:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
fd = self._master_fd
|
||||||
|
remaining = memoryview(data)
|
||||||
try:
|
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()
|
self.last_activity = time.time()
|
||||||
except (OSError, IOError) as exc:
|
except (OSError, IOError) as exc:
|
||||||
logger.debug("PTY write error for session %s: %s", self.session_id, exc)
|
logger.debug("PTY write error for session %s: %s", self.session_id, exc)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Unit tests for TerminalSession docker exec invocation."""
|
"""Unit tests for TerminalSession PTY handling."""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest # pyright: ignore[reportMissingImports]
|
||||||
|
|
||||||
from src.services.terminal.terminal_session import TerminalSession
|
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",
|
container_user="dev",
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with (
|
||||||
"src.services.terminal.terminal_session.pty.openpty",
|
patch(
|
||||||
return_value=(1, 2),
|
"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"):
|
await session.start()
|
||||||
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()
|
|
||||||
|
|
||||||
args, _kwargs = mock_exec.call_args
|
args, _kwargs = mock_exec.call_args
|
||||||
assert "docker" in 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")
|
user_index = args.index("--user")
|
||||||
assert args[user_index + 1] == "dev"
|
assert args[user_index + 1] == "dev"
|
||||||
assert "container-123" in args
|
assert "container-123" in args
|
||||||
|
set_blocking.assert_called_once_with(1, False)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
@@ -50,17 +54,44 @@ async def test_start_omits_user_when_not_configured() -> None:
|
|||||||
container_id="container-123",
|
container_id="container-123",
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with (
|
||||||
"src.services.terminal.terminal_session.pty.openpty",
|
patch(
|
||||||
return_value=(1, 2),
|
"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"):
|
await session.start()
|
||||||
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()
|
|
||||||
|
|
||||||
args, _kwargs = mock_exec.call_args
|
args, _kwargs = mock_exec.call_args
|
||||||
assert "--user" not in 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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web
|
dir: apps/web
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/.pi-map.index.md
|
index: apps/.pi-map.index.md
|
||||||
map: apps/.pi-map.md
|
map: apps/.pi-map.md
|
||||||
|
|||||||
+11
-11
@@ -4,21 +4,21 @@ dir: apps/web
|
|||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- .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
|
- 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 | 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
|
- 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 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
|
- 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 | 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-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 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
|
- 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 settings for a modern React/Vite web application with strict type checking and ES module output. | dep: TypeScript, Vite, React
|
- 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 plugin, dev server port, and Vitest test settings | dep: vite, @vitejs/plugin-react
|
- vite.config.ts | Configures Vite build tool with React support, dev server port, and Vitest testing settings | dep: vite, @vitejs/plugin-react
|
||||||
## arch
|
## 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
|
## tags
|
||||||
react, vite, application, eslint, typescript, web, nginx, xterm
|
react, vite, eslint, application, typescript, nginx, configures, web
|
||||||
## symbols
|
## symbols
|
||||||
-
|
-
|
||||||
## workflows
|
## workflows
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src
|
dir: apps/web/src
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/.pi-map.index.md
|
index: apps/web/.pi-map.index.md
|
||||||
map: apps/web/.pi-map.md
|
map: apps/web/.pi-map.md
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ dir: apps/web/src
|
|||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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 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
|
- 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 core TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
- types.ts | Defines TypeScript type declarations for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
|
||||||
## arch
|
## 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
|
## tags
|
||||||
pages, styles, css, router, react, session, project, summary
|
pages, styles, css, router, react, session, dom, project
|
||||||
## symbols
|
## symbols
|
||||||
- AppRouter
|
- AppRouter
|
||||||
- SessionUser
|
- SessionUser
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components
|
dir: apps/web/src/components
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/.pi-map.index.md
|
index: apps/web/src/.pi-map.index.md
|
||||||
map: apps/web/src/.pi-map.md
|
map: apps/web/src/.pi-map.md
|
||||||
|
|||||||
@@ -4,22 +4,22 @@ dir: apps/web/src/components
|
|||||||
index: apps/web/src/components/.pi-map.index.md
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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-highlighted code editor with line numbers using a lightweight editor library. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
|
- 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 | Defines reusable React components for displaying loading, error, and empty data states in a UI. | exp: LoadingState, ErrorState, EmptyState | dep: ./icon, icon
|
- 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 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
|
- 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 spinner overlay with optional text label that conditionally displays based on visibility prop | exp: func:LoadingOverlay({ visible, label }: LoadingOverlayProps) | dep: ./icon, icon
|
- 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 | dep: @testing-library/react, react-router-dom, vitest, ./protected-route, ../state/auth
|
- 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 | A React component that conditionally renders children or redirects to login based on authentication state | exp: ProtectedRoute | dep: react-router-dom, ../state/auth
|
- 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 blocks with optional line numbers, language badge, and copy-to-clipboard functionality. | exp: SyntaxHighlighter | dep: react, ./icon, ../utils/language
|
- 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 toast notification rules that map instance events to UI categories and severity levels. | dep: vitest, ./toast-rules, ../types/events
|
- 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 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
|
- 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
|
## 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
|
## tags
|
||||||
toast, state, react, icon, loading, event, code, editor
|
toast, state, icon, react, loading, code, event, editor
|
||||||
## symbols
|
## symbols
|
||||||
- LoadingOverlay
|
- LoadingOverlay
|
||||||
- mapEventToCategory
|
- mapEventToCategory
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features
|
dir: apps/web/src/components/features
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/components/.pi-map.index.md
|
index: apps/web/src/components/.pi-map.index.md
|
||||||
map: apps/web/src/components/.pi-map.md
|
map: apps/web/src/components/.pi-map.md
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
|
|||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## files
|
||||||
## arch
|
## arch
|
||||||
Feature-based component colocation pattern with domain-driven folder structure, separating shared/common components from page-specific feature implementations.
|
Contains 0 files.
|
||||||
## tags
|
## tags
|
||||||
-
|
-
|
||||||
## symbols
|
## symbols
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
dir: apps/web/src/components/features/terminal
|
dir: apps/web/src/components/features/terminal
|
||||||
|
|
||||||
## role
|
## 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
|
## parent
|
||||||
index: apps/web/src/components/features/.pi-map.index.md
|
index: apps/web/src/components/features/.pi-map.index.md
|
||||||
map: apps/web/src/components/features/.pi-map.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
|
index: apps/web/src/components/features/terminal/.pi-map.index.md
|
||||||
|
|
||||||
## role
|
## 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
|
## 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
|
- 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-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
|
- 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 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-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 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
|
- 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 | 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.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 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-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 | 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
|
- 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
|
## 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
|
## tags
|
||||||
terminal, special, keys, session, tabs, react, view, strip
|
terminal, special, session, keys, react, tabs, view, strip
|
||||||
## symbols
|
## symbols
|
||||||
- DesktopTerminalView
|
- DesktopTerminalView
|
||||||
- MobileTerminalView
|
- MobileTerminalView
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -578,19 +578,23 @@ export const InstanceList = ({
|
|||||||
{showCreate && (
|
{showCreate && (
|
||||||
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
||||||
<div className="dialog">
|
<div className="dialog">
|
||||||
<h2>Launch Tool</h2>
|
<div className="dialog-header">
|
||||||
<CreateSessionForm
|
<h2>Launch Tool</h2>
|
||||||
projects={[]}
|
</div>
|
||||||
repositories={[]}
|
<div className="dialog-body">
|
||||||
toolTypes={toolTypes}
|
<CreateSessionForm
|
||||||
fixedProjectId={projectId}
|
projects={[]}
|
||||||
fixedRepoId={repoId}
|
repositories={[]}
|
||||||
projectName={projectName}
|
toolTypes={toolTypes}
|
||||||
repoName={repoName}
|
fixedProjectId={projectId}
|
||||||
onSuccess={handleCreateSuccess}
|
fixedRepoId={repoId}
|
||||||
onCancel={() => setShowCreate(false)}
|
projectName={projectName}
|
||||||
submitLabel="Launch"
|
repoName={repoName}
|
||||||
/>
|
onSuccess={handleCreateSuccess}
|
||||||
|
onCancel={() => setShowCreate(false)}
|
||||||
|
submitLabel="Launch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export function StartToolFAB() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-body">
|
||||||
{workspacesLoading ? (
|
{workspacesLoading ? (
|
||||||
<p className="muted">Loading workspaces...</p>
|
<p className="muted">Loading workspaces...</p>
|
||||||
) : workspaces.length === 0 ? (
|
) : workspaces.length === 0 ? (
|
||||||
@@ -110,6 +111,7 @@ export function StartToolFAB() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</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 && (
|
{showModal && (
|
||||||
<div className="dialog-overlay" onClick={() => setShowModal(false)}>
|
<div className="dialog-overlay" onClick={() => setShowModal(false)}>
|
||||||
<div className="dialog" onClick={(e) => e.stopPropagation()}>
|
<div className="dialog" onClick={(e) => e.stopPropagation()}>
|
||||||
<h3>Start Tool</h3>
|
<div className="dialog-header">
|
||||||
<ToolStarter
|
<h3>Start Tool</h3>
|
||||||
workspace={workspace}
|
</div>
|
||||||
onStarted={() => {
|
<div className="dialog-body">
|
||||||
setShowModal(false);
|
<ToolStarter
|
||||||
void refresh();
|
workspace={workspace}
|
||||||
}}
|
onStarted={() => {
|
||||||
onCancel={() => setShowModal(false)}
|
setShowModal(false);
|
||||||
/>
|
void refresh();
|
||||||
|
}}
|
||||||
|
onCancel={() => setShowModal(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -169,6 +169,10 @@ export const useTerminalPage = () => {
|
|||||||
const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey;
|
const isAltShift = e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey;
|
||||||
if (!isAltShift) return;
|
if (!isAltShift) return;
|
||||||
|
|
||||||
|
const activeSessionIndex = activeSessionId
|
||||||
|
? sessions.findIndex((session) => session.id === activeSessionId)
|
||||||
|
: -1;
|
||||||
|
|
||||||
switch (e.key.toLowerCase()) {
|
switch (e.key.toLowerCase()) {
|
||||||
case "n":
|
case "n":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -187,17 +191,14 @@ export const useTerminalPage = () => {
|
|||||||
break;
|
break;
|
||||||
case "arrowleft":
|
case "arrowleft":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (activeSessionId) {
|
if (activeSessionIndex > 0) {
|
||||||
const idx = sessions.findIndex((s) => s.id === activeSessionId);
|
setActiveSessionId(sessions[activeSessionIndex - 1].id);
|
||||||
if (idx > 0) setActiveSessionId(sessions[idx - 1].id);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "arrowright":
|
case "arrowright":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (activeSessionId) {
|
if (activeSessionIndex >= 0 && activeSessionIndex < sessions.length - 1) {
|
||||||
const idx = sessions.findIndex((s) => s.id === activeSessionId);
|
setActiveSessionId(sessions[activeSessionIndex + 1].id);
|
||||||
if (idx < sessions.length - 1)
|
|
||||||
setActiveSessionId(sessions[idx + 1].id);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "r":
|
case "r":
|
||||||
@@ -208,6 +209,8 @@ export const useTerminalPage = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsFullscreen((prev) => !prev);
|
setIsFullscreen((prev) => !prev);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { ProjectsPage } from "./ProjectsPage";
|
import { ProjectsPage } from "./ProjectsPage";
|
||||||
import * as projectsApi from "../api/projects";
|
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",
|
id: "proj-1",
|
||||||
name: "Alpha Project",
|
name: "Alpha Project",
|
||||||
description: "First project",
|
description: "First project",
|
||||||
owner_id: "user-1",
|
owner_id: "user-1",
|
||||||
default_ssh_key_id: null,
|
default_ssh_key_id: null,
|
||||||
|
repositories: [],
|
||||||
|
created_at: "2026-07-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "proj-2",
|
id: "proj-2",
|
||||||
@@ -19,6 +28,8 @@ const mockProjects = [
|
|||||||
description: null,
|
description: null,
|
||||||
owner_id: "user-1",
|
owner_id: "user-1",
|
||||||
default_ssh_key_id: null,
|
default_ssh_key_id: null,
|
||||||
|
repositories: [],
|
||||||
|
created_at: "2026-07-01T00:00:00Z",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -31,9 +42,7 @@ describe("ProjectsPage", () => {
|
|||||||
it("renders loading state initially", () => {
|
it("renders loading state initially", () => {
|
||||||
vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {}));
|
vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {}));
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
|
expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -41,9 +50,7 @@ describe("ProjectsPage", () => {
|
|||||||
it("renders project list after loading", async () => {
|
it("renders project list after loading", async () => {
|
||||||
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
|
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -56,9 +63,7 @@ describe("ProjectsPage", () => {
|
|||||||
it("renders empty state when no projects", async () => {
|
it("renders empty state when no projects", async () => {
|
||||||
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
|
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -69,9 +74,7 @@ describe("ProjectsPage", () => {
|
|||||||
it("renders error state with retry button", async () => {
|
it("renders error state with retry button", async () => {
|
||||||
vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
|
vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail"));
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -85,9 +88,7 @@ describe("ProjectsPage", () => {
|
|||||||
const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]);
|
const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -118,9 +119,7 @@ describe("ProjectsPage", () => {
|
|||||||
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
|
vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -138,16 +137,14 @@ describe("ProjectsPage", () => {
|
|||||||
const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);
|
const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
|
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");
|
if (!alphaCard) throw new Error("Card not found");
|
||||||
|
|
||||||
fireEvent.click(within(alphaCard).getByRole("button", { name: /edit/i }));
|
fireEvent.click(within(alphaCard).getByRole("button", { name: /edit/i }));
|
||||||
@@ -171,16 +168,14 @@ describe("ProjectsPage", () => {
|
|||||||
const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined);
|
const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter>
|
<MemoryRouter><SessionsProvider><ProjectsPage /></SessionsProvider></MemoryRouter>
|
||||||
<ProjectsPage />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
|
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");
|
if (!alphaCard) throw new Error("Card not found");
|
||||||
|
|
||||||
fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
|
fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
|
||||||
|
|||||||
@@ -453,12 +453,16 @@
|
|||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
.dialog,
|
||||||
|
.modal-content,
|
||||||
|
.commit-dialog {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 32rem;
|
max-width: 32rem;
|
||||||
max-height: calc(100vh - var(--space-8));
|
max-height: calc(100vh - var(--space-8));
|
||||||
|
max-height: calc(100dvh - var(--space-8));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -468,16 +472,6 @@
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
/* deprecated alias */
|
/* 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 {
|
.dialog-lg {
|
||||||
@@ -500,9 +494,13 @@
|
|||||||
line-height: var(--line-height-tight);
|
line-height: var(--line-height-tight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-body {
|
.dialog-body,
|
||||||
|
.modal-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,9 +541,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialog,
|
.dialog,
|
||||||
.modal-content {
|
.modal-content,
|
||||||
|
.commit-dialog {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: calc(100vh - var(--space-6));
|
max-height: calc(100vh - var(--space-6));
|
||||||
|
max-height: calc(100dvh - var(--space-6));
|
||||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.font-mono {
|
.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 {
|
.font-medium {
|
||||||
@@ -2326,8 +2328,10 @@ a.nav-item,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
|
max-height: 70dvh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
animation: slide-up 0.2s ease-out;
|
animation: slide-up 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2363,8 +2367,12 @@ a.nav-item,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-bottom-sheet-content {
|
.mobile-bottom-sheet-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-bottom-sheet-item {
|
.mobile-bottom-sheet-item {
|
||||||
@@ -3238,7 +3246,10 @@ a:active,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
|
max-height: 80dvh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
animation: slideUp 0.3s ease;
|
animation: slideUp 0.3s ease;
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
}
|
}
|
||||||
@@ -3600,6 +3611,7 @@ a:active,
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
|
max-height: 70dvh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3817,7 +3829,9 @@ a:active,
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--radius-md);
|
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 {
|
.delete-btn:hover {
|
||||||
@@ -3844,7 +3858,9 @@ a:active,
|
|||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-decoration: none;
|
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 {
|
.workspace-header-action-btn:hover {
|
||||||
@@ -3919,7 +3935,9 @@ a:active,
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.12s ease, color 0.12s ease;
|
transition:
|
||||||
|
background 0.12s ease,
|
||||||
|
color 0.12s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-button:hover {
|
.copy-button:hover {
|
||||||
@@ -4165,7 +4183,10 @@ a:active,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.drag-item-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);
|
border-color: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4181,7 +4202,9 @@ a:active,
|
|||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
overflow: auto;
|
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);
|
font-size: var(--font-size-sm);
|
||||||
line-height: var(--line-height-normal);
|
line-height: var(--line-height-normal);
|
||||||
}
|
}
|
||||||
@@ -4329,7 +4352,9 @@ a:active,
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: border-color 0.15s, color 0.15s;
|
transition:
|
||||||
|
border-color 0.15s,
|
||||||
|
color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-create-button:hover {
|
.sidebar-create-button:hover {
|
||||||
@@ -4354,7 +4379,9 @@ a:active,
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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 {
|
.mobile-fab:hover {
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user