diff --git a/.agents/rules/commits.md b/.agents/rules/commits.md index 4f8a2cb8..ecfd74b1 100644 --- a/.agents/rules/commits.md +++ b/.agents/rules/commits.md @@ -1,36 +1,5 @@ -# Commit and History Rules +# Commit and history rules -## Commit Units - -- 하나의 commit은 하나의 목적만 담고, 독립적으로 리뷰·revert 가능해야 한다. -- 큰 작업도 작은 commit으로 나눈다. 단, 의미 있는 작업 단위가 깨질 정도로 쪼개지 않는다. -- 각 commit 시점에 빌드와 테스트가 통과해야 한다. 일반 게이트는 루트 `AGENTS.md`가 가리키는 `docs/getting-started.md`에 있고, 훅이 이 원칙을 대신 지키지는 않는다. - 훅은 이것을 강제하지 않는다 — `pre-commit`은 형식만 보고, `pre-push`는 통합 기준점 대비 - 변경 범위의 tip만 검사한다(문서만 바꾼 push는 Rust 게이트를 건너뛴다). 따라서 이 항목은 - 도구가 아니라 작성자가 지키는 규칙이며, 깨지면 `git bisect`가 못 쓰게 된다. 범위 전체를 검증하려면 - `NIGHTCROW_VERIFY_EACH_COMMIT=1 git push`. - -## Feature-scoped Workflow - -- 구현을 먼저 커밋하고 테스트는 별도 commit으로 분리할 수 있다. 단, 인터페이스를 바꿀 때는 - 인터페이스 정의와 그 contract test를 같은 commit에 넣는다 (`testing.md`의 선행 규칙). -- 기능 단위 간 의존성이 있으면 의존되는 쪽을 먼저 커밋한다. -- 코드와 직접 연결된 문서 변경은 같은 commit 또는 바로 이어지는 commit에 넣는다. - -## Message Format - -- `type: message` 또는 `type(scope): message`. type은 `feat`, `fix`, `refactor`, `test`, `docs`, `chore`. -- 무엇을 바꿨는지 짧고 구체적으로 쓴다. -- 작업 과정이나 도구 이름을 메시지에 쓰지 않는다 - (✗ "codex review 반영", ✗ "리뷰 수정", ✓ "리뷰 중단 기준에 순환 판단 조건 추가"). - -## History - -- 작업이 끝나기 전에도 의미 있는 milestone마다 commit을 남긴다. -- commit history만 읽어도 구현 순서와 의도를 따라갈 수 있어야 한다. -- 나중에 squash할 생각의 임시 잡탕 commit보다 읽히는 history를 우선한다. - -## Branch - -- 브랜치 네이밍: `type/short-description` (예: `feat/user-auth`, `fix/token-expiry`). type set은 위와 같다. -- 기본 브랜치(`dev`)에 직접 커밋은 문서·설정 등 단순 변경에 한한다. +- Each commit must pass the applicable build and tests independently. The [push hook](../../.githooks/pre-push) normally verifies only the tip, not every intermediate commit. +- Commit interface definitions and their contract tests together. Include related documentation in the same commit or the immediately following commit. +- Direct commits to `dev` are limited to simple documentation or configuration changes. diff --git a/.agents/rules/dependencies.md b/.agents/rules/dependencies.md deleted file mode 100644 index 5793ef7a..00000000 --- a/.agents/rules/dependencies.md +++ /dev/null @@ -1,15 +0,0 @@ -# Dependency Rules - -## Selecting - -- 새 의존성 전에 stdlib 또는 이미 있는 의존성으로 되는지 먼저 확인한다. -- 후보 비교는 **웹 검색으로 현재 정보를 확인한다**. 지식 컷오프 기준으로 판단하지 않는다. - 공식/권장 여부, 채택도, 최근 릴리스·이슈 대응, 라이선스, transitive dependency 규모를 본다. -- 공식 또는 널리 채택된 쪽을 우선한다. -- 선정 근거와 비교한 대안을 `docs/architecture.md`에 기록한다. - -## Updating / Removing - -- 업데이트 시 breaking change를 확인한다. major는 changelog과 migration guide를 읽는다. -- 업데이트 후 빌드와 테스트 통과를 확인한다. -- 쓰지 않는 의존성은 제거한다. 제거 전 실제 사용처가 없는지 검색으로 확인한다. diff --git a/.agents/rules/docs.md b/.agents/rules/docs.md deleted file mode 100644 index 0f02d18f..00000000 --- a/.agents/rules/docs.md +++ /dev/null @@ -1,15 +0,0 @@ -# Documentation Rules - -## Content - -- 공개 인터페이스의 계약: 입력, 출력, 에러, 부작용. -- 비자명한 제약: 순서 의존성, 호출 전제 조건, 스레드/동시성 안전성. -- 코드가 표현하지 못하는 맥락과 "왜". -- 내부용 함수는 이름과 시그니처가 명확하면 문서화하지 않는다. 타입이 말하는 것을 주석으로 반복하지 않는다. -- 필요한 문서만 유지한다. 유지보수할 수 없는 문서나 형식적인 빈 섹션(Contributing, License 등)은 만들지 않는다. - -## Quality - -- 문서는 현재 코드와 일치해야 한다. 코드 변경으로 내용이 달라지면 같은 작업 안에서 갱신한다. -- 예시 코드는 실제로 실행 가능한 상태를 유지한다. -- 추측이나 미래 계획을 사실처럼 쓰지 않는다. diff --git a/.agents/rules/guardrails.md b/.agents/rules/guardrails.md index c78cd3e6..7a160d02 100644 --- a/.agents/rules/guardrails.md +++ b/.agents/rules/guardrails.md @@ -1,38 +1,7 @@ # Guardrails -## File Size - -- 모든 소스·테스트 파일(Rust, TypeScript, TSX, JavaScript)은 300줄 이하다. 테스트 파일도 예외 없다. -- 200줄 이상은 code smell이며 분할을 검토한다. -- 분할은 동작을 바꾸지 않는 순수 리팩토링이어야 한다. 모듈, 순수 함수, 컴포넌트/훅으로 쪼갠다. -- 생성물(`target/`, `viewer-ui/dist/`)과 벤더링한 서드파티는 제외한다. - -## Platforms - -- macOS, Linux, Windows 세 곳 모두에서 도는 것을 목표로 한다. 한 곳에서만 도는 코드는 기능이 아니라 미완성이다. CI도 세 OS를 모두 돈다 (`.github/workflows/ci.yml`). -- 플랫폼 분기는 호출부에 흩지 않고 seam 한 곳에 모은다 — 경로·시그널·스레드·로깅은 `src/platform/`, 소켓 타입은 `src/daemon/transport.rs`. 새 분기가 필요하면 seam을 늘리기 전에 기존 것에 들어갈 수 있는지 먼저 본다. -- 한쪽에만 있는 API(`PermissionsExt`, `setsid`, ConPTY 동작 차이)는 대응물을 찾거나 seam 뒤에 감춘다. 대응물이 없어 동작이 달라지면 무엇을 포기했는지 문서에 남긴다. -- 테스트를 `#[cfg(unix)]`로 막는 것은 최후 수단이다. 막는 순간 그 동작은 나머지 플랫폼에서 검증되지 않으므로, 왜 막았는지 주석으로 남긴다. -- Windows에서 작업 중이면 Unix 게이트는 `docker compose run --rm unix-gate`로 돌린다 (`docs/getting-started.md`). - -## Architecture - -- `docs/architecture.md`가 설계 결정의 기준이다. 구현이 문서와 어긋나면 문서를 먼저 고치거나 구현을 조정한다. - -## Code Quality - -- 가장 단순한 해결책을 먼저 시도한다. 추상화는 반복이 실제로 발생한 후에 도입한다. -- 하나의 함수/모듈은 하나의 책임만 갖는다. -- 매직 넘버와 하드코딩 문자열은 이름 있는 상수로 뽑는다. -- 코드 내부 주석은 영어로만 작성하고 핵심적인 "why"만 설명한다. 코드·타입·이름만 보고 알 수 있는 동작은 주석으로 반복하지 않는다. - -## Error Handling - -- 에러는 처리하거나 명시적으로 전파한다. 조용히 삼키지 않는다. -- 에러 메시지에 무엇이 실패했고 입력이 무엇이었는지를 담는다. -- 복구 가능/불가능을 구분하고, 외부 호출 실패는 재시도 여부를 명시적으로 결정한다. - -## Externals - -- 외부 라이브러리·SDK의 동작이 불확실하면 추측하지 말고 공식 문서와 소스를 직접 확인한다. -- 렌더 루프 등 hot path에서는 할당·복사·변환을 최소화한다. 그 밖의 최적화는 측정 후에 한다. +- Keep Rust, TypeScript, TSX, and JavaScript source and test files at or below 300 lines; generated files and vendored third-party code are exempt. +- Support macOS, Linux, and Windows. Keep platform differences behind the [platform seams](../../docs/architecture.md#cross-cutting-invariants), and document limitations without an equivalent on another platform. +- Record why platform-specific tests exclude other platforms. For Unix verification from Windows, follow [Building and testing](../../docs/getting-started.md#building-and-testing). +- Write code comments in English. +- Handle or propagate errors. Never report external-call failures or malformed/truncated input as success; mark truncated results explicitly, as required by the [resource and error invariants](../../docs/architecture.md#cross-cutting-invariants). diff --git a/.agents/rules/releases.md b/.agents/rules/releases.md index fd7fc1b7..c1010802 100644 --- a/.agents/rules/releases.md +++ b/.agents/rules/releases.md @@ -1,10 +1,7 @@ # Release policy -- `origin` is the fork and PR head; `upstream/dev` is the development line; `upstream/main` is the only release line. -- The only supported public series is `0.1.x`. Every application package version must agree, and each official tag must advance the patch by exactly one. -- The only no-tag bootstrap is `0.1.1`. Major and minor changes require an explicit maintainer decision and a policy change reviewed by `@code0xff`. -- Run `node scripts/prepare-release.mjs` for a dry-run. Use `--execute` only on a clean release branch, then review and commit all five version-file changes together. The version itself lives once, in the root manifest's `[workspace.package]`, and every crate inherits it. -- A release preparation PR targets `upstream/dev`. A separate promotion PR targets `upstream/main`; a push to the official `code0xff/nightcrow` `main` creates the tag and Release only after all platform builds and tests pass. -- The release workflow must never publish from a fork, a non-`main` branch, a reused tag pointing at another SHA, or an incomplete asset set. A draft may be resumed only by adding missing files after digest/size checks; published assets are immutable. - -The operational runbook is [docs/releasing.md](../../docs/releasing.md). The machine-readable policy is [.github/release-policy.json](../../.github/release-policy.json). +- Support only `0.1.x`, with matching application package versions. The no-tag bootstrap is `0.1.1`; later official tags advance exactly one patch. Major/minor changes require an explicit maintainer decision and a policy change reviewed by `@code0xff`. +- Change versions with [`prepare-release.mjs`](../../scripts/prepare-release.mjs) and commit the related files together. Follow [releasing.md](../../docs/releasing.md) and the machine-readable [release-policy.json](../../.github/release-policy.json). +- Release policy and automation changes require `@code0xff` review under [CODEOWNERS](../../.github/CODEOWNERS). +- Publish only from official `main` after all platform builds and tests pass. Reject other repositories/branches, an existing tag at another SHA, and incomplete asset sets. +- Resume drafts only by adding missing assets after checking existing digests and sizes. Published assets are immutable. diff --git a/.agents/rules/security.md b/.agents/rules/security.md deleted file mode 100644 index 8e04f29b..00000000 --- a/.agents/rules/security.md +++ /dev/null @@ -1,21 +0,0 @@ -# Security Rules - -## Input Validation - -- 시스템 경계(사용자 입력, 외부 API 응답, 파일 읽기, HTTP 요청)에서 오는 데이터는 항상 검증한다. -- 내부 모듈 간 호출에서는 과도한 방어적 검증을 피한다. 계약을 신뢰한다. - -## Secrets - -- 비밀번호, API 키, 토큰은 환경 변수나 별도 설정 파일로 주입한다. -- `.env`, credentials 파일은 `.gitignore`에 두고 절대 커밋하지 않는다. -- 로그의 민감 필드(비밀번호, 토큰, 개인식별정보)는 마스킹한다. - -## Least Privilege - -- 파일, 네트워크, API 접근 권한은 필요한 최소 범위로 제한한다. -- 로컬 소켓·HTTP 서버는 필요 이상으로 넓은 인터페이스에 바인드하지 않는다. - -## Dependencies - -- 의존성 추가 시 알려진 CVE, 유지보수 상태, 보안 이력을 확인한다. diff --git a/.agents/rules/testing.md b/.agents/rules/testing.md index d8f0fb6f..217e6b79 100644 --- a/.agents/rules/testing.md +++ b/.agents/rules/testing.md @@ -1,23 +1,5 @@ # Testing -## Which Layer - -- 변경 유형에 맞는 테스트를 추가한다: 모듈 간 계약(인터페이스)은 **contract test**, 순수 함수·개별 모듈 로직은 unit test, API endpoint·전체 요청 흐름(web viewer·daemon protocol)은 integration test, 사용자 관점 시나리오는 end-to-end test. 하나의 변경이 여러 유형에 걸치면 각각 작성한다. - -## Rules - -- contract test를 먼저 갱신하지 않고 인터페이스를 바꾸지 않는다. -- 테스트는 구현 세부사항이 아니라 계약과 동작을 검증한다. -- 성공 경로만이 아니라 실패 경로와 경계 조건(null, empty, 범위 초과, 잘못된 타입)도 명시적으로 테스트한다. -- mock은 외부 시스템 경계에만 쓴다. -- 각 테스트는 독립 실행 가능해야 한다. 테스트 간 상태 공유 금지. -- 테스트 이름은 `무엇을_하면_어떤_결과가_나온다` 패턴으로 의도를 드러낸다. -- 단위 테스트는 구현 파일에 크게 inline하지 않고 sibling `*_tests.rs` 또는 인접 `tests/`로 분리한다. crate 공개 API 통합 테스트는 루트 `tests/`에 둔다. -- TS/TSX 테스트는 sibling `*.test.ts(x)` 파일에 둔다. -- 그 밖의 배치·네이밍은 기존 컨벤션을 따른다. 공유 fixture/helper는 공통 위치에 둔다 (`src/test_util.rs`). - -## Flaky Tests - -- 실패하면 원인을 먼저 분류한다: 코드 결함 vs 환경/타이밍. -- flaky를 발견하면 즉시 고치거나, 고치기 전까지 skip하고 이슈로 남긴다. -- flaky를 이유로 전체 테스트 결과를 무시하지 않는다. +- Update contract tests before changing interfaces, including failure paths and boundary conditions. +- Keep Rust unit tests in sibling `*_tests.rs` files or adjacent `tests/` directories, crate public-API integration tests in root `tests/`, and TS/TSX tests in sibling `*.test.ts(x)` files. +- Fix flaky tests or temporarily skip them with a linked issue; do not suppress unrelated failures. diff --git a/.agents/skills/_shared/review-protocol.md b/.agents/skills/_shared/review-protocol.md deleted file mode 100644 index 35fad1c5..00000000 --- a/.agents/skills/_shared/review-protocol.md +++ /dev/null @@ -1,61 +0,0 @@ -# Review Protocol - -`/self-review`, `/security-review`가 공유하는 절차. 각 스킬은 분석 렌즈(무엇을 볼지)만 -자기 파일에 두고, 대상 수집부터 보고까지의 절차는 이 문서를 따른다. - -## 1. 변경 사항 수집 - -- 인자가 전달되면 (`/ `) 해당 브랜치를 기준점으로 사용한다. -- 인자가 없으면: `git fetch`로 remote를 최신화하고(실패 시 경고 후 local ref로 진행) - `git rev-parse --abbrev-ref @{upstream}`로 tracking branch를 찾는다. 없으면 - `git remote show origin | grep 'HEAD branch'`의 결과를 `origin/`로 쓴다. - 둘 다 실패하면 사용자에게 base branch를 요청한다. -- `git log ..HEAD --oneline`으로 unpushed commit을 확인한다. 없으면 - "리뷰 대상 없음"을 보고하고 즉시 종료한다. -- `git diff ..HEAD`로 전체 변경 범위를 파악한다. -- uncommitted 변경이 있으면 리뷰 범위에 포함할지 사용자에게 확인한다. 포함하면 - `git diff`와 `git diff --staged`도 수집한다. 커밋할 때는 리뷰 수정분만 staging하여 - 사용자의 작업이 섞이지 않게 한다. -- 변경된 파일은 전체를 읽는다 — diff만 보면 주변 컨텍스트를 놓친다. 바이너리, - lockfile, 자동 생성물은 제외한다. - -## 2. 평가 기준 - -발견 항목을 세 갈래로 분류한다. 각 갈래의 판단 경계는 스킬별 정의를 따른다. - -- **즉시 반영** — 코드를 직접 수정한다. -- **사용자 판단 필요** — 수정하지 않고 보고만 한다. -- **무시** — 개별 내용은 보고하지 않고, 건수와 대표 사유만 보고에 포함한다. - -## 3. 수정 적용 - -- 수정 후 루트 `AGENTS.md`가 가리키는 Verify 게이트(`docs/getting-started.md#building-and-testing`)를 실행해 다른 것이 깨지지 않았는지 확인한다. -- 테스트가 실패하면 원인을 먼저 분류한다. - - 수정이 원인: 수정을 되돌리고 **사용자 판단 필요**로 재분류한다. - - 기존 flaky 또는 환경 문제: 수정을 유지하고 실패 원인을 보고한다. -- 확인이 끝나면 수정 사항을 commit한다. - -## 4. 중단 기준 - -다음에 해당하면 push를 진행하지 않고 사용자에게 보고한다. 스킬별 추가 기준도 함께 적용한다. - -- 수정 적용 후 빌드 또는 테스트가 실패하고 단순 수정으로 해결되지 않는 경우 -- 동일한 지적이 수렴하지 않고 계속 발생하는 경우 (순환 판단) - -## 5. 보고 형식 - -### 즉시 반영한 항목 - -(각 항목: 파일, 변경 내용, 발견 근거. 없으면 `없음`) - -### 사용자 판단이 필요한 항목 - -(각 항목: 파일, 지적 내용, 판단을 미룬 이유. 없으면 `없음`) - -### 무시한 항목 - -(건수와 대표 사유. 없으면 `없음`) - -### 리뷰 요약 - -(1-2문장 평가) diff --git a/.agents/skills/plan/SKILL.md b/.agents/skills/plan/SKILL.md deleted file mode 100644 index 2861bb74..00000000 --- a/.agents/skills/plan/SKILL.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: plan -description: 구현 전 설계 및 계획 수립 — 요구사항 분석, 설계 대안 비교, 구현 계획을 사용자와 정렬한다 -user-invocable: true ---- - -# Plan - -## 1. 요구사항 분석 - -- 인자가 전달되면 (`/plan `) 그 내용을 작업 설명으로 쓴다. -- `docs/architecture.md`와 `.agents/rules/`를 읽어 제약을 확인하고, 관련 기존 코드를 탐색해 - 현재 구조와 패턴을 파악한다. -- 정리할 항목: **목표** / **제약**(건드리면 안 되는 것) / **영향 범위**(파일·모듈) / **불확실한 점**. - -## 2. 설계 대안 비교 - -3개 이상 파일을 고치거나 설계 판단이 필요할 때만 한다. 단순한 변경이면 건너뛴다. - -접근 방식 2-3개를 도출하고 각각에 대해: 핵심 아이디어(1-2문장), 장점, 단점/리스크, -영향받는 파일·모듈. - -## 3. 구현 계획 - -선택한 방식을 작업 단위로 분해한다. 각 단위마다 변경할 파일·모듈, 검증 방법, 예상 commit -단위(`commits.md`)를 적는다. - -- 의존되는 쪽을 먼저 구현한다. -- 각 단계가 끝난 시점에 AGENTS.md의 Verify 게이트가 통과하는 상태여야 한다. -- 새 라이브러리를 도입하면 `.agents/rules/dependencies.md`의 선정 절차를 따르고 근거를 - `docs/architecture.md`에 남긴다. - -## 4. 사용자 확인 - -계획을 다음 형식으로 보고하고 구현 전 정렬한다. - -### 목표 및 제약 - -(정리된 목표와 제약) - -### 접근 방식 - -(선택한 방식과 이유. 대안이 있었으면 비교 요약) - -### 구현 계획 - -(번호가 매겨진 단계별 목록) - -### 불확실한 점 - -(추가 확인이 필요한 사항. 없으면 `없음`) - -사용자가 계획을 승인하면 구현을 시작한다. 수정 요청이 있으면 계획을 조정한 후 재확인한다. diff --git a/.agents/skills/security-review/SKILL.md b/.agents/skills/security-review/SKILL.md deleted file mode 100644 index 70cd2749..00000000 --- a/.agents/skills/security-review/SKILL.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: security-review -description: 구현 완료 후 보안 관점 심층 리뷰 — 즉시 반영 항목을 수정하고 결과를 보고한다 -user-invocable: true ---- - -# Security Review - -절차(대상 수집, 수정 적용, 중단, 보고)는 `.agents/skills/_shared/review-protocol.md`를 읽고 -그대로 따른다. 이 문서는 무엇을 볼지만 정한다. - -`.agents/rules/security.md`를 함께 읽는다 — 프로젝트가 이미 정한 보안 규칙. - -## 분석 렌즈 (extended thinking) - -먼저 `security.md`의 규칙 준수 여부를 변경된 코드에서 확인하고, 그 위에 다음을 본다. - -### 입력과 주입 - -- 시스템 경계(사용자 입력, 외부 API 응답, 파일 경로, URL 파라미터, 헤더)의 검증 여부. -- Command Injection, XSS, Path Traversal 등 OWASP Top 10 노출 경로. -- 신뢰할 수 없는 입력의 역직렬화. - -### 인증 및 권한 - -- 인증 우회 가능성, 권한 검사가 빠진 엔드포인트·명령. -- 세션/토큰의 만료, 무효화, 저장 방식. 권한 상승 경로. - -### 정보 노출 - -- 키/토큰 하드코딩. 민감 정보가 로그, 에러 메시지, 응답 본문, 커밋 히스토리에 새는지. -- 에러 응답의 내부 세부사항(스택 트레이스, 내부 경로) 노출. -- 에러 처리가 보안 검사를 우회하는 경로를 만드는지. - -### 노출면 - -- 로컬 daemon 소켓과 web viewer의 바인드 주소·권한이 필요한 최소인지. -- CORS, CSP 등 브라우저 보안 정책 설정. -- root/admin 권한을 요구하는 구현, 불필요하게 넓은 권한을 요구하는 의존성. - -## 분류 경계 - -- **즉시 반영**: 보안 취약점, 민감 정보 노출, 인증/권한 우회, 입력 검증 누락. -- **사용자 판단 필요**: 보안-편의성 트레이드오프, 위협 모델에 따라 달라지는 항목, - 대규모 리팩토링이 필요한 보안 개선. -- **무시**: 현재 위협 모델에서 위험도가 낮은 항목, 범위 밖 개선 제안. - -보고 시에는 공통 형식의 각 항목에 **취약점 유형**을 함께 적는다. - -## 추가 중단 기준 - -- 보안 취약점(인증 우회, 권한 상승, 데이터 유출)이 즉시 반영으로 해결되지 않은 경우 -- 민감 정보가 소스 코드나 커밋 히스토리에 포함된 경우 -- 알려진 CVE가 있는 의존성이 추가된 경우 diff --git a/.agents/skills/self-review/SKILL.md b/.agents/skills/self-review/SKILL.md deleted file mode 100644 index ff542db7..00000000 --- a/.agents/skills/self-review/SKILL.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: self-review -description: 구현 완료 후 thinking mode 심층 리뷰 — 즉시 반영 항목을 수정하고 결과를 보고한다 -user-invocable: true ---- - -# Self Review - -절차(대상 수집, 수정 적용, 중단, 보고)는 `.agents/skills/_shared/review-protocol.md`를 읽고 -그대로 따른다. 이 문서는 무엇을 볼지만 정한다. - -현재 작업의 scope 문서가 있으면 함께 읽어 범위를 확인한다. - -## 분석 렌즈 (extended thinking) - -### 정합성 - -- 호출하는 함수, 의존하는 타입, 참조하는 상수가 실제로 존재하고 올바른지. -- 새 인터페이스/타입과 기존 구현체 간 계약이 맞는지. -- import 경로, export 누락, 순환 참조. - -### 로직 - -- 분기의 완전성 (switch/if-else). -- 에러 경로에서의 리소스 정리와 상태 롤백. -- 경계 조건 (null, empty, 0, max). -- 비동기 코드의 await 누락, 에러 전파 누락. - -### 설계 정합성 - -- `docs/architecture.md`의 계층 책임과 일치하는지. -- `.agents/rules/`의 규칙을 위반하지 않는지. -- scope 문서가 있으면 그 범위 내인지. -- 모듈 간 의존 방향이 설계 의도와 맞는지. -- 문서 간 충돌은 Architecture > Rules > Scope 우선순위로 해소한다. - -### 테스트 충분성 - -- 변경된 로직의 주요 경로와 에러 경로에 대응하는 테스트가 있는지. -- 테스트가 구현 세부사항이 아니라 계약/동작을 검증하는지. - -## 분류 경계 - -- **즉시 반영**: 버그, 누락된 에러 처리, 계약 불일치, 명확한 설계 위반. -- **사용자 판단 필요**: 설계 트레이드오프, 의도적일 수 있는 선택, scope 경계 항목. -- **무시**: 이전 리뷰에서 이미 다뤄진 항목, 스타일 선호, 범위 밖 개선 제안. - -## 추가 중단 기준 - -- 심각도 높은 버그(보안, 데이터 손실, 인증/권한 우회)가 즉시 반영으로 해결되지 않은 경우 -- 동일 변경에서 버그가 2개 이상 발견되고 즉시 반영으로 해결되지 않은 경우 -- 설계 원칙 위반이 발견된 경우 diff --git a/.claude/skills b/.claude/skills deleted file mode 120000 index 2b7a412b..00000000 --- a/.claude/skills +++ /dev/null @@ -1 +0,0 @@ -../.agents/skills \ No newline at end of file diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 7d5d8f29..32bf3383 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -6,7 +6,7 @@ # prevent: a red CI run. CI triggers on `pull_request` and on a push to `dev`, # so that failure becomes real when a branch reaches the remote, not when a local # commit is made. A full test run per commit bought nothing pre-push does not, -# and it taxed exactly the milestone commits `.agents/rules/commits.md` asks for. +# and it slowed down local commits. # # Formatting stays here because it is the one gate cheap enough to run # unconditionally and annoying enough to fix late: a `cargo fmt` at push time diff --git a/AGENTS.md b/AGENTS.md index 5fc37761..5ba35050 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,38 +1,28 @@ # nightcrow -체크아웃 루트에 `AGENTS.local.md`가 있으면 이 문서와 함께 읽고 적용한다. +Keep personal tool settings, account/access details, and checkout-specific procedures in untracked `AGENTS.local.md`; read it when present at the checkout root. -Agent-adjacent Rust TUI: 상단은 git diff/commit log 뷰어, 하단은 split-view 멀티 터미널 패널. -설계 기준은 `docs/architecture.md`, 설치·실행과 사용법은 `README.md`와 `docs/`다. +The session daemon owns repositories and terminal panes; the TUI and web viewer attach to the same session. Follow the ownership boundaries and shared invariants in [architecture.md](docs/architecture.md). -## 에이전트 설정 +## Applicable guides -원본은 `.agents/`에 두고 도구별 디렉터리는 symlink만 둔다 (`.claude/rules`, `.claude/skills` → `../.agents/...`). 새 도구를 붙일 때도 복사하지 말고 링크한다. Windows에서 링크를 체크아웃하려면 개발자 모드와 `git config core.symlinks true`가 필요하다. 그렇지 않으면 링크가 경로 문자열을 담은 일반 파일로 풀린다. +The rules in [`.agents/rules/`](.agents/rules/) always apply. Tool-specific rule directories must symlink to this source rather than copy it. -`.agents/rules/`는 항상 적용되는 규칙, `.agents/skills/`는 `/plan`, `/self-review`, `/security-review` 절차다. 스킬 공통 절차는 `.agents/skills/_shared/`에서 관리하며 이 문서에 복제하지 않는다. +Before changing a path, read its full applicable `AGENTS.md` hierarchy and explicitly read any applicable `AGENTS.local.md`. At each scope, `AGENTS.override.md` replaces that scope's `AGENTS.md`. Read every scoped guide when a change crosses scopes: [docs](docs/AGENTS.md), [src](src/AGENTS.md), [viewer-ui](viewer-ui/AGENTS.md), and [plugins](plugins/AGENTS.md). -## Scope guides +## Source organization -Release governance and the fork-to-upstream promotion contract are in [`.agents/rules/releases.md`](.agents/rules/releases.md). +Group growing implementation areas by stable responsibility so flat directories do not become the default. Add a subdirectory when it represents a coherent responsibility or ownership boundary; avoid directories that only wrap a lone file without a durable reason. Keep facades stable when splitting their implementation, including the existing visibility of their items, and keep cross-layer design contracts in [architecture.md](docs/architecture.md) rather than duplicating them in local guides. -변경 범위에 해당하는 scope guide도 함께 읽는다. 공통 규칙을 scope guide에 다시 적지 않는다. +## Changes and verification -- `docs/AGENTS.md` — `docs/` -- `src/AGENTS.md` — `src/` -- `viewer-ui/AGENTS.md` — `viewer-ui/` -- `plugins/AGENTS.md` — `plugins/` +- Follow [guardrails.md](.agents/rules/guardrails.md) for platform and code constraints. +- Update the relevant tests and documentation when behavior or interfaces change; follow [testing.md](.agents/rules/testing.md). +- Use [Building and testing](docs/getting-started.md#building-and-testing) for local verification and [CI](.github/workflows/ci.yml) for the required gates. Report any applicable checks that were not run. +- Follow [commits.md](.agents/rules/commits.md) for commit and history constraints. -## 개발 흐름 +## Pull requests and releases -### Git 병합 운영 방침 - -- PR은 CI 통과 후 merge commit을 생성하는 **Merge** 방식으로 병합한다. Rebase merge나 Squash merge는 사용하지 않는다. -- 개발 변경은 `upstream/dev`에 통합하고, 릴리스는 `dev → main` 승격 PR로 병합한다. `main → dev` 동기화도 기존 커밋 이력을 보존하는 merge 방식으로 수행한다. - -### 작업 순서 - -1. **Plan** — 변경이 단순하지 않으면 `/plan`으로 사용자와 정렬한 뒤 구현한다. 단순한 버그 수정·설정 변경은 바로 구현한다. -2. **Implement** — `docs/architecture.md`와 해당 scope guide의 경계를 따른다. 공통 플랫폼·코드 품질 제약은 `.agents/rules/guardrails.md`에 있다. -3. **Verify** — 빌드·테스트·포맷·다른 플랫폼·viewer bundle 게이트는 [`docs/getting-started.md`](docs/getting-started.md)의 [Building and testing](docs/getting-started.md#building-and-testing)을 따른다. 커밋별 green과 history 규칙은 [`commits.md`](.agents/rules/commits.md)에 있다. -4. **Review** — `/self-review`로 자체 점검하고, 인증·보안·공개 API 등 민감한 변경이면 `/security-review`도 실행한다. 각 스킬의 절차는 해당 `SKILL.md`를 따른다. -5. **Commit** — [`.agents/rules/commits.md`](.agents/rules/commits.md)를 따른다. push는 사용자가 결정한다. +- Development PRs target `code0xff/nightcrow:dev` and include their purpose and verification results. +- Merge only after CI passes, using merge commits. Do not use rebase or squash merges; synchronize `main` into `dev` with a history-preserving merge as well. +- Release through a separate `dev` to `main` promotion PR; follow [releases.md](.agents/rules/releases.md). diff --git a/Cargo.lock b/Cargo.lock index 7d7e7d6b..a5296b6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1269,7 +1269,7 @@ dependencies = [ [[package]] name = "nightcrow" -version = "0.1.14" +version = "0.1.15" dependencies = [ "alacritty_terminal", "anyhow", @@ -1306,7 +1306,7 @@ dependencies = [ [[package]] name = "nightcrow-recovery" -version = "0.1.14" +version = "0.1.15" dependencies = [ "anyhow", "serde", diff --git a/Cargo.toml b/Cargo.toml index d6d47586..c499ca12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [".", "plugins/nightcrow-recovery"] # was never built at — a class of drift the release check could only report # after the fact. [workspace.package] -version = "0.1.14" +version = "0.1.15" [package] name = "nightcrow" diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 06422d48..39453dcc 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -1,7 +1,7 @@ # `docs/` scope -`docs/` 아래 문서의 공통 작성 규칙은 [`.agents/rules/docs.md`](../.agents/rules/docs.md)를 따른다. - 설계 근거·계층·모듈 책임·핵심 결정은 [`architecture.md`](architecture.md)와 그 상세 문서에 둔다. 설치·실행의 요약은 루트 [`README.md`](../README.md)에, 기능별 절차는 해당 문서에 둔다. 같은 설명을 여러 문서에 복제하지 말고 기준 문서로 링크한다. +공개 인터페이스의 입력·출력·오류·부작용과 비자명한 순서·동시성 제약을 문서화한다. 현재 구현과 실행 가능한 예시를 기준으로 쓰고, 미래 계획은 현재 동작과 구분한다. + Markdown prose는 고정 열 기준으로 hard-wrap하지 않는다. 문단은 논리적으로 한 줄로 유지하고 표·목록·코드처럼 구조상 필요한 개행만 둔다. diff --git a/docs/architecture.md b/docs/architecture.md index 91541c92..f65bf1e4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -23,6 +23,8 @@ filesystem/git ──> per-repository runtime ──> session daemon | `web/` + `viewer-ui/` | HTTP/WebSocket 인증·wire·브라우저 기하와 viewer preference | JSON/SSE/terminal binary 및 DOM 렌더 | | `plugin/` | provider별 감지·복구 프로세스 | 제한된 NDJSON event/command | +TUI의 [`app.rs`](../src/app.rs)는 crate 내부 facade로 현재 항목의 노출 범위를 유지한다. 구현은 `src/app/git/`의 Git view와 `src/app/navigation/`의 focus/input 이동으로 묶고, application wiring과 session I/O는 app 경계의 루트에 둔다. 다른 계층의 구조는 위 소유권 경계를 따르며, 세부 계약은 해당 영역의 상세 문서에 둔다. + 저장소 catalog는 membership(경로·순서·숨김·opaque id)과 runtime(worker·terminal hub)을 분리한다. 변경은 하나의 catalog transaction에서 membership을 계산하고 runtime을 reconcile한다. 같은 경로의 entry는 유지해 watcher·SSE·hub를 불필요하게 교체하지 않으며, retired worker의 종료는 catalog lock을 놓은 뒤 수행한다. status는 저장소별 snapshot worker가 파일시스템 변화에 반응해 읽고, 구독자가 없으면 읽거나 감시하지 않는다. 구독자가 없는 `/api/status`의 on-demand 요청만 한 번 읽을 수 있다. status payload는 최신 상태만 의미하므로 conflate할 수 있지만 terminal byte는 순서가 있는 스트림이라 버릴 수 없다. git diff/file/log 선택 로드는 `git2::Repository`를 소유하는 수명 긴 worker에서 lane별로 합치고 `(repository, generation)`이 현재 의도와 다르면 늦은 결과를 버린다. tree는 필요한 directory만 UI 경계에서 lazy-read한다. diff --git a/docs/architecture/web.md b/docs/architecture/web.md index b8ace5e6..21613248 100644 --- a/docs/architecture/web.md +++ b/docs/architecture/web.md @@ -59,7 +59,7 @@ request body 상한은 route마다 다르다. 대부분은 작은 control payloa pane에 붙여넣거나 떨어뜨린 이미지는 panel 하나에 건 capture 단계 listener가 가로챈다. 어느 pane인지는 event가 온 cell(`data-pane-id`)에서 읽으므로, paste는 포커스된 terminal이고 drop은 커서 아래 cell이다. 텍스트가 함께 실린 클립보드는 텍스트 붙여넣기로 둔다 — rich editor에서 복사하면 둘 다 실리고, 복사한 사람이 원한 것은 글자다. 이미지가 아닌 모든 것은 xterm 자신의 경로로 흘려보낸다. -편집 세션은 `hooks/useHtmlEditor.ts`가 들고 `components/content/HtmlEditor.tsx`가 그린다. 파일 패널의 연필 토글은 **working tree의 HTML 파일에만** 뜬다 — 커밋의 사본은 히스토리라 제자리 수정 대상이 아니다. 세션은 파싱한 소스 문자열과 그에 대한 patch 목록을 ref로 들고(키 입력이 프레임을 리렌더해 열린 편집을 무너뜨리지 않도록), 저장 시 원본+patch로 결과를 새로 만들어 `POST /api/file`에 보낸다. 렌더 후 검증(agent의 `ready` → `applyLiveLocks` → `locked` 회신)이 끝나기 전에는 편집이 열리지 않으며, 잠긴 블록 클릭은 편집 대신 이유를 보여준다. 툴바 저장은 프레임에 `flush`를 보내 열려 있는 편집을 먼저 커밋시키고(프레임 안 Ctrl+S는 agent가 커밋 후 `save`를 보내므로 같은 채널 순서로 이미 반영됨), 디스크가 앞서 있으면 편집을 쥔 채 덮어쓸지 묻는다. 프레임은 일반 프리뷰와 같은 sandbox(절대 `allow-same-origin` 없음)이고 네트워크를 직접 만지지 않는다 — 저장은 인증된 부모만 한다. 편집 엔진 청크는 lazy load라 읽기만 하는 세션은 비용을 치르지 않는다. +편집 세션은 `hooks/git/useHtmlEditor.ts`가 들고 `components/content/HtmlEditor.tsx`가 그린다. 파일 패널의 연필 토글은 **working tree의 HTML 파일에만** 뜬다 — 커밋의 사본은 히스토리라 제자리 수정 대상이 아니다. 세션은 파싱한 소스 문자열과 그에 대한 patch 목록을 ref로 들고(키 입력이 프레임을 리렌더해 열린 편집을 무너뜨리지 않도록), 저장 시 원본+patch로 결과를 새로 만들어 `POST /api/file`에 보낸다. 렌더 후 검증(agent의 `ready` → `applyLiveLocks` → `locked` 회신)이 끝나기 전에는 편집이 열리지 않으며, 잠긴 블록 클릭은 편집 대신 이유를 보여준다. 툴바 저장은 프레임에 `flush`를 보내 열려 있는 편집을 먼저 커밋시키고(프레임 안 Ctrl+S는 agent가 커밋 후 `save`를 보내므로 같은 채널 순서로 이미 반영됨), 디스크가 앞서 있으면 편집을 쥔 채 덮어쓸지 묻는다. 프레임은 일반 프리뷰와 같은 sandbox(절대 `allow-same-origin` 없음)이고 네트워크를 직접 만지지 않는다 — 저장은 인증된 부모만 한다. 편집 엔진 청크는 lazy load라 읽기만 하는 세션은 비용을 치르지 않는다. HTML 프리뷰의 인라인 편집 엔진은 `lib/edit/`에 순수 로직으로 둔다 — 소스 문자열을 파싱해 편집 가능한 블록과 그 바이트 오프셋을 뽑고(`parse`), 렌더된 텍스트를 소스로 되짚을 마커를 심고(`markers`), 사용자 편집을 오프셋 splice로 적용한다(`patch`). 원본은 문자열이며 재직렬화하지 않는다 — DOM을 다시 serialize하면 들여쓰기·속성 순서·엔티티가 문서 전체에 걸쳐 재작성되어 한 블록을 고쳐도 diff가 파일 전체로 번지기 때문이다. 그래서 소스 오프셋을 주는 spec 준수 파서가 필요해 **parse5**를 쓴다(`sourceCodeLocationInfo`; htmlparser2는 빠르나 spec-exact가 아니고 위치 정보가 약해 기각). 엔티티 인코딩/디코딩은 명명 엔티티가 2000개를 넘어 손으로 표를 만들면 반드시 틀리므로 표준 **entities** 라이브러리에 맡긴다. 엔진은 nighteditor에서 이식했다. diff --git a/src/AGENTS.md b/src/AGENTS.md index cb7ef2cd..3167dc4c 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -8,6 +8,10 @@ - attached TUI의 daemon socket transport와 browser viewer의 HTTP/WebSocket transport는 서로 다른 보안 경계다. 전자는 소켓 파일 권한을 전제로 하고 후자는 웹 인증을 전제로 하므로, 공통 상태 변경은 `session/`에 두되 두 transport의 인증·wire 처리를 합치지 않는다. 웹 계층의 상세는 [web design](../docs/architecture/web.md)을 따른다. - `TerminalBackend`는 로컬 `PtyBackend`와 데몬 공유 세션의 `HubBackend`를 잇는 경계다. pane 생성·종료·재정렬·resize는 backend event 계약을 통해 관찰하고, 실제 PTY 크기는 session-level ownership과 확인된 `Resized` 이벤트를 따른다. 이 경계를 우회해 frontend가 PTY나 hub 내부 상태를 직접 갱신하지 않는다. +## Source organization + +- `app.rs`는 crate 내부 TUI facade로 기존 `pub`/`pub(crate)` 노출 범위를 유지한다. 내부 구현은 책임별로 `app/git/`의 Git view 처리와 `app/navigation/`의 포커스·입력 이동 처리에 모으고, application wiring과 session I/O는 `app/` 루트에 둔다. 새 동작은 가장 가까운 책임 경계에 넣어 flat module 집합이 다시 커지지 않게 한다. + ## Protocol and platform seams - daemon frame은 control JSON과 raw terminal bytes를 구분한다. framing의 종류·길이 검증·truncated stream 처리와 terminal payload 분할 불변식을 바꾸면 [session design](../docs/architecture/session.md)과 해당 wire/contract tests를 함께 갱신한다. session repository set의 통지는 watcher 단일 producer 경계를 유지해 client별 응답 경쟁으로 순서가 갈라지지 않게 한다. diff --git a/src/app.rs b/src/app.rs index 791c2eb8..821dc951 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,25 +1,8 @@ use crate::git::diff::{ChangedFile, RepoSnapshot}; mod app_impl; -mod auto_follow; -mod commit_log_apply; -mod commit_log_fetch; -mod commit_log_pagination; -mod diff_load; -mod file_view_load; -mod focus; -mod git_view_manager; -mod interaction; -mod load_apply; -mod load_controller; -mod log_nav; +mod git; mod navigation; -mod repository_view; -mod scroll; mod session_io; -mod snapshot_io; -mod terminal_ctrl; -mod tree; -mod tree_nav; #[cfg(test)] pub use crate::runtime::snapshot::SnapshotChannel; @@ -31,10 +14,10 @@ pub use crate::runtime::terminal::TerminalState; pub(crate) use crate::runtime::terminal::strip_escape_sequences; pub use crate::ui::diff_pane::DiffPaneView; pub use crate::ui::file_view::{FileViewKey, FileViewState}; -pub use git_view_manager::GitViewManager; -pub(crate) use interaction::{InteractionState, leader_label_of}; +pub use git::GitViewManager; #[cfg(test)] -pub use repository_view::RepositoryView; +pub use git::RepositoryView; +pub(crate) use navigation::interaction::{InteractionState, leader_label_of}; pub(crate) const LIST_PAGE_SIZE: usize = 10; pub(crate) const DIFF_PAGE_SIZE: usize = 20; diff --git a/src/app/git.rs b/src/app/git.rs new file mode 100644 index 00000000..a5051f8f --- /dev/null +++ b/src/app/git.rs @@ -0,0 +1,16 @@ +mod auto_follow; +mod commit_log_apply; +mod commit_log_fetch; +mod commit_log_pagination; +pub(super) mod diff_load; +mod file_view_load; +mod git_view_manager; +mod load_apply; +mod load_controller; +mod repository_view; +mod snapshot_io; +mod tree; + +pub use git_view_manager::GitViewManager; +#[cfg(test)] +pub use repository_view::RepositoryView; diff --git a/src/app/auto_follow.rs b/src/app/git/auto_follow.rs similarity index 98% rename from src/app/auto_follow.rs rename to src/app/git/auto_follow.rs index 54abe1af..10ce0272 100644 --- a/src/app/auto_follow.rs +++ b/src/app/git/auto_follow.rs @@ -1,4 +1,4 @@ -use super::{App, Focus, ViewMode}; +use crate::app::{App, Focus, ViewMode}; use std::time::{Duration, Instant, SystemTime}; impl App { diff --git a/src/app/commit_log_apply.rs b/src/app/git/commit_log_apply.rs similarity index 98% rename from src/app/commit_log_apply.rs rename to src/app/git/commit_log_apply.rs index 84018038..b66d364e 100644 --- a/src/app/commit_log_apply.rs +++ b/src/app/git/commit_log_apply.rs @@ -1,10 +1,10 @@ use git2::Oid; -use super::App; use super::commit_log_fetch::{CommitLogFetchKind, CommitLogPageMsg}; +use crate::app::App; impl App { - pub(super) fn handle_commit_log_page_msg(&mut self, msg: CommitLogPageMsg) { + pub(in crate::app) fn handle_commit_log_page_msg(&mut self, msg: CommitLogPageMsg) { if msg.generation != self.git.commit_log.generation() { return; } diff --git a/src/app/commit_log_fetch.rs b/src/app/git/commit_log_fetch.rs similarity index 99% rename from src/app/commit_log_fetch.rs rename to src/app/git/commit_log_fetch.rs index d311e31f..610ed730 100644 --- a/src/app/commit_log_fetch.rs +++ b/src/app/git/commit_log_fetch.rs @@ -11,8 +11,7 @@ use crate::platform::threading::{REAP_TIMEOUT, try_timed_join}; use git2::{Oid, Repository}; -use super::App; -use super::ViewMode; +use crate::app::{App, ViewMode}; use crate::git::diff::{CommitEntry, load_commit_log_page}; // `Tail` extends the loaded list at the current tail. `Refresh` replaces or diff --git a/src/app/commit_log_pagination.rs b/src/app/git/commit_log_pagination.rs similarity index 100% rename from src/app/commit_log_pagination.rs rename to src/app/git/commit_log_pagination.rs diff --git a/src/app/diff_load.rs b/src/app/git/diff_load.rs similarity index 99% rename from src/app/diff_load.rs rename to src/app/git/diff_load.rs index a2ecc1e8..8fa6263d 100644 --- a/src/app/diff_load.rs +++ b/src/app/git/diff_load.rs @@ -1,5 +1,5 @@ use super::load_controller::{DiffLoadMode, DiffTarget}; -use super::{App, DiffPaneView, FileViewState, NoticeKind, ViewMode}; +use crate::app::{App, DiffPaneView, FileViewState, NoticeKind, ViewMode}; use crate::git::diff::{DiffHunk, parse_hunk_new_start}; // Replaces a prior 3-flag signature where `reset_scroll`/`keep_scroll` were diff --git a/src/app/file_view_load.rs b/src/app/git/file_view_load.rs similarity index 98% rename from src/app/file_view_load.rs rename to src/app/git/file_view_load.rs index f3b9646e..258d416a 100644 --- a/src/app/file_view_load.rs +++ b/src/app/git/file_view_load.rs @@ -1,5 +1,5 @@ use super::load_controller::{DiffLoadMode, DiffTarget}; -use super::{App, DiffPaneView, FileViewKey, FileViewState, ViewMode}; +use crate::app::{App, DiffPaneView, FileViewKey, FileViewState, ViewMode}; impl App { pub(crate) fn current_file_view_key(&self) -> Option { diff --git a/src/app/git_view_manager.rs b/src/app/git/git_view_manager.rs similarity index 100% rename from src/app/git_view_manager.rs rename to src/app/git/git_view_manager.rs diff --git a/src/app/load_apply.rs b/src/app/git/load_apply.rs similarity index 97% rename from src/app/load_apply.rs rename to src/app/git/load_apply.rs index 22a68809..f3108b4f 100644 --- a/src/app/load_apply.rs +++ b/src/app/git/load_apply.rs @@ -2,7 +2,7 @@ use std::sync::mpsc; use super::diff_load::DiffApply; use super::load_controller::{DiffLoadMode, DiffTarget}; -use super::{App, DiffPaneView, FileViewState, NoticeKind, ViewMode}; +use crate::app::{App, DiffPaneView, FileViewState, NoticeKind, ViewMode}; use crate::git::diff::{GitLoadPayload, GitLoadReply, LoadLane}; impl App { @@ -87,11 +87,11 @@ impl App { fn diff_target_is_current(&self, target: &DiffTarget) -> bool { match target { DiffTarget::Status(path) => { - self.git.view.mode == super::ViewMode::Status + self.git.view.mode == ViewMode::Status && self.selected_filtered_status_path().as_deref() == Some(path) } DiffTarget::Commit(oid) => { - self.git.view.mode == super::ViewMode::Log + self.git.view.mode == ViewMode::Log && !self.git.view.log.drill_down && self .git @@ -102,7 +102,7 @@ impl App { .is_some_and(|commit| commit.oid == *oid) } DiffTarget::CommitFile { oid, path } => { - self.git.view.mode == super::ViewMode::Log + self.git.view.mode == ViewMode::Log && self.git.view.log.drill_down && self .git diff --git a/src/app/load_controller.rs b/src/app/git/load_controller.rs similarity index 100% rename from src/app/load_controller.rs rename to src/app/git/load_controller.rs diff --git a/src/app/repository_view.rs b/src/app/git/repository_view.rs similarity index 98% rename from src/app/repository_view.rs rename to src/app/git/repository_view.rs index 93b4feae..4aeb6a17 100644 --- a/src/app/repository_view.rs +++ b/src/app/git/repository_view.rs @@ -7,7 +7,7 @@ use crate::ui::log_view::LogView; use crate::ui::status_view::StatusView; use crate::ui::tree_view::TreeView; -use super::ViewMode; +use crate::app::ViewMode; #[derive(Default)] pub struct AutoFollow { diff --git a/src/app/snapshot_io.rs b/src/app/git/snapshot_io.rs similarity index 98% rename from src/app/snapshot_io.rs rename to src/app/git/snapshot_io.rs index b87af493..df62e63f 100644 --- a/src/app/snapshot_io.rs +++ b/src/app/git/snapshot_io.rs @@ -1,4 +1,4 @@ -use super::{App, NoticeKind, RepoSnapshot, SnapshotMsg, ViewMode}; +use crate::app::{App, NoticeKind, RepoSnapshot, SnapshotMsg, ViewMode}; use std::collections::HashMap; use std::time::SystemTime; diff --git a/src/app/tree.rs b/src/app/git/tree.rs similarity index 99% rename from src/app/tree.rs rename to src/app/git/tree.rs index 90e1a0f2..ac9682d4 100644 --- a/src/app/tree.rs +++ b/src/app/git/tree.rs @@ -3,7 +3,7 @@ //! Directory I/O is synchronous on the UI thread (one level per expansion); //! the git-status snapshot worker is never involved. -use super::{App, DiffPaneView, FileViewKey, FileViewState, NoticeKind, ViewMode}; +use crate::app::{App, DiffPaneView, FileViewKey, FileViewState, NoticeKind, ViewMode}; use std::collections::BTreeSet; impl App { diff --git a/src/app/navigation.rs b/src/app/navigation.rs index 06a0b52b..43880e10 100644 --- a/src/app/navigation.rs +++ b/src/app/navigation.rs @@ -1,259 +1,8 @@ -use super::{App, ChangedFile, DIFF_PAGE_SIZE, DiffPaneView, Focus, LIST_PAGE_SIZE, ViewMode}; - -impl App { - pub(crate) fn restore_selection(&mut self, previous_path: Option<&str>) -> Option { - if self.git.view.status.files.is_empty() { - self.git.view.status.selected = 0; - return None; - } - - if let Some(path) = previous_path - && let Some(index) = self - .git - .view - .status - .files - .iter() - .position(|file| file.path == path) - { - self.git.view.status.selected = index; - return Some(path.to_string()); - } - - self.git.view.status.selected = self - .git - .view - .status - .selected - .min(self.git.view.status.files.len().saturating_sub(1)); - self.git - .view - .status - .files - .get(self.git.view.status.selected) - .map(|file| file.path.clone()) - } - - pub fn filtered_indices(&self) -> &[usize] { - &self.git.view.status.filter_cache - } - - pub fn start_search(&mut self) { - self.git.view.status.start_search(); - } - - pub fn cancel_search(&mut self) { - self.git.view.status.cancel_search(); - self.refresh_status_diff_after_filter_change(); - } - - pub fn confirm_search(&mut self) { - if self.git.view.status.confirm_search() { - self.refresh_status_diff_after_filter_change(); - } - } - - pub fn search_push(&mut self, ch: char) { - self.git.view.status.search_push(ch); - self.refresh_status_diff_after_filter_change(); - } - - pub fn search_pop(&mut self) { - self.git.view.status.search_pop(); - self.refresh_status_diff_after_filter_change(); - } - - pub(crate) fn selected_filtered_status_path(&self) -> Option { - self.selected_filtered_status_file().map(|f| f.path.clone()) - } - - // Borrow-only counterpart of `selected_filtered_status_path` so callers - // that just need to read don't allocate. `binary_search` works because - // `filter_cache` is built in ascending order by `recompute_filter`. - pub fn selected_filtered_status_file(&self) -> Option<&ChangedFile> { - if self - .filtered_indices() - .binary_search(&self.git.view.status.selected) - .is_err() - { - return None; - } - self.git - .view - .status - .files - .get(self.git.view.status.selected) - } - - pub(crate) fn sync_selection_to_filter(&mut self) -> bool { - let target = { - let indices = self.filtered_indices(); - if indices.is_empty() { - return false; - } - if indices.contains(&self.git.view.status.selected) { - self.git.view.status.selected - } else { - indices[0] - } - }; - - if target == self.git.view.status.selected { - false - } else { - self.git.view.status.selected = target; - // Match `move_selected_in_filter`: drop the previous file's - // horizontal scroll so the newly-shown path starts from column 0. - self.git.view.status.file_scroll_x = 0; - true - } - } - - fn refresh_status_diff_after_filter_change(&mut self) { - let selection_changed = self.sync_selection_to_filter(); - if self.selected_filtered_status_path().is_none() { - self.clear_diff_state(); - } else if selection_changed || self.git.view.diff.hunks().is_empty() { - self.reload_diff(); - } - } - - pub(crate) fn move_selected_in_filter(&mut self, delta: isize) { - // Resolve the new selection in a scoped block so the borrow on - // filtered_indices does not outlive the mutating reload below. - let resolved = { - let indices = self.filtered_indices(); - if indices.is_empty() { - None - } else { - let pos = indices - .iter() - .position(|&i| i == self.git.view.status.selected); - let new_pos = match pos { - Some(p) => { - let last = indices.len() as isize - 1; - (p as isize + delta).clamp(0, last) as usize - } - None => 0, - }; - Some((pos, new_pos, indices[new_pos])) - } - }; - if let Some((pos, new_pos, new_selected)) = resolved - && (Some(new_pos) != pos || self.git.view.status.selected != new_selected) - { - // Mark only after confirming the selection actually changed so - // bumping against either end doesn't reset the auto-follow - // steered-path memory. - self.mark_user_navigated(); - self.git.view.status.selected = new_selected; - self.git.view.status.file_scroll_x = 0; - self.reload_diff(); - } - } - - pub fn select_up(&mut self) { - match self.focus { - Focus::FileList => { - if self.git.view.mode == ViewMode::Tree { - self.tree_select_up(); - return; - } - if self.navigate_log_list(Self::log_select_up, Self::log_file_select_up) { - return; - } - self.move_selected_in_filter(-1); - } - Focus::DiffViewer => { - if self.git.view.diff.view == DiffPaneView::File { - self.git.view.diff.file_view.scroll_up(1); - } else { - self.git.view.diff.scroll = self.git.view.diff.scroll.saturating_sub(1); - } - } - Focus::Terminal => {} - } - } - - pub fn select_down(&mut self) { - match self.focus { - Focus::FileList => { - if self.git.view.mode == ViewMode::Tree { - self.tree_select_down(); - return; - } - if self.navigate_log_list(Self::log_select_down, Self::log_file_select_down) { - return; - } - self.move_selected_in_filter(1); - } - Focus::DiffViewer => { - if self.git.view.diff.view == DiffPaneView::File { - self.git.view.diff.file_view.scroll_down(1); - } else { - self.git.view.diff.scroll = self - .git - .view - .diff - .scroll - .saturating_add(1) - .min(self.git.view.diff.max_scroll()); - } - } - Focus::Terminal => {} - } - } - - pub fn page_up(&mut self) { - match self.focus { - Focus::FileList => { - if self.git.view.mode == ViewMode::Tree { - self.tree_page_up(); - return; - } - if self.navigate_log_list(Self::log_page_up, Self::log_file_page_up) { - return; - } - self.move_selected_in_filter(-(LIST_PAGE_SIZE as isize)); - } - Focus::DiffViewer => { - if self.git.view.diff.view == DiffPaneView::File { - self.git.view.diff.file_view.scroll_up(DIFF_PAGE_SIZE); - } else { - self.git.view.diff.scroll = - self.git.view.diff.scroll.saturating_sub(DIFF_PAGE_SIZE); - } - } - Focus::Terminal => {} - } - } - - pub fn page_down(&mut self) { - match self.focus { - Focus::FileList => { - if self.git.view.mode == ViewMode::Tree { - self.tree_page_down(); - return; - } - if self.navigate_log_list(Self::log_page_down, Self::log_file_page_down) { - return; - } - self.move_selected_in_filter(LIST_PAGE_SIZE as isize); - } - Focus::DiffViewer => { - if self.git.view.diff.view == DiffPaneView::File { - self.git.view.diff.file_view.scroll_down(DIFF_PAGE_SIZE); - } else { - self.git.view.diff.scroll = self - .git - .view - .diff - .scroll - .saturating_add(DIFF_PAGE_SIZE) - .min(self.git.view.diff.max_scroll()); - } - } - Focus::Terminal => {} - } - } -} +mod focus; +mod log_nav; +mod scroll; +mod selection; +mod terminal_ctrl; +mod tree_nav; + +pub(super) mod interaction; diff --git a/src/app/focus.rs b/src/app/navigation/focus.rs similarity index 99% rename from src/app/focus.rs rename to src/app/navigation/focus.rs index 80e1c812..0a08f543 100644 --- a/src/app/focus.rs +++ b/src/app/navigation/focus.rs @@ -1,4 +1,4 @@ -use super::{App, Focus, ViewMode}; +use crate::app::{App, Focus, ViewMode}; use crate::runtime::terminal::TerminalFullscreen; impl App { diff --git a/src/app/interaction.rs b/src/app/navigation/interaction.rs similarity index 100% rename from src/app/interaction.rs rename to src/app/navigation/interaction.rs diff --git a/src/app/log_nav.rs b/src/app/navigation/log_nav.rs similarity index 98% rename from src/app/log_nav.rs rename to src/app/navigation/log_nav.rs index 2ee0f820..66dbbe89 100644 --- a/src/app/log_nav.rs +++ b/src/app/navigation/log_nav.rs @@ -1,4 +1,4 @@ -use super::{App, LIST_PAGE_SIZE, ViewMode}; +use crate::app::{App, LIST_PAGE_SIZE, ViewMode}; fn resolve_filtered_selection(indices: &[usize], selected: usize, delta: isize) -> Option { if indices.is_empty() { @@ -134,7 +134,7 @@ impl App { } } - pub(super) fn navigate_log_list( + pub(in crate::app) fn navigate_log_list( &mut self, commit_nav: fn(&mut Self), file_nav: fn(&mut Self), diff --git a/src/app/scroll.rs b/src/app/navigation/scroll.rs similarity index 98% rename from src/app/scroll.rs rename to src/app/navigation/scroll.rs index 1d98ed29..64c21a62 100644 --- a/src/app/scroll.rs +++ b/src/app/navigation/scroll.rs @@ -1,4 +1,4 @@ -use super::{App, ViewMode}; +use crate::app::{App, ViewMode}; use std::cell::Cell; impl App { diff --git a/src/app/navigation/selection.rs b/src/app/navigation/selection.rs new file mode 100644 index 00000000..f443e775 --- /dev/null +++ b/src/app/navigation/selection.rs @@ -0,0 +1,259 @@ +use crate::app::{App, ChangedFile, DIFF_PAGE_SIZE, DiffPaneView, Focus, LIST_PAGE_SIZE, ViewMode}; + +impl App { + pub(crate) fn restore_selection(&mut self, previous_path: Option<&str>) -> Option { + if self.git.view.status.files.is_empty() { + self.git.view.status.selected = 0; + return None; + } + + if let Some(path) = previous_path + && let Some(index) = self + .git + .view + .status + .files + .iter() + .position(|file| file.path == path) + { + self.git.view.status.selected = index; + return Some(path.to_string()); + } + + self.git.view.status.selected = self + .git + .view + .status + .selected + .min(self.git.view.status.files.len().saturating_sub(1)); + self.git + .view + .status + .files + .get(self.git.view.status.selected) + .map(|file| file.path.clone()) + } + + pub fn filtered_indices(&self) -> &[usize] { + &self.git.view.status.filter_cache + } + + pub fn start_search(&mut self) { + self.git.view.status.start_search(); + } + + pub fn cancel_search(&mut self) { + self.git.view.status.cancel_search(); + self.refresh_status_diff_after_filter_change(); + } + + pub fn confirm_search(&mut self) { + if self.git.view.status.confirm_search() { + self.refresh_status_diff_after_filter_change(); + } + } + + pub fn search_push(&mut self, ch: char) { + self.git.view.status.search_push(ch); + self.refresh_status_diff_after_filter_change(); + } + + pub fn search_pop(&mut self) { + self.git.view.status.search_pop(); + self.refresh_status_diff_after_filter_change(); + } + + pub(crate) fn selected_filtered_status_path(&self) -> Option { + self.selected_filtered_status_file().map(|f| f.path.clone()) + } + + // Borrow-only counterpart of `selected_filtered_status_path` so callers + // that just need to read don't allocate. `binary_search` works because + // `filter_cache` is built in ascending order by `recompute_filter`. + pub fn selected_filtered_status_file(&self) -> Option<&ChangedFile> { + if self + .filtered_indices() + .binary_search(&self.git.view.status.selected) + .is_err() + { + return None; + } + self.git + .view + .status + .files + .get(self.git.view.status.selected) + } + + pub(crate) fn sync_selection_to_filter(&mut self) -> bool { + let target = { + let indices = self.filtered_indices(); + if indices.is_empty() { + return false; + } + if indices.contains(&self.git.view.status.selected) { + self.git.view.status.selected + } else { + indices[0] + } + }; + + if target == self.git.view.status.selected { + false + } else { + self.git.view.status.selected = target; + // Match `move_selected_in_filter`: drop the previous file's + // horizontal scroll so the newly-shown path starts from column 0. + self.git.view.status.file_scroll_x = 0; + true + } + } + + fn refresh_status_diff_after_filter_change(&mut self) { + let selection_changed = self.sync_selection_to_filter(); + if self.selected_filtered_status_path().is_none() { + self.clear_diff_state(); + } else if selection_changed || self.git.view.diff.hunks().is_empty() { + self.reload_diff(); + } + } + + pub(crate) fn move_selected_in_filter(&mut self, delta: isize) { + // Resolve the new selection in a scoped block so the borrow on + // filtered_indices does not outlive the mutating reload below. + let resolved = { + let indices = self.filtered_indices(); + if indices.is_empty() { + None + } else { + let pos = indices + .iter() + .position(|&i| i == self.git.view.status.selected); + let new_pos = match pos { + Some(p) => { + let last = indices.len() as isize - 1; + (p as isize + delta).clamp(0, last) as usize + } + None => 0, + }; + Some((pos, new_pos, indices[new_pos])) + } + }; + if let Some((pos, new_pos, new_selected)) = resolved + && (Some(new_pos) != pos || self.git.view.status.selected != new_selected) + { + // Mark only after confirming the selection actually changed so + // bumping against either end doesn't reset the auto-follow + // steered-path memory. + self.mark_user_navigated(); + self.git.view.status.selected = new_selected; + self.git.view.status.file_scroll_x = 0; + self.reload_diff(); + } + } + + pub fn select_up(&mut self) { + match self.focus { + Focus::FileList => { + if self.git.view.mode == ViewMode::Tree { + self.tree_select_up(); + return; + } + if self.navigate_log_list(Self::log_select_up, Self::log_file_select_up) { + return; + } + self.move_selected_in_filter(-1); + } + Focus::DiffViewer => { + if self.git.view.diff.view == DiffPaneView::File { + self.git.view.diff.file_view.scroll_up(1); + } else { + self.git.view.diff.scroll = self.git.view.diff.scroll.saturating_sub(1); + } + } + Focus::Terminal => {} + } + } + + pub fn select_down(&mut self) { + match self.focus { + Focus::FileList => { + if self.git.view.mode == ViewMode::Tree { + self.tree_select_down(); + return; + } + if self.navigate_log_list(Self::log_select_down, Self::log_file_select_down) { + return; + } + self.move_selected_in_filter(1); + } + Focus::DiffViewer => { + if self.git.view.diff.view == DiffPaneView::File { + self.git.view.diff.file_view.scroll_down(1); + } else { + self.git.view.diff.scroll = self + .git + .view + .diff + .scroll + .saturating_add(1) + .min(self.git.view.diff.max_scroll()); + } + } + Focus::Terminal => {} + } + } + + pub fn page_up(&mut self) { + match self.focus { + Focus::FileList => { + if self.git.view.mode == ViewMode::Tree { + self.tree_page_up(); + return; + } + if self.navigate_log_list(Self::log_page_up, Self::log_file_page_up) { + return; + } + self.move_selected_in_filter(-(LIST_PAGE_SIZE as isize)); + } + Focus::DiffViewer => { + if self.git.view.diff.view == DiffPaneView::File { + self.git.view.diff.file_view.scroll_up(DIFF_PAGE_SIZE); + } else { + self.git.view.diff.scroll = + self.git.view.diff.scroll.saturating_sub(DIFF_PAGE_SIZE); + } + } + Focus::Terminal => {} + } + } + + pub fn page_down(&mut self) { + match self.focus { + Focus::FileList => { + if self.git.view.mode == ViewMode::Tree { + self.tree_page_down(); + return; + } + if self.navigate_log_list(Self::log_page_down, Self::log_file_page_down) { + return; + } + self.move_selected_in_filter(LIST_PAGE_SIZE as isize); + } + Focus::DiffViewer => { + if self.git.view.diff.view == DiffPaneView::File { + self.git.view.diff.file_view.scroll_down(DIFF_PAGE_SIZE); + } else { + self.git.view.diff.scroll = self + .git + .view + .diff + .scroll + .saturating_add(DIFF_PAGE_SIZE) + .min(self.git.view.diff.max_scroll()); + } + } + Focus::Terminal => {} + } + } +} diff --git a/src/app/terminal_ctrl.rs b/src/app/navigation/terminal_ctrl.rs similarity index 99% rename from src/app/terminal_ctrl.rs rename to src/app/navigation/terminal_ctrl.rs index 55e845e2..a70c30e9 100644 --- a/src/app/terminal_ctrl.rs +++ b/src/app/navigation/terminal_ctrl.rs @@ -2,7 +2,7 @@ //! (focus, status line, fullscreen flags). Pure terminal logic lives on //! `TerminalState` in `runtime/terminal.rs`. -use super::{App, Focus, NoticeKind}; +use crate::app::{App, Focus, NoticeKind}; use crate::runtime::terminal::TerminalFullscreen; impl App { diff --git a/src/app/tree_nav.rs b/src/app/navigation/tree_nav.rs similarity index 99% rename from src/app/tree_nav.rs rename to src/app/navigation/tree_nav.rs index 0e0d6cd2..be6f879a 100644 --- a/src/app/tree_nav.rs +++ b/src/app/navigation/tree_nav.rs @@ -1,4 +1,4 @@ -use super::{App, LIST_PAGE_SIZE}; +use crate::app::{App, LIST_PAGE_SIZE}; use crate::ui::tree_view::{TreeIndexEntry, parent_path}; impl App { diff --git a/src/app/tests/mod.rs b/src/app/tests/mod.rs index d888bf25..dac4d51a 100644 --- a/src/app/tests/mod.rs +++ b/src/app/tests/mod.rs @@ -4,7 +4,7 @@ mod helpers; // (App, Focus, ViewMode, Notice, NoticeKind, DiffPaneView, FileViewKey, // FileViewState, SnapshotChannel, etc.) // so every test submodule can pull them in with `use super::*;`. -use super::diff_load::DiffApply; +use super::git::diff_load::DiffApply; use super::strip_escape_sequences; use super::*; use crate::git::diff::{ChangedFile, CommitEntry, RepoSnapshot, StatusKind, load_commit_log}; diff --git a/src/daemon/client_tests.rs b/src/daemon/client_tests.rs index c89a49f2..41653e5d 100644 --- a/src/daemon/client_tests.rs +++ b/src/daemon/client_tests.rs @@ -237,6 +237,7 @@ fn a_closed_connection_reads_as_gone() { let dir = tempfile::TempDir::new().unwrap(); let path = dir.path().join("bare.sock"); let listener = UnixListener::bind(&path).expect("binds"); + let (close_peer, wait_to_close) = std::sync::mpsc::channel(); let peer = std::thread::spawn(move || { let (mut stream, _) = listener.accept().expect("accepts"); let hello = serde_json::to_vec(&ServerMessage::Hello { @@ -248,10 +249,20 @@ fn a_closed_connection_reads_as_gone() { stream.flush().expect("flushes"); // Read what the client said before closing, so the close is what ends // the connection rather than an unread request. - let _ = read_frame(&mut stream); + read_frame(&mut stream) + .expect("reads the client hello") + .expect("the client hello arrives"); + // macOS can reject setting the handshake timeout after peer EOF. Keep + // the peer open until connect has completed so this tests reader EOF. + let _ = wait_to_close.recv(); }); let client = DaemonClient::connect(&path).expect("attaches"); + assert!( + client.is_connected(), + "the attached client starts connected" + ); + close_peer.send(()).expect("releases the peer"); peer.join().expect("the peer ends"); // The reader thread ends on the peer's close and clears the flag with it. diff --git a/viewer-ui/AGENTS.md b/viewer-ui/AGENTS.md index 76b2b4f8..48f48d8f 100644 --- a/viewer-ui/AGENTS.md +++ b/viewer-ui/AGENTS.md @@ -12,4 +12,5 @@ - `viewer-ui/dist/`는 Vite가 생성하는 커밋 대상 릴리스 번들이며 Rust 서버가 바이너리에 임베드한다. 번들에 영향을 주는 소스·설정·public asset 변경 뒤에는 반드시 build하고, 최종 변경에는 소스와 일치하는 `dist` 결과를 포함한다. clean checkout에서 같은 build를 다시 실행해 `dist`에 미커밋 차이가 없어야 한다. - `vite.config.ts`의 relative asset base와 `/api`, `/login`, `/ws` 개발 프록시는 임베드 서버와 Vite 개발 서버 사이의 배포 계약이다. mount path나 서버 포트를 바꿀 때는 Rust route와 문서·검증을 함께 확인한다. -- 화면 조립은 `pages/`, 재사용 UI는 `components/`, 상태·효과는 `hooks/`, API 외 순수 로직은 `lib/`에 둔다. 서버 wire 문자열을 각 hook에서 다시 해석하지 말고 `api/`의 경계에서 검증된 타입을 전달한다. +- 화면 조립은 `pages/`, 재사용 UI는 `components/`의 기능별 디렉터리, 상태·효과는 `hooks/`의 책임별 디렉터리에 둔다. 서버 wire 계약은 `api/` 경계에서 다루고, 여러 화면이나 hook이 함께 쓰는 동작·계산 도우미는 `lib/`의 책임별 디렉터리에 둔다. `lib/`에는 React 기반 hook 의존성이 포함될 수 있으므로 순수 함수 전용 계층으로 간주하지 않는다. 서버 wire 문자열은 hook에서 다시 해석하지 말고 `api/`에서 검증된 타입을 전달한다. +- 테스트와 harness는 대상 모듈의 기능 디렉터리에 두고, `lib/shared/`에는 여러 기능이 함께 쓰는 도우미만 둔다. 파일을 옮길 때 동적 import의 lazy-loading 경계를 유지한다. diff --git a/viewer-ui/package-lock.json b/viewer-ui/package-lock.json index f295a0b2..8ff28d98 100644 --- a/viewer-ui/package-lock.json +++ b/viewer-ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "nightcrow-viewer-ui", - "version": "0.1.14", + "version": "0.1.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nightcrow-viewer-ui", - "version": "0.1.14", + "version": "0.1.15", "dependencies": { "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", diff --git a/viewer-ui/package.json b/viewer-ui/package.json index 0e4c3372..0b6f5272 100644 --- a/viewer-ui/package.json +++ b/viewer-ui/package.json @@ -1,7 +1,7 @@ { "name": "nightcrow-viewer-ui", "private": true, - "version": "0.1.14", + "version": "0.1.15", "type": "module", "scripts": { "dev": "vite", diff --git a/viewer-ui/src/api/terminal.ts b/viewer-ui/src/api/terminal.ts index 99d6627b..37908d21 100644 --- a/viewer-ui/src/api/terminal.ts +++ b/viewer-ui/src/api/terminal.ts @@ -1,5 +1,5 @@ -import type { ClearKeyReport } from "../lib/clearKeyProbe"; -import type { RecoveryFrame } from "../lib/recovery"; +import type { ClearKeyReport } from "../lib/terminal/clearKeyProbe"; +import type { RecoveryFrame } from "../lib/terminal/recovery"; export interface PaneSize { rows: number; diff --git a/viewer-ui/src/components/DiffView.tsx b/viewer-ui/src/components/content/DiffView.tsx similarity index 94% rename from viewer-ui/src/components/DiffView.tsx rename to viewer-ui/src/components/content/DiffView.tsx index 9459c276..93e54465 100644 --- a/viewer-ui/src/components/DiffView.tsx +++ b/viewer-ui/src/components/content/DiffView.tsx @@ -1,9 +1,9 @@ -import { splitHunkRows } from "../lib/diffLayout"; -import { linenoDigits } from "../lib/gutter"; -import { diffLineBg } from "../lib/utils"; +import { splitHunkRows } from "../../lib/content/diffLayout"; +import { linenoDigits } from "../../lib/content/gutter"; +import { diffLineBg } from "../../lib/shared/utils"; import { LineNos } from "./LineNos"; -import type { Diff, DiffLine } from "../api"; -import { VIRTUAL_THRESHOLD, type ScrollViewport } from "../lib/virtualWindow"; +import type { Diff, DiffLine } from "../../api"; +import { VIRTUAL_THRESHOLD, type ScrollViewport } from "../../lib/ui/virtualWindow"; import { VirtualDiffView } from "./VirtualDiffView"; function DiffLineContent({ line }: { line: DiffLine }) { diff --git a/viewer-ui/src/components/FileLines.tsx b/viewer-ui/src/components/content/FileLines.tsx similarity index 89% rename from viewer-ui/src/components/FileLines.tsx rename to viewer-ui/src/components/content/FileLines.tsx index 5936e42d..6d226538 100644 --- a/viewer-ui/src/components/FileLines.tsx +++ b/viewer-ui/src/components/content/FileLines.tsx @@ -1,5 +1,5 @@ -import type { Span } from "../api"; -import { digitsFor } from "../lib/gutter"; +import type { Span } from "../../api"; +import { digitsFor } from "../../lib/content/gutter"; import { LineNos } from "./LineNos"; /** Small files stay fully mounted so native selection and find remain exact. */ diff --git a/viewer-ui/src/components/FilePane.tsx b/viewer-ui/src/components/content/FilePane.tsx similarity index 92% rename from viewer-ui/src/components/FilePane.tsx rename to viewer-ui/src/components/content/FilePane.tsx index b49ebcab..bcb6d45d 100644 --- a/viewer-ui/src/components/FilePane.tsx +++ b/viewer-ui/src/components/content/FilePane.tsx @@ -1,34 +1,34 @@ import { Suspense, lazy, useEffect, useRef, useState } from "react"; -import { useDiffLayout } from "../lib/diffLayout"; -import { fileViewSource, isHtmlPath, isPreviewablePath } from "../lib/fileView"; -import { anchorWithin, hunkAtTop } from "../lib/diffAnchor"; -import { useScrollViewport } from "../hooks/ui/useScrollViewport"; +import { useDiffLayout } from "../../lib/content/diffLayout"; +import { fileViewSource, isHtmlPath, isPreviewablePath } from "../../lib/content/fileView"; +import { anchorWithin, hunkAtTop } from "../../lib/content/diffAnchor"; +import { useScrollViewport } from "../../hooks/ui/useScrollViewport"; import { lineScrollTop, VIRTUAL_THRESHOLD, -} from "../lib/virtualWindow"; -import { sourceKey } from "../lib/otherFace"; -import { focusRegionAttrs } from "../lib/shortcutDom"; +} from "../../lib/ui/virtualWindow"; +import { sourceKey } from "../../lib/content/otherFace"; +import { focusRegionAttrs } from "../../lib/shortcuts/shortcutDom"; import { FilePaneActions } from "./FilePaneActions"; import { DiffView } from "./DiffView"; -import { ErrorBoundary } from "./feedback/ErrorBoundary"; +import { ErrorBoundary } from "../feedback/ErrorBoundary"; import { FileLines } from "./FileLines"; import { VirtualFileLines } from "./VirtualFileLines"; import { PathLabel } from "./PathLabel"; -import { api, type Status } from "../api"; -import type { FileSource, Pane } from "../types"; +import { api, type Status } from "../../api"; +import type { FileSource, Pane } from "../../types"; // Keep the markdown pipeline out of the initial chunk. const MarkdownView = lazy(() => - import("./content/Markdown").then((m) => ({ default: m.MarkdownView })), + import("./Markdown").then((m) => ({ default: m.MarkdownView })), ); const HtmlView = lazy(() => - import("./content/Html").then((m) => ({ default: m.HtmlView })), + import("./Html").then((m) => ({ default: m.HtmlView })), ); // The edit engine (a parser and the agent) rides its own chunk: most sessions // only ever read. const HtmlEditor = lazy(() => - import("./content/HtmlEditor").then((m) => ({ default: m.HtmlEditor })), + import("./HtmlEditor").then((m) => ({ default: m.HtmlEditor })), ); export interface FilePaneProps { diff --git a/viewer-ui/src/components/FilePaneActions.tsx b/viewer-ui/src/components/content/FilePaneActions.tsx similarity index 91% rename from viewer-ui/src/components/FilePaneActions.tsx rename to viewer-ui/src/components/content/FilePaneActions.tsx index 200513ec..eb986f41 100644 --- a/viewer-ui/src/components/FilePaneActions.tsx +++ b/viewer-ui/src/components/content/FilePaneActions.tsx @@ -1,15 +1,15 @@ -import { useDiffLayout } from "../lib/diffLayout"; -import { isPreviewablePath } from "../lib/fileView"; -import { useShortcutHint } from "../hooks/shortcutLeader"; -import { otherFace } from "../lib/otherFace"; +import { useDiffLayout } from "../../lib/content/diffLayout"; +import { isPreviewablePath } from "../../lib/content/fileView"; +import { useShortcutHint } from "../../hooks/shortcuts/shortcutLeader"; +import { otherFace } from "../../lib/content/otherFace"; import { MaximizeIcon, PreviewIcon, SplitViewIcon, WholeFileIcon, -} from "./icons/layout"; -import { PencilIcon } from "./icons/actions"; -import type { Pane } from "../types"; +} from "../icons/layout"; +import { PencilIcon } from "../icons/actions"; +import type { Pane } from "../../types"; export interface FilePaneActionsProps { pane: Pane; diff --git a/viewer-ui/src/components/content/HtmlEditor.tsx b/viewer-ui/src/components/content/HtmlEditor.tsx index 8dc65e59..05dbcf48 100644 --- a/viewer-ui/src/components/content/HtmlEditor.tsx +++ b/viewer-ui/src/components/content/HtmlEditor.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef } from "react"; -import { useHtmlEditor } from "../../hooks/useHtmlEditor"; +import { useHtmlEditor } from "../../hooks/git/useHtmlEditor"; import type { FromPreview, ToPreview } from "../../lib/edit/protocol"; /** How long to wait for the preview to commit an open edit before saving anyway. */ diff --git a/viewer-ui/src/components/LineNos.tsx b/viewer-ui/src/components/content/LineNos.tsx similarity index 100% rename from viewer-ui/src/components/LineNos.tsx rename to viewer-ui/src/components/content/LineNos.tsx diff --git a/viewer-ui/src/components/PathLabel.tsx b/viewer-ui/src/components/content/PathLabel.tsx similarity index 100% rename from viewer-ui/src/components/PathLabel.tsx rename to viewer-ui/src/components/content/PathLabel.tsx diff --git a/viewer-ui/src/components/VirtualDiffView.tsx b/viewer-ui/src/components/content/VirtualDiffView.tsx similarity index 93% rename from viewer-ui/src/components/VirtualDiffView.tsx rename to viewer-ui/src/components/content/VirtualDiffView.tsx index 99ee57e7..5aa502b9 100644 --- a/viewer-ui/src/components/VirtualDiffView.tsx +++ b/viewer-ui/src/components/content/VirtualDiffView.tsx @@ -1,9 +1,9 @@ import { useEffect, useMemo, useState } from "react"; -import type { Diff, DiffLine } from "../api"; -import { splitHunkRows } from "../lib/diffLayout"; -import { linenoDigits } from "../lib/gutter"; -import { diffLineBg } from "../lib/utils"; -import { virtualWindow, type ScrollViewport } from "../lib/virtualWindow"; +import type { Diff, DiffLine } from "../../api"; +import { splitHunkRows } from "../../lib/content/diffLayout"; +import { linenoDigits } from "../../lib/content/gutter"; +import { diffLineBg } from "../../lib/shared/utils"; +import { virtualWindow, type ScrollViewport } from "../../lib/ui/virtualWindow"; import { LineNos } from "./LineNos"; type Row = diff --git a/viewer-ui/src/components/VirtualFileLines.tsx b/viewer-ui/src/components/content/VirtualFileLines.tsx similarity index 86% rename from viewer-ui/src/components/VirtualFileLines.tsx rename to viewer-ui/src/components/content/VirtualFileLines.tsx index bca45055..650c9ed1 100644 --- a/viewer-ui/src/components/VirtualFileLines.tsx +++ b/viewer-ui/src/components/content/VirtualFileLines.tsx @@ -1,6 +1,6 @@ -import type { Span } from "../api"; -import { digitsFor } from "../lib/gutter"; -import { virtualWindow, type ScrollViewport } from "../lib/virtualWindow"; +import type { Span } from "../../api"; +import { digitsFor } from "../../lib/content/gutter"; +import { virtualWindow, type ScrollViewport } from "../../lib/ui/virtualWindow"; import { LineNos } from "./LineNos"; export function VirtualFileLines({ lines, viewport }: { lines: Span[][]; viewport: ScrollViewport }) { diff --git a/viewer-ui/src/components/virtualContent.test.tsx b/viewer-ui/src/components/content/virtualContent.test.tsx similarity index 99% rename from viewer-ui/src/components/virtualContent.test.tsx rename to viewer-ui/src/components/content/virtualContent.test.tsx index d87a3507..df666dec 100644 --- a/viewer-ui/src/components/virtualContent.test.tsx +++ b/viewer-ui/src/components/content/virtualContent.test.tsx @@ -2,7 +2,7 @@ import { cleanup, render } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import type { Diff, DiffLine, Span } from "../api"; +import type { Diff, DiffLine, Span } from "../../api"; import { DiffView } from "./DiffView"; import { VirtualDiffView } from "./VirtualDiffView"; import { VirtualFileLines } from "./VirtualFileLines"; diff --git a/viewer-ui/src/components/ConfirmCloseDialog.test.tsx b/viewer-ui/src/components/feedback/ConfirmCloseDialog.test.tsx similarity index 100% rename from viewer-ui/src/components/ConfirmCloseDialog.test.tsx rename to viewer-ui/src/components/feedback/ConfirmCloseDialog.test.tsx diff --git a/viewer-ui/src/components/ConfirmCloseDialog.tsx b/viewer-ui/src/components/feedback/ConfirmCloseDialog.tsx similarity index 100% rename from viewer-ui/src/components/ConfirmCloseDialog.tsx rename to viewer-ui/src/components/feedback/ConfirmCloseDialog.tsx diff --git a/viewer-ui/src/components/feedback/ErrorBoundary.tsx b/viewer-ui/src/components/feedback/ErrorBoundary.tsx index 1c8d0f7b..7bae38a4 100644 --- a/viewer-ui/src/components/feedback/ErrorBoundary.tsx +++ b/viewer-ui/src/components/feedback/ErrorBoundary.tsx @@ -1,5 +1,5 @@ import { Component, type ErrorInfo, type ReactNode } from "react"; -import { isChunkLoadError } from "../../lib/chunkError"; +import { isChunkLoadError } from "../../lib/runtime/chunkError"; /** * Keeps one failed subtree from taking the page with it. @@ -8,7 +8,7 @@ import { isChunkLoadError } from "../../lib/chunkError"; * error and the viewer goes blank — indistinguishable, to the person looking at * it, from the server having died. That is the shape a missing chunk arrived in: * open an HTML preview against a build the server has replaced, and the page - * disappears with nothing said. See `lib/chunkError.ts` for how that case is + * disappears with nothing said. See `lib/runtime/chunkError.ts` for how that case is * told apart, and why reloading is the only way out of it. * * A class because this is the one thing React has no hook for — `componentDidCatch` diff --git a/viewer-ui/src/components/feedback/Toaster.tsx b/viewer-ui/src/components/feedback/Toaster.tsx index bd527287..22e3db24 100644 --- a/viewer-ui/src/components/feedback/Toaster.tsx +++ b/viewer-ui/src/components/feedback/Toaster.tsx @@ -5,7 +5,7 @@ import { subscribeToasts, type Toast, type ToastKind, -} from "../../lib/toast"; +} from "../../lib/runtime/toast"; const DURATION_MS: Record = { error: 7000, diff --git a/viewer-ui/src/components/FolderPicker.tsx b/viewer-ui/src/components/repository/FolderPicker.tsx similarity index 97% rename from viewer-ui/src/components/FolderPicker.tsx rename to viewer-ui/src/components/repository/FolderPicker.tsx index 9d969f1e..f6edb153 100644 --- a/viewer-ui/src/components/FolderPicker.tsx +++ b/viewer-ui/src/components/repository/FolderPicker.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { api, type Browse, type Repo } from "../api"; -import { toast } from "../lib/toast"; -import { XIcon } from "./icons/actions"; +import { api, type Browse, type Repo } from "../../api"; +import { toast } from "../../lib/runtime/toast"; +import { XIcon } from "../icons/actions"; export function FolderPicker({ onClose, diff --git a/viewer-ui/src/components/LogList.tsx b/viewer-ui/src/components/repository/LogList.tsx similarity index 95% rename from viewer-ui/src/components/LogList.tsx rename to viewer-ui/src/components/repository/LogList.tsx index 7dda125e..fbad7a5d 100644 --- a/viewer-ui/src/components/LogList.tsx +++ b/viewer-ui/src/components/repository/LogList.tsx @@ -1,7 +1,7 @@ -import { PathLabel } from "./PathLabel"; -import { formatRelativeTime, statusColor } from "../lib/utils"; -import type { Commit } from "../api"; -import type { CommitDrillDown } from "../hooks/useLog"; +import { PathLabel } from "../content/PathLabel"; +import { formatRelativeTime, statusColor } from "../../lib/shared/utils"; +import type { Commit } from "../../api"; +import type { CommitDrillDown } from "../../hooks/git/useLog"; export interface LogListProps { visibleCommits: Commit[]; diff --git a/viewer-ui/src/components/ProjectMenu.tsx b/viewer-ui/src/components/repository/ProjectMenu.tsx similarity index 95% rename from viewer-ui/src/components/ProjectMenu.tsx rename to viewer-ui/src/components/repository/ProjectMenu.tsx index f9eca51a..b9e83190 100644 --- a/viewer-ui/src/components/ProjectMenu.tsx +++ b/viewer-ui/src/components/repository/ProjectMenu.tsx @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from "react"; -import { PlusIcon, XIcon } from "./icons/actions"; -import { ChevronIcon } from "./icons/navigation"; -import { useShortcutHint } from "../hooks/shortcutLeader"; -import type { Repo } from "../api"; +import { PlusIcon, XIcon } from "../icons/actions"; +import { ChevronIcon } from "../icons/navigation"; +import { useShortcutHint } from "../../hooks/shortcuts/shortcutLeader"; +import type { Repo } from "../../api"; export function ProjectMenu({ repos, diff --git a/viewer-ui/src/components/ProjectStrip.test.tsx b/viewer-ui/src/components/repository/ProjectStrip.test.tsx similarity index 95% rename from viewer-ui/src/components/ProjectStrip.test.tsx rename to viewer-ui/src/components/repository/ProjectStrip.test.tsx index e0d02052..3de02e3b 100644 --- a/viewer-ui/src/components/ProjectStrip.test.tsx +++ b/viewer-ui/src/components/repository/ProjectStrip.test.tsx @@ -3,9 +3,9 @@ import { cleanup, fireEvent, render, screen, within } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { ProjectStrip, type ProjectStripProps } from "./ProjectStrip"; -import { ShortcutLeaderProvider } from "../hooks/shortcutLeader"; -import { DEFAULT_LEADER } from "../lib/leaderChord"; -import type { Repo } from "../api"; +import { ShortcutLeaderProvider } from "../../hooks/shortcuts/shortcutLeader"; +import { DEFAULT_LEADER } from "../../lib/shortcuts/leaderChord"; +import type { Repo } from "../../api"; afterEach(cleanup); diff --git a/viewer-ui/src/components/ProjectStrip.tsx b/viewer-ui/src/components/repository/ProjectStrip.tsx similarity index 93% rename from viewer-ui/src/components/ProjectStrip.tsx rename to viewer-ui/src/components/repository/ProjectStrip.tsx index da781101..e61388b3 100644 --- a/viewer-ui/src/components/ProjectStrip.tsx +++ b/viewer-ui/src/components/repository/ProjectStrip.tsx @@ -1,9 +1,9 @@ -import { PlusIcon, XIcon } from "./icons/actions"; -import { useLeaderChord, useShortcutHint } from "../hooks/shortcutLeader"; -import { ariaKeyShortcuts, shortcutHintText } from "../lib/shortcutHint"; -import { tabLabel } from "../lib/tabLabel"; -import type { TabStripSide } from "../lib/tabStripSide"; -import type { Repo } from "../api"; +import { PlusIcon, XIcon } from "../icons/actions"; +import { useLeaderChord, useShortcutHint } from "../../hooks/shortcuts/shortcutLeader"; +import { ariaKeyShortcuts, shortcutHintText } from "../../lib/shortcuts/shortcutHint"; +import { tabLabel } from "../../lib/repository/tabLabel"; +import type { TabStripSide } from "../../lib/ui/tabStripSide"; +import type { Repo } from "../../api"; export interface ProjectStripProps { /** Across the header, or down the page's left edge — the same tabs, laid diff --git a/viewer-ui/src/components/Sidebar.tsx b/viewer-ui/src/components/repository/Sidebar.tsx similarity index 95% rename from viewer-ui/src/components/Sidebar.tsx rename to viewer-ui/src/components/repository/Sidebar.tsx index 555d9cd9..ffcc6b22 100644 --- a/viewer-ui/src/components/Sidebar.tsx +++ b/viewer-ui/src/components/repository/Sidebar.tsx @@ -1,17 +1,17 @@ import { useEffect, useRef } from "react"; import type { PointerEvent as ReactPointerEvent } from "react"; -import { focusRegionAttrs } from "../lib/shortcutDom"; -import { useTree } from "../hooks/useTree"; -import { buildTreeRows } from "../lib/tree"; -import { ancestorDirs, toggled } from "../lib/treeCache"; +import { focusRegionAttrs } from "../../lib/shortcuts/shortcutDom"; +import { useTree } from "../../hooks/git/useTree"; +import { buildTreeRows } from "../../lib/repository/tree"; +import { ancestorDirs, toggled } from "../../lib/repository/treeCache"; import { SidebarTabs } from "./SidebarTabs"; import { StatusList } from "./StatusList"; import { LogList } from "./LogList"; import { TreeList } from "./TreeList"; -import type { ChangedFile, Commit, Status } from "../api"; -import type { CommitDrillDown } from "../hooks/useLog"; -import type { Tab } from "../types"; -import type { MobileView } from "../types"; +import type { ChangedFile, Commit, Status } from "../../api"; +import type { CommitDrillDown } from "../../hooks/git/useLog"; +import type { Tab } from "../../types"; +import type { MobileView } from "../../types"; export interface SidebarProps { tab: Tab; diff --git a/viewer-ui/src/components/SidebarTabs.tsx b/viewer-ui/src/components/repository/SidebarTabs.tsx similarity index 91% rename from viewer-ui/src/components/SidebarTabs.tsx rename to viewer-ui/src/components/repository/SidebarTabs.tsx index 892c54eb..cf700824 100644 --- a/viewer-ui/src/components/SidebarTabs.tsx +++ b/viewer-ui/src/components/repository/SidebarTabs.tsx @@ -1,7 +1,7 @@ -import { SearchIcon } from "./icons/actions"; -import { useShortcutHint } from "../hooks/shortcutLeader"; -import type { ShortcutActionId } from "../lib/shortcutActions"; -import type { Tab } from "../types"; +import { SearchIcon } from "../icons/actions"; +import { useShortcutHint } from "../../hooks/shortcuts/shortcutLeader"; +import type { ShortcutActionId } from "../../lib/shortcuts/shortcutActions"; +import type { Tab } from "../../types"; // Lifted out of `Sidebar` unchanged, because the keys these three tabs answer to // have to be named on them and `Sidebar` had no room left: the row is a self diff --git a/viewer-ui/src/components/StatusList.tsx b/viewer-ui/src/components/repository/StatusList.tsx similarity index 85% rename from viewer-ui/src/components/StatusList.tsx rename to viewer-ui/src/components/repository/StatusList.tsx index 61566afd..bcb5fe9a 100644 --- a/viewer-ui/src/components/StatusList.tsx +++ b/viewer-ui/src/components/repository/StatusList.tsx @@ -1,8 +1,8 @@ -import { PathLabel } from "./PathLabel"; -import { HOT_CLASS } from "../hooks/ui/useHotClock"; -import { classifyHot } from "../lib/hot"; -import { statusColor } from "../lib/utils"; -import type { ChangedFile, Status } from "../api"; +import { PathLabel } from "../content/PathLabel"; +import { HOT_CLASS } from "../../hooks/ui/useHotClock"; +import { classifyHot } from "../../lib/repository/hot"; +import { statusColor } from "../../lib/shared/utils"; +import type { ChangedFile, Status } from "../../api"; export interface StatusListProps { status: Status | null; diff --git a/viewer-ui/src/components/TreeList.tsx b/viewer-ui/src/components/repository/TreeList.tsx similarity index 93% rename from viewer-ui/src/components/TreeList.tsx rename to viewer-ui/src/components/repository/TreeList.tsx index f046293d..55dee351 100644 --- a/viewer-ui/src/components/TreeList.tsx +++ b/viewer-ui/src/components/repository/TreeList.tsx @@ -1,6 +1,6 @@ -import { ChevronIcon } from "./icons/navigation"; -import type { TreeMatch } from "../api"; -import type { TreeRow } from "../lib/tree"; +import { ChevronIcon } from "../icons/navigation"; +import type { TreeMatch } from "../../api"; +import type { TreeRow } from "../../lib/repository/tree"; export interface TreeListProps { treeSearching: boolean; diff --git a/viewer-ui/src/components/Brand.tsx b/viewer-ui/src/components/shell/Brand.tsx similarity index 100% rename from viewer-ui/src/components/Brand.tsx rename to viewer-ui/src/components/shell/Brand.tsx diff --git a/viewer-ui/src/components/Header.test.tsx b/viewer-ui/src/components/shell/Header.test.tsx similarity index 96% rename from viewer-ui/src/components/Header.test.tsx rename to viewer-ui/src/components/shell/Header.test.tsx index 602443d4..f0d679ed 100644 --- a/viewer-ui/src/components/Header.test.tsx +++ b/viewer-ui/src/components/shell/Header.test.tsx @@ -3,9 +3,9 @@ import { cleanup, fireEvent, render, screen, within } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { Header, type HeaderProps } from "./Header"; -import { ShortcutLeaderProvider } from "../hooks/shortcutLeader"; -import { DEFAULT_LEADER } from "../lib/leaderChord"; -import type { Repo } from "../api"; +import { ShortcutLeaderProvider } from "../../hooks/shortcuts/shortcutLeader"; +import { DEFAULT_LEADER } from "../../lib/shortcuts/leaderChord"; +import type { Repo } from "../../api"; afterEach(cleanup); diff --git a/viewer-ui/src/components/Header.tsx b/viewer-ui/src/components/shell/Header.tsx similarity index 92% rename from viewer-ui/src/components/Header.tsx rename to viewer-ui/src/components/shell/Header.tsx index 571c0be1..eee26f04 100644 --- a/viewer-ui/src/components/Header.tsx +++ b/viewer-ui/src/components/shell/Header.tsx @@ -1,12 +1,12 @@ import { Brand } from "./Brand"; -import { ProjectMenu } from "./ProjectMenu"; -import { ProjectStrip } from "./ProjectStrip"; -import { LogOutIcon } from "./icons/actions"; -import { TabStripIcon } from "./icons/layout"; +import { ProjectMenu } from "../repository/ProjectMenu"; +import { ProjectStrip } from "../repository/ProjectStrip"; +import { LogOutIcon } from "../icons/actions"; +import { TabStripIcon } from "../icons/layout"; import { SettingsMenu } from "./SettingsMenu"; -import type { TabStrip } from "../hooks/ui/tabStripSide"; -import { otherSide } from "../lib/tabStripSide"; -import type { Repo } from "../api"; +import type { TabStrip } from "../../hooks/ui/tabStripSide"; +import { otherSide } from "../../lib/ui/tabStripSide"; +import type { Repo } from "../../api"; export interface HeaderProps { repos: Repo[]; diff --git a/viewer-ui/src/components/LoadingSplash.tsx b/viewer-ui/src/components/shell/LoadingSplash.tsx similarity index 100% rename from viewer-ui/src/components/LoadingSplash.tsx rename to viewer-ui/src/components/shell/LoadingSplash.tsx diff --git a/viewer-ui/src/components/Login.tsx b/viewer-ui/src/components/shell/Login.tsx similarity index 98% rename from viewer-ui/src/components/Login.tsx rename to viewer-ui/src/components/shell/Login.tsx index 7c3b98b4..7f4c23a8 100644 --- a/viewer-ui/src/components/Login.tsx +++ b/viewer-ui/src/components/shell/Login.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { api } from "../api"; +import { api } from "../../api"; import { Mark } from "./Mark"; export function Login({ onSuccess }: { onSuccess: () => void }) { diff --git a/viewer-ui/src/components/Mark.tsx b/viewer-ui/src/components/shell/Mark.tsx similarity index 91% rename from viewer-ui/src/components/Mark.tsx rename to viewer-ui/src/components/shell/Mark.tsx index 4f550ed8..2c15993b 100644 --- a/viewer-ui/src/components/Mark.tsx +++ b/viewer-ui/src/components/shell/Mark.tsx @@ -3,7 +3,7 @@ // separately-fetched crow raced that unmount and flickered in and out depending // on the network. Baked into the bundle it paints with the tile, every time — // and stays correct even if `/crow-mono.svg` is not served. -import crowMonoDataUri from "../../public/crow-mono.svg?inline"; +import crowMonoDataUri from "../../../public/crow-mono.svg?inline"; export function Mark({ className }: { className?: string }) { return ( diff --git a/viewer-ui/src/components/RepoMobileNav.tsx b/viewer-ui/src/components/shell/RepoMobileNav.tsx similarity index 92% rename from viewer-ui/src/components/RepoMobileNav.tsx rename to viewer-ui/src/components/shell/RepoMobileNav.tsx index a2c13676..912ff447 100644 --- a/viewer-ui/src/components/RepoMobileNav.tsx +++ b/viewer-ui/src/components/shell/RepoMobileNav.tsx @@ -1,6 +1,6 @@ import type { ComponentType, SVGProps } from "react"; -import type { MobileView } from "../types"; -import { FileTextIcon, ListIcon, TerminalIcon } from "./icons/navigation"; +import type { MobileView } from "../../types"; +import { FileTextIcon, ListIcon, TerminalIcon } from "../icons/navigation"; interface MobileDestination { key: MobileView; diff --git a/viewer-ui/src/components/RepoShell.tsx b/viewer-ui/src/components/shell/RepoShell.tsx similarity index 92% rename from viewer-ui/src/components/RepoShell.tsx rename to viewer-ui/src/components/shell/RepoShell.tsx index 5a2efbd1..f31f0f9a 100644 --- a/viewer-ui/src/components/RepoShell.tsx +++ b/viewer-ui/src/components/shell/RepoShell.tsx @@ -1,19 +1,19 @@ import { Suspense, lazy, useEffect } from "react"; import type { CSSProperties } from "react"; -import { MAX_SIDEBAR_VIEWPORT_FRACTION } from "../hooks/ui/sidebar"; -import { Sidebar } from "./Sidebar"; -import type { SidebarProps } from "./Sidebar"; -import { FilePane } from "./FilePane"; -import type { FilePaneProps } from "./FilePane"; -import type { Repo, Status } from "../api"; -import type { ShellLayout } from "../hooks/useShellLayout"; -import type { Maximized, MobileView } from "../types"; +import { MAX_SIDEBAR_VIEWPORT_FRACTION } from "../../hooks/ui/sidebar"; +import { Sidebar } from "../repository/Sidebar"; +import type { SidebarProps } from "../repository/Sidebar"; +import { FilePane } from "../content/FilePane"; +import type { FilePaneProps } from "../content/FilePane"; +import type { Repo, Status } from "../../api"; +import type { ShellLayout } from "../../hooks/ui/useShellLayout"; +import type { Maximized, MobileView } from "../../types"; import { RepoMobileNav } from "./RepoMobileNav"; -import { ErrorBoundary } from "./feedback/ErrorBoundary"; +import { ErrorBoundary } from "../feedback/ErrorBoundary"; // Keep xterm out of the initial login and git-viewer bundle. const TerminalPanel = lazy(() => - import("./terminal/Terminal").then((m) => ({ default: m.TerminalPanel })), + import("../terminal/Terminal").then((m) => ({ default: m.TerminalPanel })), ); export interface RepoShellProps { diff --git a/viewer-ui/src/components/SettingsMenu.test.tsx b/viewer-ui/src/components/shell/SettingsMenu.test.tsx similarity index 92% rename from viewer-ui/src/components/SettingsMenu.test.tsx rename to viewer-ui/src/components/shell/SettingsMenu.test.tsx index 81c7a7b9..d244786f 100644 --- a/viewer-ui/src/components/SettingsMenu.test.tsx +++ b/viewer-ui/src/components/shell/SettingsMenu.test.tsx @@ -4,11 +4,11 @@ import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import type { ComponentProps } from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { SettingsMenu } from "./SettingsMenu"; -import { ShortcutLeaderProvider } from "../hooks/shortcutLeader"; -import { ScreenScaleProvider } from "../hooks/ui/screenScale"; -import { DEFAULT_LEADER } from "../lib/leaderChord"; -import { describeShortcutTarget } from "../lib/shortcutDom"; -import { shortcutsSuppressed } from "../lib/shortcutTarget"; +import { ShortcutLeaderProvider } from "../../hooks/shortcuts/shortcutLeader"; +import { ScreenScaleProvider } from "../../hooks/ui/screenScale"; +import { DEFAULT_LEADER } from "../../lib/shortcuts/leaderChord"; +import { describeShortcutTarget } from "../../lib/shortcuts/shortcutDom"; +import { shortcutsSuppressed } from "../../lib/shortcuts/shortcutTarget"; afterEach(() => { cleanup(); diff --git a/viewer-ui/src/components/SettingsMenu.tsx b/viewer-ui/src/components/shell/SettingsMenu.tsx similarity index 97% rename from viewer-ui/src/components/SettingsMenu.tsx rename to viewer-ui/src/components/shell/SettingsMenu.tsx index e608adc5..f0fb035d 100644 --- a/viewer-ui/src/components/SettingsMenu.tsx +++ b/viewer-ui/src/components/shell/SettingsMenu.tsx @@ -1,8 +1,8 @@ import { useLayoutEffect, useRef, useState } from "react"; -import { RefreshIcon, SettingsIcon } from "./icons/actions"; -import { KeyboardIcon } from "./icons/layout"; -import { useShortcutHint } from "../hooks/shortcutLeader"; -import { SCREEN_SCALES, useScreenScale, type ScreenScale } from "../hooks/ui/screenScale"; +import { RefreshIcon, SettingsIcon } from "../icons/actions"; +import { KeyboardIcon } from "../icons/layout"; +import { useShortcutHint } from "../../hooks/shortcuts/shortcutLeader"; +import { SCREEN_SCALES, useScreenScale, type ScreenScale } from "../../hooks/ui/screenScale"; const TITLE_ID = "nc-settings-title"; const DIALOG_ID = "nc-settings-dialog"; diff --git a/viewer-ui/src/components/shortcuts/LeaderSettings.tsx b/viewer-ui/src/components/shortcuts/LeaderSettings.tsx index dfd0d19d..221b8994 100644 --- a/viewer-ui/src/components/shortcuts/LeaderSettings.tsx +++ b/viewer-ui/src/components/shortcuts/LeaderSettings.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import type { ShortcutSettings } from "../../hooks/useShortcutSettings"; +import type { ShortcutSettings } from "../../hooks/shortcuts/useShortcutSettings"; /** * The leader, and the three things that can be done to it. diff --git a/viewer-ui/src/components/ShortcutHelp.harness.tsx b/viewer-ui/src/components/shortcuts/ShortcutHelp.harness.tsx similarity index 92% rename from viewer-ui/src/components/ShortcutHelp.harness.tsx rename to viewer-ui/src/components/shortcuts/ShortcutHelp.harness.tsx index 98020896..f120c3cf 100644 --- a/viewer-ui/src/components/ShortcutHelp.harness.tsx +++ b/viewer-ui/src/components/shortcuts/ShortcutHelp.harness.tsx @@ -5,12 +5,12 @@ import { ShortcutIntentProvider, useRegisterShortcutHandlers, type ShortcutHandlers, -} from "../hooks/shortcutIntents"; +} from "../../hooks/shortcuts/shortcutIntents"; import { useShortcutSettings, type ShortcutSettings, -} from "../hooks/useShortcutSettings"; -import { stubLocalStorage } from "../lib/fakeStorage"; +} from "../../hooks/shortcuts/useShortcutSettings"; +import { stubLocalStorage } from "../../lib/shared/fakeStorage"; /** Stands in for the page and the terminal panel: whatever the test says can * run right now is what the sheet is allowed to dim or offer. */ diff --git a/viewer-ui/src/components/ShortcutHelp.leader.test.tsx b/viewer-ui/src/components/shortcuts/ShortcutHelp.leader.test.tsx similarity index 100% rename from viewer-ui/src/components/ShortcutHelp.leader.test.tsx rename to viewer-ui/src/components/shortcuts/ShortcutHelp.leader.test.tsx diff --git a/viewer-ui/src/components/ShortcutHelp.test.tsx b/viewer-ui/src/components/shortcuts/ShortcutHelp.test.tsx similarity index 99% rename from viewer-ui/src/components/ShortcutHelp.test.tsx rename to viewer-ui/src/components/shortcuts/ShortcutHelp.test.tsx index 378154df..015266b6 100644 --- a/viewer-ui/src/components/ShortcutHelp.test.tsx +++ b/viewer-ui/src/components/shortcuts/ShortcutHelp.test.tsx @@ -6,7 +6,7 @@ import { mount, row, sheet } from "./ShortcutHelp.harness"; import { SHORTCUT_ACTIONS, UNSUPPORTED_TUI_ACTIONS, -} from "../lib/shortcutActions"; +} from "../../lib/shortcuts/shortcutActions"; afterEach(cleanup); diff --git a/viewer-ui/src/components/ShortcutHelp.tsx b/viewer-ui/src/components/shortcuts/ShortcutHelp.tsx similarity index 94% rename from viewer-ui/src/components/ShortcutHelp.tsx rename to viewer-ui/src/components/shortcuts/ShortcutHelp.tsx index 80f4d46c..52ba663f 100644 --- a/viewer-ui/src/components/ShortcutHelp.tsx +++ b/viewer-ui/src/components/shortcuts/ShortcutHelp.tsx @@ -1,16 +1,16 @@ import { useEffect, useRef } from "react"; -import { XIcon } from "./icons/actions"; -import { LeaderSettings } from "./shortcuts/LeaderSettings"; -import { ShortcutRow } from "./shortcuts/ShortcutRow"; -import { useShortcutIntents, useShortcutAvailability } from "../hooks/shortcutIntents"; -import type { ShortcutSettings } from "../hooks/useShortcutSettings"; -import { ariaKeyShortcuts, shortcutKeys } from "../lib/shortcutHint"; +import { XIcon } from "../icons/actions"; +import { LeaderSettings } from "./LeaderSettings"; +import { ShortcutRow } from "./ShortcutRow"; +import { useShortcutIntents, useShortcutAvailability } from "../../hooks/shortcuts/shortcutIntents"; +import type { ShortcutSettings } from "../../hooks/shortcuts/useShortcutSettings"; +import { ariaKeyShortcuts, shortcutKeys } from "../../lib/shortcuts/shortcutHint"; import { SHORTCUT_ACTIONS, UNSUPPORTED_TUI_ACTIONS, type ShortcutAction, type ShortcutGroup, -} from "../lib/shortcutActions"; +} from "../../lib/shortcuts/shortcutActions"; // What the keyboard can do, said out loud. // diff --git a/viewer-ui/src/components/shortcuts/ShortcutHintBar.test.tsx b/viewer-ui/src/components/shortcuts/ShortcutHintBar.test.tsx index bceb3a75..779f16ad 100644 --- a/viewer-ui/src/components/shortcuts/ShortcutHintBar.test.tsx +++ b/viewer-ui/src/components/shortcuts/ShortcutHintBar.test.tsx @@ -2,11 +2,11 @@ import { cleanup, fireEvent, render } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { ShortcutIntentProvider, useRegisterShortcutHandlers } from "../../hooks/shortcutIntents"; -import { ShortcutLeaderProvider } from "../../hooks/shortcutLeader"; -import { DEFAULT_LEADER, type ChordSpec } from "../../lib/leaderChord"; -import { IDLE_LEADER, type LeaderState } from "../../lib/leaderState"; -import type { HintClick } from "../../lib/shortcutHintBar"; +import { ShortcutIntentProvider, useRegisterShortcutHandlers } from "../../hooks/shortcuts/shortcutIntents"; +import { ShortcutLeaderProvider } from "../../hooks/shortcuts/shortcutLeader"; +import { DEFAULT_LEADER, type ChordSpec } from "../../lib/shortcuts/leaderChord"; +import { IDLE_LEADER, type LeaderState } from "../../lib/shortcuts/leaderState"; +import type { HintClick } from "../../lib/shortcuts/shortcutHintBar"; import { ShortcutHintBar } from "./ShortcutHintBar"; afterEach(cleanup); diff --git a/viewer-ui/src/components/shortcuts/ShortcutHintBar.tsx b/viewer-ui/src/components/shortcuts/ShortcutHintBar.tsx index b6c7a0ef..ec779fc7 100644 --- a/viewer-ui/src/components/shortcuts/ShortcutHintBar.tsx +++ b/viewer-ui/src/components/shortcuts/ShortcutHintBar.tsx @@ -1,8 +1,8 @@ import { Fragment } from "react"; -import { useLeaderChord } from "../../hooks/shortcutLeader"; -import { useShortcutAvailability } from "../../hooks/shortcutIntents"; -import type { ShortcutHint } from "../../hooks/useAppShortcuts"; -import { hintLine, type HintSegment } from "../../lib/shortcutHintBar"; +import { useLeaderChord } from "../../hooks/shortcuts/shortcutLeader"; +import { useShortcutAvailability } from "../../hooks/shortcuts/shortcutIntents"; +import type { ShortcutHint } from "../../hooks/shortcuts/useAppShortcuts"; +import { hintLine, type HintSegment } from "../../lib/shortcuts/shortcutHintBar"; // The line under the page that says what the keyboard does next, as the TUI // has under every screen. The text comes from `hintLine`; this only prints it. diff --git a/viewer-ui/src/components/shortcuts/ShortcutRow.tsx b/viewer-ui/src/components/shortcuts/ShortcutRow.tsx index 5f0ca730..801000db 100644 --- a/viewer-ui/src/components/shortcuts/ShortcutRow.tsx +++ b/viewer-ui/src/components/shortcuts/ShortcutRow.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import type { ShortcutAction } from "../../lib/shortcutActions"; +import type { ShortcutAction } from "../../lib/shortcuts/shortcutActions"; /** The keys of one shortcut, one `` per step: the leader chord and then * its follow-up key, or a standalone chord on its own. */ diff --git a/viewer-ui/src/components/terminal/AttachNotice.tsx b/viewer-ui/src/components/terminal/AttachNotice.tsx index 570c842b..d4017acb 100644 --- a/viewer-ui/src/components/terminal/AttachNotice.tsx +++ b/viewer-ui/src/components/terminal/AttachNotice.tsx @@ -1,4 +1,4 @@ -import { attachLabel, type AttachStatus } from "../../lib/attachStatus"; +import { attachLabel, type AttachStatus } from "../../lib/terminal/attachStatus"; /** * What a panel holding no terminals says for itself. diff --git a/viewer-ui/src/components/terminal/ComposeDialog.tsx b/viewer-ui/src/components/terminal/ComposeDialog.tsx index 5bc17020..0f2a5274 100644 --- a/viewer-ui/src/components/terminal/ComposeDialog.tsx +++ b/viewer-ui/src/components/terminal/ComposeDialog.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { XIcon } from "../icons/actions"; -import { isSendable, sendChordLabel } from "../../lib/composeInput"; +import { isSendable, sendChordLabel } from "../../lib/terminal/composeInput"; const TITLE_ID = "nc-compose-title"; diff --git a/viewer-ui/src/components/terminal/PaneGrid.tsx b/viewer-ui/src/components/terminal/PaneGrid.tsx index 6d759948..af8fd0dc 100644 --- a/viewer-ui/src/components/terminal/PaneGrid.tsx +++ b/viewer-ui/src/components/terminal/PaneGrid.tsx @@ -1,7 +1,7 @@ import type { CSSProperties, MutableRefObject } from "react"; -import type { CellPlacement } from "../../lib/terminalLayout"; -import type { RecoveryByPane } from "../../lib/recovery"; -import { stackedCellStyle, type PaneViewMode } from "../../lib/paneViewMode"; +import type { CellPlacement } from "../../lib/terminal/terminalLayout"; +import type { RecoveryByPane } from "../../lib/terminal/recovery"; +import { stackedCellStyle, type PaneViewMode } from "../../lib/terminal/paneViewMode"; import { TerminalCell } from "./TerminalCell"; import { StartupSlots } from "./StartupSlots"; diff --git a/viewer-ui/src/components/terminal/PaneTabs.tsx b/viewer-ui/src/components/terminal/PaneTabs.tsx index ce57b465..8d824042 100644 --- a/viewer-ui/src/components/terminal/PaneTabs.tsx +++ b/viewer-ui/src/components/terminal/PaneTabs.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; import { XIcon } from "../icons/actions"; -import { TAB_TITLE_MAX_CELLS, truncateCells } from "../../lib/terminalLayout"; +import { TAB_TITLE_MAX_CELLS, truncateCells } from "../../lib/terminal/terminalLayout"; export interface PaneTabsProps { panes: number[]; diff --git a/viewer-ui/src/components/terminal/PanelToolbar.tsx b/viewer-ui/src/components/terminal/PanelToolbar.tsx index 7661d56c..3e1d7317 100644 --- a/viewer-ui/src/components/terminal/PanelToolbar.tsx +++ b/viewer-ui/src/components/terminal/PanelToolbar.tsx @@ -9,10 +9,10 @@ import { TabViewIcon, } from "../icons/layout"; import { RecoveryChip } from "./RecoveryChip"; -import { useShortcutHint } from "../../hooks/shortcutLeader"; -import { orphanRecovery, type RecoveryByPane } from "../../lib/recovery"; -import { focusFillsEmptyPanel } from "../../lib/paneFocus"; -import type { PaneViewMode } from "../../lib/paneViewMode"; +import { useShortcutHint } from "../../hooks/shortcuts/shortcutLeader"; +import { orphanRecovery, type RecoveryByPane } from "../../lib/terminal/recovery"; +import { focusFillsEmptyPanel } from "../../lib/terminal/paneFocus"; +import type { PaneViewMode } from "../../lib/terminal/paneViewMode"; export interface PanelToolbarProps { mode: PaneViewMode; diff --git a/viewer-ui/src/components/terminal/RecoveryChip.tsx b/viewer-ui/src/components/terminal/RecoveryChip.tsx index 025b2c34..c1bf242b 100644 --- a/viewer-ui/src/components/terminal/RecoveryChip.tsx +++ b/viewer-ui/src/components/terminal/RecoveryChip.tsx @@ -1,5 +1,5 @@ import { XIcon } from "../icons/actions"; -import { recoverySummary, type PaneRecovery } from "../../lib/recovery"; +import { recoverySummary, type PaneRecovery } from "../../lib/terminal/recovery"; interface RecoveryChipProps { report: PaneRecovery; diff --git a/viewer-ui/src/components/terminal/TermKeyBar.tsx b/viewer-ui/src/components/terminal/TermKeyBar.tsx index 0ec35f23..2affedc7 100644 --- a/viewer-ui/src/components/terminal/TermKeyBar.tsx +++ b/viewer-ui/src/components/terminal/TermKeyBar.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { PencilIcon } from "../icons/actions"; -import { TERM_KEY_BAR, type TermKey } from "../../lib/termKeys"; +import { TERM_KEY_BAR, type TermKey } from "../../lib/terminal/termKeys"; import type { ModifierLatch } from "../../hooks/terminal/useModifierLatch"; const KEY_BUTTON = diff --git a/viewer-ui/src/components/terminal/Terminal.tsx b/viewer-ui/src/components/terminal/Terminal.tsx index 0ca01886..a8210e18 100644 --- a/viewer-ui/src/components/terminal/Terminal.tsx +++ b/viewer-ui/src/components/terminal/Terminal.tsx @@ -1,5 +1,5 @@ import { useCallback } from "react"; -import { planLayout } from "../../lib/terminalLayout"; +import { planLayout } from "../../lib/terminal/terminalLayout"; import { usePaneDrag } from "../../hooks/terminal/usePaneDrag"; import { usePaneRecovery } from "../../hooks/terminal/usePaneRecovery"; import { usePaneCommands } from "../../hooks/terminal/usePaneCommands"; @@ -20,12 +20,12 @@ import { TermKeyBar } from "./TermKeyBar"; import { useTouchScroll } from "../../hooks/terminal/useTouchScroll"; import { usePaneViewMode } from "../../hooks/ui/paneViewMode"; import { useTermKeyBar } from "../../hooks/ui/termKeyBar"; -import { rememberPane } from "../../lib/lastPane"; -import { shownTab } from "../../lib/paneViewMode"; +import { rememberPane } from "../../lib/terminal/lastPane"; +import { shownTab } from "../../lib/terminal/paneViewMode"; import { PanelDivider, type PanelDividerProps } from "./PanelDivider"; import { PanelToolbar } from "./PanelToolbar"; -import { renderedZoom } from "../../lib/zoom"; -import { attachLabel, attachStatus } from "../../lib/attachStatus"; +import { renderedZoom } from "../../lib/terminal/zoom"; +import { attachLabel, attachStatus } from "../../lib/terminal/attachStatus"; import { useScreenScale } from "../../hooks/ui/screenScale"; export function TerminalPanel({ diff --git a/viewer-ui/src/components/terminal/TerminalCell.tsx b/viewer-ui/src/components/terminal/TerminalCell.tsx index 46b0ff43..16cbb1b1 100644 --- a/viewer-ui/src/components/terminal/TerminalCell.tsx +++ b/viewer-ui/src/components/terminal/TerminalCell.tsx @@ -2,9 +2,9 @@ import type { CSSProperties } from "react"; import { XIcon } from "../icons/actions"; import { MaximizeIcon } from "../icons/layout"; import { RecoveryChip } from "./RecoveryChip"; -import { useShortcutHint } from "../../hooks/shortcutLeader"; -import type { PaneRecovery } from "../../lib/recovery"; -import { TAB_TITLE_MAX_CELLS, truncateCells } from "../../lib/terminalLayout"; +import { useShortcutHint } from "../../hooks/shortcuts/shortcutLeader"; +import type { PaneRecovery } from "../../lib/terminal/recovery"; +import { TAB_TITLE_MAX_CELLS, truncateCells } from "../../lib/terminal/terminalLayout"; interface TerminalCellProps { pane: number; diff --git a/viewer-ui/src/components/terminal/TerminalDialogs.tsx b/viewer-ui/src/components/terminal/TerminalDialogs.tsx index 4875ce41..f23097f1 100644 --- a/viewer-ui/src/components/terminal/TerminalDialogs.tsx +++ b/viewer-ui/src/components/terminal/TerminalDialogs.tsx @@ -1,4 +1,4 @@ -import { ConfirmCloseDialog } from "../ConfirmCloseDialog"; +import { ConfirmCloseDialog } from "../feedback/ConfirmCloseDialog"; import { ComposeDialog } from "./ComposeDialog"; import type { useCompose } from "../../hooks/terminal/useCompose"; diff --git a/viewer-ui/src/hooks/useAppViewModel.ts b/viewer-ui/src/hooks/app/useAppViewModel.ts similarity index 91% rename from viewer-ui/src/hooks/useAppViewModel.ts rename to viewer-ui/src/hooks/app/useAppViewModel.ts index 677b65d3..3e396bee 100644 --- a/viewer-ui/src/hooks/useAppViewModel.ts +++ b/viewer-ui/src/hooks/app/useAppViewModel.ts @@ -1,16 +1,16 @@ import { useCallback, useState } from "react"; -import { isUnauthorized } from "../api"; -import { appRows } from "../layout/appLayout"; -import { toast } from "../lib/toast"; -import { useAppShortcuts } from "./useAppShortcuts"; -import { useClone } from "./useClone"; -import { useProjectTabs } from "./useProjectTabs"; +import { isUnauthorized } from "../../api"; +import { appRows } from "../../layout/appLayout"; +import { toast } from "../../lib/runtime/toast"; +import { useAppShortcuts } from "../shortcuts/useAppShortcuts"; +import { useClone } from "../repository/useClone"; +import { useProjectTabs } from "../repository/useProjectTabs"; import { useReloadConfig } from "./useReloadConfig"; -import { useRepoActions } from "./useRepoActions"; -import { useRepoWorkspace } from "./useRepoWorkspace"; +import { useRepoActions } from "../repository/useRepoActions"; +import { useRepoWorkspace } from "../git/useRepoWorkspace"; import { useResumeTick } from "./useResumeTick"; -import { useShellLayout } from "./useShellLayout"; -import { useTabStripSide } from "./ui/tabStripSide"; +import { useShellLayout } from "../ui/useShellLayout"; +import { useTabStripSide } from "../ui/tabStripSide"; /** The page-level seams between authentication, projects, and repository UI. */ export function useAppViewModel() { diff --git a/viewer-ui/src/hooks/useReloadConfig.ts b/viewer-ui/src/hooks/app/useReloadConfig.ts similarity index 94% rename from viewer-ui/src/hooks/useReloadConfig.ts rename to viewer-ui/src/hooks/app/useReloadConfig.ts index 73bd1248..e91546db 100644 --- a/viewer-ui/src/hooks/useReloadConfig.ts +++ b/viewer-ui/src/hooks/app/useReloadConfig.ts @@ -1,6 +1,6 @@ import { useCallback, useRef, useState } from "react"; -import { api } from "../api"; -import { toast } from "../lib/toast"; +import { api } from "../../api"; +import { toast } from "../../lib/runtime/toast"; /** * Ask the server to re-read `config.toml`. diff --git a/viewer-ui/src/hooks/useResumeTick.ts b/viewer-ui/src/hooks/app/useResumeTick.ts similarity index 100% rename from viewer-ui/src/hooks/useResumeTick.ts rename to viewer-ui/src/hooks/app/useResumeTick.ts diff --git a/viewer-ui/src/hooks/useDrillDownEviction.ts b/viewer-ui/src/hooks/git/useDrillDownEviction.ts similarity index 96% rename from viewer-ui/src/hooks/useDrillDownEviction.ts rename to viewer-ui/src/hooks/git/useDrillDownEviction.ts index 99f9eff0..1a48be2c 100644 --- a/viewer-ui/src/hooks/useDrillDownEviction.ts +++ b/viewer-ui/src/hooks/git/useDrillDownEviction.ts @@ -1,5 +1,5 @@ import { useLayoutEffect } from "react"; -import type { Commit } from "../api"; +import type { Commit } from "../../api"; import type { CommitDrillDown } from "./useLog"; /** diff --git a/viewer-ui/src/hooks/useHtmlEditor.test.ts b/viewer-ui/src/hooks/git/useHtmlEditor.test.ts similarity index 99% rename from viewer-ui/src/hooks/useHtmlEditor.test.ts rename to viewer-ui/src/hooks/git/useHtmlEditor.test.ts index 7a403b36..ebb61212 100644 --- a/viewer-ui/src/hooks/useHtmlEditor.test.ts +++ b/viewer-ui/src/hooks/git/useHtmlEditor.test.ts @@ -9,7 +9,7 @@ import { act, renderHook, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("../api", () => ({ +vi.mock("../../api", () => ({ api: { previewUrl: (repo: string, path: string) => `/api/preview?repo=${repo}&path=${path}`, editPreviewUrl: (token: string) => `/api/preview/edit?token=${token}`, @@ -18,7 +18,7 @@ vi.mock("../api", () => ({ }, })); -import { api } from "../api"; +import { api } from "../../api"; import { useHtmlEditor } from "./useHtmlEditor"; const editPreview = api.editPreview as ReturnType; diff --git a/viewer-ui/src/hooks/useHtmlEditor.ts b/viewer-ui/src/hooks/git/useHtmlEditor.ts similarity index 95% rename from viewer-ui/src/hooks/useHtmlEditor.ts rename to viewer-ui/src/hooks/git/useHtmlEditor.ts index b3d2bfed..3a4db3b7 100644 --- a/viewer-ui/src/hooks/useHtmlEditor.ts +++ b/viewer-ui/src/hooks/git/useHtmlEditor.ts @@ -1,10 +1,10 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { api } from "../api"; -import { applyLiveLocks } from "../lib/edit/verify"; -import { applyPatches } from "../lib/edit/patch"; -import { parseBlocks } from "../lib/edit/parse"; -import { previewInserts } from "../lib/edit/preview"; -import type { Block, LockReason, Patch } from "../lib/edit/types"; +import { api } from "../../api"; +import { applyLiveLocks } from "../../lib/edit/verify"; +import { applyPatches } from "../../lib/edit/patch"; +import { parseBlocks } from "../../lib/edit/parse"; +import { previewInserts } from "../../lib/edit/preview"; +import type { Block, LockReason, Patch } from "../../lib/edit/types"; /** Why a block refused the click, in the words the pane shows. */ const LOCK_REASON: Record = { diff --git a/viewer-ui/src/hooks/useLog.test.ts b/viewer-ui/src/hooks/git/useLog.test.ts similarity index 98% rename from viewer-ui/src/hooks/useLog.test.ts rename to viewer-ui/src/hooks/git/useLog.test.ts index c84106e1..9ba2b681 100644 --- a/viewer-ui/src/hooks/useLog.test.ts +++ b/viewer-ui/src/hooks/git/useLog.test.ts @@ -3,14 +3,14 @@ import { act, cleanup, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useLog } from "./useLog"; -import type { Commit, Log } from "../api"; +import type { Commit, Log } from "../../api"; -vi.mock("../api", () => ({ +vi.mock("../../api", () => ({ api: { log: vi.fn() }, })); // The mock above replaces the module, so this import resolves to it. -import { api } from "../api"; +import { api } from "../../api"; const apiLog = vi.mocked(api.log); function commit(oid: string): Commit { diff --git a/viewer-ui/src/hooks/useLog.ts b/viewer-ui/src/hooks/git/useLog.ts similarity index 98% rename from viewer-ui/src/hooks/useLog.ts rename to viewer-ui/src/hooks/git/useLog.ts index f0112daf..882c4670 100644 --- a/viewer-ui/src/hooks/useLog.ts +++ b/viewer-ui/src/hooks/git/useLog.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { api, type Commit, type CommitFiles } from "../api"; -import { reconcileLog } from "../lib/logRefresh"; +import { api, type Commit, type CommitFiles } from "../../api"; +import { reconcileLog } from "../../lib/repository/logRefresh"; export interface CommitDrillDown extends CommitFiles { commit: Commit; diff --git a/viewer-ui/src/hooks/usePaneOpeners.ts b/viewer-ui/src/hooks/git/usePaneOpeners.ts similarity index 97% rename from viewer-ui/src/hooks/usePaneOpeners.ts rename to viewer-ui/src/hooks/git/usePaneOpeners.ts index e4730596..9c144c3c 100644 --- a/viewer-ui/src/hooks/usePaneOpeners.ts +++ b/viewer-ui/src/hooks/git/usePaneOpeners.ts @@ -6,12 +6,12 @@ import { type Diff, type FileView, type Status, -} from "../api"; +} from "../../api"; import type { CommitDrillDown } from "./useLog"; -import type { Pane } from "../types"; -import { anchorLine, anchorOffset } from "../lib/diffAnchor"; -import { hasWorkingCopy, otherFace, showsText } from "../lib/otherFace"; -import type { MobileView } from "../types"; +import type { Pane } from "../../types"; +import { anchorLine, anchorOffset } from "../../lib/content/diffAnchor"; +import { hasWorkingCopy, otherFace, showsText } from "../../lib/content/otherFace"; +import type { MobileView } from "../../types"; export interface UsePaneOpenersArgs { repo: string | null; diff --git a/viewer-ui/src/hooks/useRepoData.ts b/viewer-ui/src/hooks/git/useRepoData.ts similarity index 95% rename from viewer-ui/src/hooks/useRepoData.ts rename to viewer-ui/src/hooks/git/useRepoData.ts index c021c540..c17fe147 100644 --- a/viewer-ui/src/hooks/useRepoData.ts +++ b/viewer-ui/src/hooks/git/useRepoData.ts @@ -1,7 +1,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react"; -import type { HotConfig } from "../api"; -import type { Pane, Tab } from "../types"; -import { useHotClock } from "./ui/useHotClock"; +import type { HotConfig } from "../../api"; +import type { Pane, Tab } from "../../types"; +import { useHotClock } from "../ui/useHotClock"; import { useLog } from "./useLog"; import { useStatus } from "./useStatus"; diff --git a/viewer-ui/src/hooks/useRepoPaneActions.ts b/viewer-ui/src/hooks/git/useRepoPaneActions.ts similarity index 94% rename from viewer-ui/src/hooks/useRepoPaneActions.ts rename to viewer-ui/src/hooks/git/useRepoPaneActions.ts index 0ee08f1b..0d98c14b 100644 --- a/viewer-ui/src/hooks/useRepoPaneActions.ts +++ b/viewer-ui/src/hooks/git/useRepoPaneActions.ts @@ -1,6 +1,6 @@ import { useMemo, useRef, useState } from "react"; -import type { Status } from "../api"; -import type { MobileView, Pane } from "../types"; +import type { Status } from "../../api"; +import type { MobileView, Pane } from "../../types"; import type { CommitDrillDown } from "./useLog"; import { usePaneOpeners } from "./usePaneOpeners"; diff --git a/viewer-ui/src/hooks/useRepoView.test.ts b/viewer-ui/src/hooks/git/useRepoView.test.ts similarity index 97% rename from viewer-ui/src/hooks/useRepoView.test.ts rename to viewer-ui/src/hooks/git/useRepoView.test.ts index ea850345..8e1e2101 100644 --- a/viewer-ui/src/hooks/useRepoView.test.ts +++ b/viewer-ui/src/hooks/git/useRepoView.test.ts @@ -4,15 +4,15 @@ import { act, cleanup, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useRepoView } from "./useRepoView"; import { useRepoViewMemory } from "./useRepoViewMemory"; -import { blankView, workdirFile } from "../lib/repoView"; -import type { RepoView } from "../api"; +import { blankView, workdirFile } from "../../lib/repository/repoView"; +import type { RepoView } from "../../api"; -vi.mock("../api", () => ({ +vi.mock("../../api", () => ({ api: { setRepoView: vi.fn(() => Promise.resolve({})) }, })); // The mock above replaces the module, so this import resolves to it. -import { api } from "../api"; +import { api } from "../../api"; const setRepoView = vi.mocked(api.setRepoView); const aView: RepoView = { ...blankView(), file: workdirFile("a.ts", "diff") }; diff --git a/viewer-ui/src/hooks/useRepoView.ts b/viewer-ui/src/hooks/git/useRepoView.ts similarity index 96% rename from viewer-ui/src/hooks/useRepoView.ts rename to viewer-ui/src/hooks/git/useRepoView.ts index 22831a31..923ccbd5 100644 --- a/viewer-ui/src/hooks/useRepoView.ts +++ b/viewer-ui/src/hooks/git/useRepoView.ts @@ -1,8 +1,8 @@ import { useCallback, useRef, useState } from "react"; -import { createSerialWriter } from "../lib/serialWrite"; -import { api } from "../api"; -import type { RepoView, RepoViewByRepo } from "../api"; -import { sameView } from "../lib/repoView"; +import { createSerialWriter } from "../../lib/shared/serialWrite"; +import { api } from "../../api"; +import type { RepoView, RepoViewByRepo } from "../../api"; +import { sameView } from "../../lib/repository/repoView"; /** * What each project was last showing. diff --git a/viewer-ui/src/hooks/useRepoViewMemory.test.ts b/viewer-ui/src/hooks/git/useRepoViewMemory.test.ts similarity index 98% rename from viewer-ui/src/hooks/useRepoViewMemory.test.ts rename to viewer-ui/src/hooks/git/useRepoViewMemory.test.ts index 2c6e64bd..e594f19b 100644 --- a/viewer-ui/src/hooks/useRepoViewMemory.test.ts +++ b/viewer-ui/src/hooks/git/useRepoViewMemory.test.ts @@ -7,8 +7,8 @@ import { act, cleanup, renderHook } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { useRepoViewMemory } from "./useRepoViewMemory"; -import { blankView, workdirFile } from "../lib/repoView"; -import type { RepoView, RepoViewByRepo } from "../api"; +import { blankView, workdirFile } from "../../lib/repository/repoView"; +import type { RepoView, RepoViewByRepo } from "../../api"; interface HarnessProps { repo: string | null; diff --git a/viewer-ui/src/hooks/useRepoViewMemory.ts b/viewer-ui/src/hooks/git/useRepoViewMemory.ts similarity index 98% rename from viewer-ui/src/hooks/useRepoViewMemory.ts rename to viewer-ui/src/hooks/git/useRepoViewMemory.ts index 51fc3996..c037b85a 100644 --- a/viewer-ui/src/hooks/useRepoViewMemory.ts +++ b/viewer-ui/src/hooks/git/useRepoViewMemory.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import type { RepoView, ViewFile } from "../api"; -import type { Tab } from "../types"; -import { blankView, cappedTree, restoreOpen, restoreTab } from "../lib/repoView"; +import type { RepoView, ViewFile } from "../../api"; +import type { Tab } from "../../types"; +import { blankView, cappedTree, restoreOpen, restoreTab } from "../../lib/repository/repoView"; import type { OpenOptions } from "./usePaneOpeners"; interface UseRepoViewMemoryArgs { diff --git a/viewer-ui/src/hooks/useRepoViewPersistence.test.ts b/viewer-ui/src/hooks/git/useRepoViewPersistence.test.ts similarity index 96% rename from viewer-ui/src/hooks/useRepoViewPersistence.test.ts rename to viewer-ui/src/hooks/git/useRepoViewPersistence.test.ts index dd66979e..da458a23 100644 --- a/viewer-ui/src/hooks/useRepoViewPersistence.test.ts +++ b/viewer-ui/src/hooks/git/useRepoViewPersistence.test.ts @@ -2,8 +2,8 @@ import { act, cleanup, renderHook } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import type { RepoView } from "../api"; -import { blankView, workdirFile } from "../lib/repoView"; +import type { RepoView } from "../../api"; +import { blankView, workdirFile } from "../../lib/repository/repoView"; import type { UsePaneOpenersResult } from "./usePaneOpeners"; import { useRepoViewPersistence } from "./useRepoViewPersistence"; diff --git a/viewer-ui/src/hooks/useRepoViewPersistence.ts b/viewer-ui/src/hooks/git/useRepoViewPersistence.ts similarity index 92% rename from viewer-ui/src/hooks/useRepoViewPersistence.ts rename to viewer-ui/src/hooks/git/useRepoViewPersistence.ts index edf0d6b7..96d44d80 100644 --- a/viewer-ui/src/hooks/useRepoViewPersistence.ts +++ b/viewer-ui/src/hooks/git/useRepoViewPersistence.ts @@ -1,7 +1,7 @@ import { useCallback, useMemo } from "react"; -import type { Commit, RepoView } from "../api"; -import { commitFile, workdirFile } from "../lib/repoView"; -import type { FileSource, Tab } from "../types"; +import type { Commit, RepoView } from "../../api"; +import { commitFile, workdirFile } from "../../lib/repository/repoView"; +import type { FileSource, Tab } from "../../types"; import type { UsePaneOpenersResult } from "./usePaneOpeners"; import { useRepoViewMemory } from "./useRepoViewMemory"; diff --git a/viewer-ui/src/hooks/useRepoWorkspace.test.ts b/viewer-ui/src/hooks/git/useRepoWorkspace.test.ts similarity index 95% rename from viewer-ui/src/hooks/useRepoWorkspace.test.ts rename to viewer-ui/src/hooks/git/useRepoWorkspace.test.ts index 62d52582..a9a1b492 100644 --- a/viewer-ui/src/hooks/useRepoWorkspace.test.ts +++ b/viewer-ui/src/hooks/git/useRepoWorkspace.test.ts @@ -11,11 +11,11 @@ import { act, cleanup, renderHook } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { useRepoWorkspace } from "./useRepoWorkspace"; -import type { ShellLayout } from "./useShellLayout"; -import type { Diff } from "../api"; -import type { Maximized, Pane } from "../types"; +import type { ShellLayout } from "../ui/useShellLayout"; +import type { Diff } from "../../api"; +import type { Maximized, Pane } from "../../types"; -vi.mock("../api", () => ({ +vi.mock("../../api", () => ({ api: { log: vi.fn(() => Promise.resolve({ commits: [], truncated: false })), setRepoView: vi.fn(() => Promise.resolve({})), diff --git a/viewer-ui/src/hooks/useRepoWorkspace.ts b/viewer-ui/src/hooks/git/useRepoWorkspace.ts similarity index 95% rename from viewer-ui/src/hooks/useRepoWorkspace.ts rename to viewer-ui/src/hooks/git/useRepoWorkspace.ts index c4149428..8e3498b7 100644 --- a/viewer-ui/src/hooks/useRepoWorkspace.ts +++ b/viewer-ui/src/hooks/git/useRepoWorkspace.ts @@ -1,13 +1,13 @@ import { useCallback } from "react"; -import type { HotConfig, Repo, RepoView } from "../api"; -import { otherFace } from "../lib/otherFace"; -import { applyTabChoice } from "../lib/tabChoice"; -import type { Maximized, Tab } from "../types"; +import type { HotConfig, Repo, RepoView } from "../../api"; +import { otherFace } from "../../lib/content/otherFace"; +import { applyTabChoice } from "../../lib/repository/tabChoice"; +import type { Maximized, Tab } from "../../types"; import { useDrillDownEviction } from "./useDrillDownEviction"; import { useRepoData } from "./useRepoData"; import { useRepoPaneActions } from "./useRepoPaneActions"; import { useRepoViewPersistence } from "./useRepoViewPersistence"; -import type { ShellLayout } from "./useShellLayout"; +import type { ShellLayout } from "../ui/useShellLayout"; export interface RepoProjectContract { repo: string | null; diff --git a/viewer-ui/src/hooks/useStatus.ts b/viewer-ui/src/hooks/git/useStatus.ts similarity index 94% rename from viewer-ui/src/hooks/useStatus.ts rename to viewer-ui/src/hooks/git/useStatus.ts index 209d246e..72f0c005 100644 --- a/viewer-ui/src/hooks/useStatus.ts +++ b/viewer-ui/src/hooks/git/useStatus.ts @@ -1,7 +1,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react"; -import { api, subscribeStatus, type Status } from "../api"; -import type { Pane, Tab } from "../types"; -import { hasWorkingCopy, showsText } from "../lib/otherFace"; +import { api, subscribeStatus, type Status } from "../../api"; +import type { Pane, Tab } from "../../types"; +import { hasWorkingCopy, showsText } from "../../lib/content/otherFace"; export type { Pane, Tab }; diff --git a/viewer-ui/src/hooks/useTree.ts b/viewer-ui/src/hooks/git/useTree.ts similarity index 98% rename from viewer-ui/src/hooks/useTree.ts rename to viewer-ui/src/hooks/git/useTree.ts index db4cb76c..de50a579 100644 --- a/viewer-ui/src/hooks/useTree.ts +++ b/viewer-ui/src/hooks/git/useTree.ts @@ -1,13 +1,13 @@ import { useCallback, useEffect, useState } from "react"; -import { api, isUnauthorized, type TreeEntry, type TreeMatch } from "../api"; -import { latestRequest } from "../lib/latestRequest"; +import { api, isUnauthorized, type TreeEntry, type TreeMatch } from "../../api"; +import { latestRequest } from "../../lib/shared/latestRequest"; import { ancestorDirs, emptyTreeCache, withChildren, withRevealed, withToggled, -} from "../lib/treeCache"; +} from "../../lib/repository/treeCache"; const TREE_SEARCH_DEBOUNCE_MS = 180; diff --git a/viewer-ui/src/hooks/useClone.ts b/viewer-ui/src/hooks/repository/useClone.ts similarity index 98% rename from viewer-ui/src/hooks/useClone.ts rename to viewer-ui/src/hooks/repository/useClone.ts index 594bf9f1..61e15c0c 100644 --- a/viewer-ui/src/hooks/useClone.ts +++ b/viewer-ui/src/hooks/repository/useClone.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { api, isUnauthorized, type Repo } from "../api"; -import { ApiError } from "../api/errors"; -import { toast } from "../lib/toast"; +import { api, isUnauthorized, type Repo } from "../../api"; +import { ApiError } from "../../api/errors"; +import { toast } from "../../lib/runtime/toast"; /** How often to ask the server whether the clone has finished. A clone is a * network transfer measured in seconds at best, so a slower poll than the diff --git a/viewer-ui/src/hooks/useProjectTabs.ts b/viewer-ui/src/hooks/repository/useProjectTabs.ts similarity index 98% rename from viewer-ui/src/hooks/useProjectTabs.ts rename to viewer-ui/src/hooks/repository/useProjectTabs.ts index 3e176455..556a8e8e 100644 --- a/viewer-ui/src/hooks/useProjectTabs.ts +++ b/viewer-ui/src/hooks/repository/useProjectTabs.ts @@ -1,7 +1,7 @@ import { useRef } from "react"; import { useRepoPoll, type UseRepoPollResult } from "./useRepoPoll"; import { useRepoOrder } from "./useRepoOrder"; -import type { MaximizedByRepo, RepoViewByRepo } from "../api"; +import type { MaximizedByRepo, RepoViewByRepo } from "../../api"; export interface UseProjectTabsArgs { authed: boolean | null; diff --git a/viewer-ui/src/hooks/useRepoActions.ts b/viewer-ui/src/hooks/repository/useRepoActions.ts similarity index 95% rename from viewer-ui/src/hooks/useRepoActions.ts rename to viewer-ui/src/hooks/repository/useRepoActions.ts index e29d913c..06224e90 100644 --- a/viewer-ui/src/hooks/useRepoActions.ts +++ b/viewer-ui/src/hooks/repository/useRepoActions.ts @@ -1,7 +1,7 @@ import { useCallback, useRef } from "react"; -import { api, type Repo } from "../api"; -import { successorOf } from "../lib/successor"; -import type { Pane, Tab } from "../types"; +import { api, type Repo } from "../../api"; +import { successorOf } from "../../lib/repository/successor"; +import type { Pane, Tab } from "../../types"; export interface UseRepoActionsArgs { /** The project on screen, so opening the one already open changes nothing. */ diff --git a/viewer-ui/src/hooks/useRepoDrag.ts b/viewer-ui/src/hooks/repository/useRepoDrag.ts similarity index 97% rename from viewer-ui/src/hooks/useRepoDrag.ts rename to viewer-ui/src/hooks/repository/useRepoDrag.ts index 13738359..d57b0bbf 100644 --- a/viewer-ui/src/hooks/useRepoDrag.ts +++ b/viewer-ui/src/hooks/repository/useRepoDrag.ts @@ -1,5 +1,5 @@ import { useRef, useState, type PointerEvent as ReactPointerEvent } from "react"; -import { reorderByDrop } from "../lib/paneOrder"; +import { reorderByDrop } from "../../lib/terminal/paneOrder"; const DRAG_THRESHOLD_PX = 4; diff --git a/viewer-ui/src/hooks/useRepoOrder.ts b/viewer-ui/src/hooks/repository/useRepoOrder.ts similarity index 94% rename from viewer-ui/src/hooks/useRepoOrder.ts rename to viewer-ui/src/hooks/repository/useRepoOrder.ts index 237a6cb9..295006af 100644 --- a/viewer-ui/src/hooks/useRepoOrder.ts +++ b/viewer-ui/src/hooks/repository/useRepoOrder.ts @@ -1,6 +1,6 @@ import { useCallback } from "react"; -import { api, type Repo } from "../api"; -import { reconcileOrder } from "../lib/paneOrder"; +import { api, type Repo } from "../../api"; +import { reconcileOrder } from "../../lib/terminal/paneOrder"; import { useRepoDrag } from "./useRepoDrag"; interface UseRepoOrderArgs { diff --git a/viewer-ui/src/hooks/useRepoPoll.test.ts b/viewer-ui/src/hooks/repository/useRepoPoll.test.ts similarity index 98% rename from viewer-ui/src/hooks/useRepoPoll.test.ts rename to viewer-ui/src/hooks/repository/useRepoPoll.test.ts index f4a3946f..5be35b44 100644 --- a/viewer-ui/src/hooks/useRepoPoll.test.ts +++ b/viewer-ui/src/hooks/repository/useRepoPoll.test.ts @@ -11,12 +11,12 @@ // way the batching this logic must survive does. import { Profiler, StrictMode, createElement, memo } from "react"; -import { PROTOCOL_VERSION } from "../api/types"; +import { PROTOCOL_VERSION } from "../../api/types"; import { act, cleanup, render, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useRepoPoll, type UseRepoPollArgs } from "./useRepoPoll"; -vi.mock("../api", () => ({ +vi.mock("../../api", () => ({ api: { repos: vi.fn(), setActiveRepo: vi.fn(() => Promise.resolve()), @@ -25,7 +25,7 @@ vi.mock("../api", () => ({ isUnauthorized: () => false, })); -import { api } from "../api"; +import { api } from "../../api"; const repos = api.repos as ReturnType; const setActiveRepo = api.setActiveRepo as ReturnType; diff --git a/viewer-ui/src/hooks/useRepoPoll.ts b/viewer-ui/src/hooks/repository/useRepoPoll.ts similarity index 95% rename from viewer-ui/src/hooks/useRepoPoll.ts rename to viewer-ui/src/hooks/repository/useRepoPoll.ts index 01f4d8d8..24ce5afe 100644 --- a/viewer-ui/src/hooks/useRepoPoll.ts +++ b/viewer-ui/src/hooks/repository/useRepoPoll.ts @@ -5,14 +5,14 @@ import { isUnauthorized, type HotConfig, type Repo, -} from "../api"; -import { resolveActiveRepo } from "../lib/activeRepo"; -import { nextClockOffset } from "../lib/hot"; -import { createSerialWriter } from "../lib/serialWrite"; -import { reconcileOrder } from "../lib/paneOrder"; -import { retainHot, retainRepos } from "../lib/repoSnapshot"; -import { noteViewerBuild } from "../lib/viewerBuild"; -import type { MaximizedByRepo, RepoViewByRepo } from "../api"; +} from "../../api"; +import { resolveActiveRepo } from "../../lib/repository/activeRepo"; +import { nextClockOffset } from "../../lib/repository/hot"; +import { createSerialWriter } from "../../lib/shared/serialWrite"; +import { reconcileOrder } from "../../lib/terminal/paneOrder"; +import { retainHot, retainRepos } from "../../lib/repository/repoSnapshot"; +import { noteViewerBuild } from "../../lib/runtime/viewerBuild"; +import type { MaximizedByRepo, RepoViewByRepo } from "../../api"; const REPO_POLL_MS = 3000; diff --git a/viewer-ui/src/hooks/shortcutIntents.test.tsx b/viewer-ui/src/hooks/shortcuts/shortcutIntents.test.tsx similarity index 100% rename from viewer-ui/src/hooks/shortcutIntents.test.tsx rename to viewer-ui/src/hooks/shortcuts/shortcutIntents.test.tsx diff --git a/viewer-ui/src/hooks/shortcutIntents.tsx b/viewer-ui/src/hooks/shortcuts/shortcutIntents.tsx similarity index 99% rename from viewer-ui/src/hooks/shortcutIntents.tsx rename to viewer-ui/src/hooks/shortcuts/shortcutIntents.tsx index 0c9ac375..fcef04d4 100644 --- a/viewer-ui/src/hooks/shortcutIntents.tsx +++ b/viewer-ui/src/hooks/shortcuts/shortcutIntents.tsx @@ -8,7 +8,7 @@ import { useState, type ReactNode, } from "react"; -import type { ShortcutActionId } from "../lib/shortcutActions"; +import type { ShortcutActionId } from "../../lib/shortcuts/shortcutActions"; // One bus, so a command has exactly one implementation. // diff --git a/viewer-ui/src/hooks/shortcutLeader.test.tsx b/viewer-ui/src/hooks/shortcuts/shortcutLeader.test.tsx similarity index 97% rename from viewer-ui/src/hooks/shortcutLeader.test.tsx rename to viewer-ui/src/hooks/shortcuts/shortcutLeader.test.tsx index 027cc47d..efb48725 100644 --- a/viewer-ui/src/hooks/shortcutLeader.test.tsx +++ b/viewer-ui/src/hooks/shortcuts/shortcutLeader.test.tsx @@ -3,7 +3,7 @@ import { cleanup, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; import { ShortcutLeaderProvider, useShortcutHint } from "./shortcutLeader"; -import { DEFAULT_LEADER, parseChord, type ChordSpec } from "../lib/leaderChord"; +import { DEFAULT_LEADER, parseChord, type ChordSpec } from "../../lib/shortcuts/leaderChord"; // Vitest runs without globals, so RTL cannot auto-register its cleanup. afterEach(cleanup); diff --git a/viewer-ui/src/hooks/shortcutLeader.tsx b/viewer-ui/src/hooks/shortcuts/shortcutLeader.tsx similarity index 94% rename from viewer-ui/src/hooks/shortcutLeader.tsx rename to viewer-ui/src/hooks/shortcuts/shortcutLeader.tsx index 690fbbba..f1476c7f 100644 --- a/viewer-ui/src/hooks/shortcutLeader.tsx +++ b/viewer-ui/src/hooks/shortcuts/shortcutLeader.tsx @@ -1,11 +1,11 @@ import { createContext, useCallback, useContext, type ReactNode } from "react"; -import type { ChordSpec } from "../lib/leaderChord"; +import type { ChordSpec } from "../../lib/shortcuts/leaderChord"; import { ariaKeyShortcuts, shortcutHintText, titleWithShortcut, -} from "../lib/shortcutHint"; -import type { ShortcutActionId } from "../lib/shortcutActions"; +} from "../../lib/shortcuts/shortcutHint"; +import type { ShortcutActionId } from "../../lib/shortcuts/shortcutActions"; // The current leader, published to every control that has to name its key. // diff --git a/viewer-ui/src/hooks/useAppShortcuts.focus.test.tsx b/viewer-ui/src/hooks/shortcuts/useAppShortcuts.focus.test.tsx similarity index 98% rename from viewer-ui/src/hooks/useAppShortcuts.focus.test.tsx rename to viewer-ui/src/hooks/shortcuts/useAppShortcuts.focus.test.tsx index ebf14d07..afe4c6cb 100644 --- a/viewer-ui/src/hooks/useAppShortcuts.focus.test.tsx +++ b/viewer-ui/src/hooks/shortcuts/useAppShortcuts.focus.test.tsx @@ -8,7 +8,7 @@ import { cleanup } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { focusRegionAttrs } from "../lib/shortcutDom"; +import { focusRegionAttrs } from "../../lib/shortcuts/shortcutDom"; import { hit, mount } from "./useAppShortcuts.harness"; import type { Init } from "./useShortcuts.harness"; diff --git a/viewer-ui/src/hooks/useAppShortcuts.harness.tsx b/viewer-ui/src/hooks/shortcuts/useAppShortcuts.harness.tsx similarity index 97% rename from viewer-ui/src/hooks/useAppShortcuts.harness.tsx rename to viewer-ui/src/hooks/shortcuts/useAppShortcuts.harness.tsx index 17140f1b..889bddef 100644 --- a/viewer-ui/src/hooks/useAppShortcuts.harness.tsx +++ b/viewer-ui/src/hooks/shortcuts/useAppShortcuts.harness.tsx @@ -1,6 +1,6 @@ import { act, render } from "@testing-library/react"; import { vi } from "vitest"; -import { stubLocalStorage } from "../lib/fakeStorage"; +import { stubLocalStorage } from "../../lib/shared/fakeStorage"; import { ShortcutIntentProvider, useRegisterShortcutHandlers, diff --git a/viewer-ui/src/hooks/useAppShortcuts.project.test.tsx b/viewer-ui/src/hooks/shortcuts/useAppShortcuts.project.test.tsx similarity index 100% rename from viewer-ui/src/hooks/useAppShortcuts.project.test.tsx rename to viewer-ui/src/hooks/shortcuts/useAppShortcuts.project.test.tsx diff --git a/viewer-ui/src/hooks/useAppShortcuts.test.tsx b/viewer-ui/src/hooks/shortcuts/useAppShortcuts.test.tsx similarity index 99% rename from viewer-ui/src/hooks/useAppShortcuts.test.tsx rename to viewer-ui/src/hooks/shortcuts/useAppShortcuts.test.tsx index 17f7255b..07a01740 100644 --- a/viewer-ui/src/hooks/useAppShortcuts.test.tsx +++ b/viewer-ui/src/hooks/shortcuts/useAppShortcuts.test.tsx @@ -6,7 +6,7 @@ import { act, cleanup } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; -import type { Maximized } from "../types"; +import type { Maximized } from "../../types"; import { command, hit, mount } from "./useAppShortcuts.harness"; import { el, mounted, pane } from "./useShortcuts.harness"; diff --git a/viewer-ui/src/hooks/useAppShortcuts.ts b/viewer-ui/src/hooks/shortcuts/useAppShortcuts.ts similarity index 94% rename from viewer-ui/src/hooks/useAppShortcuts.ts rename to viewer-ui/src/hooks/shortcuts/useAppShortcuts.ts index 0c0ba93a..7f241715 100644 --- a/viewer-ui/src/hooks/useAppShortcuts.ts +++ b/viewer-ui/src/hooks/shortcuts/useAppShortcuts.ts @@ -1,15 +1,15 @@ import { useCallback, useMemo, useState } from "react"; -import { nextFocus, type FocusSpot } from "../lib/focusCycle"; -import { GRID_MIN_VIEWPORT_PX } from "../lib/paneViewMode"; -import { neighborRepo } from "../lib/projectCycle"; +import { nextFocus, type FocusSpot } from "../../lib/shortcuts/focusCycle"; +import { GRID_MIN_VIEWPORT_PX } from "../../lib/terminal/paneViewMode"; +import { neighborRepo } from "../../lib/repository/projectCycle"; import { focusShortcutRegion, keyboardRegion, terminalPanelHasFocus, -} from "../lib/shortcutDom"; -import type { LeaderState } from "../lib/leaderState"; -import type { HintClick } from "../lib/shortcutHintBar"; -import type { Maximized, MobileView, Tab } from "../types"; +} from "../../lib/shortcuts/shortcutDom"; +import type { LeaderState } from "../../lib/shortcuts/leaderState"; +import type { HintClick } from "../../lib/shortcuts/shortcutHintBar"; +import type { Maximized, MobileView, Tab } from "../../types"; import { useRegisterShortcutHandlers, useShortcutIntents, diff --git a/viewer-ui/src/hooks/useGlobalKeydown.test.ts b/viewer-ui/src/hooks/shortcuts/useGlobalKeydown.test.ts similarity index 100% rename from viewer-ui/src/hooks/useGlobalKeydown.test.ts rename to viewer-ui/src/hooks/shortcuts/useGlobalKeydown.test.ts diff --git a/viewer-ui/src/hooks/useGlobalKeydown.ts b/viewer-ui/src/hooks/shortcuts/useGlobalKeydown.ts similarity index 100% rename from viewer-ui/src/hooks/useGlobalKeydown.ts rename to viewer-ui/src/hooks/shortcuts/useGlobalKeydown.ts diff --git a/viewer-ui/src/hooks/useShortcutSettings.test.ts b/viewer-ui/src/hooks/shortcuts/useShortcutSettings.test.ts similarity index 96% rename from viewer-ui/src/hooks/useShortcutSettings.test.ts rename to viewer-ui/src/hooks/shortcuts/useShortcutSettings.test.ts index cc2c9cb9..4aa793ec 100644 --- a/viewer-ui/src/hooks/useShortcutSettings.test.ts +++ b/viewer-ui/src/hooks/shortcuts/useShortcutSettings.test.ts @@ -2,8 +2,8 @@ import { act, cleanup, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { stubLocalStorage } from "../lib/fakeStorage"; -import { DEFAULT_LEADER, formatChord } from "../lib/leaderChord"; +import { stubLocalStorage } from "../../lib/shared/fakeStorage"; +import { DEFAULT_LEADER, formatChord } from "../../lib/shortcuts/leaderChord"; import { useShortcutSettings } from "./useShortcutSettings"; const KEY = "nightcrow.shortcut.leader"; diff --git a/viewer-ui/src/hooks/useShortcutSettings.ts b/viewer-ui/src/hooks/shortcuts/useShortcutSettings.ts similarity index 98% rename from viewer-ui/src/hooks/useShortcutSettings.ts rename to viewer-ui/src/hooks/shortcuts/useShortcutSettings.ts index 1958ba0f..52ec1590 100644 --- a/viewer-ui/src/hooks/useShortcutSettings.ts +++ b/viewer-ui/src/hooks/shortcuts/useShortcutSettings.ts @@ -5,7 +5,7 @@ import { leaderConflict, parseChord, type ChordSpec, -} from "../lib/leaderChord"; +} from "../../lib/shortcuts/leaderChord"; // The leader is a per-browser preference, so it stays in the browser. // diff --git a/viewer-ui/src/hooks/useShortcuts.disarm.test.tsx b/viewer-ui/src/hooks/shortcuts/useShortcuts.disarm.test.tsx similarity index 100% rename from viewer-ui/src/hooks/useShortcuts.disarm.test.tsx rename to viewer-ui/src/hooks/shortcuts/useShortcuts.disarm.test.tsx diff --git a/viewer-ui/src/hooks/useShortcuts.guards.test.tsx b/viewer-ui/src/hooks/shortcuts/useShortcuts.guards.test.tsx similarity index 100% rename from viewer-ui/src/hooks/useShortcuts.guards.test.tsx rename to viewer-ui/src/hooks/shortcuts/useShortcuts.guards.test.tsx diff --git a/viewer-ui/src/hooks/useShortcuts.harness.tsx b/viewer-ui/src/hooks/shortcuts/useShortcuts.harness.tsx similarity index 96% rename from viewer-ui/src/hooks/useShortcuts.harness.tsx rename to viewer-ui/src/hooks/shortcuts/useShortcuts.harness.tsx index c9906da5..e5d61059 100644 --- a/viewer-ui/src/hooks/useShortcuts.harness.tsx +++ b/viewer-ui/src/hooks/shortcuts/useShortcuts.harness.tsx @@ -1,7 +1,7 @@ import { render } from "@testing-library/react"; import { vi, type Mock } from "vitest"; -import { DEFAULT_LEADER } from "../lib/leaderChord"; -import { SHORTCUT_ACTIONS, type ShortcutActionId } from "../lib/shortcutActions"; +import { DEFAULT_LEADER } from "../../lib/shortcuts/leaderChord"; +import { SHORTCUT_ACTIONS, type ShortcutActionId } from "../../lib/shortcuts/shortcutActions"; import { ShortcutIntentProvider, useRegisterShortcutHandlers, diff --git a/viewer-ui/src/hooks/useShortcuts.race.test.tsx b/viewer-ui/src/hooks/shortcuts/useShortcuts.race.test.tsx similarity index 95% rename from viewer-ui/src/hooks/useShortcuts.race.test.tsx rename to viewer-ui/src/hooks/shortcuts/useShortcuts.race.test.tsx index becfadab..6fc43fd1 100644 --- a/viewer-ui/src/hooks/useShortcuts.race.test.tsx +++ b/viewer-ui/src/hooks/shortcuts/useShortcuts.race.test.tsx @@ -8,16 +8,16 @@ // already moved past. What that costs is measured here rather than assumed. import { StrictMode, createElement, useCallback } from "react"; -import { PROTOCOL_VERSION } from "../api/types"; +import { PROTOCOL_VERSION } from "../../api/types"; import { act, cleanup, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { stubLocalStorage } from "../lib/fakeStorage"; +import { stubLocalStorage } from "../../lib/shared/fakeStorage"; import { ShortcutIntentProvider } from "./shortcutIntents"; import { useAppShortcuts } from "./useAppShortcuts"; -import { useRepoPoll, type UseRepoPollArgs } from "./useRepoPoll"; +import { useRepoPoll, type UseRepoPollArgs } from "../repository/useRepoPoll"; import { press } from "./useShortcuts.harness"; -vi.mock("../api", () => ({ +vi.mock("../../api", () => ({ api: { repos: vi.fn(), setActiveRepo: vi.fn(() => Promise.resolve()), @@ -26,7 +26,7 @@ vi.mock("../api", () => ({ isUnauthorized: () => false, })); -import { api } from "../api"; +import { api } from "../../api"; const repos = api.repos as ReturnType; const setActiveRepo = api.setActiveRepo as ReturnType; diff --git a/viewer-ui/src/hooks/useShortcuts.state.test.tsx b/viewer-ui/src/hooks/shortcuts/useShortcuts.state.test.tsx similarity index 92% rename from viewer-ui/src/hooks/useShortcuts.state.test.tsx rename to viewer-ui/src/hooks/shortcuts/useShortcuts.state.test.tsx index ebbe3af2..42db5934 100644 --- a/viewer-ui/src/hooks/useShortcuts.state.test.tsx +++ b/viewer-ui/src/hooks/shortcuts/useShortcuts.state.test.tsx @@ -2,8 +2,8 @@ import { act, cleanup, render } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; -import { DEFAULT_LEADER } from "../lib/leaderChord"; -import type { LeaderState } from "../lib/leaderState"; +import { DEFAULT_LEADER } from "../../lib/shortcuts/leaderChord"; +import type { LeaderState } from "../../lib/shortcuts/leaderState"; import { ShortcutIntentProvider } from "./shortcutIntents"; import { useShortcuts, type ShortcutEngine } from "./useShortcuts"; import { leader, press } from "./useShortcuts.harness"; diff --git a/viewer-ui/src/hooks/useShortcuts.test.tsx b/viewer-ui/src/hooks/shortcuts/useShortcuts.test.tsx similarity index 100% rename from viewer-ui/src/hooks/useShortcuts.test.tsx rename to viewer-ui/src/hooks/shortcuts/useShortcuts.test.tsx diff --git a/viewer-ui/src/hooks/useShortcuts.ts b/viewer-ui/src/hooks/shortcuts/useShortcuts.ts similarity index 95% rename from viewer-ui/src/hooks/useShortcuts.ts rename to viewer-ui/src/hooks/shortcuts/useShortcuts.ts index 09c81990..5e7074f8 100644 --- a/viewer-ui/src/hooks/useShortcuts.ts +++ b/viewer-ui/src/hooks/shortcuts/useShortcuts.ts @@ -1,15 +1,15 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import type { ChordSpec } from "../lib/leaderChord"; +import type { ChordSpec } from "../../lib/shortcuts/leaderChord"; import { IDLE_LEADER, reduceLeader, type LeaderEffect, type LeaderEvent, type LeaderState, -} from "../lib/leaderState"; -import { classifyShortcutKey } from "../lib/shortcutKeys"; -import { describeShortcutTarget } from "../lib/shortcutDom"; -import { isTextEntryTarget, shortcutsSuppressed } from "../lib/shortcutTarget"; +} from "../../lib/shortcuts/leaderState"; +import { classifyShortcutKey } from "../../lib/shortcuts/shortcutKeys"; +import { describeShortcutTarget } from "../../lib/shortcuts/shortcutDom"; +import { isTextEntryTarget, shortcutsSuppressed } from "../../lib/shortcuts/shortcutTarget"; import { useGlobalKeydown } from "./useGlobalKeydown"; import { useShortcutIntents, type ShortcutIntents } from "./shortcutIntents"; diff --git a/viewer-ui/src/hooks/terminal/terminalSocketMessages.test.ts b/viewer-ui/src/hooks/terminal/terminalSocketMessages.test.ts index d70accea..3a9d00e3 100644 --- a/viewer-ui/src/hooks/terminal/terminalSocketMessages.test.ts +++ b/viewer-ui/src/hooks/terminal/terminalSocketMessages.test.ts @@ -1,12 +1,12 @@ import { afterEach, describe, expect, it } from "vitest"; import { Terminal } from "@xterm/xterm"; -import type { PaneView } from "../../lib/terminalLayout"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; import { createTerminalReplayState, installReplayReplyGate, queueReplayEnd, type PendingTerminalWrite, -} from "../../lib/terminalReplay"; +} from "../../lib/terminal/terminalReplay"; import { handleTerminalSocketMessage, type TerminalMessageContext } from "./terminalSocketMessages"; const terminals: Terminal[] = []; diff --git a/viewer-ui/src/hooks/terminal/terminalSocketMessages.ts b/viewer-ui/src/hooks/terminal/terminalSocketMessages.ts index e6bde36b..afbbd4e1 100644 --- a/viewer-ui/src/hooks/terminal/terminalSocketMessages.ts +++ b/viewer-ui/src/hooks/terminal/terminalSocketMessages.ts @@ -5,18 +5,18 @@ import { type PaneSize, type TerminalServerMessage, } from "../../api/terminal"; -import type { LinkState } from "../../lib/attachStatus"; -import { forgetPane, lastPaneOf, rememberPane } from "../../lib/lastPane"; -import { reconcileOrder } from "../../lib/paneOrder"; -import { applyRecovery, type RecoveryByPane } from "../../lib/recovery"; -import type { PaneView } from "../../lib/terminalLayout"; +import type { LinkState } from "../../lib/terminal/attachStatus"; +import { forgetPane, lastPaneOf, rememberPane } from "../../lib/terminal/lastPane"; +import { reconcileOrder } from "../../lib/terminal/paneOrder"; +import { applyRecovery, type RecoveryByPane } from "../../lib/terminal/recovery"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; import type { PendingTerminalWrite, TerminalReplayState, -} from "../../lib/terminalReplay"; -import { queueReplayEnd } from "../../lib/terminalReplay"; -import { toast } from "../../lib/toast"; -import { applySizeOwnerUpdate } from "../../lib/sizeActivity"; +} from "../../lib/terminal/terminalReplay"; +import { queueReplayEnd } from "../../lib/terminal/terminalReplay"; +import { toast } from "../../lib/runtime/toast"; +import { applySizeOwnerUpdate } from "../../lib/terminal/sizeActivity"; type Setter = Dispatch>; diff --git a/viewer-ui/src/hooks/terminal/useCompose.test.tsx b/viewer-ui/src/hooks/terminal/useCompose.test.tsx index 61c30c21..01e92817 100644 --- a/viewer-ui/src/hooks/terminal/useCompose.test.tsx +++ b/viewer-ui/src/hooks/terminal/useCompose.test.tsx @@ -3,7 +3,7 @@ import { act, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useCompose } from "./useCompose"; -import type { PaneView } from "../../lib/terminalLayout"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; function fakeSocket(open = true) { const sent: unknown[] = []; diff --git a/viewer-ui/src/hooks/terminal/useCompose.ts b/viewer-ui/src/hooks/terminal/useCompose.ts index f5b844ff..d6776f51 100644 --- a/viewer-ui/src/hooks/terminal/useCompose.ts +++ b/viewer-ui/src/hooks/terminal/useCompose.ts @@ -1,7 +1,7 @@ import { useState, type MutableRefObject } from "react"; import { sendTerminalMessage } from "../../api/terminal"; -import { SUBMIT_KEY, composedInput, isSendable } from "../../lib/composeInput"; -import type { PaneView } from "../../lib/terminalLayout"; +import { SUBMIT_KEY, composedInput, isSendable } from "../../lib/terminal/composeInput"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; /** * How long the Return waits behind the message. Sent together, a program that diff --git a/viewer-ui/src/hooks/terminal/useModifierLatch.ts b/viewer-ui/src/hooks/terminal/useModifierLatch.ts index 7301f75f..a4c26507 100644 --- a/viewer-ui/src/hooks/terminal/useModifierLatch.ts +++ b/viewer-ui/src/hooks/terminal/useModifierLatch.ts @@ -14,7 +14,7 @@ import { altLatchStep, ctrlLatchStep, type CtrlLatchStep, -} from "../../lib/termKeys"; +} from "../../lib/terminal/termKeys"; export interface ModifierLatch { /** Whether the next character is being modified — the button's pressed state. */ diff --git a/viewer-ui/src/hooks/terminal/usePaneCommands.ts b/viewer-ui/src/hooks/terminal/usePaneCommands.ts index cdadf54b..9fd7e918 100644 --- a/viewer-ui/src/hooks/terminal/usePaneCommands.ts +++ b/viewer-ui/src/hooks/terminal/usePaneCommands.ts @@ -1,11 +1,11 @@ import type { MutableRefObject } from "react"; -import type { PaneView } from "../../lib/terminalLayout"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; import { sendTerminalMessage, type TerminalClientMessage, } from "../../api/terminal"; -import { termKeySequence, type TermKey } from "../../lib/termKeys"; -import { zoomRequest } from "../../lib/zoom"; +import { termKeySequence, type TermKey } from "../../lib/terminal/termKeys"; +import { zoomRequest } from "../../lib/terminal/zoom"; interface UsePaneCommandsArgs { socketRef: MutableRefObject; diff --git a/viewer-ui/src/hooks/terminal/usePaneDrag.ts b/viewer-ui/src/hooks/terminal/usePaneDrag.ts index b12bbf7c..651f8c52 100644 --- a/viewer-ui/src/hooks/terminal/usePaneDrag.ts +++ b/viewer-ui/src/hooks/terminal/usePaneDrag.ts @@ -1,6 +1,6 @@ import { useRef, useState } from "react"; -import { reorderByDrop } from "../../lib/paneOrder"; -import { PANE_DRAG_THRESHOLD_PX } from "../../lib/terminalLayout"; +import { reorderByDrop } from "../../lib/terminal/paneOrder"; +import { PANE_DRAG_THRESHOLD_PX } from "../../lib/terminal/terminalLayout"; interface UsePaneDragArgs { panes: number[]; diff --git a/viewer-ui/src/hooks/terminal/usePaneFocus.ts b/viewer-ui/src/hooks/terminal/usePaneFocus.ts index da754b3a..3b079a47 100644 --- a/viewer-ui/src/hooks/terminal/usePaneFocus.ts +++ b/viewer-ui/src/hooks/terminal/usePaneFocus.ts @@ -1,15 +1,15 @@ import { useEffect, useRef } from "react"; import type { MutableRefObject } from "react"; -import type { PaneView } from "../../lib/terminalLayout"; -import type { PaneViewMode } from "../../lib/paneViewMode"; -import { lastPaneOf, rememberPane } from "../../lib/lastPane"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; +import type { PaneViewMode } from "../../lib/terminal/paneViewMode"; +import { lastPaneOf, rememberPane } from "../../lib/terminal/lastPane"; import { focusHolder, focusIsTakeable, focusOnAttach, focusStep, -} from "../../lib/paneFocus"; -import { zoomPending } from "../../lib/zoom"; +} from "../../lib/terminal/paneFocus"; +import { zoomPending } from "../../lib/terminal/zoom"; interface UsePaneFocusArgs { repo: string; diff --git a/viewer-ui/src/hooks/terminal/usePaneRecovery.ts b/viewer-ui/src/hooks/terminal/usePaneRecovery.ts index 673d83ba..557d3a22 100644 --- a/viewer-ui/src/hooks/terminal/usePaneRecovery.ts +++ b/viewer-ui/src/hooks/terminal/usePaneRecovery.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from "react"; import type { MutableRefObject } from "react"; -import type { RecoveryByPane } from "../../lib/recovery"; +import type { RecoveryByPane } from "../../lib/terminal/recovery"; import { sendTerminalMessage } from "../../api/terminal"; /// Per-pane recovery reports, and the one control that acts on them. diff --git a/viewer-ui/src/hooks/terminal/usePaneSizes.test.tsx b/viewer-ui/src/hooks/terminal/usePaneSizes.test.tsx index 9fa4df90..aceb66ca 100644 --- a/viewer-ui/src/hooks/terminal/usePaneSizes.test.tsx +++ b/viewer-ui/src/hooks/terminal/usePaneSizes.test.tsx @@ -2,7 +2,7 @@ import { act, cleanup, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { PaneView } from "../../lib/terminalLayout"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; import { usePaneSizes } from "./usePaneSizes"; afterEach(() => { diff --git a/viewer-ui/src/hooks/terminal/usePaneSizes.ts b/viewer-ui/src/hooks/terminal/usePaneSizes.ts index 283118db..250f2ed6 100644 --- a/viewer-ui/src/hooks/terminal/usePaneSizes.ts +++ b/viewer-ui/src/hooks/terminal/usePaneSizes.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef } from "react"; import type { MutableRefObject } from "react"; -import type { PaneView } from "../../lib/terminalLayout"; -import type { PaneViewMode } from "../../lib/paneViewMode"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; +import type { PaneViewMode } from "../../lib/terminal/paneViewMode"; import type { ScreenScale } from "../ui/screenScale"; import { sendTerminalMessage, diff --git a/viewer-ui/src/hooks/terminal/usePasteImage.test.tsx b/viewer-ui/src/hooks/terminal/usePasteImage.test.tsx index 479a91bc..0b6afc6b 100644 --- a/viewer-ui/src/hooks/terminal/usePasteImage.test.tsx +++ b/viewer-ui/src/hooks/terminal/usePasteImage.test.tsx @@ -3,14 +3,14 @@ import { renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { usePasteImage } from "./usePasteImage"; -import type { PaneView } from "../../lib/terminalLayout"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; const { pasteImage, toastError } = vi.hoisted(() => ({ pasteImage: vi.fn(), toastError: vi.fn(), })); vi.mock("../../api", () => ({ api: { pasteImage } })); -vi.mock("../../lib/toast", () => ({ toast: { error: toastError } })); +vi.mock("../../lib/runtime/toast", () => ({ toast: { error: toastError } })); function fakeSocket() { const sent: unknown[] = []; diff --git a/viewer-ui/src/hooks/terminal/usePasteImage.ts b/viewer-ui/src/hooks/terminal/usePasteImage.ts index bc9e506a..802dafc2 100644 --- a/viewer-ui/src/hooks/terminal/usePasteImage.ts +++ b/viewer-ui/src/hooks/terminal/usePasteImage.ts @@ -2,10 +2,10 @@ import { useEffect } from "react"; import type { MutableRefObject, RefObject } from "react"; import { api } from "../../api"; import { sendTerminalMessage } from "../../api/terminal"; -import { composedInput } from "../../lib/composeInput"; -import { pastedImage, pathInput } from "../../lib/pasteImage"; -import type { PaneView } from "../../lib/terminalLayout"; -import { toast } from "../../lib/toast"; +import { composedInput } from "../../lib/terminal/composeInput"; +import { pastedImage, pathInput } from "../../lib/terminal/pasteImage"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; +import { toast } from "../../lib/runtime/toast"; /** Which pane an event happened in, from the cell it came from. */ function paneOf(target: EventTarget | null): number | null { diff --git a/viewer-ui/src/hooks/terminal/useStartupSizes.ts b/viewer-ui/src/hooks/terminal/useStartupSizes.ts index 5c89d96f..c01ff4d6 100644 --- a/viewer-ui/src/hooks/terminal/useStartupSizes.ts +++ b/viewer-ui/src/hooks/terminal/useStartupSizes.ts @@ -2,7 +2,7 @@ import { useEffect } from "react"; import type { MutableRefObject } from "react"; import { Terminal } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; -import { terminalFontOptions } from "../../lib/termFont"; +import { terminalFontOptions } from "../../lib/terminal/termFont"; import { sendTerminalMessage, type PaneSize, diff --git a/viewer-ui/src/hooks/terminal/useTerminalPanelState.ts b/viewer-ui/src/hooks/terminal/useTerminalPanelState.ts index 5574c8df..3dd65505 100644 --- a/viewer-ui/src/hooks/terminal/useTerminalPanelState.ts +++ b/viewer-ui/src/hooks/terminal/useTerminalPanelState.ts @@ -1,5 +1,5 @@ import { useState } from "react"; -import type { LinkState } from "../../lib/attachStatus"; +import type { LinkState } from "../../lib/terminal/attachStatus"; export function useTerminalPanelState() { const [pending, setPending] = useState(null); diff --git a/viewer-ui/src/hooks/terminal/useTerminalRefs.ts b/viewer-ui/src/hooks/terminal/useTerminalRefs.ts index e4eca5d7..86486695 100644 --- a/viewer-ui/src/hooks/terminal/useTerminalRefs.ts +++ b/viewer-ui/src/hooks/terminal/useTerminalRefs.ts @@ -1,10 +1,10 @@ import { useRef } from "react"; -import type { PaneView } from "../../lib/terminalLayout"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; import type { PaneSize } from "../../api/terminal"; import { createTerminalReplayState, type PendingTerminalWrite, -} from "../../lib/terminalReplay"; +} from "../../lib/terminal/terminalReplay"; /** * The mutable state the terminal panel's hooks share. diff --git a/viewer-ui/src/hooks/terminal/useTerminalShortcuts.test.tsx b/viewer-ui/src/hooks/terminal/useTerminalShortcuts.test.tsx index 5bfd7562..3c7a4bbf 100644 --- a/viewer-ui/src/hooks/terminal/useTerminalShortcuts.test.tsx +++ b/viewer-ui/src/hooks/terminal/useTerminalShortcuts.test.tsx @@ -7,13 +7,13 @@ import { cleanup, render } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { DEFAULT_LEADER } from "../../lib/leaderChord"; +import { DEFAULT_LEADER } from "../../lib/shortcuts/leaderChord"; import { ShortcutIntentProvider, useShortcutIntents, type ShortcutIntents, -} from "../shortcutIntents"; -import { ShortcutEngine, leader, pane, press } from "../useShortcuts.harness"; +} from "../shortcuts/shortcutIntents"; +import { ShortcutEngine, leader, pane, press } from "../shortcuts/useShortcuts.harness"; import { useTerminalShortcuts, type UseTerminalShortcutsArgs, diff --git a/viewer-ui/src/hooks/terminal/useTerminalShortcuts.ts b/viewer-ui/src/hooks/terminal/useTerminalShortcuts.ts index 31aaf032..63384fea 100644 --- a/viewer-ui/src/hooks/terminal/useTerminalShortcuts.ts +++ b/viewer-ui/src/hooks/terminal/useTerminalShortcuts.ts @@ -1,14 +1,14 @@ import { useEffect, useMemo, useRef } from "react"; import type { MutableRefObject } from "react"; import { sendTerminalMessage } from "../../api/terminal"; -import type { LinkState } from "../../lib/attachStatus"; -import { paneAt, swapOrder } from "../../lib/paneOrder"; -import { SHORTCUT_ACTIONS, focusPaneNumber } from "../../lib/shortcutActions"; +import type { LinkState } from "../../lib/terminal/attachStatus"; +import { paneAt, swapOrder } from "../../lib/terminal/paneOrder"; +import { SHORTCUT_ACTIONS, focusPaneNumber } from "../../lib/shortcuts/shortcutActions"; import { useRegisterShortcutHandlers, useShortcutIntents, type ShortcutHandlers, -} from "../shortcutIntents"; +} from "../shortcuts/shortcutIntents"; // The panel's half of the shortcut registry. // diff --git a/viewer-ui/src/hooks/terminal/useTerminalSizingControl.ts b/viewer-ui/src/hooks/terminal/useTerminalSizingControl.ts index b74490e2..eb543365 100644 --- a/viewer-ui/src/hooks/terminal/useTerminalSizingControl.ts +++ b/viewer-ui/src/hooks/terminal/useTerminalSizingControl.ts @@ -1,9 +1,9 @@ import { useCallback, useState } from "react"; import type { MutableRefObject } from "react"; import { sendTerminalMessage } from "../../api/terminal"; -import type { LinkState } from "../../lib/attachStatus"; -import { shouldRequestSizeForActivity } from "../../lib/sizeActivity"; -import { useViewerActivity } from "../viewerActivity"; +import type { LinkState } from "../../lib/terminal/attachStatus"; +import { shouldRequestSizeForActivity } from "../../lib/terminal/sizeActivity"; +import { useViewerActivity } from "../ui/viewerActivity"; export function useTerminalSizingControl( link: LinkState, diff --git a/viewer-ui/src/hooks/terminal/useTerminalSocket.ts b/viewer-ui/src/hooks/terminal/useTerminalSocket.ts index 1a950665..a918e56b 100644 --- a/viewer-ui/src/hooks/terminal/useTerminalSocket.ts +++ b/viewer-ui/src/hooks/terminal/useTerminalSocket.ts @@ -1,14 +1,14 @@ import { useLayoutEffect, useRef } from "react"; import type { MutableRefObject } from "react"; -import type { LinkState } from "../../lib/attachStatus"; -import type { RecoveryByPane } from "../../lib/recovery"; -import { viewerId } from "../../lib/viewerId"; -import type { PaneView } from "../../lib/terminalLayout"; +import type { LinkState } from "../../lib/terminal/attachStatus"; +import type { RecoveryByPane } from "../../lib/terminal/recovery"; +import { viewerId } from "../../lib/runtime/viewerId"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; import type { PaneSize } from "../../api/terminal"; import type { PendingTerminalWrite, TerminalReplayState, -} from "../../lib/terminalReplay"; +} from "../../lib/terminal/terminalReplay"; import { handleTerminalSocketMessage } from "./terminalSocketMessages"; interface UseTerminalSocketArgs { diff --git a/viewer-ui/src/hooks/terminal/useTerminalViews.ts b/viewer-ui/src/hooks/terminal/useTerminalViews.ts index 47976748..41e52541 100644 --- a/viewer-ui/src/hooks/terminal/useTerminalViews.ts +++ b/viewer-ui/src/hooks/terminal/useTerminalViews.ts @@ -2,18 +2,18 @@ import { useEffect } from "react"; import type { MutableRefObject } from "react"; import { Terminal } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; -import type { PaneView } from "../../lib/terminalLayout"; -import type { PaneViewMode } from "../../lib/paneViewMode"; -import { terminalFontOptions } from "../../lib/termFont"; -import { ClearKeyProbe } from "../../lib/clearKeyProbe"; -import { browserHandlesKey, overriddenKeySequence } from "../../lib/hardwareKeys"; -import { receivePaneClipboard } from "../../lib/paneClipboard"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; +import type { PaneViewMode } from "../../lib/terminal/paneViewMode"; +import { terminalFontOptions } from "../../lib/terminal/termFont"; +import { ClearKeyProbe } from "../../lib/terminal/clearKeyProbe"; +import { browserHandlesKey, overriddenKeySequence } from "../../lib/terminal/hardwareKeys"; +import { receivePaneClipboard } from "../../lib/terminal/paneClipboard"; import { installReplayReplyGate, queueReplayEnd, type PendingTerminalWrite, type TerminalReplayState, -} from "../../lib/terminalReplay"; +} from "../../lib/terminal/terminalReplay"; import { sendTerminalMessage, type PaneSize } from "../../api/terminal"; import type { ScreenScale } from "../ui/screenScale"; diff --git a/viewer-ui/src/hooks/terminal/useTerminalWiring.ts b/viewer-ui/src/hooks/terminal/useTerminalWiring.ts index 7215ae27..b8b9d4d3 100644 --- a/viewer-ui/src/hooks/terminal/useTerminalWiring.ts +++ b/viewer-ui/src/hooks/terminal/useTerminalWiring.ts @@ -1,9 +1,9 @@ import { useCallback } from "react"; -import type { LinkState } from "../../lib/attachStatus"; -import type { PaneViewMode } from "../../lib/paneViewMode"; -import type { RecoveryByPane } from "../../lib/recovery"; +import type { LinkState } from "../../lib/terminal/attachStatus"; +import type { PaneViewMode } from "../../lib/terminal/paneViewMode"; +import type { RecoveryByPane } from "../../lib/terminal/recovery"; import type { ScreenScale } from "../ui/screenScale"; -import { zoomPending } from "../../lib/zoom"; +import { zoomPending } from "../../lib/terminal/zoom"; import type { TerminalRefs } from "./useTerminalRefs"; import { usePaneFocus } from "./usePaneFocus"; import { usePaneSizes } from "./usePaneSizes"; diff --git a/viewer-ui/src/hooks/terminal/useTouchScroll.ts b/viewer-ui/src/hooks/terminal/useTouchScroll.ts index 7d6d3042..53f30310 100644 --- a/viewer-ui/src/hooks/terminal/useTouchScroll.ts +++ b/viewer-ui/src/hooks/terminal/useTouchScroll.ts @@ -1,11 +1,11 @@ import { useCallback, useRef } from "react"; import type { MutableRefObject } from "react"; -import type { PaneView } from "../../lib/terminalLayout"; +import type { PaneView } from "../../lib/terminal/terminalLayout"; import { advanceTouchScroll, beginTouchScroll, type TouchScroll, -} from "../../lib/touchScroll"; +} from "../../lib/terminal/touchScroll"; interface UseTouchScrollArgs { viewsRef: MutableRefObject>; diff --git a/viewer-ui/src/hooks/ui/paneViewMode.ts b/viewer-ui/src/hooks/ui/paneViewMode.ts index 3156dae9..b28508b6 100644 --- a/viewer-ui/src/hooks/ui/paneViewMode.ts +++ b/viewer-ui/src/hooks/ui/paneViewMode.ts @@ -8,7 +8,7 @@ import { defaultPaneViewMode, parsePaneViewMode, type PaneViewMode, -} from "../../lib/paneViewMode"; +} from "../../lib/terminal/paneViewMode"; const STORAGE_KEY = "nightcrow.paneViewMode"; diff --git a/viewer-ui/src/hooks/ui/tabStripSide.test.ts b/viewer-ui/src/hooks/ui/tabStripSide.test.ts index f431ccb5..eda19d50 100644 --- a/viewer-ui/src/hooks/ui/tabStripSide.test.ts +++ b/viewer-ui/src/hooks/ui/tabStripSide.test.ts @@ -2,7 +2,7 @@ import { act, cleanup, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { stubLocalStorage } from "../../lib/fakeStorage"; +import { stubLocalStorage } from "../../lib/shared/fakeStorage"; import { useTabStripSide } from "./tabStripSide"; beforeEach(stubLocalStorage); diff --git a/viewer-ui/src/hooks/ui/tabStripSide.ts b/viewer-ui/src/hooks/ui/tabStripSide.ts index bb6f6a7e..bef1dcb2 100644 --- a/viewer-ui/src/hooks/ui/tabStripSide.ts +++ b/viewer-ui/src/hooks/ui/tabStripSide.ts @@ -10,7 +10,7 @@ import { otherSide, parseTabStripSide, type TabStripSide, -} from "../../lib/tabStripSide"; +} from "../../lib/ui/tabStripSide"; const STORAGE_KEY = "nightcrow.tabStripSide"; diff --git a/viewer-ui/src/hooks/ui/termKeyBar.test.ts b/viewer-ui/src/hooks/ui/termKeyBar.test.ts index 05b634cf..2280266e 100644 --- a/viewer-ui/src/hooks/ui/termKeyBar.test.ts +++ b/viewer-ui/src/hooks/ui/termKeyBar.test.ts @@ -15,8 +15,8 @@ import { act, cleanup, renderHook } from "@testing-library/react"; import type { Window as HappyDOMWindow } from "happy-dom"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { useTermKeyBar } from "./termKeyBar"; -import { stubLocalStorage } from "../../lib/fakeStorage"; -import { KEYBOARD_MIN_VIEWPORT_PX } from "../../lib/termKeys"; +import { stubLocalStorage } from "../../lib/shared/fakeStorage"; +import { KEYBOARD_MIN_VIEWPORT_PX } from "../../lib/terminal/termKeys"; const WIDE = KEYBOARD_MIN_VIEWPORT_PX + 300; diff --git a/viewer-ui/src/hooks/ui/termKeyBar.ts b/viewer-ui/src/hooks/ui/termKeyBar.ts index c7dcdb9b..3dc3ceb5 100644 --- a/viewer-ui/src/hooks/ui/termKeyBar.ts +++ b/viewer-ui/src/hooks/ui/termKeyBar.ts @@ -8,7 +8,7 @@ import { defaultKeyBarShown, parseKeyBarPref, type KeyBarPref, -} from "../../lib/termKeys"; +} from "../../lib/terminal/termKeys"; const STORAGE_KEY = "nightcrow.termKeyBar"; const COARSE_QUERY = "(pointer: coarse)"; diff --git a/viewer-ui/src/hooks/ui/upperPct.ts b/viewer-ui/src/hooks/ui/upperPct.ts index b5012798..be01dbe5 100644 --- a/viewer-ui/src/hooks/ui/upperPct.ts +++ b/viewer-ui/src/hooks/ui/upperPct.ts @@ -6,7 +6,7 @@ import { DEFAULT_UPPER_PCT, clampUpperPct, clampUpperPctExact, -} from "../../lib/upperPct"; +} from "../../lib/ui/upperPct"; const STORAGE_KEY = "nightcrow.upperPct"; diff --git a/viewer-ui/src/hooks/ui/useHotClock.ts b/viewer-ui/src/hooks/ui/useHotClock.ts index d7a3ae9e..9be7aa03 100644 --- a/viewer-ui/src/hooks/ui/useHotClock.ts +++ b/viewer-ui/src/hooks/ui/useHotClock.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { anyHot, HOT_TICK_MS, type HotStage } from "../../lib/hot"; +import { anyHot, HOT_TICK_MS, type HotStage } from "../../lib/repository/hot"; import type { ChangedFile } from "../../api"; /** Keep change-kind colors stable as highlights fade. */ diff --git a/viewer-ui/src/hooks/useMaximized.ts b/viewer-ui/src/hooks/ui/useMaximized.ts similarity index 95% rename from viewer-ui/src/hooks/useMaximized.ts rename to viewer-ui/src/hooks/ui/useMaximized.ts index 38c0e8ec..f8165df7 100644 --- a/viewer-ui/src/hooks/useMaximized.ts +++ b/viewer-ui/src/hooks/ui/useMaximized.ts @@ -1,8 +1,8 @@ import { useCallback, useRef, useState } from "react"; -import { createSerialWriter } from "../lib/serialWrite"; -import { api } from "../api"; -import type { MaximizedByRepo } from "../api"; -import type { Maximized } from "../types"; +import { createSerialWriter } from "../../lib/shared/serialWrite"; +import { api } from "../../api"; +import type { MaximizedByRepo } from "../../api"; +import type { Maximized } from "../../types"; /** * Which panel each project is maximized in. diff --git a/viewer-ui/src/hooks/ui/useScrollViewport.ts b/viewer-ui/src/hooks/ui/useScrollViewport.ts index 5810eee9..b8c84635 100644 --- a/viewer-ui/src/hooks/ui/useScrollViewport.ts +++ b/viewer-ui/src/hooks/ui/useScrollViewport.ts @@ -1,5 +1,5 @@ import { useCallback, useLayoutEffect, useState } from "react"; -import type { ScrollViewport } from "../../lib/virtualWindow"; +import type { ScrollViewport } from "../../lib/ui/virtualWindow"; const FALLBACK_HEIGHT = 600; diff --git a/viewer-ui/src/hooks/useShellLayout.ts b/viewer-ui/src/hooks/ui/useShellLayout.ts similarity index 100% rename from viewer-ui/src/hooks/useShellLayout.ts rename to viewer-ui/src/hooks/ui/useShellLayout.ts diff --git a/viewer-ui/src/hooks/useSidebarDrag.ts b/viewer-ui/src/hooks/ui/useSidebarDrag.ts similarity index 97% rename from viewer-ui/src/hooks/useSidebarDrag.ts rename to viewer-ui/src/hooks/ui/useSidebarDrag.ts index 5be7068e..2d5785dc 100644 --- a/viewer-ui/src/hooks/useSidebarDrag.ts +++ b/viewer-ui/src/hooks/ui/useSidebarDrag.ts @@ -1,6 +1,6 @@ import { useCallback, useRef } from "react"; import type { PointerEvent as ReactPointerEvent } from "react"; -import { useDividerDrag } from "./ui/useDividerDrag"; +import { useDividerDrag } from "./useDividerDrag"; export interface UseSidebarDragArgs { sidebarRef: React.RefObject; diff --git a/viewer-ui/src/hooks/ui/useSoftKeyboard.test.tsx b/viewer-ui/src/hooks/ui/useSoftKeyboard.test.tsx index 42d32c9b..d82270d6 100644 --- a/viewer-ui/src/hooks/ui/useSoftKeyboard.test.tsx +++ b/viewer-ui/src/hooks/ui/useSoftKeyboard.test.tsx @@ -2,7 +2,7 @@ import { act, cleanup, render } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; -import type { KeyboardWindowLike } from "../../lib/visualViewport"; +import type { KeyboardWindowLike } from "../../lib/ui/visualViewport"; import { useSoftKeyboardOpen } from "./useSoftKeyboard"; afterEach(cleanup); diff --git a/viewer-ui/src/hooks/ui/useSoftKeyboard.ts b/viewer-ui/src/hooks/ui/useSoftKeyboard.ts index da1d569f..9f049baf 100644 --- a/viewer-ui/src/hooks/ui/useSoftKeyboard.ts +++ b/viewer-ui/src/hooks/ui/useSoftKeyboard.ts @@ -2,7 +2,7 @@ import { useSyncExternalStore } from "react"; import { softKeyboardInset, type KeyboardWindowLike, -} from "../../lib/visualViewport"; +} from "../../lib/ui/visualViewport"; /** * Whether a soft keyboard is covering part of the page. diff --git a/viewer-ui/src/hooks/useUpperPctDrag.ts b/viewer-ui/src/hooks/ui/useUpperPctDrag.ts similarity index 96% rename from viewer-ui/src/hooks/useUpperPctDrag.ts rename to viewer-ui/src/hooks/ui/useUpperPctDrag.ts index ce298bd7..26062b7a 100644 --- a/viewer-ui/src/hooks/useUpperPctDrag.ts +++ b/viewer-ui/src/hooks/ui/useUpperPctDrag.ts @@ -1,7 +1,7 @@ import { useCallback, useRef } from "react"; import type { PointerEvent as ReactPointerEvent } from "react"; -import { useDividerDrag } from "./ui/useDividerDrag"; -import { upperPctAt } from "../lib/upperPct"; +import { useDividerDrag } from "./useDividerDrag"; +import { upperPctAt } from "../../lib/ui/upperPct"; export interface UseUpperPctDragArgs { /** The diff panel — the split region's top edge. */ diff --git a/viewer-ui/src/hooks/useViewerPrefs.ts b/viewer-ui/src/hooks/ui/useViewerPrefs.ts similarity index 93% rename from viewer-ui/src/hooks/useViewerPrefs.ts rename to viewer-ui/src/hooks/ui/useViewerPrefs.ts index 9c531679..dd7ea7ae 100644 --- a/viewer-ui/src/hooks/useViewerPrefs.ts +++ b/viewer-ui/src/hooks/ui/useViewerPrefs.ts @@ -1,9 +1,9 @@ import { useCallback, useRef } from "react"; -import { useAccent } from "./ui/theme"; -import { useSidebarWidth } from "./ui/sidebar"; -import { useUpperPct } from "./ui/upperPct"; +import { useAccent } from "./theme"; +import { useSidebarWidth } from "./sidebar"; +import { useUpperPct } from "./upperPct"; import { useMaximized } from "./useMaximized"; -import { useRepoView } from "./useRepoView"; +import { useRepoView } from "../git/useRepoView"; /** * The preferences this page owns locally, and the bookkeeping that keeps the diff --git a/viewer-ui/src/hooks/viewerActivity.test.tsx b/viewer-ui/src/hooks/ui/viewerActivity.test.tsx similarity index 97% rename from viewer-ui/src/hooks/viewerActivity.test.tsx rename to viewer-ui/src/hooks/ui/viewerActivity.test.tsx index 5a6f4eda..e5971700 100644 --- a/viewer-ui/src/hooks/viewerActivity.test.tsx +++ b/viewer-ui/src/hooks/ui/viewerActivity.test.tsx @@ -2,7 +2,7 @@ import { act, cleanup, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { useGlobalKeydown } from "./useGlobalKeydown"; +import { useGlobalKeydown } from "../shortcuts/useGlobalKeydown"; import { useViewerActivity, ViewerActivityProvider } from "./viewerActivity"; afterEach(() => { diff --git a/viewer-ui/src/hooks/viewerActivity.tsx b/viewer-ui/src/hooks/ui/viewerActivity.tsx similarity index 100% rename from viewer-ui/src/hooks/viewerActivity.tsx rename to viewer-ui/src/hooks/ui/viewerActivity.tsx diff --git a/viewer-ui/src/lib/diffAnchor.test.ts b/viewer-ui/src/lib/content/diffAnchor.test.ts similarity index 98% rename from viewer-ui/src/lib/diffAnchor.test.ts rename to viewer-ui/src/lib/content/diffAnchor.test.ts index 9cfc2062..8ae1de5f 100644 --- a/viewer-ui/src/lib/diffAnchor.test.ts +++ b/viewer-ui/src/lib/content/diffAnchor.test.ts @@ -5,7 +5,7 @@ import { anchorWithin, hunkAtTop, } from "./diffAnchor"; -import type { Diff, DiffLine } from "../api"; +import type { Diff, DiffLine } from "../../api"; const line = (new_lineno?: number): DiffLine => ({ kind: new_lineno === undefined ? "-" : " ", diff --git a/viewer-ui/src/lib/diffAnchor.ts b/viewer-ui/src/lib/content/diffAnchor.ts similarity index 98% rename from viewer-ui/src/lib/diffAnchor.ts rename to viewer-ui/src/lib/content/diffAnchor.ts index dbc179fc..58fa608a 100644 --- a/viewer-ui/src/lib/diffAnchor.ts +++ b/viewer-ui/src/lib/content/diffAnchor.ts @@ -1,4 +1,4 @@ -import type { Diff } from "../api"; +import type { Diff } from "../../api"; /** * The line of the whole file to open at, given the diff being left. diff --git a/viewer-ui/src/lib/diffLayout.test.ts b/viewer-ui/src/lib/content/diffLayout.test.ts similarity index 98% rename from viewer-ui/src/lib/diffLayout.test.ts rename to viewer-ui/src/lib/content/diffLayout.test.ts index 841faa5e..3061fbfc 100644 --- a/viewer-ui/src/lib/diffLayout.test.ts +++ b/viewer-ui/src/lib/content/diffLayout.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { splitHunkRows } from "./diffLayout"; -import type { DiffLine } from "../api"; +import type { DiffLine } from "../../api"; // Kind-only fixtures: the pairing logic looks at `kind` alone, so the spans // carry a single marker span whose text echoes the kind for readable asserts. diff --git a/viewer-ui/src/lib/diffLayout.ts b/viewer-ui/src/lib/content/diffLayout.ts similarity index 97% rename from viewer-ui/src/lib/diffLayout.ts rename to viewer-ui/src/lib/content/diffLayout.ts index f6303666..4983f835 100644 --- a/viewer-ui/src/lib/diffLayout.ts +++ b/viewer-ui/src/lib/content/diffLayout.ts @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import type { DiffLine } from "../api"; +import type { DiffLine } from "../../api"; export type DiffLayout = "unified" | "split"; diff --git a/viewer-ui/src/lib/fileView.test.ts b/viewer-ui/src/lib/content/fileView.test.ts similarity index 100% rename from viewer-ui/src/lib/fileView.test.ts rename to viewer-ui/src/lib/content/fileView.test.ts diff --git a/viewer-ui/src/lib/fileView.ts b/viewer-ui/src/lib/content/fileView.ts similarity index 95% rename from viewer-ui/src/lib/fileView.ts rename to viewer-ui/src/lib/content/fileView.ts index 130601a8..28c7187c 100644 --- a/viewer-ui/src/lib/fileView.ts +++ b/viewer-ui/src/lib/content/fileView.ts @@ -1,4 +1,4 @@ -import type { Span } from "../api"; +import type { Span } from "../../api"; const MARKDOWN_EXTENSIONS = [".md", ".markdown"]; const HTML_EXTENSIONS = [".html", ".htm"]; diff --git a/viewer-ui/src/lib/gutter.test.ts b/viewer-ui/src/lib/content/gutter.test.ts similarity index 97% rename from viewer-ui/src/lib/gutter.test.ts rename to viewer-ui/src/lib/content/gutter.test.ts index fa561689..efedb5e9 100644 --- a/viewer-ui/src/lib/gutter.test.ts +++ b/viewer-ui/src/lib/content/gutter.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { digitsFor, linenoDigits } from "./gutter"; -import type { DiffHunk, DiffLine } from "../api"; +import type { DiffHunk, DiffLine } from "../../api"; function hunk(lines: DiffLine[]): DiffHunk { return { header: "@@", lines }; diff --git a/viewer-ui/src/lib/gutter.ts b/viewer-ui/src/lib/content/gutter.ts similarity index 92% rename from viewer-ui/src/lib/gutter.ts rename to viewer-ui/src/lib/content/gutter.ts index eba277dd..45b0bd51 100644 --- a/viewer-ui/src/lib/gutter.ts +++ b/viewer-ui/src/lib/content/gutter.ts @@ -1,9 +1,9 @@ /** * Line-number gutter arithmetic, mirroring the TUI's `ui/diff_viewer/gutter.rs` * so the same file reads the same width in both frontends. The geometry the - * digits feed lives in `components/LineNos.tsx`. + * digits feed lives in `components/content/LineNos.tsx`. */ -import type { DiffHunk } from "../api"; +import type { DiffHunk } from "../../api"; /** Minimum digits reserved for one line-number column. Keeps the gutter — and * with it the body's left edge — from twitching between a 99-line file and a diff --git a/viewer-ui/src/lib/otherFace.test.ts b/viewer-ui/src/lib/content/otherFace.test.ts similarity index 98% rename from viewer-ui/src/lib/otherFace.test.ts rename to viewer-ui/src/lib/content/otherFace.test.ts index 30053c7f..356a85f2 100644 --- a/viewer-ui/src/lib/otherFace.test.ts +++ b/viewer-ui/src/lib/content/otherFace.test.ts @@ -5,8 +5,8 @@ import { showsText, sourceKey, } from "./otherFace"; -import type { DiffLine } from "../api"; -import type { FileSource, Pane } from "../types"; +import type { DiffLine } from "../../api"; +import type { FileSource, Pane } from "../../types"; const line = (new_lineno?: number): DiffLine => ({ kind: " ", diff --git a/viewer-ui/src/lib/otherFace.ts b/viewer-ui/src/lib/content/otherFace.ts similarity index 97% rename from viewer-ui/src/lib/otherFace.ts rename to viewer-ui/src/lib/content/otherFace.ts index 1c6e345c..0ecdcdbc 100644 --- a/viewer-ui/src/lib/otherFace.ts +++ b/viewer-ui/src/lib/content/otherFace.ts @@ -1,5 +1,5 @@ -import type { ChangedFile, Diff } from "../api"; -import type { FileSource, Pane } from "../types"; +import type { ChangedFile, Diff } from "../../api"; +import type { FileSource, Pane } from "../../types"; /** * What a pane's other face is, or `null` when it has none. diff --git a/viewer-ui/src/lib/activeRepo.test.ts b/viewer-ui/src/lib/repository/activeRepo.test.ts similarity index 100% rename from viewer-ui/src/lib/activeRepo.test.ts rename to viewer-ui/src/lib/repository/activeRepo.test.ts diff --git a/viewer-ui/src/lib/activeRepo.ts b/viewer-ui/src/lib/repository/activeRepo.ts similarity index 100% rename from viewer-ui/src/lib/activeRepo.ts rename to viewer-ui/src/lib/repository/activeRepo.ts diff --git a/viewer-ui/src/lib/hot.test.ts b/viewer-ui/src/lib/repository/hot.test.ts similarity index 100% rename from viewer-ui/src/lib/hot.test.ts rename to viewer-ui/src/lib/repository/hot.test.ts diff --git a/viewer-ui/src/lib/hot.ts b/viewer-ui/src/lib/repository/hot.ts similarity index 100% rename from viewer-ui/src/lib/hot.ts rename to viewer-ui/src/lib/repository/hot.ts diff --git a/viewer-ui/src/lib/logRefresh.test.ts b/viewer-ui/src/lib/repository/logRefresh.test.ts similarity index 98% rename from viewer-ui/src/lib/logRefresh.test.ts rename to viewer-ui/src/lib/repository/logRefresh.test.ts index 5156aa35..9a54b12a 100644 --- a/viewer-ui/src/lib/logRefresh.test.ts +++ b/viewer-ui/src/lib/repository/logRefresh.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { reconcileLog } from "./logRefresh"; -import type { Commit, Log } from "../api"; +import type { Commit, Log } from "../../api"; function commit(oid: string): Commit { return { oid, short_id: oid.slice(0, 7), summary: oid, author: "a", time: 0 }; diff --git a/viewer-ui/src/lib/logRefresh.ts b/viewer-ui/src/lib/repository/logRefresh.ts similarity index 98% rename from viewer-ui/src/lib/logRefresh.ts rename to viewer-ui/src/lib/repository/logRefresh.ts index a58b8dba..15c71c62 100644 --- a/viewer-ui/src/lib/logRefresh.ts +++ b/viewer-ui/src/lib/repository/logRefresh.ts @@ -1,4 +1,4 @@ -import type { Commit, Log } from "../api"; +import type { Commit, Log } from "../../api"; /// What a head-move refresh decided to show. `anchor` is what later pages pin /// to, and `done` whether the history continues past what is held. diff --git a/viewer-ui/src/lib/projectCycle.test.ts b/viewer-ui/src/lib/repository/projectCycle.test.ts similarity index 100% rename from viewer-ui/src/lib/projectCycle.test.ts rename to viewer-ui/src/lib/repository/projectCycle.test.ts diff --git a/viewer-ui/src/lib/projectCycle.ts b/viewer-ui/src/lib/repository/projectCycle.ts similarity index 100% rename from viewer-ui/src/lib/projectCycle.ts rename to viewer-ui/src/lib/repository/projectCycle.ts diff --git a/viewer-ui/src/lib/repoSnapshot.ts b/viewer-ui/src/lib/repository/repoSnapshot.ts similarity index 93% rename from viewer-ui/src/lib/repoSnapshot.ts rename to viewer-ui/src/lib/repository/repoSnapshot.ts index 1fb7badc..9e1446bb 100644 --- a/viewer-ui/src/lib/repoSnapshot.ts +++ b/viewer-ui/src/lib/repository/repoSnapshot.ts @@ -1,4 +1,4 @@ -import type { HotConfig, Repo } from "../api"; +import type { HotConfig, Repo } from "../../api"; export function retainHot( current: HotConfig | null, diff --git a/viewer-ui/src/lib/repoView.test.ts b/viewer-ui/src/lib/repository/repoView.test.ts similarity index 98% rename from viewer-ui/src/lib/repoView.test.ts rename to viewer-ui/src/lib/repository/repoView.test.ts index 25b550a7..de1387e8 100644 --- a/viewer-ui/src/lib/repoView.test.ts +++ b/viewer-ui/src/lib/repository/repoView.test.ts @@ -9,7 +9,7 @@ import { sameView, workdirFile, } from "./repoView"; -import type { RepoView } from "../api"; +import type { RepoView } from "../../api"; describe("what a tap records", () => { it("워킹트리_파일은_경로와_어느_면인지로_남는다", () => { diff --git a/viewer-ui/src/lib/repoView.ts b/viewer-ui/src/lib/repository/repoView.ts similarity index 97% rename from viewer-ui/src/lib/repoView.ts rename to viewer-ui/src/lib/repository/repoView.ts index b964fb1f..adf99f3b 100644 --- a/viewer-ui/src/lib/repoView.ts +++ b/viewer-ui/src/lib/repository/repoView.ts @@ -6,8 +6,8 @@ * one side knows is a view that never comes back. */ -import type { RepoView, ViewFile } from "../api"; -import type { Tab } from "../types"; +import type { RepoView, ViewFile } from "../../api"; +import type { Tab } from "../../types"; const TABS: Tab[] = ["status", "log", "tree"]; diff --git a/viewer-ui/src/lib/successor.test.ts b/viewer-ui/src/lib/repository/successor.test.ts similarity index 100% rename from viewer-ui/src/lib/successor.test.ts rename to viewer-ui/src/lib/repository/successor.test.ts diff --git a/viewer-ui/src/lib/successor.ts b/viewer-ui/src/lib/repository/successor.ts similarity index 100% rename from viewer-ui/src/lib/successor.ts rename to viewer-ui/src/lib/repository/successor.ts diff --git a/viewer-ui/src/lib/tabChoice.test.ts b/viewer-ui/src/lib/repository/tabChoice.test.ts similarity index 100% rename from viewer-ui/src/lib/tabChoice.test.ts rename to viewer-ui/src/lib/repository/tabChoice.test.ts diff --git a/viewer-ui/src/lib/tabChoice.ts b/viewer-ui/src/lib/repository/tabChoice.ts similarity index 93% rename from viewer-ui/src/lib/tabChoice.ts rename to viewer-ui/src/lib/repository/tabChoice.ts index b8e764f2..6e49d257 100644 --- a/viewer-ui/src/lib/tabChoice.ts +++ b/viewer-ui/src/lib/repository/tabChoice.ts @@ -5,14 +5,14 @@ // so a switch that only records the tab leaves the previous list's diff on // screen, lets a pane request already in flight land after the switch, and // re-enters the log on a stale page. The TUI does all of it in one place -// (`src/app/focus.rs::toggle_mode`, which clears the diff and resets the +// (`src/app/navigation/focus.rs::toggle_mode`, which clears the diff and resets the // drill-down), and this is the web's. // // Pure: the caller supplies the four operations and this decides whether and in // which order they run, so the tab row and the keyboard cannot each grow their // own version of the sequence. -import type { Tab } from "../types"; +import type { Tab } from "../../types"; export interface TabChoiceOps { /** Invalidate pane requests in flight, so a reply asked for by the list being diff --git a/viewer-ui/src/lib/tabLabel.test.ts b/viewer-ui/src/lib/repository/tabLabel.test.ts similarity index 100% rename from viewer-ui/src/lib/tabLabel.test.ts rename to viewer-ui/src/lib/repository/tabLabel.test.ts diff --git a/viewer-ui/src/lib/tabLabel.ts b/viewer-ui/src/lib/repository/tabLabel.ts similarity index 100% rename from viewer-ui/src/lib/tabLabel.ts rename to viewer-ui/src/lib/repository/tabLabel.ts diff --git a/viewer-ui/src/lib/tree.ts b/viewer-ui/src/lib/repository/tree.ts similarity index 93% rename from viewer-ui/src/lib/tree.ts rename to viewer-ui/src/lib/repository/tree.ts index 5f330a46..c9d1b0b9 100644 --- a/viewer-ui/src/lib/tree.ts +++ b/viewer-ui/src/lib/repository/tree.ts @@ -1,4 +1,4 @@ -import type { TreeEntry } from "../api"; +import type { TreeEntry } from "../../api"; export interface TreeRow { path: string; diff --git a/viewer-ui/src/lib/treeCache.test.ts b/viewer-ui/src/lib/repository/treeCache.test.ts similarity index 97% rename from viewer-ui/src/lib/treeCache.test.ts rename to viewer-ui/src/lib/repository/treeCache.test.ts index 9cf732d6..23333b1e 100644 --- a/viewer-ui/src/lib/treeCache.test.ts +++ b/viewer-ui/src/lib/repository/treeCache.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import type { TreeEntry } from "../api"; +import type { TreeEntry } from "../../api"; import { ancestorDirs, emptyTreeCache, diff --git a/viewer-ui/src/lib/treeCache.ts b/viewer-ui/src/lib/repository/treeCache.ts similarity index 97% rename from viewer-ui/src/lib/treeCache.ts rename to viewer-ui/src/lib/repository/treeCache.ts index 18fa6175..7e66d555 100644 --- a/viewer-ui/src/lib/treeCache.ts +++ b/viewer-ui/src/lib/repository/treeCache.ts @@ -1,4 +1,4 @@ -import type { TreeEntry } from "../api"; +import type { TreeEntry } from "../../api"; /** * What the file tree remembers while a project is open: which directories are diff --git a/viewer-ui/src/lib/chunkError.test.ts b/viewer-ui/src/lib/runtime/chunkError.test.ts similarity index 100% rename from viewer-ui/src/lib/chunkError.test.ts rename to viewer-ui/src/lib/runtime/chunkError.test.ts diff --git a/viewer-ui/src/lib/chunkError.ts b/viewer-ui/src/lib/runtime/chunkError.ts similarity index 100% rename from viewer-ui/src/lib/chunkError.ts rename to viewer-ui/src/lib/runtime/chunkError.ts diff --git a/viewer-ui/src/lib/toast.test.ts b/viewer-ui/src/lib/runtime/toast.test.ts similarity index 100% rename from viewer-ui/src/lib/toast.test.ts rename to viewer-ui/src/lib/runtime/toast.test.ts diff --git a/viewer-ui/src/lib/toast.ts b/viewer-ui/src/lib/runtime/toast.ts similarity index 100% rename from viewer-ui/src/lib/toast.ts rename to viewer-ui/src/lib/runtime/toast.ts diff --git a/viewer-ui/src/lib/viewerBuild.test.ts b/viewer-ui/src/lib/runtime/viewerBuild.test.ts similarity index 100% rename from viewer-ui/src/lib/viewerBuild.test.ts rename to viewer-ui/src/lib/runtime/viewerBuild.test.ts diff --git a/viewer-ui/src/lib/viewerBuild.ts b/viewer-ui/src/lib/runtime/viewerBuild.ts similarity index 97% rename from viewer-ui/src/lib/viewerBuild.ts rename to viewer-ui/src/lib/runtime/viewerBuild.ts index c8fd2c5f..35d27b8f 100644 --- a/viewer-ui/src/lib/viewerBuild.ts +++ b/viewer-ui/src/lib/runtime/viewerBuild.ts @@ -5,7 +5,7 @@ * The bundle is split, and the chunks it loads on demand are named by content * hash — so a rebuild does not replace what this page is running, it deletes * the names this page would ask for next. What that produces is a failure at - * the worst moment (`lib/chunkError.ts`), or nothing at all: a tab that opens + * the worst moment (`lib/runtime/chunkError.ts`), or nothing at all: a tab that opens * no preview and no new panel keeps running the old code indefinitely, with no * sign that a fix has already shipped. * diff --git a/viewer-ui/src/lib/viewerId.test.ts b/viewer-ui/src/lib/runtime/viewerId.test.ts similarity index 94% rename from viewer-ui/src/lib/viewerId.test.ts rename to viewer-ui/src/lib/runtime/viewerId.test.ts index de2904cb..1ab2f1ab 100644 --- a/viewer-ui/src/lib/viewerId.test.ts +++ b/viewer-ui/src/lib/runtime/viewerId.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from "vitest"; -import { stubSessionStorage } from "./fakeStorage"; +import { stubSessionStorage } from "../shared/fakeStorage"; import { viewerId } from "./viewerId"; describe("viewerId", () => { diff --git a/viewer-ui/src/lib/viewerId.ts b/viewer-ui/src/lib/runtime/viewerId.ts similarity index 100% rename from viewer-ui/src/lib/viewerId.ts rename to viewer-ui/src/lib/runtime/viewerId.ts diff --git a/viewer-ui/src/lib/fakeStorage.ts b/viewer-ui/src/lib/shared/fakeStorage.ts similarity index 100% rename from viewer-ui/src/lib/fakeStorage.ts rename to viewer-ui/src/lib/shared/fakeStorage.ts diff --git a/viewer-ui/src/lib/latestRequest.test.ts b/viewer-ui/src/lib/shared/latestRequest.test.ts similarity index 100% rename from viewer-ui/src/lib/latestRequest.test.ts rename to viewer-ui/src/lib/shared/latestRequest.test.ts diff --git a/viewer-ui/src/lib/latestRequest.ts b/viewer-ui/src/lib/shared/latestRequest.ts similarity index 100% rename from viewer-ui/src/lib/latestRequest.ts rename to viewer-ui/src/lib/shared/latestRequest.ts diff --git a/viewer-ui/src/lib/serialWrite.test.ts b/viewer-ui/src/lib/shared/serialWrite.test.ts similarity index 100% rename from viewer-ui/src/lib/serialWrite.test.ts rename to viewer-ui/src/lib/shared/serialWrite.test.ts diff --git a/viewer-ui/src/lib/serialWrite.ts b/viewer-ui/src/lib/shared/serialWrite.ts similarity index 100% rename from viewer-ui/src/lib/serialWrite.ts rename to viewer-ui/src/lib/shared/serialWrite.ts diff --git a/viewer-ui/src/lib/utils.ts b/viewer-ui/src/lib/shared/utils.ts similarity index 100% rename from viewer-ui/src/lib/utils.ts rename to viewer-ui/src/lib/shared/utils.ts diff --git a/viewer-ui/src/lib/focusCycle.test.ts b/viewer-ui/src/lib/shortcuts/focusCycle.test.ts similarity index 98% rename from viewer-ui/src/lib/focusCycle.test.ts rename to viewer-ui/src/lib/shortcuts/focusCycle.test.ts index 8881019c..279e5de4 100644 --- a/viewer-ui/src/lib/focusCycle.test.ts +++ b/viewer-ui/src/lib/shortcuts/focusCycle.test.ts @@ -16,7 +16,7 @@ function ring(over: Partial = {}): FocusRing { }; } -// The ring `src/app/focus.rs` walks: list, content, each pane, back to the list. +// The ring `src/app/navigation/focus.rs` walks: list, content, each pane, back to the list. describe("nextFocus 전체 화면", () => { it("목록에서_앞으로_가면_콘텐츠다", () => { expect(nextFocus(ring({ at: list }), 1)).toEqual(content); diff --git a/viewer-ui/src/lib/focusCycle.ts b/viewer-ui/src/lib/shortcuts/focusCycle.ts similarity index 95% rename from viewer-ui/src/lib/focusCycle.ts rename to viewer-ui/src/lib/shortcuts/focusCycle.ts index 5ea2f103..ed57fdd1 100644 --- a/viewer-ui/src/lib/focusCycle.ts +++ b/viewer-ui/src/lib/shortcuts/focusCycle.ts @@ -1,6 +1,6 @@ // Where `Shift+Left` / `Shift+Right` send the keyboard. // -// The TUI walks one ring (`src/app/focus.rs::cycle_focus_forward`): the file +// The TUI walks one ring (`src/app/navigation/focus.rs::cycle_focus_forward`): the file // list, the content pane, then each terminal pane in order, and round to the // list again. The web binds the same meaning. What the two sides share is that // ring; what differs is how each knows what is on screen — the TUI has its @@ -11,7 +11,7 @@ // Pure and DOM-free: the page reads where the keyboard is and how many panes // there are; this says where it goes next. -import type { Maximized, MobileView } from "../types"; +import type { Maximized, MobileView } from "../../types"; export type FocusSpot = | { kind: "list" } diff --git a/viewer-ui/src/lib/leaderChord.test.ts b/viewer-ui/src/lib/shortcuts/leaderChord.test.ts similarity index 98% rename from viewer-ui/src/lib/leaderChord.test.ts rename to viewer-ui/src/lib/shortcuts/leaderChord.test.ts index 751b1eb9..e3e73463 100644 --- a/viewer-ui/src/lib/leaderChord.test.ts +++ b/viewer-ui/src/lib/shortcuts/leaderChord.test.ts @@ -185,7 +185,7 @@ describe("leaderConflict", () => { describe("literalLeaderSequence", () => { it("Ctrl_글자는_C0_제어문자다", () => { - // This matches `src/input/encode.rs` and `termKeys.ts::ctrlSequence`. + // This matches `src/input/encode.rs` and `lib/terminal/termKeys.ts::ctrlSequence`. expect(literalLeaderSequence(DEFAULT_LEADER)).toBe("\x06"); expect(literalLeaderSequence(spec({ key: "A", ctrl: true }))).toBe("\x01"); expect(literalLeaderSequence(spec({ key: "B", ctrl: true }))).toBe("\x02"); diff --git a/viewer-ui/src/lib/leaderChord.ts b/viewer-ui/src/lib/shortcuts/leaderChord.ts similarity index 97% rename from viewer-ui/src/lib/leaderChord.ts rename to viewer-ui/src/lib/shortcuts/leaderChord.ts index 50f3f17b..83849738 100644 --- a/viewer-ui/src/lib/leaderChord.ts +++ b/viewer-ui/src/lib/shortcuts/leaderChord.ts @@ -3,7 +3,7 @@ // // The TUI encodes the literal leader with `src/input/encode.rs::encode_key`; // the web's terminal-side encoder for the same bytes is -// `viewer-ui/src/lib/termKeys.ts::ctrlSequence`. `literalLeaderSequence` below +// `viewer-ui/src/lib/terminal/termKeys.ts::ctrlSequence`. `literalLeaderSequence` below // is the same formula applied to a leader chord rather than to a keystroke — // `Ctrl+` is the letter's position in the alphabet, `Alt+` takes // the ESC (Meta) prefix — because the byte a person sees in their shell must @@ -228,7 +228,7 @@ export function literalLeaderSequence(spec: ChordSpec): string | null { /** * The C0 control character for a Ctrl chord: the character's place in the ASCII * table with the top bits cleared. Same table as - * `termKeys.ts::ctrlSequence`, kept here so a chord spec does not have to be + * `lib/terminal/termKeys.ts::ctrlSequence`, kept here so a chord spec does not have to be * turned into typed input first. */ function controlByte(typed: string): string | null { diff --git a/viewer-ui/src/lib/leaderState.test.ts b/viewer-ui/src/lib/shortcuts/leaderState.test.ts similarity index 100% rename from viewer-ui/src/lib/leaderState.test.ts rename to viewer-ui/src/lib/shortcuts/leaderState.test.ts diff --git a/viewer-ui/src/lib/leaderState.ts b/viewer-ui/src/lib/shortcuts/leaderState.ts similarity index 100% rename from viewer-ui/src/lib/leaderState.ts rename to viewer-ui/src/lib/shortcuts/leaderState.ts diff --git a/viewer-ui/src/lib/shortcutActions.test.ts b/viewer-ui/src/lib/shortcuts/shortcutActions.test.ts similarity index 100% rename from viewer-ui/src/lib/shortcutActions.test.ts rename to viewer-ui/src/lib/shortcuts/shortcutActions.test.ts diff --git a/viewer-ui/src/lib/shortcutActions.ts b/viewer-ui/src/lib/shortcuts/shortcutActions.ts similarity index 100% rename from viewer-ui/src/lib/shortcutActions.ts rename to viewer-ui/src/lib/shortcuts/shortcutActions.ts diff --git a/viewer-ui/src/lib/shortcutDom.test.ts b/viewer-ui/src/lib/shortcuts/shortcutDom.test.ts similarity index 100% rename from viewer-ui/src/lib/shortcutDom.test.ts rename to viewer-ui/src/lib/shortcuts/shortcutDom.test.ts diff --git a/viewer-ui/src/lib/shortcutDom.ts b/viewer-ui/src/lib/shortcuts/shortcutDom.ts similarity index 98% rename from viewer-ui/src/lib/shortcutDom.ts rename to viewer-ui/src/lib/shortcuts/shortcutDom.ts index 09178329..1a30894c 100644 --- a/viewer-ui/src/lib/shortcutDom.ts +++ b/viewer-ui/src/lib/shortcuts/shortcutDom.ts @@ -8,7 +8,7 @@ // in a hidden `