From 22fde5bf9f3b4b543582205515728f27758468f5 Mon Sep 17 00:00:00 2001 From: Thalida Noel Date: Sat, 8 Aug 2026 18:03:40 -0400 Subject: [PATCH 01/19] feat: publish the build version on /api/config --- api/models/responses.py | 1 + api/routers/meta.py | 2 + api/tests/test_models.py | 6 +- api/tests/test_server_config.py | 16 + api/tests/test_server_health.py | 8 +- app/src/api/config.ts | 5 +- app/src/types/manifest.generated.ts | 2056 ++++++++++++++------------- app/tests/api/config.test.ts | 16 + app/tests/api/pathBatcher.test.ts | 8 +- 9 files changed, 1089 insertions(+), 1029 deletions(-) diff --git a/api/models/responses.py b/api/models/responses.py index 0fda90c1..e3998308 100644 --- a/api/models/responses.py +++ b/api/models/responses.py @@ -39,6 +39,7 @@ class HealthResponse(BaseModel): class ConfigResponse(BaseModel): allowLocalRepos: bool maxBatchPaths: int + version: str class CommitDetailResponse(BaseModel): diff --git a/api/routers/meta.py b/api/routers/meta.py index 0c97813e..5308f7bd 100644 --- a/api/routers/meta.py +++ b/api/routers/meta.py @@ -6,6 +6,7 @@ from fastapi import APIRouter +from api import __version__ from api.config import MAX_BATCH_PATHS, local_repos_allowed from api.models.responses import ConfigResponse, HealthResponse @@ -22,4 +23,5 @@ def config() -> ConfigResponse: return ConfigResponse( allowLocalRepos=local_repos_allowed(), maxBatchPaths=MAX_BATCH_PATHS, + version=__version__, ) diff --git a/api/tests/test_models.py b/api/tests/test_models.py index 5999c124..4419a9db 100644 --- a/api/tests/test_models.py +++ b/api/tests/test_models.py @@ -169,8 +169,10 @@ def test_health_and_config(self) -> None: self.assertEqual(HealthResponse(ok=True).model_dump(), {"ok": True}) self.assertEqual( - ConfigResponse(allowLocalRepos=False, maxBatchPaths=64).model_dump(), - {"allowLocalRepos": False, "maxBatchPaths": 64}, + ConfigResponse( + allowLocalRepos=False, maxBatchPaths=64, version="1.2.3" + ).model_dump(), + {"allowLocalRepos": False, "maxBatchPaths": 64, "version": "1.2.3"}, ) def test_sse_event_serialization(self) -> None: diff --git a/api/tests/test_server_config.py b/api/tests/test_server_config.py index 4f80ea4e..5a118d38 100644 --- a/api/tests/test_server_config.py +++ b/api/tests/test_server_config.py @@ -20,18 +20,24 @@ def client(tmp_path: Path) -> TestClient: def test_config_enabled(client: TestClient, monkeypatch) -> None: + from api import __version__ + monkeypatch.setenv("CODECITY_ALLOW_LOCAL_REPOS", "1") assert client.get("/api/config").json() == { "allowLocalRepos": True, "maxBatchPaths": MAX_BATCH_PATHS, + "version": __version__, } def test_config_disabled(client: TestClient, monkeypatch) -> None: + from api import __version__ + monkeypatch.delenv("CODECITY_ALLOW_LOCAL_REPOS", raising=False) assert client.get("/api/config").json() == { "allowLocalRepos": False, "maxBatchPaths": MAX_BATCH_PATHS, + "version": __version__, } @@ -41,3 +47,13 @@ def test_config_publishes_the_cap_the_batch_routes_enforce() -> None: from api.routers import file as file_router assert file_router.MAX_BATCH_PATHS == MAX_BATCH_PATHS + + +def test_config_reports_the_running_package_version(client: TestClient) -> None: + """The footer shows which build is running, so /api/config carries the + package version rather than the client guessing from a bundled constant.""" + from api import __version__ + + body = client.get("/api/config").json() + assert body["version"] == __version__ + assert body["version"] diff --git a/api/tests/test_server_health.py b/api/tests/test_server_health.py index 3534cf6e..1ce828a1 100644 --- a/api/tests/test_server_health.py +++ b/api/tests/test_server_health.py @@ -34,6 +34,8 @@ def test_config_default_disabled( # The conftest sets CODECITY_ALLOW_LOCAL_REPOS=1 session-wide for # tests that exercise local scan paths. Override it here to verify the # default-disabled state that the real endpoint exposes. + from api import __version__ + monkeypatch.delenv("CODECITY_ALLOW_LOCAL_REPOS", raising=False) static = tmp_path / "static" static.mkdir() @@ -41,7 +43,11 @@ def test_config_default_disabled( app = create_app(static_dir=static) r = TestClient(app).get("/api/config") assert r.status_code == 200 - assert r.json() == {"allowLocalRepos": False, "maxBatchPaths": MAX_BATCH_PATHS} + assert r.json() == { + "allowLocalRepos": False, + "maxBatchPaths": MAX_BATCH_PATHS, + "version": __version__, + } def test_root_serves_index(client: TestClient) -> None: diff --git a/app/src/api/config.ts b/app/src/api/config.ts index c17585b1..5b2081e8 100644 --- a/app/src/api/config.ts +++ b/app/src/api/config.ts @@ -9,10 +9,12 @@ import type { components } from '@/types/manifest.generated'; export type ServerConfig = components['schemas']['ConfigResponse']; // Pre-boot defaults. maxBatchPaths guesses low: too high silently truncates a -// batch's tail, too low only costs an extra request. +// batch's tail, too low only costs an extra request. `version` matches the +// backend's own metadata-lookup fallback. export const DEFAULT_SERVER_CONFIG: ServerConfig = { allowLocalRepos: false, maxBatchPaths: 16, + version: '0.0.0+unknown', }; let _cached: Promise | null = null; @@ -38,6 +40,7 @@ export async function fetchServerConfig(): Promise { ...(typeof body.maxBatchPaths === 'number' && body.maxBatchPaths > 0 ? { maxBatchPaths: body.maxBatchPaths } : {}), + ...(typeof body.version === 'string' && body.version ? { version: body.version } : {}), }; } catch (_) { return DEFAULT_SERVER_CONFIG; diff --git a/app/src/types/manifest.generated.ts b/app/src/types/manifest.generated.ts index 81e4d21f..c2d5abf0 100644 --- a/app/src/types/manifest.generated.ts +++ b/app/src/types/manifest.generated.ts @@ -4,1049 +4,1059 @@ */ export interface paths { - "/api/health": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Health */ - get: operations["health_api_health_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/config": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Config */ - get: operations["config_api_config_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/file": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get File */ - get: operations["get_file_api_file_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/images": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Get Images - * @description Batch image fetch — {path: {mime, b64}} for many small images in one round - * trip. NOT a plural of GET /api/file: it inlines base64, serves images only, - * and omits anything it can't serve. It exists so the scene's billboard loader - * doesn't exhaust the browser's HTTP/1.1 connection pool on a media-heavy repo. - * - * Each path is trust-checked exactly like GET /api/file. Paths that are out of - * root, missing, non-image, or larger than _MAX_BATCH_IMAGE_BYTES are silently - * omitted; the client falls back to the streaming GET for those. Videos are - * never batched (they stream their poster frame), so this is images only. - */ - post: operations["get_images_api_images_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/fingerprints": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Get Fingerprints - * @description Batch byte-pattern fingerprint fetch — {path: {b64}}, one round trip for - * many buildings. Trust-checked like GET /api/file; out-of-root / missing / - * unreadable paths are silently omitted. Raw binary bytes never leave the - * server — only the head is read, and only the fingerprint image returned. - */ - post: operations["get_fingerprints_api_fingerprints_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/commit": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get Commit */ - get: operations["get_commit_api_commit_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/branches": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get Branches */ - get: operations["get_branches_api_branches_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/manifest/signature": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Signature */ - get: operations["signature_api_manifest_signature_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/timeline": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Timeline */ - get: operations["timeline_api_timeline_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/manifest": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Manifest */ - get: operations["manifest_api_manifest_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + '/api/health': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Health */ + get: operations['health_api_health_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/config': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Config */ + get: operations['config_api_config_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/file': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get File */ + get: operations['get_file_api_file_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/images': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Images + * @description Batch image fetch — {path: {mime, b64}} for many small images in one round + * trip. NOT a plural of GET /api/file: it inlines base64, serves images only, + * and omits anything it can't serve. It exists so the scene's billboard loader + * doesn't exhaust the browser's HTTP/1.1 connection pool on a media-heavy repo. + * + * Each path is trust-checked exactly like GET /api/file. Paths that are out of + * root, missing, non-image, or larger than _MAX_BATCH_IMAGE_BYTES are silently + * omitted; the client falls back to the streaming GET for those. Videos are + * never batched (they stream their poster frame), so this is images only. + */ + post: operations['get_images_api_images_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/fingerprints': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Fingerprints + * @description Batch byte-pattern fingerprint fetch — {path: {b64}}, one round trip for + * many buildings. Trust-checked like GET /api/file; out-of-root / missing / + * unreadable paths are silently omitted. Raw binary bytes never leave the + * server — only the head is read, and only the fingerprint image returned. + */ + post: operations['get_fingerprints_api_fingerprints_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/commit': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Commit */ + get: operations['get_commit_api_commit_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/branches': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; + /** Get Branches */ + get: operations['get_branches_api_branches_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/manifest/signature': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Signature */ + get: operations['signature_api_manifest_signature_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/timeline': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Timeline */ + get: operations['timeline_api_timeline_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/manifest': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Manifest */ + get: operations['manifest_api_manifest_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { - schemas: { - /** AuthorStat */ - AuthorStat: { - /** Name */ - name: string; - /** Commits */ - commits: number; - /** - * Hue - * @description Stable 0-359 hue from the name hash; the display colour is built from it client-side - */ - hue: number; - }; - /** BranchListResponse */ - BranchListResponse: { - /** Branches */ - branches: string[]; - /** Default */ - default: string | null; - }; - /** BusynessThresholds */ - BusynessThresholds: { - /** Avg */ - avg: number; - /** Busy */ - busy: number; - }; - /** - * CloneProgressEvent - * @description `clone-progress` — git source is being cloned; carries clone progress. - * - * A normal progress tick has `stage` + `percent`. A heartbeat during the - * silent promisor blob fetch instead carries `mb_on_disk` (and no percent), - * so the UI shows the working tree materializing rather than freezing. - */ - CloneProgressEvent: { - /** Label */ - label?: string; - /** - * Stage - * @enum {string} - */ - stage?: "receiving" | "resolving" | "counting" | "updating"; - /** Percent */ - percent?: number; - /** Mb On Disk */ - mb_on_disk?: number; - }; - /** CommitDateRange */ - CommitDateRange: { - /** - * Oldest - * @description Oldest commit date (YYYY-MM-DD), or null when the repo has no commits - */ - oldest: string | null; - /** - * Newest - * @description Newest commit date (YYYY-MM-DD), or null when the repo has no commits - */ - newest: string | null; - }; - /** CommitDetailResponse */ - CommitDetailResponse: { - /** Sha */ - sha: string; - /** Authors */ - authors: string[]; - /** Date */ - date: string; - /** Subject */ - subject: string; - /** Body */ - body: string; - }; - /** CommitEntry */ - CommitEntry: { - /** - * Date - * @description ISO-8601 UTC, e.g. 2026-07-25T14:03:21Z - */ - date: string; - /** Files */ - files: number; - /** Sha */ - sha: string; - /** Authors */ - authors: string[]; - /** Subject */ - subject: string; - /** Same Day Total */ - same_day_total: number; - }; - /** CommitLeader */ - CommitLeader: { - /** Sha */ - sha: string; - /** Files */ - files: number; - }; - /** - * CompleteManifestEvent - * @description `manifest-complete` — a manifest with real, fully-populated metadata (a - * fresh scan's final pass, or a warm cache hit). - */ - CompleteManifestEvent: { - manifest: components["schemas"]["Manifest"]; - }; - /** ConfigResponse */ - ConfigResponse: { - /** Allowlocalrepos */ - allowLocalRepos: boolean; - /** Maxbatchpaths */ - maxBatchPaths: number; - }; - /** DateRangeMs */ - DateRangeMs: { - /** Mincreated */ - minCreated: number; - /** Maxcreated */ - maxCreated: number; - /** Minmodified */ - minModified: number; - /** Maxmodified */ - maxModified: number; - }; - /** DateRanges */ - DateRanges: { - /** - * Mincreated - * @description Earliest resolved create date (ISO), or null for an empty tree - */ - minCreated: string | null; - /** - * Maxcreated - * @description Latest resolved create date (ISO), or null for an empty tree - */ - maxCreated: string | null; - /** - * Minmodified - * @description Earliest resolved modify date (ISO), or null for an empty tree - */ - minModified: string | null; - /** - * Maxmodified - * @description Latest resolved modify date (ISO), or null for an empty tree - */ - maxModified: string | null; - }; - /** DayLeader */ - DayLeader: { - /** Date */ - date: string; - /** Count */ - count: number; - }; - /** DirLeader */ - DirLeader: { - /** Path */ - path: string; - /** Depth */ - depth: number; - /** Children */ - children: number; - /** Descendants */ - descendants: number; - }; - /** DirNode */ - DirNode: { - /** Name */ - name: string; - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "directory"; - /** Path */ - path: string; - /** Fullpath */ - fullPath: string; - /** Children */ - children: (components["schemas"]["FileNode"] | components["schemas"]["DirNode"])[]; - /** Children Count */ - children_count: number; - /** Children File Count */ - children_file_count: number; - /** Children Dir Count */ - children_dir_count: number; - /** Descendants Count */ - descendants_count: number; - /** Descendants File Count */ - descendants_file_count: number; - /** Descendants Dir Count */ - descendants_dir_count: number; - /** Descendants Size */ - descendants_size: number; - /** Descendants Created Min */ - descendants_created_min: string | null; - /** Descendants Modified Max */ - descendants_modified_max: string | null; - /** Descendants Ext Breakdown */ - descendants_ext_breakdown: components["schemas"]["ExtBreakdownEntry"][]; - }; - /** - * ErrorEvent - * @description `error` — a failure after the stream began; carries the message. - */ - ErrorEvent: { - /** Error */ - error: string; - }; - /** ExtBreakdownEntry */ - ExtBreakdownEntry: { - /** Ext */ - ext: string | null; - /** Count */ - count: number; - /** Size */ - size: number; - }; - /** FileLeader */ - FileLeader: { - /** Path */ - path: string; - /** Lines */ - lines: number; - /** Bytes */ - bytes: number; - /** Created */ - created: string; - /** Modified */ - modified: string; - /** Media Width */ - media_width?: number; - /** Media Height */ - media_height?: number; - }; - /** FileNode */ - FileNode: { - /** Name */ - name: string; - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "file"; - /** Path */ - path: string; - /** Fullpath */ - fullPath: string; - /** Extension */ - extension: string; - /** Size */ - size: number; - /** Lines */ - lines: number; - /** Binary */ - binary: boolean; - /** - * Dirty - * @description Working-tree differs from HEAD for this tracked file (staged or unstaged). Always False for clean/remote repos. - */ - dirty: boolean; - /** - * Created - * @description ISO create date (UTC, Z-suffixed), resolved server-side: git history date when the file has one, filesystem date otherwise - */ - created: string; - /** - * Modified - * @description ISO modify date (UTC, Z-suffixed), resolved server-side: git history date when the file has one, filesystem date otherwise. When dirty is true, this is always the working-tree filesystem date, regardless of git history - */ - modified: string; - /** - * Mediakind - * @description Media classification by extension (single source for the frontend); null for non-media files - */ - mediaKind?: ("image" | "video") | null; - /** Media Width */ - media_width?: number; - /** Media Height */ - media_height?: number; - /** Binarytype */ - binaryType?: string; - }; - /** - * FingerprintEntry - * @description One binary file's byte-pattern fingerprint in a POST /api/fingerprints - * batch response: a base64-encoded grayscale PNG (image/png implied), keyed - * by request path. Computed server-side from the file's head — raw binary - * bytes never ship to the client. - */ - FingerprintEntry: { - /** B64 */ - b64: string; - }; - /** HTTPValidationError */ - HTTPValidationError: { - /** Detail */ - detail?: components["schemas"]["ValidationError"][]; - }; - /** HealthResponse */ - HealthResponse: { - /** Ok */ - ok: boolean; - }; - /** - * ImageBatchEntry - * @description One image in a POST /api/images batch response: its content-type and - * base64-encoded bytes, keyed by request path in the response map. - */ - ImageBatchEntry: { - /** Mime */ - mime: string; - /** B64 */ - b64: string; - }; - /** Manifest */ - Manifest: { - /** Root */ - root: string; - /** Scanned At */ - scanned_at: string; - /** Content Signature */ - content_signature: string; - /** Structure Signature */ - structure_signature: string; - /** Layout Signature */ - layout_signature: string; - tree: components["schemas"]["DirNode"]; - repo: components["schemas"]["RepoInfo"]; - /** Commits */ - commits: components["schemas"]["CommitEntry"][]; - busyness: components["schemas"]["BusynessThresholds"]; - dateRanges: components["schemas"]["DateRanges"]; - stats: components["schemas"]["RepoStats"]; - /** - * Pending - * @description Stages still to come. 'metadata': per-file lines/binary are placeholders. 'history': dates are filesystem dates and commits is empty. Empty list means every field is final. - */ - pending: ("metadata" | "history")[]; - /** - * Readmepath - * @description Absolute path of the root README, or null if there isn't one - */ - readmePath: string | null; - /** - * Readmemodified - * @description That README's mtime, for cache-busting the fetch - */ - readmeModified: string | null; - }; - /** - * PartialManifestEvent - * @description `manifest-partial` — a manifest with the real tree structure but - * placeholder file metadata, sent so the UI can paint the city before - * per-file metadata is resolved. - */ - PartialManifestEvent: { - manifest: components["schemas"]["Manifest"]; - }; - /** PathBatchRequest */ - PathBatchRequest: { - /** Paths */ - paths: string[]; - /** Shas */ - shas?: { - [key: string]: string; - } | null; - }; - /** RangeStat */ - RangeStat: { - /** Min */ - min: number; - /** Max */ - max: number; - }; - /** RepoInfo */ - RepoInfo: { - /** Branch */ - branch: string | null; - /** Remote Url */ - remote_url: string | null; - /** Head Sha */ - head_sha: string | null; - /** Head Subject */ - head_subject: string | null; - /** Dirty */ - dirty: boolean; - }; - /** RepoStats */ - RepoStats: { - lineCountRange: components["schemas"]["RangeStat"]; - byteSizeRange: components["schemas"]["RangeStat"]; - oldestCreatedFile: components["schemas"]["FileLeader"] | null; - newestCreatedFile: components["schemas"]["FileLeader"] | null; - newestModifiedFile: components["schemas"]["FileLeader"] | null; - oldestModifiedFile: components["schemas"]["FileLeader"] | null; - maxLinesFile: components["schemas"]["FileLeader"] | null; - minLinesFile: components["schemas"]["FileLeader"] | null; - maxBytesFile: components["schemas"]["FileLeader"] | null; - minBytesFile: components["schemas"]["FileLeader"] | null; - maxMediaBytesFile: components["schemas"]["FileLeader"] | null; - minMediaBytesFile: components["schemas"]["FileLeader"] | null; - maxMediaPixelsFile: components["schemas"]["FileLeader"] | null; - minMediaPixelsFile: components["schemas"]["FileLeader"] | null; - maxBinaryBytesFile: components["schemas"]["FileLeader"] | null; - minBinaryBytesFile: components["schemas"]["FileLeader"] | null; - /** Mediacount */ - mediaCount: number; - /** Binarycount */ - binaryCount: number; - /** Totallines */ - totalLines: number; - /** Dirtyfilecount */ - dirtyFileCount: number; - /** Codebytes */ - codeBytes: number; - maxDepthDir: components["schemas"]["DirLeader"] | null; - maxChildrenDir: components["schemas"]["DirLeader"] | null; - minChildrenDir: components["schemas"]["DirLeader"] | null; - maxFilesPerCommit: components["schemas"]["CommitLeader"] | null; - minFilesPerCommit: components["schemas"]["CommitLeader"] | null; - commitDates: components["schemas"]["CommitDateRange"]; - maxCommitsPerDay: components["schemas"]["DayLeader"] | null; - /** Maxcommitstreakdays */ - maxCommitStreakDays: number; - /** Authors */ - authors: components["schemas"]["AuthorStat"][]; - }; - /** - * ScanProgressEvent - * @description `scan-progress` — the working tree is being walked; carries the - * heartbeat files-scanned count. - */ - ScanProgressEvent: { - /** Label */ - label?: string; - /** Files Scanned */ - files_scanned?: number; - }; - /** SignatureResponse */ - SignatureResponse: { - /** Root */ - root: string; - /** Scanned At */ - scanned_at: string; - /** Content Signature */ - content_signature: string; - }; - /** - * TimelineBundle - * @description Wire schema for the scrub bundle; mirrors manifest_types.TimelineBundle. - */ - TimelineBundle: { - /** Commits */ - commits: components["schemas"]["CommitEntry"][]; - unionManifest: components["schemas"]["Manifest"]; - /** Deltas */ - deltas: components["schemas"]["TimelineDelta"][]; - /** Bloblines */ - blobLines: { - [key: string]: number; - }; - /** Blobsizes */ - blobSizes: { - [key: string]: number; - }; - /** Commitlineranges */ - commitLineRanges: components["schemas"]["RangeStat"][]; - /** Commitdateranges */ - commitDateRanges: components["schemas"]["DateRangeMs"][]; - /** Note */ - note: string | null; - }; - /** TimelineChange */ - TimelineChange: { - /** Path */ - path: string; - /** - * Sha - * @description New blob sha, or null when deleted - */ - sha: string | null; - }; - /** - * TimelineCompleteEvent - * @description `timeline-complete` — the full replay bundle (fresh build or warm - * cache hit). - */ - TimelineCompleteEvent: { - bundle: components["schemas"]["TimelineBundle"]; - }; - /** TimelineDelta */ - TimelineDelta: { - /** Sha */ - sha: string; - /** Changes */ - changes: components["schemas"]["TimelineChange"][]; - }; - /** - * TimelineProgressEvent - * @description `timeline-progress` — the history walk, blob-table resolution, or (for a - * blobless remote clone) the up-front blob backfill is in progress. The - * `fetch` stage carries `percent`; `history` carries `commits`; `blobs` - * carries `blobsDone`/`blobsTotal` (the total is known up front from the batch - * blob lookup, so that stage reports two ticks, not a live stream). - */ - TimelineProgressEvent: { - /** - * Stage - * @enum {string} - */ - stage: "fetch" | "history" | "blobs"; - /** Percent */ - percent?: number; - /** Commits */ - commits?: number; - /** Blobsdone */ - blobsDone?: number; - /** Blobstotal */ - blobsTotal?: number; - /** Label */ - label?: string; - }; - /** ValidationError */ - ValidationError: { - /** Location */ - loc: (string | number)[]; - /** Message */ - msg: string; - /** Error Type */ - type: string; - /** Input */ - input?: unknown; - /** Context */ - ctx?: Record; - }; + schemas: { + /** AuthorStat */ + AuthorStat: { + /** Name */ + name: string; + /** Commits */ + commits: number; + /** + * Hue + * @description Stable 0-359 hue from the name hash; the display colour is built from it client-side + */ + hue: number; + }; + /** BranchListResponse */ + BranchListResponse: { + /** Branches */ + branches: string[]; + /** Default */ + default: string | null; + }; + /** BusynessThresholds */ + BusynessThresholds: { + /** Avg */ + avg: number; + /** Busy */ + busy: number; + }; + /** + * CloneProgressEvent + * @description `clone-progress` — git source is being cloned; carries clone progress. + * + * A normal progress tick has `stage` + `percent`. A heartbeat during the + * silent promisor blob fetch instead carries `mb_on_disk` (and no percent), + * so the UI shows the working tree materializing rather than freezing. + */ + CloneProgressEvent: { + /** Label */ + label?: string; + /** + * Stage + * @enum {string} + */ + stage?: 'receiving' | 'resolving' | 'counting' | 'updating'; + /** Percent */ + percent?: number; + /** Mb On Disk */ + mb_on_disk?: number; + }; + /** CommitDateRange */ + CommitDateRange: { + /** + * Oldest + * @description Oldest commit date (YYYY-MM-DD), or null when the repo has no commits + */ + oldest: string | null; + /** + * Newest + * @description Newest commit date (YYYY-MM-DD), or null when the repo has no commits + */ + newest: string | null; + }; + /** CommitDetailResponse */ + CommitDetailResponse: { + /** Sha */ + sha: string; + /** Authors */ + authors: string[]; + /** Date */ + date: string; + /** Subject */ + subject: string; + /** Body */ + body: string; }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + /** CommitEntry */ + CommitEntry: { + /** + * Date + * @description ISO-8601 UTC, e.g. 2026-07-25T14:03:21Z + */ + date: string; + /** Files */ + files: number; + /** Sha */ + sha: string; + /** Authors */ + authors: string[]; + /** Subject */ + subject: string; + /** Same Day Total */ + same_day_total: number; + }; + /** CommitLeader */ + CommitLeader: { + /** Sha */ + sha: string; + /** Files */ + files: number; + }; + /** + * CompleteManifestEvent + * @description `manifest-complete` — a manifest with real, fully-populated metadata (a + * fresh scan's final pass, or a warm cache hit). + */ + CompleteManifestEvent: { + manifest: components['schemas']['Manifest']; + }; + /** ConfigResponse */ + ConfigResponse: { + /** Allowlocalrepos */ + allowLocalRepos: boolean; + /** Maxbatchpaths */ + maxBatchPaths: number; + /** Version */ + version: string; + }; + /** DateRangeMs */ + DateRangeMs: { + /** Mincreated */ + minCreated: number; + /** Maxcreated */ + maxCreated: number; + /** Minmodified */ + minModified: number; + /** Maxmodified */ + maxModified: number; + }; + /** DateRanges */ + DateRanges: { + /** + * Mincreated + * @description Earliest resolved create date (ISO), or null for an empty tree + */ + minCreated: string | null; + /** + * Maxcreated + * @description Latest resolved create date (ISO), or null for an empty tree + */ + maxCreated: string | null; + /** + * Minmodified + * @description Earliest resolved modify date (ISO), or null for an empty tree + */ + minModified: string | null; + /** + * Maxmodified + * @description Latest resolved modify date (ISO), or null for an empty tree + */ + maxModified: string | null; + }; + /** DayLeader */ + DayLeader: { + /** Date */ + date: string; + /** Count */ + count: number; + }; + /** DirLeader */ + DirLeader: { + /** Path */ + path: string; + /** Depth */ + depth: number; + /** Children */ + children: number; + /** Descendants */ + descendants: number; + }; + /** DirNode */ + DirNode: { + /** Name */ + name: string; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: 'directory'; + /** Path */ + path: string; + /** Fullpath */ + fullPath: string; + /** Children */ + children: (components['schemas']['FileNode'] | components['schemas']['DirNode'])[]; + /** Children Count */ + children_count: number; + /** Children File Count */ + children_file_count: number; + /** Children Dir Count */ + children_dir_count: number; + /** Descendants Count */ + descendants_count: number; + /** Descendants File Count */ + descendants_file_count: number; + /** Descendants Dir Count */ + descendants_dir_count: number; + /** Descendants Size */ + descendants_size: number; + /** Descendants Created Min */ + descendants_created_min: string | null; + /** Descendants Modified Max */ + descendants_modified_max: string | null; + /** Descendants Ext Breakdown */ + descendants_ext_breakdown: components['schemas']['ExtBreakdownEntry'][]; + }; + /** + * ErrorEvent + * @description `error` — a failure after the stream began; carries the message. + */ + ErrorEvent: { + /** Error */ + error: string; + }; + /** ExtBreakdownEntry */ + ExtBreakdownEntry: { + /** Ext */ + ext: string | null; + /** Count */ + count: number; + /** Size */ + size: number; + }; + /** FileLeader */ + FileLeader: { + /** Path */ + path: string; + /** Lines */ + lines: number; + /** Bytes */ + bytes: number; + /** Created */ + created: string; + /** Modified */ + modified: string; + /** Media Width */ + media_width?: number; + /** Media Height */ + media_height?: number; + }; + /** FileNode */ + FileNode: { + /** Name */ + name: string; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: 'file'; + /** Path */ + path: string; + /** Fullpath */ + fullPath: string; + /** Extension */ + extension: string; + /** Size */ + size: number; + /** Lines */ + lines: number; + /** Binary */ + binary: boolean; + /** + * Dirty + * @description Working-tree differs from HEAD for this tracked file (staged or unstaged). Always False for clean/remote repos. + */ + dirty: boolean; + /** + * Created + * @description ISO create date (UTC, Z-suffixed), resolved server-side: git history date when the file has one, filesystem date otherwise + */ + created: string; + /** + * Modified + * @description ISO modify date (UTC, Z-suffixed), resolved server-side: git history date when the file has one, filesystem date otherwise. When dirty is true, this is always the working-tree filesystem date, regardless of git history + */ + modified: string; + /** + * Mediakind + * @description Media classification by extension (single source for the frontend); null for non-media files + */ + mediaKind?: ('image' | 'video') | null; + /** Media Width */ + media_width?: number; + /** Media Height */ + media_height?: number; + /** Binarytype */ + binaryType?: string; + }; + /** + * FingerprintEntry + * @description One binary file's byte-pattern fingerprint in a POST /api/fingerprints + * batch response: a base64-encoded grayscale PNG (image/png implied), keyed + * by request path. Computed server-side from the file's head — raw binary + * bytes never ship to the client. + */ + FingerprintEntry: { + /** B64 */ + b64: string; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components['schemas']['ValidationError'][]; + }; + /** HealthResponse */ + HealthResponse: { + /** Ok */ + ok: boolean; + }; + /** + * ImageBatchEntry + * @description One image in a POST /api/images batch response: its content-type and + * base64-encoded bytes, keyed by request path in the response map. + */ + ImageBatchEntry: { + /** Mime */ + mime: string; + /** B64 */ + b64: string; + }; + /** Manifest */ + Manifest: { + /** Root */ + root: string; + /** Scanned At */ + scanned_at: string; + /** Content Signature */ + content_signature: string; + /** Structure Signature */ + structure_signature: string; + /** Layout Signature */ + layout_signature: string; + tree: components['schemas']['DirNode']; + repo: components['schemas']['RepoInfo']; + /** Commits */ + commits: components['schemas']['CommitEntry'][]; + busyness: components['schemas']['BusynessThresholds']; + dateRanges: components['schemas']['DateRanges']; + stats: components['schemas']['RepoStats']; + /** + * Pending + * @description Stages still to come. 'metadata': per-file lines/binary are placeholders. 'history': dates are filesystem dates and commits is empty. Empty list means every field is final. + */ + pending: ('metadata' | 'history')[]; + /** + * Readmepath + * @description Absolute path of the root README, or null if there isn't one + */ + readmePath: string | null; + /** + * Readmemodified + * @description That README's mtime, for cache-busting the fetch + */ + readmeModified: string | null; + }; + /** + * PartialManifestEvent + * @description `manifest-partial` — a manifest with the real tree structure but + * placeholder file metadata, sent so the UI can paint the city before + * per-file metadata is resolved. + */ + PartialManifestEvent: { + manifest: components['schemas']['Manifest']; + }; + /** PathBatchRequest */ + PathBatchRequest: { + /** Paths */ + paths: string[]; + /** Shas */ + shas?: { + [key: string]: string; + } | null; + }; + /** RangeStat */ + RangeStat: { + /** Min */ + min: number; + /** Max */ + max: number; + }; + /** RepoInfo */ + RepoInfo: { + /** Branch */ + branch: string | null; + /** Remote Url */ + remote_url: string | null; + /** Head Sha */ + head_sha: string | null; + /** Head Subject */ + head_subject: string | null; + /** Dirty */ + dirty: boolean; + }; + /** RepoStats */ + RepoStats: { + lineCountRange: components['schemas']['RangeStat']; + byteSizeRange: components['schemas']['RangeStat']; + oldestCreatedFile: components['schemas']['FileLeader'] | null; + newestCreatedFile: components['schemas']['FileLeader'] | null; + newestModifiedFile: components['schemas']['FileLeader'] | null; + oldestModifiedFile: components['schemas']['FileLeader'] | null; + maxLinesFile: components['schemas']['FileLeader'] | null; + minLinesFile: components['schemas']['FileLeader'] | null; + maxBytesFile: components['schemas']['FileLeader'] | null; + minBytesFile: components['schemas']['FileLeader'] | null; + maxMediaBytesFile: components['schemas']['FileLeader'] | null; + minMediaBytesFile: components['schemas']['FileLeader'] | null; + maxMediaPixelsFile: components['schemas']['FileLeader'] | null; + minMediaPixelsFile: components['schemas']['FileLeader'] | null; + maxBinaryBytesFile: components['schemas']['FileLeader'] | null; + minBinaryBytesFile: components['schemas']['FileLeader'] | null; + /** Mediacount */ + mediaCount: number; + /** Binarycount */ + binaryCount: number; + /** Totallines */ + totalLines: number; + /** Dirtyfilecount */ + dirtyFileCount: number; + /** Codebytes */ + codeBytes: number; + maxDepthDir: components['schemas']['DirLeader'] | null; + maxChildrenDir: components['schemas']['DirLeader'] | null; + minChildrenDir: components['schemas']['DirLeader'] | null; + maxFilesPerCommit: components['schemas']['CommitLeader'] | null; + minFilesPerCommit: components['schemas']['CommitLeader'] | null; + commitDates: components['schemas']['CommitDateRange']; + maxCommitsPerDay: components['schemas']['DayLeader'] | null; + /** Maxcommitstreakdays */ + maxCommitStreakDays: number; + /** Authors */ + authors: components['schemas']['AuthorStat'][]; + }; + /** + * ScanProgressEvent + * @description `scan-progress` — the working tree is being walked; carries the + * heartbeat files-scanned count. + */ + ScanProgressEvent: { + /** Label */ + label?: string; + /** Files Scanned */ + files_scanned?: number; + }; + /** SignatureResponse */ + SignatureResponse: { + /** Root */ + root: string; + /** Scanned At */ + scanned_at: string; + /** Content Signature */ + content_signature: string; + }; + /** + * TimelineBundle + * @description Wire schema for the scrub bundle; mirrors manifest_types.TimelineBundle. + */ + TimelineBundle: { + /** Commits */ + commits: components['schemas']['CommitEntry'][]; + unionManifest: components['schemas']['Manifest']; + /** Deltas */ + deltas: components['schemas']['TimelineDelta'][]; + /** Bloblines */ + blobLines: { + [key: string]: number; + }; + /** Blobsizes */ + blobSizes: { + [key: string]: number; + }; + /** Commitlineranges */ + commitLineRanges: components['schemas']['RangeStat'][]; + /** Commitdateranges */ + commitDateRanges: components['schemas']['DateRangeMs'][]; + /** Note */ + note: string | null; + }; + /** TimelineChange */ + TimelineChange: { + /** Path */ + path: string; + /** + * Sha + * @description New blob sha, or null when deleted + */ + sha: string | null; + }; + /** + * TimelineCompleteEvent + * @description `timeline-complete` — the full replay bundle (fresh build or warm + * cache hit). + */ + TimelineCompleteEvent: { + bundle: components['schemas']['TimelineBundle']; + }; + /** TimelineDelta */ + TimelineDelta: { + /** Sha */ + sha: string; + /** Changes */ + changes: components['schemas']['TimelineChange'][]; + }; + /** + * TimelineProgressEvent + * @description `timeline-progress` — the history walk, blob-table resolution, or (for a + * blobless remote clone) the up-front blob backfill is in progress. The + * `fetch` stage carries `percent`; `history` carries `commits`; `blobs` + * carries `blobsDone`/`blobsTotal` (the total is known up front from the batch + * blob lookup, so that stage reports two ticks, not a live stream). + */ + TimelineProgressEvent: { + /** + * Stage + * @enum {string} + */ + stage: 'fetch' | 'history' | 'blobs'; + /** Percent */ + percent?: number; + /** Commits */ + commits?: number; + /** Blobsdone */ + blobsDone?: number; + /** Blobstotal */ + blobsTotal?: number; + /** Label */ + label?: string; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + /** Input */ + input?: unknown; + /** Context */ + ctx?: Record; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - health_api_health_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HealthResponse"]; - }; - }; - }; + health_api_health_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - config_api_config_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ConfigResponse"]; - }; - }; + content: { + 'application/json': components['schemas']['HealthResponse']; }; + }; }; - get_file_api_file_get: { - parameters: { - query: { - /** @description Absolute path inside a scanned root */ - path: string; - /** @description Blob sha to read instead of the working tree */ - sha?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; + }; + config_api_config_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - get_images_api_images_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; }; - requestBody: { - content: { - "application/json": components["schemas"]["PathBatchRequest"]; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - [key: string]: components["schemas"]["ImageBatchEntry"]; - }; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; + content: { + 'application/json': components['schemas']['ConfigResponse']; }; + }; }; - get_fingerprints_api_fingerprints_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["PathBatchRequest"]; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - [key: string]: components["schemas"]["FingerprintEntry"]; - }; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; + }; + get_file_api_file_get: { + parameters: { + query: { + /** @description Absolute path inside a scanned root */ + path: string; + /** @description Blob sha to read instead of the working tree */ + sha?: string | null; + }; + header?: never; + path?: never; + cookie?: never; }; - get_commit_api_commit_get: { - parameters: { - query: { - sha: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CommitDetailResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; }; - get_branches_api_branches_get: { - parameters: { - query: { - src: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BranchListResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; + }; + get_images_api_images_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - signature_api_manifest_signature_get: { - parameters: { - query: { - src: string; - branch?: string | null; - no_cache?: boolean; - exclude?: string[]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SignatureResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; + requestBody: { + content: { + 'application/json': components['schemas']['PathBatchRequest']; + }; }; - timeline_api_timeline_get: { - parameters: { - query: { - src: string; - branch?: string | null; - no_cache?: boolean; - exclude?: string[]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Server-Sent Events stream (`text/event-stream`). Named events and their JSON `data` payloads: `timeline-progress` (TimelineProgressEvent, one or more while the history walk / blob resolution run), `timeline-complete` (TimelineCompleteEvent, the full bundle), `error` (ErrorEvent). A warm cache hit emits only `timeline-complete`, no progress. The client closes the connection on `timeline-complete`/`error`. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["TimelineProgressEvent"] | components["schemas"]["TimelineCompleteEvent"] | components["schemas"]["ErrorEvent"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: components['schemas']['ImageBatchEntry']; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; }; - manifest_api_manifest_get: { - parameters: { - query?: { - src?: string; - branch?: string | null; - no_cache?: boolean; - exclude?: string[]; - ref?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Server-Sent Events stream (`text/event-stream`). Named events and their JSON `data` payloads: `clone-progress` (CloneProgressEvent), `scan-progress` (ScanProgressEvent), `manifest-partial` (PartialManifestEvent), `manifest-complete` (CompleteManifestEvent), `error` (ErrorEvent). The client closes the connection on `manifest-complete`/`error`. When `ref` is set, the manifest is reconstructed as of that commit instead of the working tree (a remote source still emits `clone-progress` if it isn't cloned yet, but never `scan-progress`/`manifest-partial` for the reconstruction itself — the city is already drawn, so a skeleton would flash placeholders). */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CloneProgressEvent"] | components["schemas"]["ScanProgressEvent"] | components["schemas"]["PartialManifestEvent"] | components["schemas"]["CompleteManifestEvent"] | components["schemas"]["ErrorEvent"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; + }; + get_fingerprints_api_fingerprints_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['PathBatchRequest']; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: components['schemas']['FingerprintEntry']; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_commit_api_commit_get: { + parameters: { + query: { + sha: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['CommitDetailResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_branches_api_branches_get: { + parameters: { + query: { + src: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['BranchListResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + signature_api_manifest_signature_get: { + parameters: { + query: { + src: string; + branch?: string | null; + no_cache?: boolean; + exclude?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SignatureResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + timeline_api_timeline_get: { + parameters: { + query: { + src: string; + branch?: string | null; + no_cache?: boolean; + exclude?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Server-Sent Events stream (`text/event-stream`). Named events and their JSON `data` payloads: `timeline-progress` (TimelineProgressEvent, one or more while the history walk / blob resolution run), `timeline-complete` (TimelineCompleteEvent, the full bundle), `error` (ErrorEvent). A warm cache hit emits only `timeline-complete`, no progress. The client closes the connection on `timeline-complete`/`error`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | components['schemas']['TimelineProgressEvent'] + | components['schemas']['TimelineCompleteEvent'] + | components['schemas']['ErrorEvent']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + manifest_api_manifest_get: { + parameters: { + query?: { + src?: string; + branch?: string | null; + no_cache?: boolean; + exclude?: string[]; + ref?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Server-Sent Events stream (`text/event-stream`). Named events and their JSON `data` payloads: `clone-progress` (CloneProgressEvent), `scan-progress` (ScanProgressEvent), `manifest-partial` (PartialManifestEvent), `manifest-complete` (CompleteManifestEvent), `error` (ErrorEvent). The client closes the connection on `manifest-complete`/`error`. When `ref` is set, the manifest is reconstructed as of that commit instead of the working tree (a remote source still emits `clone-progress` if it isn't cloned yet, but never `scan-progress`/`manifest-partial` for the reconstruction itself — the city is already drawn, so a skeleton would flash placeholders). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | components['schemas']['CloneProgressEvent'] + | components['schemas']['ScanProgressEvent'] + | components['schemas']['PartialManifestEvent'] + | components['schemas']['CompleteManifestEvent'] + | components['schemas']['ErrorEvent']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; }; + }; } diff --git a/app/tests/api/config.test.ts b/app/tests/api/config.test.ts index 3f8cba55..9e8e62a6 100644 --- a/app/tests/api/config.test.ts +++ b/app/tests/api/config.test.ts @@ -59,6 +59,22 @@ describe('fetchServerConfig', () => { const cfg = await fetchServerConfig(); expect(cfg.maxBatchPaths).toBe(DEFAULT_SERVER_CONFIG.maxBatchPaths); }); + + it('carries the version through from the server', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(JSON.stringify({ allowLocalRepos: false, version: '1.3.0' }), { status: 200 }) + ); + const cfg = await fetchServerConfig(); + expect(cfg.version).toBe('1.3.0'); + }); + + it('keeps the unknown-version default when the server omits it', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( + new Response(JSON.stringify({ allowLocalRepos: false }), { status: 200 }) + ); + const cfg = await fetchServerConfig(); + expect(cfg.version).toBe('0.0.0+unknown'); + }); }); describe('getServerConfig', () => { diff --git a/app/tests/api/pathBatcher.test.ts b/app/tests/api/pathBatcher.test.ts index 973adf3b..21f76bba 100644 --- a/app/tests/api/pathBatcher.test.ts +++ b/app/tests/api/pathBatcher.test.ts @@ -6,7 +6,7 @@ import { createPathBatcher } from '@/api/pathBatcher'; import { serverConfigNow } from '@/api/config'; vi.mock('@/api/config', () => ({ - serverConfigNow: vi.fn(() => ({ allowLocalRepos: false, maxBatchPaths: 3 })), + serverConfigNow: vi.fn(() => ({ allowLocalRepos: false, maxBatchPaths: 3, version: '1.0.0' })), })); interface Entry { @@ -67,7 +67,11 @@ describe('createPathBatcher', () => { it('re-reads the cap per flush, so a config that arrives late is honoured', async () => { const bodies = mockFetch(echo); - vi.mocked(serverConfigNow).mockReturnValueOnce({ allowLocalRepos: false, maxBatchPaths: 2 }); + vi.mocked(serverConfigNow).mockReturnValueOnce({ + allowLocalRepos: false, + maxBatchPaths: 2, + version: '1.0.0', + }); const batcher = makeBatcher(); const all = Promise.all(['a', 'b', 'c'].map((p) => batcher.request(p))); await vi.runAllTimersAsync(); From 8c2a0a07f3348006098298869d65844b812d05bd Mon Sep 17 00:00:00 2001 From: Thalida Noel Date: Sat, 8 Aug 2026 18:05:26 -0400 Subject: [PATCH 02/19] fix: don't prettier-format manifest.generated.ts app/.prettierignore excludes this file (auto-generated by openapi-typescript, prettier --write from the repo root skipped that ignore file since it isn't at the repo root). Regenerate to restore raw openapi-typescript output. --- app/src/types/manifest.generated.ts | 2058 +++++++++++++-------------- 1 file changed, 1025 insertions(+), 1033 deletions(-) diff --git a/app/src/types/manifest.generated.ts b/app/src/types/manifest.generated.ts index c2d5abf0..0e36f0ec 100644 --- a/app/src/types/manifest.generated.ts +++ b/app/src/types/manifest.generated.ts @@ -4,1059 +4,1051 @@ */ export interface paths { - '/api/health': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Health */ - get: operations['health_api_health_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/config': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Config */ - get: operations['config_api_config_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/file': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get File */ - get: operations['get_file_api_file_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/images': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Get Images - * @description Batch image fetch — {path: {mime, b64}} for many small images in one round - * trip. NOT a plural of GET /api/file: it inlines base64, serves images only, - * and omits anything it can't serve. It exists so the scene's billboard loader - * doesn't exhaust the browser's HTTP/1.1 connection pool on a media-heavy repo. - * - * Each path is trust-checked exactly like GET /api/file. Paths that are out of - * root, missing, non-image, or larger than _MAX_BATCH_IMAGE_BYTES are silently - * omitted; the client falls back to the streaming GET for those. Videos are - * never batched (they stream their poster frame), so this is images only. - */ - post: operations['get_images_api_images_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/fingerprints': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Get Fingerprints - * @description Batch byte-pattern fingerprint fetch — {path: {b64}}, one round trip for - * many buildings. Trust-checked like GET /api/file; out-of-root / missing / - * unreadable paths are silently omitted. Raw binary bytes never leave the - * server — only the head is read, and only the fingerprint image returned. - */ - post: operations['get_fingerprints_api_fingerprints_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/commit': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get Commit */ - get: operations['get_commit_api_commit_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/branches': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get Branches */ - get: operations['get_branches_api_branches_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/manifest/signature': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Signature */ - get: operations['signature_api_manifest_signature_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/timeline': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Timeline */ - get: operations['timeline_api_timeline_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/manifest': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/api/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Health */ + get: operations["health_api_health_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Config */ + get: operations["config_api_config_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/file": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get File */ + get: operations["get_file_api_file_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/images": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Images + * @description Batch image fetch — {path: {mime, b64}} for many small images in one round + * trip. NOT a plural of GET /api/file: it inlines base64, serves images only, + * and omits anything it can't serve. It exists so the scene's billboard loader + * doesn't exhaust the browser's HTTP/1.1 connection pool on a media-heavy repo. + * + * Each path is trust-checked exactly like GET /api/file. Paths that are out of + * root, missing, non-image, or larger than _MAX_BATCH_IMAGE_BYTES are silently + * omitted; the client falls back to the streaming GET for those. Videos are + * never batched (they stream their poster frame), so this is images only. + */ + post: operations["get_images_api_images_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/fingerprints": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get Fingerprints + * @description Batch byte-pattern fingerprint fetch — {path: {b64}}, one round trip for + * many buildings. Trust-checked like GET /api/file; out-of-root / missing / + * unreadable paths are silently omitted. Raw binary bytes never leave the + * server — only the head is read, and only the fingerprint image returned. + */ + post: operations["get_fingerprints_api_fingerprints_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/commit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Commit */ + get: operations["get_commit_api_commit_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/branches": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Branches */ + get: operations["get_branches_api_branches_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/manifest/signature": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Signature */ + get: operations["signature_api_manifest_signature_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/timeline": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Timeline */ + get: operations["timeline_api_timeline_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/manifest": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Manifest */ + get: operations["manifest_api_manifest_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** Manifest */ - get: operations['manifest_api_manifest_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; } export type webhooks = Record; export interface components { - schemas: { - /** AuthorStat */ - AuthorStat: { - /** Name */ - name: string; - /** Commits */ - commits: number; - /** - * Hue - * @description Stable 0-359 hue from the name hash; the display colour is built from it client-side - */ - hue: number; - }; - /** BranchListResponse */ - BranchListResponse: { - /** Branches */ - branches: string[]; - /** Default */ - default: string | null; - }; - /** BusynessThresholds */ - BusynessThresholds: { - /** Avg */ - avg: number; - /** Busy */ - busy: number; - }; - /** - * CloneProgressEvent - * @description `clone-progress` — git source is being cloned; carries clone progress. - * - * A normal progress tick has `stage` + `percent`. A heartbeat during the - * silent promisor blob fetch instead carries `mb_on_disk` (and no percent), - * so the UI shows the working tree materializing rather than freezing. - */ - CloneProgressEvent: { - /** Label */ - label?: string; - /** - * Stage - * @enum {string} - */ - stage?: 'receiving' | 'resolving' | 'counting' | 'updating'; - /** Percent */ - percent?: number; - /** Mb On Disk */ - mb_on_disk?: number; - }; - /** CommitDateRange */ - CommitDateRange: { - /** - * Oldest - * @description Oldest commit date (YYYY-MM-DD), or null when the repo has no commits - */ - oldest: string | null; - /** - * Newest - * @description Newest commit date (YYYY-MM-DD), or null when the repo has no commits - */ - newest: string | null; - }; - /** CommitDetailResponse */ - CommitDetailResponse: { - /** Sha */ - sha: string; - /** Authors */ - authors: string[]; - /** Date */ - date: string; - /** Subject */ - subject: string; - /** Body */ - body: string; - }; - /** CommitEntry */ - CommitEntry: { - /** - * Date - * @description ISO-8601 UTC, e.g. 2026-07-25T14:03:21Z - */ - date: string; - /** Files */ - files: number; - /** Sha */ - sha: string; - /** Authors */ - authors: string[]; - /** Subject */ - subject: string; - /** Same Day Total */ - same_day_total: number; - }; - /** CommitLeader */ - CommitLeader: { - /** Sha */ - sha: string; - /** Files */ - files: number; - }; - /** - * CompleteManifestEvent - * @description `manifest-complete` — a manifest with real, fully-populated metadata (a - * fresh scan's final pass, or a warm cache hit). - */ - CompleteManifestEvent: { - manifest: components['schemas']['Manifest']; - }; - /** ConfigResponse */ - ConfigResponse: { - /** Allowlocalrepos */ - allowLocalRepos: boolean; - /** Maxbatchpaths */ - maxBatchPaths: number; - /** Version */ - version: string; - }; - /** DateRangeMs */ - DateRangeMs: { - /** Mincreated */ - minCreated: number; - /** Maxcreated */ - maxCreated: number; - /** Minmodified */ - minModified: number; - /** Maxmodified */ - maxModified: number; - }; - /** DateRanges */ - DateRanges: { - /** - * Mincreated - * @description Earliest resolved create date (ISO), or null for an empty tree - */ - minCreated: string | null; - /** - * Maxcreated - * @description Latest resolved create date (ISO), or null for an empty tree - */ - maxCreated: string | null; - /** - * Minmodified - * @description Earliest resolved modify date (ISO), or null for an empty tree - */ - minModified: string | null; - /** - * Maxmodified - * @description Latest resolved modify date (ISO), or null for an empty tree - */ - maxModified: string | null; - }; - /** DayLeader */ - DayLeader: { - /** Date */ - date: string; - /** Count */ - count: number; - }; - /** DirLeader */ - DirLeader: { - /** Path */ - path: string; - /** Depth */ - depth: number; - /** Children */ - children: number; - /** Descendants */ - descendants: number; - }; - /** DirNode */ - DirNode: { - /** Name */ - name: string; - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: 'directory'; - /** Path */ - path: string; - /** Fullpath */ - fullPath: string; - /** Children */ - children: (components['schemas']['FileNode'] | components['schemas']['DirNode'])[]; - /** Children Count */ - children_count: number; - /** Children File Count */ - children_file_count: number; - /** Children Dir Count */ - children_dir_count: number; - /** Descendants Count */ - descendants_count: number; - /** Descendants File Count */ - descendants_file_count: number; - /** Descendants Dir Count */ - descendants_dir_count: number; - /** Descendants Size */ - descendants_size: number; - /** Descendants Created Min */ - descendants_created_min: string | null; - /** Descendants Modified Max */ - descendants_modified_max: string | null; - /** Descendants Ext Breakdown */ - descendants_ext_breakdown: components['schemas']['ExtBreakdownEntry'][]; - }; - /** - * ErrorEvent - * @description `error` — a failure after the stream began; carries the message. - */ - ErrorEvent: { - /** Error */ - error: string; - }; - /** ExtBreakdownEntry */ - ExtBreakdownEntry: { - /** Ext */ - ext: string | null; - /** Count */ - count: number; - /** Size */ - size: number; - }; - /** FileLeader */ - FileLeader: { - /** Path */ - path: string; - /** Lines */ - lines: number; - /** Bytes */ - bytes: number; - /** Created */ - created: string; - /** Modified */ - modified: string; - /** Media Width */ - media_width?: number; - /** Media Height */ - media_height?: number; - }; - /** FileNode */ - FileNode: { - /** Name */ - name: string; - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: 'file'; - /** Path */ - path: string; - /** Fullpath */ - fullPath: string; - /** Extension */ - extension: string; - /** Size */ - size: number; - /** Lines */ - lines: number; - /** Binary */ - binary: boolean; - /** - * Dirty - * @description Working-tree differs from HEAD for this tracked file (staged or unstaged). Always False for clean/remote repos. - */ - dirty: boolean; - /** - * Created - * @description ISO create date (UTC, Z-suffixed), resolved server-side: git history date when the file has one, filesystem date otherwise - */ - created: string; - /** - * Modified - * @description ISO modify date (UTC, Z-suffixed), resolved server-side: git history date when the file has one, filesystem date otherwise. When dirty is true, this is always the working-tree filesystem date, regardless of git history - */ - modified: string; - /** - * Mediakind - * @description Media classification by extension (single source for the frontend); null for non-media files - */ - mediaKind?: ('image' | 'video') | null; - /** Media Width */ - media_width?: number; - /** Media Height */ - media_height?: number; - /** Binarytype */ - binaryType?: string; - }; - /** - * FingerprintEntry - * @description One binary file's byte-pattern fingerprint in a POST /api/fingerprints - * batch response: a base64-encoded grayscale PNG (image/png implied), keyed - * by request path. Computed server-side from the file's head — raw binary - * bytes never ship to the client. - */ - FingerprintEntry: { - /** B64 */ - b64: string; - }; - /** HTTPValidationError */ - HTTPValidationError: { - /** Detail */ - detail?: components['schemas']['ValidationError'][]; - }; - /** HealthResponse */ - HealthResponse: { - /** Ok */ - ok: boolean; - }; - /** - * ImageBatchEntry - * @description One image in a POST /api/images batch response: its content-type and - * base64-encoded bytes, keyed by request path in the response map. - */ - ImageBatchEntry: { - /** Mime */ - mime: string; - /** B64 */ - b64: string; - }; - /** Manifest */ - Manifest: { - /** Root */ - root: string; - /** Scanned At */ - scanned_at: string; - /** Content Signature */ - content_signature: string; - /** Structure Signature */ - structure_signature: string; - /** Layout Signature */ - layout_signature: string; - tree: components['schemas']['DirNode']; - repo: components['schemas']['RepoInfo']; - /** Commits */ - commits: components['schemas']['CommitEntry'][]; - busyness: components['schemas']['BusynessThresholds']; - dateRanges: components['schemas']['DateRanges']; - stats: components['schemas']['RepoStats']; - /** - * Pending - * @description Stages still to come. 'metadata': per-file lines/binary are placeholders. 'history': dates are filesystem dates and commits is empty. Empty list means every field is final. - */ - pending: ('metadata' | 'history')[]; - /** - * Readmepath - * @description Absolute path of the root README, or null if there isn't one - */ - readmePath: string | null; - /** - * Readmemodified - * @description That README's mtime, for cache-busting the fetch - */ - readmeModified: string | null; - }; - /** - * PartialManifestEvent - * @description `manifest-partial` — a manifest with the real tree structure but - * placeholder file metadata, sent so the UI can paint the city before - * per-file metadata is resolved. - */ - PartialManifestEvent: { - manifest: components['schemas']['Manifest']; - }; - /** PathBatchRequest */ - PathBatchRequest: { - /** Paths */ - paths: string[]; - /** Shas */ - shas?: { - [key: string]: string; - } | null; - }; - /** RangeStat */ - RangeStat: { - /** Min */ - min: number; - /** Max */ - max: number; - }; - /** RepoInfo */ - RepoInfo: { - /** Branch */ - branch: string | null; - /** Remote Url */ - remote_url: string | null; - /** Head Sha */ - head_sha: string | null; - /** Head Subject */ - head_subject: string | null; - /** Dirty */ - dirty: boolean; - }; - /** RepoStats */ - RepoStats: { - lineCountRange: components['schemas']['RangeStat']; - byteSizeRange: components['schemas']['RangeStat']; - oldestCreatedFile: components['schemas']['FileLeader'] | null; - newestCreatedFile: components['schemas']['FileLeader'] | null; - newestModifiedFile: components['schemas']['FileLeader'] | null; - oldestModifiedFile: components['schemas']['FileLeader'] | null; - maxLinesFile: components['schemas']['FileLeader'] | null; - minLinesFile: components['schemas']['FileLeader'] | null; - maxBytesFile: components['schemas']['FileLeader'] | null; - minBytesFile: components['schemas']['FileLeader'] | null; - maxMediaBytesFile: components['schemas']['FileLeader'] | null; - minMediaBytesFile: components['schemas']['FileLeader'] | null; - maxMediaPixelsFile: components['schemas']['FileLeader'] | null; - minMediaPixelsFile: components['schemas']['FileLeader'] | null; - maxBinaryBytesFile: components['schemas']['FileLeader'] | null; - minBinaryBytesFile: components['schemas']['FileLeader'] | null; - /** Mediacount */ - mediaCount: number; - /** Binarycount */ - binaryCount: number; - /** Totallines */ - totalLines: number; - /** Dirtyfilecount */ - dirtyFileCount: number; - /** Codebytes */ - codeBytes: number; - maxDepthDir: components['schemas']['DirLeader'] | null; - maxChildrenDir: components['schemas']['DirLeader'] | null; - minChildrenDir: components['schemas']['DirLeader'] | null; - maxFilesPerCommit: components['schemas']['CommitLeader'] | null; - minFilesPerCommit: components['schemas']['CommitLeader'] | null; - commitDates: components['schemas']['CommitDateRange']; - maxCommitsPerDay: components['schemas']['DayLeader'] | null; - /** Maxcommitstreakdays */ - maxCommitStreakDays: number; - /** Authors */ - authors: components['schemas']['AuthorStat'][]; - }; - /** - * ScanProgressEvent - * @description `scan-progress` — the working tree is being walked; carries the - * heartbeat files-scanned count. - */ - ScanProgressEvent: { - /** Label */ - label?: string; - /** Files Scanned */ - files_scanned?: number; - }; - /** SignatureResponse */ - SignatureResponse: { - /** Root */ - root: string; - /** Scanned At */ - scanned_at: string; - /** Content Signature */ - content_signature: string; - }; - /** - * TimelineBundle - * @description Wire schema for the scrub bundle; mirrors manifest_types.TimelineBundle. - */ - TimelineBundle: { - /** Commits */ - commits: components['schemas']['CommitEntry'][]; - unionManifest: components['schemas']['Manifest']; - /** Deltas */ - deltas: components['schemas']['TimelineDelta'][]; - /** Bloblines */ - blobLines: { - [key: string]: number; - }; - /** Blobsizes */ - blobSizes: { - [key: string]: number; - }; - /** Commitlineranges */ - commitLineRanges: components['schemas']['RangeStat'][]; - /** Commitdateranges */ - commitDateRanges: components['schemas']['DateRangeMs'][]; - /** Note */ - note: string | null; - }; - /** TimelineChange */ - TimelineChange: { - /** Path */ - path: string; - /** - * Sha - * @description New blob sha, or null when deleted - */ - sha: string | null; - }; - /** - * TimelineCompleteEvent - * @description `timeline-complete` — the full replay bundle (fresh build or warm - * cache hit). - */ - TimelineCompleteEvent: { - bundle: components['schemas']['TimelineBundle']; - }; - /** TimelineDelta */ - TimelineDelta: { - /** Sha */ - sha: string; - /** Changes */ - changes: components['schemas']['TimelineChange'][]; - }; - /** - * TimelineProgressEvent - * @description `timeline-progress` — the history walk, blob-table resolution, or (for a - * blobless remote clone) the up-front blob backfill is in progress. The - * `fetch` stage carries `percent`; `history` carries `commits`; `blobs` - * carries `blobsDone`/`blobsTotal` (the total is known up front from the batch - * blob lookup, so that stage reports two ticks, not a live stream). - */ - TimelineProgressEvent: { - /** - * Stage - * @enum {string} - */ - stage: 'fetch' | 'history' | 'blobs'; - /** Percent */ - percent?: number; - /** Commits */ - commits?: number; - /** Blobsdone */ - blobsDone?: number; - /** Blobstotal */ - blobsTotal?: number; - /** Label */ - label?: string; - }; - /** ValidationError */ - ValidationError: { - /** Location */ - loc: (string | number)[]; - /** Message */ - msg: string; - /** Error Type */ - type: string; - /** Input */ - input?: unknown; - /** Context */ - ctx?: Record; + schemas: { + /** AuthorStat */ + AuthorStat: { + /** Name */ + name: string; + /** Commits */ + commits: number; + /** + * Hue + * @description Stable 0-359 hue from the name hash; the display colour is built from it client-side + */ + hue: number; + }; + /** BranchListResponse */ + BranchListResponse: { + /** Branches */ + branches: string[]; + /** Default */ + default: string | null; + }; + /** BusynessThresholds */ + BusynessThresholds: { + /** Avg */ + avg: number; + /** Busy */ + busy: number; + }; + /** + * CloneProgressEvent + * @description `clone-progress` — git source is being cloned; carries clone progress. + * + * A normal progress tick has `stage` + `percent`. A heartbeat during the + * silent promisor blob fetch instead carries `mb_on_disk` (and no percent), + * so the UI shows the working tree materializing rather than freezing. + */ + CloneProgressEvent: { + /** Label */ + label?: string; + /** + * Stage + * @enum {string} + */ + stage?: "receiving" | "resolving" | "counting" | "updating"; + /** Percent */ + percent?: number; + /** Mb On Disk */ + mb_on_disk?: number; + }; + /** CommitDateRange */ + CommitDateRange: { + /** + * Oldest + * @description Oldest commit date (YYYY-MM-DD), or null when the repo has no commits + */ + oldest: string | null; + /** + * Newest + * @description Newest commit date (YYYY-MM-DD), or null when the repo has no commits + */ + newest: string | null; + }; + /** CommitDetailResponse */ + CommitDetailResponse: { + /** Sha */ + sha: string; + /** Authors */ + authors: string[]; + /** Date */ + date: string; + /** Subject */ + subject: string; + /** Body */ + body: string; + }; + /** CommitEntry */ + CommitEntry: { + /** + * Date + * @description ISO-8601 UTC, e.g. 2026-07-25T14:03:21Z + */ + date: string; + /** Files */ + files: number; + /** Sha */ + sha: string; + /** Authors */ + authors: string[]; + /** Subject */ + subject: string; + /** Same Day Total */ + same_day_total: number; + }; + /** CommitLeader */ + CommitLeader: { + /** Sha */ + sha: string; + /** Files */ + files: number; + }; + /** + * CompleteManifestEvent + * @description `manifest-complete` — a manifest with real, fully-populated metadata (a + * fresh scan's final pass, or a warm cache hit). + */ + CompleteManifestEvent: { + manifest: components["schemas"]["Manifest"]; + }; + /** ConfigResponse */ + ConfigResponse: { + /** Allowlocalrepos */ + allowLocalRepos: boolean; + /** Maxbatchpaths */ + maxBatchPaths: number; + /** Version */ + version: string; + }; + /** DateRangeMs */ + DateRangeMs: { + /** Mincreated */ + minCreated: number; + /** Maxcreated */ + maxCreated: number; + /** Minmodified */ + minModified: number; + /** Maxmodified */ + maxModified: number; + }; + /** DateRanges */ + DateRanges: { + /** + * Mincreated + * @description Earliest resolved create date (ISO), or null for an empty tree + */ + minCreated: string | null; + /** + * Maxcreated + * @description Latest resolved create date (ISO), or null for an empty tree + */ + maxCreated: string | null; + /** + * Minmodified + * @description Earliest resolved modify date (ISO), or null for an empty tree + */ + minModified: string | null; + /** + * Maxmodified + * @description Latest resolved modify date (ISO), or null for an empty tree + */ + maxModified: string | null; + }; + /** DayLeader */ + DayLeader: { + /** Date */ + date: string; + /** Count */ + count: number; + }; + /** DirLeader */ + DirLeader: { + /** Path */ + path: string; + /** Depth */ + depth: number; + /** Children */ + children: number; + /** Descendants */ + descendants: number; + }; + /** DirNode */ + DirNode: { + /** Name */ + name: string; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "directory"; + /** Path */ + path: string; + /** Fullpath */ + fullPath: string; + /** Children */ + children: (components["schemas"]["FileNode"] | components["schemas"]["DirNode"])[]; + /** Children Count */ + children_count: number; + /** Children File Count */ + children_file_count: number; + /** Children Dir Count */ + children_dir_count: number; + /** Descendants Count */ + descendants_count: number; + /** Descendants File Count */ + descendants_file_count: number; + /** Descendants Dir Count */ + descendants_dir_count: number; + /** Descendants Size */ + descendants_size: number; + /** Descendants Created Min */ + descendants_created_min: string | null; + /** Descendants Modified Max */ + descendants_modified_max: string | null; + /** Descendants Ext Breakdown */ + descendants_ext_breakdown: components["schemas"]["ExtBreakdownEntry"][]; + }; + /** + * ErrorEvent + * @description `error` — a failure after the stream began; carries the message. + */ + ErrorEvent: { + /** Error */ + error: string; + }; + /** ExtBreakdownEntry */ + ExtBreakdownEntry: { + /** Ext */ + ext: string | null; + /** Count */ + count: number; + /** Size */ + size: number; + }; + /** FileLeader */ + FileLeader: { + /** Path */ + path: string; + /** Lines */ + lines: number; + /** Bytes */ + bytes: number; + /** Created */ + created: string; + /** Modified */ + modified: string; + /** Media Width */ + media_width?: number; + /** Media Height */ + media_height?: number; + }; + /** FileNode */ + FileNode: { + /** Name */ + name: string; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "file"; + /** Path */ + path: string; + /** Fullpath */ + fullPath: string; + /** Extension */ + extension: string; + /** Size */ + size: number; + /** Lines */ + lines: number; + /** Binary */ + binary: boolean; + /** + * Dirty + * @description Working-tree differs from HEAD for this tracked file (staged or unstaged). Always False for clean/remote repos. + */ + dirty: boolean; + /** + * Created + * @description ISO create date (UTC, Z-suffixed), resolved server-side: git history date when the file has one, filesystem date otherwise + */ + created: string; + /** + * Modified + * @description ISO modify date (UTC, Z-suffixed), resolved server-side: git history date when the file has one, filesystem date otherwise. When dirty is true, this is always the working-tree filesystem date, regardless of git history + */ + modified: string; + /** + * Mediakind + * @description Media classification by extension (single source for the frontend); null for non-media files + */ + mediaKind?: ("image" | "video") | null; + /** Media Width */ + media_width?: number; + /** Media Height */ + media_height?: number; + /** Binarytype */ + binaryType?: string; + }; + /** + * FingerprintEntry + * @description One binary file's byte-pattern fingerprint in a POST /api/fingerprints + * batch response: a base64-encoded grayscale PNG (image/png implied), keyed + * by request path. Computed server-side from the file's head — raw binary + * bytes never ship to the client. + */ + FingerprintEntry: { + /** B64 */ + b64: string; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** HealthResponse */ + HealthResponse: { + /** Ok */ + ok: boolean; + }; + /** + * ImageBatchEntry + * @description One image in a POST /api/images batch response: its content-type and + * base64-encoded bytes, keyed by request path in the response map. + */ + ImageBatchEntry: { + /** Mime */ + mime: string; + /** B64 */ + b64: string; + }; + /** Manifest */ + Manifest: { + /** Root */ + root: string; + /** Scanned At */ + scanned_at: string; + /** Content Signature */ + content_signature: string; + /** Structure Signature */ + structure_signature: string; + /** Layout Signature */ + layout_signature: string; + tree: components["schemas"]["DirNode"]; + repo: components["schemas"]["RepoInfo"]; + /** Commits */ + commits: components["schemas"]["CommitEntry"][]; + busyness: components["schemas"]["BusynessThresholds"]; + dateRanges: components["schemas"]["DateRanges"]; + stats: components["schemas"]["RepoStats"]; + /** + * Pending + * @description Stages still to come. 'metadata': per-file lines/binary are placeholders. 'history': dates are filesystem dates and commits is empty. Empty list means every field is final. + */ + pending: ("metadata" | "history")[]; + /** + * Readmepath + * @description Absolute path of the root README, or null if there isn't one + */ + readmePath: string | null; + /** + * Readmemodified + * @description That README's mtime, for cache-busting the fetch + */ + readmeModified: string | null; + }; + /** + * PartialManifestEvent + * @description `manifest-partial` — a manifest with the real tree structure but + * placeholder file metadata, sent so the UI can paint the city before + * per-file metadata is resolved. + */ + PartialManifestEvent: { + manifest: components["schemas"]["Manifest"]; + }; + /** PathBatchRequest */ + PathBatchRequest: { + /** Paths */ + paths: string[]; + /** Shas */ + shas?: { + [key: string]: string; + } | null; + }; + /** RangeStat */ + RangeStat: { + /** Min */ + min: number; + /** Max */ + max: number; + }; + /** RepoInfo */ + RepoInfo: { + /** Branch */ + branch: string | null; + /** Remote Url */ + remote_url: string | null; + /** Head Sha */ + head_sha: string | null; + /** Head Subject */ + head_subject: string | null; + /** Dirty */ + dirty: boolean; + }; + /** RepoStats */ + RepoStats: { + lineCountRange: components["schemas"]["RangeStat"]; + byteSizeRange: components["schemas"]["RangeStat"]; + oldestCreatedFile: components["schemas"]["FileLeader"] | null; + newestCreatedFile: components["schemas"]["FileLeader"] | null; + newestModifiedFile: components["schemas"]["FileLeader"] | null; + oldestModifiedFile: components["schemas"]["FileLeader"] | null; + maxLinesFile: components["schemas"]["FileLeader"] | null; + minLinesFile: components["schemas"]["FileLeader"] | null; + maxBytesFile: components["schemas"]["FileLeader"] | null; + minBytesFile: components["schemas"]["FileLeader"] | null; + maxMediaBytesFile: components["schemas"]["FileLeader"] | null; + minMediaBytesFile: components["schemas"]["FileLeader"] | null; + maxMediaPixelsFile: components["schemas"]["FileLeader"] | null; + minMediaPixelsFile: components["schemas"]["FileLeader"] | null; + maxBinaryBytesFile: components["schemas"]["FileLeader"] | null; + minBinaryBytesFile: components["schemas"]["FileLeader"] | null; + /** Mediacount */ + mediaCount: number; + /** Binarycount */ + binaryCount: number; + /** Totallines */ + totalLines: number; + /** Dirtyfilecount */ + dirtyFileCount: number; + /** Codebytes */ + codeBytes: number; + maxDepthDir: components["schemas"]["DirLeader"] | null; + maxChildrenDir: components["schemas"]["DirLeader"] | null; + minChildrenDir: components["schemas"]["DirLeader"] | null; + maxFilesPerCommit: components["schemas"]["CommitLeader"] | null; + minFilesPerCommit: components["schemas"]["CommitLeader"] | null; + commitDates: components["schemas"]["CommitDateRange"]; + maxCommitsPerDay: components["schemas"]["DayLeader"] | null; + /** Maxcommitstreakdays */ + maxCommitStreakDays: number; + /** Authors */ + authors: components["schemas"]["AuthorStat"][]; + }; + /** + * ScanProgressEvent + * @description `scan-progress` — the working tree is being walked; carries the + * heartbeat files-scanned count. + */ + ScanProgressEvent: { + /** Label */ + label?: string; + /** Files Scanned */ + files_scanned?: number; + }; + /** SignatureResponse */ + SignatureResponse: { + /** Root */ + root: string; + /** Scanned At */ + scanned_at: string; + /** Content Signature */ + content_signature: string; + }; + /** + * TimelineBundle + * @description Wire schema for the scrub bundle; mirrors manifest_types.TimelineBundle. + */ + TimelineBundle: { + /** Commits */ + commits: components["schemas"]["CommitEntry"][]; + unionManifest: components["schemas"]["Manifest"]; + /** Deltas */ + deltas: components["schemas"]["TimelineDelta"][]; + /** Bloblines */ + blobLines: { + [key: string]: number; + }; + /** Blobsizes */ + blobSizes: { + [key: string]: number; + }; + /** Commitlineranges */ + commitLineRanges: components["schemas"]["RangeStat"][]; + /** Commitdateranges */ + commitDateRanges: components["schemas"]["DateRangeMs"][]; + /** Note */ + note: string | null; + }; + /** TimelineChange */ + TimelineChange: { + /** Path */ + path: string; + /** + * Sha + * @description New blob sha, or null when deleted + */ + sha: string | null; + }; + /** + * TimelineCompleteEvent + * @description `timeline-complete` — the full replay bundle (fresh build or warm + * cache hit). + */ + TimelineCompleteEvent: { + bundle: components["schemas"]["TimelineBundle"]; + }; + /** TimelineDelta */ + TimelineDelta: { + /** Sha */ + sha: string; + /** Changes */ + changes: components["schemas"]["TimelineChange"][]; + }; + /** + * TimelineProgressEvent + * @description `timeline-progress` — the history walk, blob-table resolution, or (for a + * blobless remote clone) the up-front blob backfill is in progress. The + * `fetch` stage carries `percent`; `history` carries `commits`; `blobs` + * carries `blobsDone`/`blobsTotal` (the total is known up front from the batch + * blob lookup, so that stage reports two ticks, not a live stream). + */ + TimelineProgressEvent: { + /** + * Stage + * @enum {string} + */ + stage: "fetch" | "history" | "blobs"; + /** Percent */ + percent?: number; + /** Commits */ + commits?: number; + /** Blobsdone */ + blobsDone?: number; + /** Blobstotal */ + blobsTotal?: number; + /** Label */ + label?: string; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + /** Input */ + input?: unknown; + /** Context */ + ctx?: Record; + }; }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - health_api_health_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; + health_api_health_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - content: { - 'application/json': components['schemas']['HealthResponse']; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; }; - }; - }; - }; - config_api_config_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; + config_api_config_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - content: { - 'application/json': components['schemas']['ConfigResponse']; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConfigResponse"]; + }; + }; }; - }; - }; - }; - get_file_api_file_get: { - parameters: { - query: { - /** @description Absolute path inside a scanned root */ - path: string; - /** @description Blob sha to read instead of the working tree */ - sha?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_images_api_images_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['PathBatchRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - [key: string]: components['schemas']['ImageBatchEntry']; - }; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_fingerprints_api_fingerprints_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; }; - requestBody: { - content: { - 'application/json': components['schemas']['PathBatchRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - [key: string]: components['schemas']['FingerprintEntry']; - }; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_commit_api_commit_get: { - parameters: { - query: { - sha: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['CommitDetailResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_branches_api_branches_get: { - parameters: { - query: { - src: string; - }; - header?: never; - path?: never; - cookie?: never; + get_file_api_file_get: { + parameters: { + query: { + /** @description Absolute path inside a scanned root */ + path: string; + /** @description Blob sha to read instead of the working tree */ + sha?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['BranchListResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; + get_images_api_images_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PathBatchRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: components["schemas"]["ImageBatchEntry"]; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; }; - }; - signature_api_manifest_signature_get: { - parameters: { - query: { - src: string; - branch?: string | null; - no_cache?: boolean; - exclude?: string[]; - }; - header?: never; - path?: never; - cookie?: never; + get_fingerprints_api_fingerprints_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PathBatchRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: components["schemas"]["FingerprintEntry"]; + }; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SignatureResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; + get_commit_api_commit_get: { + parameters: { + query: { + sha: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CommitDetailResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; }; - }; - timeline_api_timeline_get: { - parameters: { - query: { - src: string; - branch?: string | null; - no_cache?: boolean; - exclude?: string[]; - }; - header?: never; - path?: never; - cookie?: never; + get_branches_api_branches_get: { + parameters: { + query: { + src: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BranchListResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; }; - requestBody?: never; - responses: { - /** @description Server-Sent Events stream (`text/event-stream`). Named events and their JSON `data` payloads: `timeline-progress` (TimelineProgressEvent, one or more while the history walk / blob resolution run), `timeline-complete` (TimelineCompleteEvent, the full bundle), `error` (ErrorEvent). A warm cache hit emits only `timeline-complete`, no progress. The client closes the connection on `timeline-complete`/`error`. */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': - | components['schemas']['TimelineProgressEvent'] - | components['schemas']['TimelineCompleteEvent'] - | components['schemas']['ErrorEvent']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; + signature_api_manifest_signature_get: { + parameters: { + query: { + src: string; + branch?: string | null; + no_cache?: boolean; + exclude?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SignatureResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; }; - }; - manifest_api_manifest_get: { - parameters: { - query?: { - src?: string; - branch?: string | null; - no_cache?: boolean; - exclude?: string[]; - ref?: string | null; - }; - header?: never; - path?: never; - cookie?: never; + timeline_api_timeline_get: { + parameters: { + query: { + src: string; + branch?: string | null; + no_cache?: boolean; + exclude?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Server-Sent Events stream (`text/event-stream`). Named events and their JSON `data` payloads: `timeline-progress` (TimelineProgressEvent, one or more while the history walk / blob resolution run), `timeline-complete` (TimelineCompleteEvent, the full bundle), `error` (ErrorEvent). A warm cache hit emits only `timeline-complete`, no progress. The client closes the connection on `timeline-complete`/`error`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TimelineProgressEvent"] | components["schemas"]["TimelineCompleteEvent"] | components["schemas"]["ErrorEvent"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; }; - requestBody?: never; - responses: { - /** @description Server-Sent Events stream (`text/event-stream`). Named events and their JSON `data` payloads: `clone-progress` (CloneProgressEvent), `scan-progress` (ScanProgressEvent), `manifest-partial` (PartialManifestEvent), `manifest-complete` (CompleteManifestEvent), `error` (ErrorEvent). The client closes the connection on `manifest-complete`/`error`. When `ref` is set, the manifest is reconstructed as of that commit instead of the working tree (a remote source still emits `clone-progress` if it isn't cloned yet, but never `scan-progress`/`manifest-partial` for the reconstruction itself — the city is already drawn, so a skeleton would flash placeholders). */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': - | components['schemas']['CloneProgressEvent'] - | components['schemas']['ScanProgressEvent'] - | components['schemas']['PartialManifestEvent'] - | components['schemas']['CompleteManifestEvent'] - | components['schemas']['ErrorEvent']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; + manifest_api_manifest_get: { + parameters: { + query?: { + src?: string; + branch?: string | null; + no_cache?: boolean; + exclude?: string[]; + ref?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Server-Sent Events stream (`text/event-stream`). Named events and their JSON `data` payloads: `clone-progress` (CloneProgressEvent), `scan-progress` (ScanProgressEvent), `manifest-partial` (PartialManifestEvent), `manifest-complete` (CompleteManifestEvent), `error` (ErrorEvent). The client closes the connection on `manifest-complete`/`error`. When `ref` is set, the manifest is reconstructed as of that commit instead of the working tree (a remote source still emits `clone-progress` if it isn't cloned yet, but never `scan-progress`/`manifest-partial` for the reconstruction itself — the city is already drawn, so a skeleton would flash placeholders). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CloneProgressEvent"] | components["schemas"]["ScanProgressEvent"] | components["schemas"]["PartialManifestEvent"] | components["schemas"]["CompleteManifestEvent"] | components["schemas"]["ErrorEvent"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; }; - }; } From 3d75e12c234c69101d46fd5fa0530add7e7f55be Mon Sep 17 00:00:00 2001 From: Thalida Noel Date: Sat, 8 Aug 2026 18:19:28 -0400 Subject: [PATCH 03/19] feat: merge the reset gem into the project chip as a switcher --- .../ProjectSwitcher/ProjectSwitcher.css | 8 ++ .../ProjectSwitcher/ProjectSwitcher.tsx | 16 ++-- app/src/components/ResetViewButton.tsx | 25 ------- app/src/layout/App/App.tsx | 12 +-- app/src/layout/AppHeader/AppHeader.css | 38 ++-------- app/src/layout/AppHeader/AppHeader.tsx | 54 ++++++-------- app/tests/layout/AppHeader.test.tsx | 74 +++++++++++++++++++ 7 files changed, 123 insertions(+), 104 deletions(-) delete mode 100644 app/src/components/ResetViewButton.tsx create mode 100644 app/tests/layout/AppHeader.test.tsx diff --git a/app/src/components/ProjectSwitcher/ProjectSwitcher.css b/app/src/components/ProjectSwitcher/ProjectSwitcher.css index 81e9c894..e9531e13 100644 --- a/app/src/components/ProjectSwitcher/ProjectSwitcher.css +++ b/app/src/components/ProjectSwitcher/ProjectSwitcher.css @@ -14,6 +14,14 @@ flex: 0 0 auto; } +/* The gem is the chip's leading glyph. Sized to the chip's other icons and + held at its intrinsic width so a long repo name shrinks the label, not it. */ +.btn-chip-gem { + width: var(--cc-font-md); + height: var(--cc-font-md); + flex: 0 0 auto; +} + .app-header-branch-pill { display: inline-flex; align-items: center; diff --git a/app/src/components/ProjectSwitcher/ProjectSwitcher.tsx b/app/src/components/ProjectSwitcher/ProjectSwitcher.tsx index 66f99f7f..e93872b1 100644 --- a/app/src/components/ProjectSwitcher/ProjectSwitcher.tsx +++ b/app/src/components/ProjectSwitcher/ProjectSwitcher.tsx @@ -1,11 +1,13 @@ -// components/ProjectSwitcher.tsx — The project chip: label + optional @branch -// pill + a switch cue. Click opens the project switcher. The trailing glyph is -// ChevronsUpDown (a "switchable value" cue), not a down-caret, since clicking -// opens a full switcher screen rather than a dropdown menu. -// Renders nothing until a project is loaded (no label). +// components/ProjectSwitcher.tsx — The project chip: gem + label + optional +// @branch pill + a switch cue. Click opens the project switcher. The trailing +// glyph is ChevronsUpDown (a "switchable value" cue), not a down-caret, since +// clicking opens a full switcher screen rather than a dropdown menu. +// The gem doubles as the app logo, so the chip renders even before a project +// loads: gem alone, still opening the switcher. import './ProjectSwitcher.css'; import { ChevronsUpDown } from 'lucide-preact'; +import { GemIcon } from '@/components/GemIcon/GemIcon'; export interface ProjectSwitcherProps { rootLabel: string; @@ -14,7 +16,6 @@ export interface ProjectSwitcherProps { } export function ProjectSwitcher({ rootLabel, branch, onSwitchSource }: ProjectSwitcherProps) { - if (!rootLabel) return null; return ( diff --git a/app/src/components/ResetViewButton.tsx b/app/src/components/ResetViewButton.tsx deleted file mode 100644 index 73dddb70..00000000 --- a/app/src/components/ResetViewButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// components/ResetViewButton.tsx — The gem button at the header's left edge. -// Doubles as the app logo: clicking it resets the camera view (R). Renders -// nothing when no reset handler is wired (pre-boot). - -import { GemIcon } from '@/components/GemIcon/GemIcon'; -import { KEY_BINDINGS } from '@/constants/keyboard'; - -export interface ResetViewButtonProps { - onResetView?: () => void; -} - -export function ResetViewButton({ onResetView }: ResetViewButtonProps) { - if (!onResetView) return null; - return ( - - ); -} diff --git a/app/src/layout/App/App.tsx b/app/src/layout/App/App.tsx index f561fb19..3645ec2a 100644 --- a/app/src/layout/App/App.tsx +++ b/app/src/layout/App/App.tsx @@ -31,12 +31,7 @@ import { DebugModal } from '@/views/DebugModal/DebugModal'; import { LoadingOverlay } from '@/components/LoadingOverlay/LoadingOverlay'; import { HljsThemeLink } from '@/components/HljsThemeLink/HljsThemeLink'; import { SelectionAnnouncer } from '@/components/SelectionAnnouncer/SelectionAnnouncer'; -import { - resetView, - clearSelection, - runCollisionCheck, - runStemDiagnostic, -} from '@/state/stores/scene'; +import { clearSelection, runCollisionCheck, runStemDiagnostic } from '@/state/stores/scene'; import { openProjectsView, closeProjectsView, LOADING_CANCEL } from '@/state/stores/ui'; import { SOURCE_ERROR, CURRENT_SOURCE } from '@/state/stores/source'; import { MANIFEST } from '@/state/stores/manifest'; @@ -93,10 +88,7 @@ export function App() { - openProjectsView({ dismissible: true })} - onResetView={resetView} - /> + openProjectsView({ dismissible: true })} />
diff --git a/app/src/layout/AppHeader/AppHeader.css b/app/src/layout/AppHeader/AppHeader.css index f6f52a00..2ed1913a 100644 --- a/app/src/layout/AppHeader/AppHeader.css +++ b/app/src/layout/AppHeader/AppHeader.css @@ -1,17 +1,14 @@ /* ── Sitewide header ────────────────────────────────────────────────── Full-width strip across the top of the app shell. Sits in the body's - column flex above #app-body; the sidebars start at its lower edge. */ + column flex above #app-body; the sidebars start at its lower edge. + One left-aligned row: project chip, copy-source, open-on-origin. */ #app-header { flex: 0 0 auto; - /* 3-column grid keeps #app-title visually centered in the viewport - regardless of how much content the left controls (gem, project chip, - repo link) occupy. The minmax(0, 1fr) tracks share the leftover space - equally, so the center column stays at true header-center. */ - display: grid; - grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + display: flex; align-items: center; - column-gap: var(--cc-space-6); + gap: var(--cc-space-2); + min-width: 0; height: 32px; padding: 0 var(--cc-space-4); border-bottom: 1px solid var(--cc-border-subtle); @@ -22,29 +19,8 @@ user-select: none; } -#app-header-left { - display: flex; - align-items: center; - gap: var(--cc-space-2); - min-width: 0; -} - -/* Centered cluster: project switcher, copy-source, open-remote (the gem stays - left, in #app-header-left). */ -#app-title { - display: flex; - align-items: center; - justify-content: center; - gap: var(--cc-space-2); - min-width: 0; - color: var(--cc-text-secondary); -} - -/* Visual style fully in .btn-icon (with --no-drag modifier where needed). */ - -/* Header buttons (switch-source, refresh, repo-link) now use - .btn-icon + .btn-icon--no-drag (refresh/switch) or - .btn-icon + .btn-icon--link + .btn-icon--no-drag (repo-link). +/* Header buttons use .btn-icon + .btn-icon--no-drag, or + .btn-icon + .btn-icon--link + .btn-icon--no-drag for the repo link. Inner svg rendering tweak applies to any svg child of a header btn-icon. */ #app-header .btn-icon svg { display: block; diff --git a/app/src/layout/AppHeader/AppHeader.tsx b/app/src/layout/AppHeader/AppHeader.tsx index 3e1d2cef..faaf2b16 100644 --- a/app/src/layout/AppHeader/AppHeader.tsx +++ b/app/src/layout/AppHeader/AppHeader.tsx @@ -1,7 +1,7 @@ -// layout/AppHeader.tsx — Sitewide top header. Composition shell only: the reset -// gem stays at the left edge; the project switcher + its actions (copy-source, -// open-on-origin) sit centered. What's selected is shown in the right sidebar -// (open whenever there's a selection), not here. +// layout/AppHeader.tsx — Sitewide top header. Composition shell only: a single +// left-aligned row holding the project switcher (gem + name + branch) and its +// actions (copy-source, open-on-origin). What's selected is shown in the right +// sidebar (open whenever there's a selection), not here. import './AppHeader.css'; import { ExternalLink } from 'lucide-preact'; @@ -9,46 +9,38 @@ import { SOURCE_INFO } from '@/state/stores/source'; import { MANIFEST } from '@/state/stores/manifest'; import type { Manifest } from '@/types'; import { openProjectsView } from '@/state/stores/ui'; -import { ResetViewButton } from '@/components/ResetViewButton'; import { ProjectSwitcher } from '@/components/ProjectSwitcher/ProjectSwitcher'; import { CopyButton } from '@/components/CopyButton/CopyButton'; export interface AppHeaderProps { /** Fires when the user clicks the project chip to switch source. */ onSwitchSource?: () => void; - /** Fires when the user clicks the reset-view (gem) button. */ - onResetView?: () => void; } -export function AppHeader({ onSwitchSource, onResetView }: AppHeaderProps = {}) { +export function AppHeader({ onSwitchSource }: AppHeaderProps = {}) { const si = SOURCE_INFO.value; const remoteUrl = (MANIFEST.value as Manifest)?.repo?.remote_url ?? null; return (
-
- -
-
- openProjectsView({ dismissible: true }))} - /> - {si.src && } - {remoteUrl && ( - - - - )} -
+ openProjectsView({ dismissible: true }))} + /> + {si.src && } + {remoteUrl && ( + + + + )}
); } diff --git a/app/tests/layout/AppHeader.test.tsx b/app/tests/layout/AppHeader.test.tsx new file mode 100644 index 00000000..ea06fe75 --- /dev/null +++ b/app/tests/layout/AppHeader.test.tsx @@ -0,0 +1,74 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { render } from 'preact'; +import { AppHeader } from '@/layout/AppHeader/AppHeader'; +import { CURRENT_SOURCE } from '@/state/stores/source'; +import { setManifest } from '@/state/stores/manifest'; +import { EMPTY_MANIFEST } from '@/constants/manifest'; +import type { Manifest } from '@/types'; +import { flush } from '../_helpers/preact'; + +const LOADED: Manifest = { + ...EMPTY_MANIFEST, + tree: { ...EMPTY_MANIFEST.tree, name: 'codecity' }, +}; + +function loadProject() { + setManifest(LOADED); + CURRENT_SOURCE.value = { src: '/repos/codecity', branch: 'main' }; +} + +describe('AppHeader', () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + render(null, container); + document.body.removeChild(container); + CURRENT_SOURCE.value = null; + setManifest(EMPTY_MANIFEST); + }); + + it('renders the gem inside the project chip', async () => { + loadProject(); + render(, container); + await flush(); + + const chip = container.querySelector('.btn-chip'); + expect(chip).not.toBeNull(); + expect(chip!.querySelector('.gem-icon')).not.toBeNull(); + expect(chip!.textContent).toContain('codecity'); + }); + + it('opens the switcher when the chip is clicked', async () => { + const onSwitchSource = vi.fn(); + loadProject(); + render(, container); + await flush(); + + container.querySelector('.btn-chip')!.click(); + await flush(); + + expect(onSwitchSource).toHaveBeenCalledTimes(1); + }); + + it('still renders the gem before a project loads', async () => { + render(, container); + await flush(); + + const chip = container.querySelector('.btn-chip'); + expect(chip).not.toBeNull(); + expect(chip!.querySelector('.gem-icon')).not.toBeNull(); + }); + + it('has no reset-view control', async () => { + loadProject(); + render(, container); + await flush(); + + expect(container.querySelector('[aria-label="Reset view"]')).toBeNull(); + }); +}); From dda94680f4d2de2f57ecbd46d31766369ff9a25a Mon Sep 17 00:00:00 2001 From: Thalida Noel Date: Sat, 8 Aug 2026 18:28:09 -0400 Subject: [PATCH 04/19] fix: reword stale .btn-chip comment for the flex-row header --- app/src/styles/buttons.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/styles/buttons.css b/app/src/styles/buttons.css index e5e9e24e..d5e064a4 100644 --- a/app/src/styles/buttons.css +++ b/app/src/styles/buttons.css @@ -118,9 +118,9 @@ .btn-chip { appearance: none; - /* Grow with its content up to the header's left column (1fr), then shrink and - truncate — no hard cap, so it uses the space it's given before ellipsizing - (min-width: 0 lets the flex item shrink below its content width). */ + /* Grows with its content, no hard cap, then shrinks and truncates within + the header's flex row (min-width: 0 lets it shrink below its content + width) rather than pushing neighboring header buttons out of view. */ flex: 0 1 auto; min-width: 0; display: inline-flex; From ec5037a4e5c96c7219f5fc4b1bd53f431831b9f6 Mon Sep 17 00:00:00 2001 From: Thalida Noel Date: Sat, 8 Aug 2026 18:34:25 -0400 Subject: [PATCH 05/19] feat: add a version, repo, and creator credit line to the footer --- app/src/layout/AppFooter/AppFooter.css | 34 ++++++++++++++- app/src/layout/AppFooter/AppFooter.tsx | 14 +++---- app/src/layout/AppFooter/FooterMeta.tsx | 44 ++++++++++++++++++++ app/src/layout/AppFooter/FooterSep.tsx | 6 +++ app/tests/layout/AppFooter.test.tsx | 55 +++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 app/src/layout/AppFooter/FooterMeta.tsx create mode 100644 app/src/layout/AppFooter/FooterSep.tsx diff --git a/app/src/layout/AppFooter/AppFooter.css b/app/src/layout/AppFooter/AppFooter.css index 21d95f8f..a5f5f217 100644 --- a/app/src/layout/AppFooter/AppFooter.css +++ b/app/src/layout/AppFooter/AppFooter.css @@ -7,7 +7,7 @@ #app-footer { flex: 0 0 auto; display: grid; - grid-template-columns: minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: var(--cc-space-7); padding: 0 var(--cc-space-6); @@ -43,6 +43,9 @@ .app-footer-section.app-footer-right { justify-content: flex-end; } +.app-footer-section.app-footer-center { + justify-content: center; +} .app-footer-item { flex: 0 1 auto; @@ -137,6 +140,32 @@ } } +/* ── Credit line (center section) ──────────────────────────────────── */ +/* Reads as a code comment: the // and the · separators sit at the faint + * separator color, the words one step up so the two links clear 4.5:1. */ +.app-footer-meta { + display: inline-flex; + align-items: center; + gap: var(--cc-space-3); + min-width: 0; +} +.app-footer-meta-comment { + color: var(--cc-text-faint); +} +.app-footer-meta-version, +.app-footer-meta-credit { + color: var(--cc-text-muted); +} +.app-footer-meta-link { + color: var(--cc-text-muted); + text-decoration: none; +} +.app-footer-meta-link:hover, +.app-footer-meta-link:focus-visible { + color: var(--cc-text-strong); + text-decoration: underline; +} + /* ── Footer responsive narrowing ───────────────────────────────────────────── */ /* At narrow widths the status timestamp / "rebuilding…" text is secondary information — hide it so the dot alone represents the status and the @@ -149,4 +178,7 @@ .app-footer-status-detail { display: none; } + .app-footer-center { + display: none; + } } diff --git a/app/src/layout/AppFooter/AppFooter.tsx b/app/src/layout/AppFooter/AppFooter.tsx index ebc49ec0..27cdc9a8 100644 --- a/app/src/layout/AppFooter/AppFooter.tsx +++ b/app/src/layout/AppFooter/AppFooter.tsx @@ -1,4 +1,4 @@ -// layout/AppFooter.tsx — Sitewide bottom status bar. Two sections: +// layout/AppFooter.tsx — Sitewide bottom status bar. Three sections: // left — combined status indicator: [dot] detail-text // One dot, two channels of state: // color — rebuild state (green=idle, yellow=rebuilding, @@ -9,11 +9,10 @@ // A detail next to the dot shows human-readable status // ("rebuilt 5s ago", "rebuilding…", "error: ", "paused"). // title= on the wrapper is a fallback tooltip for narrow widths. +// center — credit line: build version, repo link, attribution // right — current selection metadata (language · lines · size · created // · modified for files; file/dir counts + size for directories), // then a far-right icon cluster (keyboard shortcuts, debug) -// -// The refresh/reset-view button has moved to the header (far right). import './AppFooter.css'; import { useSignal } from '@preact/signals'; @@ -34,6 +33,8 @@ import { import { openShortcuts, openDebug } from '@/state/stores/ui'; import { isDebugMode } from '@/utils/debugMode'; import { humanLanguageFor } from '@/utils/syntaxLanguages'; +import { FooterSep } from './FooterSep'; +import { FooterMeta } from './FooterMeta'; interface FooterFileSelection { kind: NodeKind.File; @@ -114,10 +115,6 @@ function FooterItem({ text, title }: FooterItemData) { ); } -function FooterSep() { - return ·; -} - interface FooterStatusSectionProps { status: FooterStatus | null; } @@ -309,6 +306,9 @@ export function AppFooter() { +