diff --git a/AGENTS.md b/AGENTS.md index 2e7898c..fe1599c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ Main entrypoints: - Do not use decorative repeated-character section headers. - Use `it('should...')` style tests. - Unit tests live next to their implementation. -- `apps/ui/src/routeTree.gen.ts` is generated and must not be manually edited. +- `src/ui/routeTree.gen.ts` is generated and must not be manually edited. ## Architecture @@ -97,9 +97,9 @@ Shared utilities: - `src/coverage-check.ts` UI source tree: -- `apps/ui/` +- `src/ui/` - TanStack Start browser UI - - API routes live under `apps/ui/src/routes/api.v1.*.ts` + - API routes live under `src/ui/routes/api.v1.*.ts` - source routes include `/threads/$threadId`, `/claude-code-sessions/$sessionId`, `/cline-tasks/$taskId`, `/grok-sessions/$sessionId`, `/kiro-sessions/$sessionId`, `/qoder-sessions/$sessionId`, `/cursor-threads/$composerId`, `/antigravity-conversations/$conversationId`, `/minimax-code-sessions/$sessionId`, and `/opencode-sessions/$sessionId` ## Stable API Contract @@ -151,7 +151,7 @@ When changing risky areas: - Source adapter changes: update the matching `src/lib/conversation-data/*-adapter.ts` tests or add one next to the adapter. - Transcript parsing/rendering: update the matching source transcript tests. - Codex browsing/delete/analytics: update `src/lib/codex-browser-db.test.ts` and `src/lib/codex-analytics.test.ts`. -- UI behavior: update/add Vitest files under `apps/ui/src/**/*.vitest.tsx`. +- UI behavior: update/add Vitest files under `src/ui/**/*.vitest.tsx`. - API route behavior: add a real UI server/browser smoke when route registration or SSR behavior changes. ## Common Commands @@ -171,7 +171,7 @@ rtk bun run test:ui - Keep root-package source modules imported by the UI available through `@spiracha/lib/*`. - The repository has one package manifest. Keep UI runtime dependencies needed by packaged `bunx spiracha` in root `dependencies` and build/test-only tooling in root `devDependencies`. -- Root-owned UI Vite commands use `apps/ui` as their internal working directory and run through `bun --bun` because TanStack and server functions depend on that application root and Bun-only modules like `bun:sqlite`. Do not add a nested manifest to achieve this. UI Vitest commands use the normal Node runtime. +- UI Vite commands run from the repository root with `bun --bun`, so TanStack, server functions, the stable API, and the browser route tree all resolve from one manifest and one dependency graph. UI Vitest commands use the normal Node runtime. - TanStack Start server functions should use `.validator(...)`, not deprecated `.inputValidator(...)`. - API routes should use route-level `server.handlers`. - Keep `*-transcript-phase.ts` modules browser-safe; UI client adapters import them directly. diff --git a/README.md b/README.md index 3820bf3..b25750d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # spiracha

- Spiracha icon + Spiracha icon

[![license](https://img.shields.io/npm/l/spiracha)](LICENSE.md) @@ -180,11 +180,11 @@ bun run test:ui `bun run coverage` enforces at least 90% line coverage independently for the root Bun suite and the UI Vitest suite, and reports function coverage and per-file hotspots for follow-up. -Run one root test file with `bun test src/lib/shared.test.ts`. Run one UI test file with `bun run test:ui --run apps/ui/src/components/export-dialog.vitest.tsx`. +Run one root test file with `bun test src/lib/shared.test.ts`. Run one UI test file with `bun run test:ui --run src/ui/components/export-dialog.vitest.tsx`. -Spiracha has one package manifest. The `apps/ui` directory remains a source boundary, but all UI and direct-client commands and dependencies are owned at the repository root. Root-owned Vite commands use `apps/ui` as their internal working directory and run through `bun --bun` because TanStack server functions import Bun-only modules such as `bun:sqlite`; Vitest uses its normal Node runtime. +Spiracha has one application boundary: the stable API, server functions, browser route tree, and UI all live under the root package and resolve through one manifest and one dependency graph. Vite runs from the repository root with Bun because TanStack server functions import Bun-only modules such as `bun:sqlite`; Vitest uses its normal Node runtime. -TanStack Router generates `apps/ui/src/routeTree.gen.ts` during development/build. Do not edit it manually; after adding or renaming route files, run `bun run build` (or start the dev server) and include the generated update. +TanStack Router generates `src/ui/routeTree.gen.ts` during development/build. Do not edit it manually; after adding or renaming route files, run `bun run build` (or start the dev server) and include the generated update. ## Breaking Consequences diff --git a/apps/ui/.gitignore b/apps/ui/.gitignore deleted file mode 100644 index 8b25bb5..0000000 --- a/apps/ui/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -node_modules -.DS_Store -dist -dist-ssr -*.local -.env -.nitro -.tanstack -.wrangler -.output -.vinxi -__unconfig* -todos.json diff --git a/apps/ui/AGENTS.md b/apps/ui/AGENTS.md deleted file mode 100644 index f75a91a..0000000 --- a/apps/ui/AGENTS.md +++ /dev/null @@ -1,126 +0,0 @@ -# AGENTS.md - -## Purpose - -This directory is the local browser UI source tree for Spiracha. It reads Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, MiniMax Code, and OpenCode local data through TanStack Start server functions and shared root-package helpers. - -## Commands - -```bash -rtk bun start -rtk bun run build -rtk bun run test:ui -rtk bun run typecheck -rtk bun run coverage -``` - -Run these commands from the repository root. There is intentionally no nested UI package manifest. - -Important: - -- `start`, `build`, and `ui:preview` run Vite through `bun --bun ...` on purpose. Do not switch them back to Node execution, because the server functions import Bun-only modules such as `bun:sqlite`. -- Those root-owned Vite scripts use `apps/ui` as their internal working directory because TanStack Start derives part of its dev-server plan from the process working directory. -- Keep UI runtime dependencies needed by the packaged `bunx spiracha` launcher in the root `dependencies`. Keep build and test tooling in the root `devDependencies`. -- Keep Vitest on its normal Node runtime; forcing it through `bun --bun` breaks test-environment module and global behavior. - -## Routing - -- This package uses TanStack Start with file-based routes in `src/routes/`. -- `src/routeTree.gen.ts` is generated. Do not edit it manually. -- If route typing behaves strangely, delete `src/routeTree.gen.ts` and rebuild with `rtk bun run build` to regenerate it cleanly. -- Stable API routes live in `src/routes/api.v1.*.ts` and should stay thin wrappers around `@spiracha/lib/conversation-api`. -- The UI supports both `/threads/$threadId` and a root shortcut route `/$threadId` that redirects straight to the thread detail page. -- Codex project inventory and project-thread search use route search params. `/codex` and `/codex/$project` use `q`. -- Codex analytics uses the `project` route search param so filtered analytics links can be bookmarked and reloaded. -- Claude Code session detail lives at `/claude-code-sessions/$sessionId`. -- Grok session detail lives at `/grok-sessions/$sessionId`. -- Kiro session detail lives at `/kiro-sessions/$sessionId`. -- Qoder session detail lives at `/qoder-sessions/$sessionId`. -- Cursor thread detail lives at `/cursor-threads/$composerId`. -- Antigravity conversation detail lives at `/antigravity-conversations/$conversationId`. -- MiniMax Code session detail lives at `/minimax-code-sessions/$sessionId`. -- OpenCode session detail lives at `/opencode-sessions/$sessionId`. -- Keep the Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, MiniMax Code, and OpenCode list/detail pages aligned around the same table-driven index/detail pattern when adding new source integrations. - -## Shared Data Layer - -The UI depends on root-package helpers via `@spiracha/*` path aliases: - -- `@spiracha/lib/codex-browser-db` -- `@spiracha/lib/codex-browser-export` -- `@spiracha/lib/codex-thread-cache` -- `@spiracha/lib/codex-analytics` -- `@spiracha/lib/concurrency` -- `@spiracha/lib/claude-code-db` -- `@spiracha/lib/claude-code-transcript-phase` -- `@spiracha/lib/claude-code-transcript` -- `@spiracha/lib/grok-db` -- `@spiracha/lib/grok-transcript-phase` -- `@spiracha/lib/grok-transcript` -- `@spiracha/lib/kiro-db` -- `@spiracha/lib/kiro-transcript-phase` -- `@spiracha/lib/kiro-transcript` -- `@spiracha/lib/qoder-db` -- `@spiracha/lib/qoder-transcript-phase` -- `@spiracha/lib/qoder-transcript` -- `@spiracha/lib/cursor-db` -- `@spiracha/lib/cursor-recovery` -- `@spiracha/lib/cursor-transcript` -- `@spiracha/lib/antigravity-db` -- `@spiracha/lib/antigravity-keychain` -- `@spiracha/lib/opencode-db` -- `@spiracha/lib/opencode-transcript-phase` -- `@spiracha/lib/opencode-think-tags` -- `@spiracha/lib/opencode-transcript` -- `@spiracha/lib/conversation-api` -- `@spiracha/lib/conversation-data` - -Keep server-only imports inside server functions or route loaders. Do not import Bun-only modules into purely client-side components. -The `*-transcript-phase` helpers are intentionally browser-safe and may be imported by client adapters. - -## Data Access Patterns - -Use the existing layers consistently: - -- TanStack Start server functions in `src/lib/codex-server.ts`, `src/lib/claude-code-server.ts`, `src/lib/grok-server.ts`, `src/lib/kiro-server.ts`, `src/lib/qoder-server.ts`, `src/lib/cursor-server.ts`, `src/lib/antigravity-server.ts`, and `src/lib/opencode-server.ts` - - Use for any browser-triggered read/write that needs Bun-only modules, DB access, filesystem access, Keychain access, or shared root-package helpers. - - Use `.validator(...)` for input validation. Do not add new `.inputValidator(...)` calls. -- TanStack Query query options in `src/lib/codex-queries.ts`, `src/lib/claude-code-queries.ts`, `src/lib/grok-queries.ts`, `src/lib/kiro-queries.ts`, `src/lib/qoder-queries.ts`, `src/lib/cursor-queries.ts`, `src/lib/antigravity-queries.ts`, and `src/lib/opencode-queries.ts` - - Use for client-side fetching, caching, retries, and invalidation of server-function results. -- Shared root-package helpers under `@spiracha/lib/*` - - Extend these when the behavior should stay shared between the UI and the stable data API. - - Use the shared source-specific phase helpers for assistant commentary/final-answer rules instead of duplicating that logic in UI adapters. - - Keep OpenCode think-tag handling in `@spiracha/lib/opencode-think-tags` so UI display and exports strip MiniMax reasoning tags consistently. -- Stable API route handlers in `src/routes/api.v1.*.ts` - - Use route-level `server.handlers`. - - Delegate to `@spiracha/lib/conversation-api` instead of duplicating parsing or response-envelope logic. -- `src/lib/source-session-export-server.ts` - - Use for single-session source exports that may return either inline content or a temporary zip download URL. -- `settings-store.tsx` - - Use only for browser-local UI preferences. Do not put server-derived source data here. - -If a feature needs new source data, prefer: -1. shared root-package helper changes -2. server-function exposure in the matching `*-server.ts` -3. TanStack Query wiring in the matching `*-queries.ts` -4. client component consumption - -For URL-backed route state, use `src/lib/route-search.ts` instead of ad hoc parsing in route files. Keep search params minimal and stable because they are user-facing links. - -## Testing - -- UI component tests live under `src/**/*.vitest.tsx`. -- Source-specific transcript adapter tests live next to their adapter files under `src/lib/*.vitest.ts`. -- Route search parsing tests live next to the helper in `src/lib/route-search.vitest.ts`. -- The root package wraps this Vitest suite from `src/ui-suite.test.ts` so `rtk bun test` exercises both the Bun suite and the UI suite. - -## Design - -- The UI is intentionally compact, text-first, and operational. Avoid adding decorative cards, large gradients, or chat-bubble styling. -- Prefer deterministic date formatting to avoid hydration mismatches between server and client. - -## Constraints - -- Do not add a second database. -- Do not duplicate transcript parsing or export rendering in the UI source tree. -- Use the shared root-package helpers instead. diff --git a/apps/ui/README.md b/apps/ui/README.md deleted file mode 100644 index f3150f8..0000000 --- a/apps/ui/README.md +++ /dev/null @@ -1,197 +0,0 @@ -# Spiracha UI - -The browser UI for browsing local Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode history, inspecting transcript details, exporting chats, and analyzing Codex usage patterns. - -## Stack - -- Bun -- TanStack Start -- TanStack Query -- TanStack Table -- TanStack Virtual -- Tailwind CSS v4 -- shadcn/ui -- Biome - -## What It Does - -- lists derived Codex projects from the Codex SQLite database -- lists Codex threads within a project in chronological order -- shows Codex thread timelines, a dedicated tool activity and definition view, recorded goals, readable sandbox policy, metadata, and raw event context -- streams optional Codex live updates through a shared worker when available and an alternate loopback origin, keeping long-lived event connections out of the page-loading connection pool -- searches Codex projects from the app shell through the URL-backed `/codex?q=...` inventory filter -- exports Codex, Claude Code, Grok, Kiro, Qoder, Cursor, and OpenCode sessions or threads as Markdown, plain text, or optional zip archives with optional metadata, commentary, and tool-call inclusion; the last submitted choices persist while canceled drafts are discarded -- lists Claude Code workspaces and sessions from local `~/.claude/projects` JSONL files -- shows dedicated Claude Code session detail pages with reasoning, tool calls, token metadata, and export actions -- lists Grok workspaces and sessions from local Grok session archives -- shows dedicated Grok session detail pages with compacted-history recovery, tool calls, metadata, export, and delete actions -- lists Kiro workspaces and sessions from local Kiro workspace session files -- shows dedicated Kiro session detail pages with image attachments, prompt logs, execution-derived tool calls and paired outputs, metadata, and matching tool-inclusive exports -- lists Qoder workspaces and sessions from local Qoder history and checkpoint storage -- shows dedicated Qoder session detail pages with prompts, checkpoint file operations, metadata, and export actions -- lists Cursor workspaces and workspace threads with the same table-based index/detail flow as Codex -- shows dedicated Cursor thread detail pages with breadcrumbs back to the workspace and source -- recovers split Cursor storage buckets, exports Cursor threads, and deletes Cursor workspaces or threads -- lists Antigravity workspaces and conversations, including transcript/artifact availability -- shows dedicated Antigravity conversation detail pages with shared metadata and export actions -- reads Antigravity trajectory databases plus generated-log supplements, including paired commands, outputs, and reasoning; encrypted legacy transcripts can be unlocked through macOS Keychain -- exports the same merged Antigravity conversation data or artifacts as Markdown or text -- lists OpenCode workspaces and sessions from the local OpenCode SQLite database -- shows dedicated OpenCode session detail pages with reasoning, tool parts, MiniMax `` blocks, token metadata, and export actions -- shows dashboard and project-scoped Codex analytics for token totals, average and median thread size, archive counts, tool usage, model tokens, client sources, and reasoning effort -- keeps Codex inventory search and analytics project filters in URL search params for reloadable and shareable views -- keeps source-specific commentary hidden by default while preserving final answers, with matching export filtering for Claude Code, Kiro, Qoder, and OpenCode - -## Commands - -```bash -rtk bun start -rtk bun run build -rtk bun run test:ui -rtk bun run typecheck -``` - -## Runtime Note - -Run these commands from the repository root. The UI is part of the root package and intentionally has no nested package manifest. - -Root-owned Vite commands use `apps/ui` as their internal working directory and run through `bun --bun`. That is required because TanStack Start derives part of its server plan from the application directory and the server functions use Bun-only modules such as `bun:sqlite`. - -Vitest uses its normal Node runtime. - -## Configuration - -Runtime configuration is intentionally small: - -- `SPIRACHA_CODEX_DB` - - Optional absolute path to the Codex SQLite database. - - If unset, Spiracha probes the default Codex locations from the shared root package. -- `SPIRACHA_UI_EXPORT_DIR` - - Optional directory for temporary browser-download artifacts such as zipped thread exports. - - If unset, Spiracha uses an OS temp directory under `spiracha-ui-exports`. -- `SPIRACHA_ANALYTICS_TRANSCRIPT_CONCURRENCY` - - Optional positive integer for Codex analytics transcript parsing concurrency. - - Defaults to `8`. -- `SPIRACHA_TRANSCRIPT_LOAD_CONCURRENCY` - - Optional positive integer for detail-page transcript loading concurrency within each source integration. - - Defaults to `3` and is capped at `16`. Each integration has an independent lane, while aggregate work is capped at `16` by default (up to `32` for higher configured lane limits), so one blocked source cannot consume another source's transcript capacity. -- `SPIRACHA_TRANSCRIPT_LOAD_LOGS` - - Set to `1` to log transcript-loader queue and timing diagnostics. Disabled by default so library and CLI consumers stay quiet. -- `SPIRACHA_CLAUDE_CODE_PROJECTS_DIR` - - Optional absolute path to the Claude Code projects directory. - - If unset, Spiracha reads `${SPIRACHA_CLAUDE_CODE_DATA_DIR:-~/.claude}/projects`. `SPIRACHA_CLAUDE_CODE_DIR` and `SPIRACHA_CLAUDE_HOME` are also accepted aliases for the Claude Code data directory. -- `SPIRACHA_GROK_SESSIONS_DIR` - - Optional path to the Grok sessions directory. - - If unset, Spiracha reads `${SPIRACHA_GROK_HOME:-~/.grok}/sessions`. `SPIRACHA_GROK_DIR` is accepted as a Grok home-directory alias. -- `SPIRACHA_KIRO_WORKSPACE_SESSIONS_DIR` - - Optional absolute path to the Kiro workspace sessions directory. - - If unset, Spiracha reads `${SPIRACHA_KIRO_DATA_DIR:-~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent}/workspace-sessions`. `SPIRACHA_KIRO_AGENT_DIR` and `SPIRACHA_KIRO_DIR` are also accepted aliases for the Kiro data directory. -- `SPIRACHA_QODER_GLOBAL_STATE_DB` - - Optional absolute path to Qoder's global `state.vscdb`. - - If unset, Spiracha reads `${SPIRACHA_QODER_USER_DIR:-~/Library/Application Support/Qoder/User}/globalStorage/state.vscdb`. `SPIRACHA_QODER_DATA_DIR` and `SPIRACHA_QODER_DIR` are also accepted aliases for the Qoder user directory. -- `SPIRACHA_QODER_WORKSPACE_STORAGE_DIR` - - Optional absolute path to Qoder's `workspaceStorage` directory. - - If unset, Spiracha reads `${SPIRACHA_QODER_USER_DIR:-~/Library/Application Support/Qoder/User}/workspaceStorage`. -- `SPIRACHA_OPENCODE_DB` - - Optional absolute path to the OpenCode SQLite database. - - If unset, Spiracha reads `${SPIRACHA_OPENCODE_DATA_DIR:-${XDG_DATA_HOME:-~/.local/share}/opencode}/opencode.db`. `SPIRACHA_OPENCODE_DIR` is also accepted as an OpenCode data-directory alias. -- `SPIRACHA_OPENCODE_DB_CONCURRENCY` - - Optional positive integer for concurrent OpenCode database reads. - - Defaults to `2`. -- `SPIRACHA_OPENCODE_DB_LOGS` - - Set to `1` to log OpenCode database queue and timing diagnostics. Disabled by default. -- `SPIRACHA_CURSOR_USER_DIR` - - Optional absolute path to Cursor's `User` directory. - - If unset, Spiracha reads the platform default Cursor user-data directory. -- `SPIRACHA_CURSOR_PROJECTS_DIR` - - Optional absolute path to Cursor's project transcript directory. - - If unset, Spiracha infers `.cursor/projects` from the Cursor user directory when possible. -- `SPIRACHA_ANTIGRAVITY_DIRS` - - Optional path-list of Antigravity roots. - - If unset, Spiracha reads `~/.gemini/antigravity-ide` and `~/.gemini/antigravity`. `SPIRACHA_ANTIGRAVITY_DIR` is accepted as a single-root alias. - -Export artifacts are served through the UI as attachment downloads from `/__exports/*`. The local dev and preview servers use the same export-directory contract. - -Default source locations: - -| Source | Default location | Primary override | -| --- | --- | --- | -| Codex | shared Codex DB probe list | `SPIRACHA_CODEX_DB` | -| Claude Code | `~/.claude/projects` | `SPIRACHA_CLAUDE_CODE_PROJECTS_DIR` | -| Grok | `~/.grok/sessions` | `SPIRACHA_GROK_SESSIONS_DIR`, `SPIRACHA_GROK_HOME` | -| Kiro | `~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent/workspace-sessions` | `SPIRACHA_KIRO_WORKSPACE_SESSIONS_DIR` | -| Qoder | `~/Library/Application Support/Qoder/User/globalStorage/state.vscdb` + `~/Library/Application Support/Qoder/User/workspaceStorage` | `SPIRACHA_QODER_GLOBAL_STATE_DB`, `SPIRACHA_QODER_WORKSPACE_STORAGE_DIR` | -| Cursor | `~/Library/Application Support/Cursor/User` on macOS | `SPIRACHA_CURSOR_USER_DIR`, `SPIRACHA_CURSOR_PROJECTS_DIR` | -| Antigravity | `~/.gemini/antigravity-ide` + `~/.gemini/antigravity` | `SPIRACHA_ANTIGRAVITY_DIRS`, `SPIRACHA_ANTIGRAVITY_DIR` | -| OpenCode | `${XDG_DATA_HOME:-~/.local/share}/opencode/opencode.db` | `SPIRACHA_OPENCODE_DB` | -| Export downloads | OS temp directory under `spiracha-ui-exports` | `SPIRACHA_UI_EXPORT_DIR` | - -Codex analytics cache keys are based on Codex DB row metadata instead of statting every rollout file before cache hits. That keeps large histories responsive. The tradeoff is that manual JSONL edits outside Codex do not invalidate analytics unless DB row metadata changes or the temporary UI cache is cleared. - -The temporary UI cache is pruned opportunistically at read/write boundaries, with cleanup scans throttled to once per minute. Entries expire after 24 hours and the retained cache is capped at 256 MiB; recently accessed entries refresh their age so hot data survives eviction. - -Transcript detail pages expose the same display controls across sources: user messages, commentary, tool calls, extra events, and raw JSON. Claude Code assistant lead-ins are classified from `stop_reason`, Kiro assistant phases are classified per user turn from session and execution files, Qoder shows local prompt history plus checkpoint file operations, and OpenCode assistant phases are classified per assistant run after stripping MiniMax `` blocks into commentary. OpenCode think-tag extraction preserves literal `` examples inside Markdown code spans and fenced code blocks. - -## Routes - -- `/` - - dashboard -- `/codex` - - Codex inventory and search, with `q` as the route search param -- `/codex/$project` - - Codex project thread listing, with `q` as the route search param -- `/claude-code` - - Claude Code workspace inventory and search -- `/claude-code/$workspaceKey` - - Claude Code workspace session listing -- `/claude-code-sessions/$sessionId` - - Claude Code session detail and export -- `/grok` - - Grok workspace inventory and search -- `/grok/$workspaceKey` - - Grok workspace session listing -- `/grok-sessions/$sessionId` - - Grok session detail, export, and delete -- `/kiro` - - Kiro workspace inventory and search -- `/kiro/$workspaceKey` - - Kiro workspace session listing -- `/kiro-sessions/$sessionId` - - Kiro session detail and export -- `/qoder` - - Qoder workspace inventory and search -- `/qoder/$workspaceKey` - - Qoder workspace session listing -- `/qoder-sessions/$sessionId` - - Qoder session detail and export -- `/cursor` - - Cursor workspace inventory and search -- `/cursor/$workspaceKey` - - Cursor workspace thread listing -- `/cursor-threads/$composerId` - - Cursor thread detail, export, and delete -- `/antigravity` - - Antigravity workspace inventory and search -- `/antigravity/$workspaceKey` - - Antigravity workspace conversation listing -- `/antigravity-conversations/$conversationId` - - Antigravity conversation detail, export, and artifact inspection -- `/opencode` - - OpenCode workspace inventory and search -- `/opencode/$workspaceKey` - - OpenCode workspace session listing -- `/opencode-sessions/$sessionId` - - OpenCode session detail and export -- `/threads/$threadId` - - Codex thread detail, transcript, export, and delete -- `/$threadId` - - shortcut redirect to the thread detail page for pasted Codex thread UUIDs -- `/analytics` - - project-scoped Codex token, archive, tool, model, client-source, and reasoning-effort analytics, with `project` as the route search param - -## Testing - -- UI component tests live under `src/**/*.vitest.tsx`. -- Source-specific transcript event adapter tests live under `src/lib/*.vitest.ts`. -- Route search parsing tests live in `src/lib/route-search.vitest.ts`. -- The repo root wraps this Vitest suite from `src/ui-suite.test.ts`, so `rtk bun test` at the root covers both the Bun tests and the UI tests. diff --git a/apps/ui/tsconfig.json b/apps/ui/tsconfig.json deleted file mode 100644 index 9adebc2..0000000 --- a/apps/ui/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - "allowImportingTsExtensions": true, - "jsx": "react-jsx", - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "module": "ESNext", - - /* Bundler mode */ - "moduleResolution": "bundler", - "noEmit": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "paths": { - "@/*": ["./src/*"], - "@spiracha/*": ["../../src/*"], - "#/*": ["./src/*"] - }, - - /* Linting */ - "skipLibCheck": true, - "strict": true, - "target": "ESNext", - "types": ["vite/client"], - "verbatimModuleSyntax": true - }, - "include": ["**/*.ts", "**/*.tsx"] -} diff --git a/apps/ui/vitest.config.ts b/apps/ui/vitest.config.ts deleted file mode 100644 index 8f3bfb0..0000000 --- a/apps/ui/vitest.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import path from 'node:path'; -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - resolve: { - alias: { - '@spiracha': path.resolve(__dirname, '../../src'), - '#': path.resolve(__dirname, './src'), - }, - }, - root: __dirname, - test: { - coverage: { - exclude: ['src/routeTree.gen.ts', 'src/integrations/**', 'src/router.tsx', 'src/routes/**'], - include: ['src/components/**/*.tsx', 'src/lib/**/*.ts', 'src/lib/**/*.tsx'], - provider: 'v8', - reporter: ['text', 'lcov'], - }, - environment: 'jsdom', - environmentOptions: { - jsdom: { - url: 'http://localhost', - }, - }, - include: ['src/**/*.vitest.ts', 'src/**/*.vitest.tsx'], - setupFiles: ['vitest.setup.ts'], - }, -}); diff --git a/bin/spiracha.test.ts b/bin/spiracha.test.ts index 730d10f..178440a 100644 --- a/bin/spiracha.test.ts +++ b/bin/spiracha.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'bun:test'; import { EventEmitter } from 'node:events'; -import path from 'node:path'; import { buildSpirachaDevServerCommand, resolveSpirachaPackageRoot, waitForSpirachaDevServer } from './spiracha'; describe('spiracha executable launcher', () => { @@ -9,7 +8,7 @@ describe('spiracha executable launcher', () => { expect(buildSpirachaDevServerCommand(packageRoot)).toEqual({ args: ['--bun', 'vite', 'dev', '--host', '127.0.0.1', '--port', '3000'], - cwd: path.join(packageRoot, 'apps', 'ui'), + cwd: packageRoot, }); }); diff --git a/bin/spiracha.ts b/bin/spiracha.ts old mode 100644 new mode 100755 index 82e7631..c19f143 --- a/bin/spiracha.ts +++ b/bin/spiracha.ts @@ -11,7 +11,7 @@ export const resolveSpirachaPackageRoot = (binDir = import.meta.dir): string => export const buildSpirachaDevServerCommand = (packageRoot = resolveSpirachaPackageRoot()): SpirachaDevServerCommand => ({ args: ['--bun', 'vite', 'dev', '--host', '127.0.0.1', '--port', process.env.PORT || '3000'], - cwd: path.join(packageRoot, 'apps', 'ui'), + cwd: packageRoot, }); type SpirachaChildProcess = { diff --git a/biome.json b/biome.json index 1f0c00b..23be357 100644 --- a/biome.json +++ b/biome.json @@ -26,8 +26,8 @@ "*.js", "*.json", "*.md", - "!!apps/ui/src/styles.css", - "!!apps/ui/src/routeTree.gen.ts", + "!!src/ui/styles.css", + "!!src/ui/routeTree.gen.ts", "!!**/node_modules", "!!**/dist", "!!coverage", diff --git a/bun.lock b/bun.lock index f43c4c1..89b857e 100644 --- a/bun.lock +++ b/bun.lock @@ -5,46 +5,46 @@ "": { "name": "spiracha", "dependencies": { - "@tailwindcss/typography": "latest", - "@tailwindcss/vite": "latest", - "@tanstack/devtools-vite": "latest", - "@tanstack/match-sorter-utils": "latest", - "@tanstack/react-devtools": "latest", - "@tanstack/react-query": "latest", - "@tanstack/react-query-devtools": "latest", - "@tanstack/react-router": "latest", - "@tanstack/react-router-devtools": "latest", - "@tanstack/react-router-ssr-query": "latest", - "@tanstack/react-start": "latest", - "@tanstack/react-table": "latest", - "@tanstack/react-virtual": "latest", - "@tanstack/router-plugin": "latest", - "@vitejs/plugin-react": "latest", - "class-variance-authority": "latest", - "clsx": "latest", - "fflate": "latest", - "lucide-react": "latest", - "radix-ui": "latest", - "react": "latest", - "react-dom": "latest", - "tailwind-merge": "latest", - "tailwindcss": "latest", - "tw-animate-css": "latest", - "vite": "latest", - "zod": "latest", + "@tailwindcss/typography": "0.5.20", + "@tailwindcss/vite": "4.3.3", + "@tanstack/devtools-vite": "0.8.3", + "@tanstack/match-sorter-utils": "8.19.4", + "@tanstack/react-devtools": "0.10.10", + "@tanstack/react-query": "5.101.4", + "@tanstack/react-query-devtools": "5.101.4", + "@tanstack/react-router": "1.170.27", + "@tanstack/react-router-devtools": "1.167.1", + "@tanstack/react-router-ssr-query": "1.167.1", + "@tanstack/react-start": "1.168.44", + "@tanstack/react-table": "8.21.3", + "@tanstack/react-virtual": "3.14.9", + "@tanstack/router-plugin": "1.168.30", + "@vitejs/plugin-react": "6.0.5", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "fflate": "0.8.3", + "lucide-react": "1.31.0", + "radix-ui": "1.6.7", + "react": "19.2.8", + "react-dom": "19.2.8", + "tailwind-merge": "3.6.0", + "tailwindcss": "4.3.3", + "tw-animate-css": "1.4.0", + "vite": "8.2.1", + "zod": "4.4.3", }, "devDependencies": { - "@biomejs/biome": "latest", - "@testing-library/dom": "latest", - "@testing-library/react": "latest", - "@types/bun": "latest", - "@types/node": "latest", - "@types/react": "latest", - "@types/react-dom": "latest", - "@vitest/coverage-v8": "latest", - "jsdom": "latest", - "typescript": "latest", - "vitest": "latest", + "@biomejs/biome": "2.5.8", + "@testing-library/dom": "10.4.1", + "@testing-library/react": "16.3.2", + "@types/bun": "^1.3.14", + "@types/node": "^26.2.0", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.4", + "@vitest/coverage-v8": "4.1.10", + "jsdom": "30.0.1", + "typescript": "^7.0.2", + "vitest": "4.1.10", }, }, }, @@ -89,23 +89,23 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@biomejs/biome": ["@biomejs/biome@2.5.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.6", "@biomejs/cli-darwin-x64": "2.5.6", "@biomejs/cli-linux-arm64": "2.5.6", "@biomejs/cli-linux-arm64-musl": "2.5.6", "@biomejs/cli-linux-x64": "2.5.6", "@biomejs/cli-linux-x64-musl": "2.5.6", "@biomejs/cli-win32-arm64": "2.5.6", "@biomejs/cli-win32-x64": "2.5.6" }, "bin": { "biome": "bin/biome" } }, "sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA=="], + "@biomejs/biome": ["@biomejs/biome@2.5.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.8", "@biomejs/cli-darwin-x64": "2.5.8", "@biomejs/cli-linux-arm64": "2.5.8", "@biomejs/cli-linux-arm64-musl": "2.5.8", "@biomejs/cli-linux-x64": "2.5.8", "@biomejs/cli-linux-x64-musl": "2.5.8", "@biomejs/cli-win32-arm64": "2.5.8", "@biomejs/cli-win32-x64": "2.5.8" }, "bin": { "biome": "bin/biome" } }, "sha512-aeAeeJB9fSDc7Gq+2GqpQxA0qBj6gj1k2R6L1cYqGePKP/baIq1WX8y6B+D+nRsO5ViQL22K/8IwbqERW0q1nw=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mk1QON9PHllvvLN5gU3f4rMxeh4syK5p9OvKyWH6/W8ueh04uaC8TUXXByhGufWf/y5mQc03ZLM45zU+cmqMjA=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-bsGwFMBNyHPyiLSsQcZJxdoRrg1V4JL+d7wEsvUBczlP9U9lwM+7mzQHxI4o1mhBsTmdOBbAb6fHU3Z3snN45w=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-XmFiA0WPYFC+uiUDC8WRFzAIH9bo7vwQLav38Uoq4ETC+T/+uBi0TsYGJECkugY3r8USl3jc+Ae2/irAF6F2lQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-VcJNbstduTHx83NGAdhp78/JOcP45BZHXL7yNsfI1uGzdUgegAz2s+mSoT7wK6PBNzLoqG0zDOXaz/RQYVtSiw=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.8", "", { "os": "linux", "cpu": "x64" }, "sha512-S5wcm9OBDvLHodD4PUaN488hCpco9QD/9ZxuYJiw4euWtr/oQvLR72z2ixItH8Wd5BCm6FZaeb+YNvOoM1xHtQ=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.8", "", { "os": "linux", "cpu": "x64" }, "sha512-kKmiyokeISRGq2FLwvr+TzsgBusfxaZ0FZNLcOYOpCK/78tRrEjeEBLvq3xLZMpqbANgJdRPI7vZX8ZL37u9/w=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-nILH0mzm3Hi3iEdd7o7GpB8kBR/mSQwfQG/tyBqyNrY2GFtcgwfV9nV8xLmbtUpMNY/Oi0Ml1XgfR4flOdq+AA=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.8", "", { "os": "win32", "cpu": "x64" }, "sha512-I2czzXTY61f3nFJxXoMDq80t7MivxDEnCjE+8sDKoFfcKMaoQdkqhIFQ3KyY0XLzeSpUBYeNAXgD+iOV/BU0VA=="], "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], @@ -149,6 +149,10 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw=="], + "@neodrag/core": ["@neodrag/core@3.0.0-next.11", "", {}, "sha512-3WQWxyrbxiaK9zS5JU2wJsW2gpoQlZBXVghduBh61JpqaeE0T0cte8R0qYK2RuJo3J2TYQYqxO19CpG/C1i5eg=="], + + "@neodrag/solid": ["@neodrag/solid@3.0.0-next.11", "", { "peerDependencies": { "@neodrag/core": "3.0.0-next.11", "solid-js": "^1.0.0" } }, "sha512-vCBIn/pimjWMQ6vhTS2/O1XNAwzVtc4eUhdbQ91WykbZWWqQ5NocDXt/1OdYrEkeRzJcpCv8wEz5PnMkgKP81Q=="], + "@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="], "@oozcitak/infra": ["@oozcitak/infra@2.0.2", "", { "dependencies": { "@oozcitak/util": "^10.0.0" } }, "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA=="], @@ -397,7 +401,7 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], - "@tanstack/devtools": ["@tanstack/devtools@0.13.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.8", "@tanstack/devtools-event-bus": "0.4.2", "@tanstack/devtools-ui": "0.6.0", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" }, "bin": { "intent": "./bin/intent.js" } }, "sha512-p/nOH9bS/OO/u3402zPjoGu+Mz6Fzi/iRqJuYghuuYRUY32kZt+C0/d+pP/bi6/2JTi1FdT6oEXI2lWlA5tXxw=="], + "@tanstack/devtools": ["@tanstack/devtools@0.14.0", "", { "dependencies": { "@neodrag/solid": "3.0.0-next.11", "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.8", "@tanstack/devtools-event-bus": "0.4.2", "@tanstack/devtools-ui": "0.7.0", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" }, "bin": { "intent": "./bin/intent.js" } }, "sha512-tN0SEi1BVaJYtkZbgYpvLsInjwNRAZkR3r6slSvz9Jm+bfkhcdjuOSrZp0cAwOGwdnauHZ1mYgtfe2AfC/V1NQ=="], "@tanstack/devtools-bundler-core": ["@tanstack/devtools-bundler-core@0.1.1", "", { "dependencies": { "@tanstack/devtools-client": "0.0.8", "@tanstack/devtools-event-bus": "0.4.2", "chalk": "^5.6.2", "launch-editor": "^2.11.1", "magic-string": "^0.30.0", "oxc-parser": "^0.120.0", "picomatch": "^4.0.3" } }, "sha512-2kowecGXNi/FAnwmJKW3WDZ6XuacHDcz4JsMmx43E21G6ZFmoQFuOJCVuv2bFkQZIR1M7+FVLQF5bdS5MQY62Q=="], @@ -407,11 +411,11 @@ "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.5.0", "", { "bin": { "intent": "./bin/intent.js" } }, "sha512-H+OH3zC6Vhu/K0NaVfQKknEKawc/+2PT+D3SB3Ox0V8SiMlTo0abbmH2rH0721R2aNYbjdMXA1oENOd8E2UVoA=="], - "@tanstack/devtools-ui": ["@tanstack/devtools-ui@0.6.0", "", { "dependencies": { "clsx": "^2.1.1", "dayjs": "^1.11.19", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-CVaM6rT6Nl5ijo83vJYFa2SjofvpuOl/uOvbYGhBrRgUhhelNHhx8zZX+hnZCHmIr0/lzM65hsocnZ72592Rvg=="], + "@tanstack/devtools-ui": ["@tanstack/devtools-ui@0.7.0", "", { "dependencies": { "clsx": "^2.1.1", "dayjs": "^1.11.19", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-px/a+JgRSVHDj/hID1cwfsIi+ly5l7t3Yw7fLw4G556HXCNgDri36fupt9h7oBeEKntpsx3ep/XtZn51rrCv2g=="], "@tanstack/devtools-vite": ["@tanstack/devtools-vite@0.8.3", "", { "dependencies": { "@tanstack/devtools-bundler-core": "0.1.1", "@tanstack/devtools-client": "0.0.8", "@tanstack/devtools-event-bus": "0.4.2", "chalk": "^5.6.2" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "bin": { "intent": "./bin/intent.js" } }, "sha512-MqqE4/rdQUG55Y8Zux1Jj1I2wIBHdqYgjAJzP1grMUtFqSZ2XIDB7BHEV9UW/vrbhK7ocl4yFgVaJWROv0zeDA=="], - "@tanstack/history": ["@tanstack/history@1.162.0", "", {}, "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA=="], + "@tanstack/history": ["@tanstack/history@1.162.1", "", {}, "sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w=="], "@tanstack/match-sorter-utils": ["@tanstack/match-sorter-utils@8.19.4", "", { "dependencies": { "remove-accents": "0.5.0" } }, "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg=="], @@ -419,25 +423,25 @@ "@tanstack/query-devtools": ["@tanstack/query-devtools@5.101.4", "", {}, "sha512-z5IPHnDX3aUWeTWlRKLyooBQekaCAw4xRpZqPQ390RiWTDBcTynjpPT221BArw0u2+pnQMdGvPQI9YNNubBcmA=="], - "@tanstack/react-devtools": ["@tanstack/react-devtools@0.10.9", "", { "dependencies": { "@tanstack/devtools": "0.13.0" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-lS6mtccEmUaodsWiRORGM/MGKT0jgzcy5v+eY6pzOPxEgzTHUDhca+WGxShFqKxmF4oneRxXjww1gkvMrWq6uw=="], + "@tanstack/react-devtools": ["@tanstack/react-devtools@0.10.10", "", { "dependencies": { "@tanstack/devtools": "0.14.0" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-hYH34MSVbajs1pUc22ftSapyKQp2gOh0w34a7ouYOdIcbXD6YPckSR2YpAWGq1rrs6N0l/rvV6LM/G1pgN5ARg=="], "@tanstack/react-query": ["@tanstack/react-query@5.101.4", "", { "dependencies": { "@tanstack/query-core": "5.101.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA=="], "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.101.4", "", { "dependencies": { "@tanstack/query-devtools": "5.101.4" }, "peerDependencies": { "@tanstack/react-query": "^5.101.4", "react": "^18 || ^19" } }, "sha512-VeK2gtmfj7kvRBjtxS7TKxt/6qKhn8VzabY4UiYMr7NV9CddjSRYRgeYyld+NpjAkgMV9dd+2Qdr8ah5I03NeA=="], - "@tanstack/react-router": ["@tanstack/react-router@1.170.18", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-wpbGYZEp/fmz1q4bn7BD8VZ+/VZ7GBqSJv5V969pU+chP8y7dquWDmKTFMohvUegb9lg12m1uPVvD6kB2wORvQ=="], + "@tanstack/react-router": ["@tanstack/react-router@1.170.27", "", { "dependencies": { "@tanstack/history": "1.162.1", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.22", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Hxl49xzd8ffWd2ZMigqfXZmpySpixWGvjq5zfh2nK2DbzzDH6IGVh+iUuwarK9MJNcnhWPZ85tOoy6o/OmNlww=="], - "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.167.0", "", { "dependencies": { "@tanstack/router-devtools-core": "1.168.0" }, "peerDependencies": { "@tanstack/react-router": "^1.170.0", "@tanstack/router-core": "^1.170.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-nGw095EG7IHx0h5NtlEmzf6vcCTaFNPWdTSuDKazajhN0ct/v/TkekJ9J6KYUCeV1a8/2ZmToc58M+0rrOyn7w=="], + "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.167.1", "", { "dependencies": { "@tanstack/router-devtools-core": "1.168.1" }, "peerDependencies": { "@tanstack/react-router": "^1.170.19", "@tanstack/router-core": "^1.171.16", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-pjfGrmjj4d7naEPM7oshqFfwBoxDPNo/UxltlHH5ePbHsJ+plBhd+JaAewm1ueYOjZ0js9hckjWWDYXpCrSfKw=="], "@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.167.1", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.169.1" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W9j5JPnBikyafvuUfykFfHIWod58OAbAAa5leNkXBcoDoocghMmu6w9uZOmUZvAWT7CSvgj5tBUtF7CM2OoHXQ=="], - "@tanstack/react-start": ["@tanstack/react-start@1.168.34", "", { "dependencies": { "@tanstack/react-router": "1.170.18", "@tanstack/react-start-client": "1.168.16", "@tanstack/react-start-rsc": "0.1.33", "@tanstack/react-start-server": "1.167.22", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.14", "@tanstack/start-plugin-core": "1.171.25", "@tanstack/start-server-core": "1.169.17", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "@vitejs/plugin-rsc": "*", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "@vitejs/plugin-rsc", "vite"] }, "sha512-W5MDbD4QlDZHtEXlqN6bJUz7SdsPv6tbNPCih1i72FZqgQlhaxN1BoeoB8H+nN8M4tXRkfJD7VW75FCdQyaQDw=="], + "@tanstack/react-start": ["@tanstack/react-start@1.168.44", "", { "dependencies": { "@tanstack/react-router": "1.170.27", "@tanstack/react-start-client": "1.168.25", "@tanstack/react-start-rsc": "0.1.43", "@tanstack/react-start-server": "1.167.32", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.22", "@tanstack/start-plugin-core": "1.171.34", "@tanstack/start-server-core": "1.169.26", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "@vitejs/plugin-rsc": "*", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "@vitejs/plugin-rsc", "vite"] }, "sha512-varBaGENYuX0qdRMy8xZvr3BpHCHfkOeBaFCQkXUaOzK3NmWdpqj6CSbPq5uMCHkvFTDXNN6PZukp5zRa5Wmcg=="], - "@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.16", "", { "dependencies": { "@tanstack/react-router": "1.170.18", "@tanstack/router-core": "1.171.15", "@tanstack/start-client-core": "1.170.14" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-1OfHgy0wpHwe2tlB3FxMeA+IMX6Il/QAMf+8UdXuimReIc2Lz3BkMLBL38k4GIxBguX9sI8EMLO5jlTZ4e1olw=="], + "@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.25", "", { "dependencies": { "@tanstack/react-router": "1.170.27", "@tanstack/router-core": "1.171.22", "@tanstack/start-client-core": "1.170.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-vwjhuXUXFEAS8btmmXa24J/cP12AuAqIn/9OdZnrVufBE489IaxUEE3KuqEyUyZ+dtwPxChgVxFPHLcG9zonwA=="], - "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.33", "", { "dependencies": { "@tanstack/react-router": "1.170.18", "@tanstack/router-core": "1.171.15", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.14", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.25", "@tanstack/start-server-core": "1.169.17", "@tanstack/start-storage-context": "1.167.17", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.30", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-G4e1xwi/InoQmIGgNQSozcWASw68/o3NpbTz+exosdMGRZzLBeUDbj0swEAajxHm6jDEOQ/reSeIcDkcEfhMfw=="], + "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.43", "", { "dependencies": { "@tanstack/react-router": "1.170.27", "@tanstack/router-core": "1.171.22", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.22", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.34", "@tanstack/start-storage-context": "1.167.24", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.30", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-5h5qytaTlhakTWX0oMz7pVxS/4FuCJeg5a/jKkbLsEU2hWy/R098zQG9tftkWbzPhq+B3pfrGk32ufUzsiwK5A=="], - "@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.22", "", { "dependencies": { "@tanstack/react-router": "1.170.18", "@tanstack/router-core": "1.171.15", "@tanstack/start-server-core": "1.169.17" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-eH2PeHuLfL3R5YzE9+y2FfcE4Ld1LNV2ZfrCNVPJMMJFt+9nXDaRHg9BsEmc+JkTAGzz3FKLyQEoWwpbG6Ehqg=="], + "@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.32", "", { "dependencies": { "@tanstack/react-router": "1.170.27", "@tanstack/router-core": "1.171.22", "@tanstack/start-server-core": "1.169.26" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-HZV8EE9f2XU+KnyJcB20SCZgAm6G3uGPUwihaqgAz+MPnuJ/xG1iNy1oyQRz/SeqI3PVnPKzWK3Dkv+7xiO7cA=="], "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], @@ -445,27 +449,27 @@ "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.9", "", { "dependencies": { "@tanstack/virtual-core": "3.17.7" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ=="], - "@tanstack/router-core": ["@tanstack/router-core@1.171.15", "", { "dependencies": { "@tanstack/history": "1.162.0", "cookie-es": "^3.0.0", "seroval": "^1.5.4", "seroval-plugins": "^1.5.4" } }, "sha512-IILCDcLaItMZQ2jEmCABHY1Nhjjn5XUvwpQp3e4Nmu+vfg0BgYFuu/QASz2SwE2ZNbVMrvt8X/wxa+Gg5aErxA=="], + "@tanstack/router-core": ["@tanstack/router-core@1.171.22", "", { "dependencies": { "@tanstack/history": "1.162.1", "cookie-es": "^3.0.0", "seroval": "^1.6.2", "seroval-plugins": "^1.6.2" } }, "sha512-sitsuRkz4qpTjIAV97S5zFCoeGv0OFd6+VWg3ZcIlQbi5R4NIXfz6ogg51n5w6rvTwSODz/lKWb5dl5tvh2bQw=="], - "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.168.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.170.0", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-wQoQhlBK7nlZgqzaqdYXKWNTpdHdsaREdaPhFZVH0/Ador+F+eM3/NF2i3f2LPeS0GgKraZUQXe1Q/1+KHyEYg=="], + "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.168.1", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.171.16", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-qr4voa4cpSMwQvS3867xkU3AB3MtJbTuovKIy+btjJ/Faju6er9w0nDylmD+005Mk/3YKw9/iueZJl2JAB7JOA=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.167.21", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.15", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-m3oXZyienj8owialdyoZ0txHQrnEx/Ra+D9kWtar5fC2cWZr5Pvxl86VY2mX5RRLC5QLKLeRGT1x4HV95wHVDQ=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.167.28", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.22", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-AxvzdqQoBxrA8hoO1fHYg4cAUVug/xLqQhsGbNG1PelU3RbYRYEtDim7+HbYQCOqJaEuvV8tCvrTPTnT69iIwg=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.23", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.15", "@tanstack/router-generator": "1.167.21", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.18", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-0+PIcvnaAimFwjoEIeV3h7LKjzC8zNnp7pH2UamdKwQ9QlY99WU9V0Xl0zbM0i9hrUa/mKgWPDAzELmPUu5fMA=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.30", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.22", "@tanstack/router-generator": "1.167.28", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.26", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-Z53FeZjSddyn3c+lmUGHOU3bhZPpaFabcynja8/s87CZeyMYS7oNgUMoaYZAhVLk9cAEC/z3fkqISXqktZadDA=="], "@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.169.1", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-rngux8s/3mPQzcjLYDLkNU31coYVyCgrVTfpdwqUdY5jIEHqGTXrO73DTkPR1PppwYUeVhmNCgl8TctRcnupjg=="], "@tanstack/router-utils": ["@tanstack/router-utils@1.162.2", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ=="], - "@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.14", "", { "dependencies": { "@tanstack/router-core": "1.171.15", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.17", "seroval": "^1.5.4" } }, "sha512-yasBgEIFSWysL4EiFIGwp638nCoXXKiTqkc48EP2oty4OyNsZPTC1yfJ82zjq2KGkTAYtIaeMl7otqqRl1n85Q=="], + "@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.22", "", { "dependencies": { "@tanstack/router-core": "1.171.22", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.24", "seroval": "^1.6.2" } }, "sha512-18gHvhrVLEmhO0qZVIJaR/y3Mb7tvC9UQnCEiooQG/aIpYg4/kqoQ06E7WUJ/0WFy4NZOkEjk8QkDQNkFED+1g=="], "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.162.0", "", {}, "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ=="], - "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.25", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.15", "@tanstack/router-generator": "1.167.21", "@tanstack/router-plugin": "1.168.23", "@tanstack/router-utils": "1.162.2", "@tanstack/start-server-core": "1.169.17", "exsolve": "^1.0.7", "lightningcss": "^1.32.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.4", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-YmMye36vohfxau/MaVpltjkpJlf+wfUBoZp3S6Ue53mpAnsHr6El3XQDmcp1wS4kicZmyX1SNcobJpVZc+2dOQ=="], + "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.34", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.22", "@tanstack/router-generator": "1.167.28", "@tanstack/router-plugin": "1.168.30", "@tanstack/router-utils": "1.162.2", "@tanstack/start-server-core": "1.169.26", "exsolve": "^1.0.7", "lightningcss": "^1.32.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.6.2", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-MMp5Mlt8HAtstkVKwHW6dY5w58CP/Vtq+DhXdH4mIPuOeEhFtl/BYErJCzpnq80QqbEpRxCfMZPf9ng2PzApaA=="], - "@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.17", "", { "dependencies": { "@tanstack/history": "1.162.0", "@tanstack/router-core": "1.171.15", "@tanstack/start-client-core": "1.170.14", "@tanstack/start-storage-context": "1.167.17", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.4" } }, "sha512-u0N+PHJhMHnzfnlXYI9F+A/qweDe3E2X0mfkORPGIEkNQgvS548RA9fjwvixR2en5b848CfpEqUzwFhm/tQ40Q=="], + "@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.26", "", { "dependencies": { "@tanstack/history": "1.162.1", "@tanstack/router-core": "1.171.22", "@tanstack/start-client-core": "1.170.22", "@tanstack/start-storage-context": "1.167.24", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.6.2" } }, "sha512-57Pvc00i/Y6l8+kqlV/QElVUh5onFUPeMUBDDRNwelAA4eoOmRDmfEWfVfkJNYlcAF4kt/s0mgnD58DlCgC9JA=="], - "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.17", "", { "dependencies": { "@tanstack/router-core": "1.171.15" } }, "sha512-ntkDyGx0PE0opIlWNAMpkMb8qkjR4uyCUOfC0CiT0STM25+EcwPuwYNfDXXeVObMrTAPgsQ4yOj3xdY0Xr4ptw=="], + "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.24", "", { "dependencies": { "@tanstack/router-core": "1.171.22" } }, "sha512-HPltgydit1LmLJFX3BhMcXMxkmUgtWJopPUx6Eg0YTC4tVwFvUhSWCrfgtTIiGWcHd1Ah+fi6fK9BAe8Y5KZtw=="], "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], @@ -491,7 +495,7 @@ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@types/node": ["@types/node@26.1.2", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg=="], + "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], @@ -711,7 +715,7 @@ "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "lucide-react": ["lucide-react@1.28.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg=="], + "lucide-react": ["lucide-react@1.31.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -781,9 +785,9 @@ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], + "seroval": ["seroval@1.6.2", "", {}, "sha512-mPT+SD2TrlB6wvte1KkYOYUkubaTbd6pZ/6Kk3C9nxzrHmCZyhxOO7XGAeL7f+yLKZglzGtM9odUVvg/EhO+vQ=="], - "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + "seroval-plugins": ["seroval-plugins@1.6.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-TfxuUjlbBESzUOWdTkTKqvSmav0ABym+itetDXLK6mDz8SmrpdI30aF8RTXE8Bvq+tH/1yIDkvy3W0lfQb1ipQ=="], "shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="], @@ -851,7 +855,7 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vite": ["vite@8.2.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.23", "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ=="], + "vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], @@ -913,6 +917,10 @@ "oxc-parser/@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="], + "solid-js/seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], + + "solid-js/seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "unplugin/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], diff --git a/apps/ui/components.json b/components.json similarity index 100% rename from apps/ui/components.json rename to components.json diff --git a/package.json b/package.json index ac01210..dd863be 100644 --- a/package.json +++ b/package.json @@ -14,37 +14,37 @@ "@tailwindcss/vite": "4.3.3", "@tanstack/devtools-vite": "0.8.3", "@tanstack/match-sorter-utils": "8.19.4", - "@tanstack/react-devtools": "0.10.9", + "@tanstack/react-devtools": "0.10.10", "@tanstack/react-query": "5.101.4", "@tanstack/react-query-devtools": "5.101.4", - "@tanstack/react-router": "1.170.18", - "@tanstack/react-router-devtools": "1.167.0", + "@tanstack/react-router": "1.170.27", + "@tanstack/react-router-devtools": "1.167.1", "@tanstack/react-router-ssr-query": "1.167.1", - "@tanstack/react-start": "1.168.34", + "@tanstack/react-start": "1.168.44", "@tanstack/react-table": "8.21.3", "@tanstack/react-virtual": "3.14.9", - "@tanstack/router-plugin": "1.168.23", + "@tanstack/router-plugin": "1.168.30", "@vitejs/plugin-react": "6.0.5", "class-variance-authority": "0.7.1", "clsx": "2.1.1", "fflate": "0.8.3", - "lucide-react": "1.28.0", + "lucide-react": "1.31.0", "radix-ui": "1.6.7", "react": "19.2.8", "react-dom": "19.2.8", "tailwind-merge": "3.6.0", "tailwindcss": "4.3.3", "tw-animate-css": "1.4.0", - "vite": "8.2.0", + "vite": "8.2.1", "zod": "4.4.3" }, "description": "Browse and expose local Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, MiniMax Code, and OpenCode conversation history through a local UI and stable data API.", "devDependencies": { - "@biomejs/biome": "2.5.6", + "@biomejs/biome": "2.5.8", "@testing-library/dom": "10.4.1", "@testing-library/react": "16.3.2", "@types/bun": "^1.3.14", - "@types/node": "^26.1.2", + "@types/node": "^26.2.0", "@types/react": "19.2.18", "@types/react-dom": "19.2.4", "@vitest/coverage-v8": "4.1.10", @@ -71,13 +71,13 @@ }, "files": [ "bin/spiracha.ts", - "apps/ui/components.json", - "apps/ui/public/**/*", - "apps/ui/src/**/*", - "!apps/ui/src/**/*.vitest.ts", - "!apps/ui/src/**/*.vitest.tsx", - "apps/ui/tsconfig.json", - "apps/ui/vite.config.ts", + "components.json", + "public/**/*", + "src/ui/**/*", + "!src/ui/**/*.vitest.ts", + "!src/ui/**/*.vitest.tsx", + "tsconfig.json", + "vite.config.ts", "src/client.ts", "src/lib/**/*.ts", "!src/lib/**/*.test.ts", @@ -87,7 +87,7 @@ ], "homepage": "https://github.com/ragaeeb/spiracha", "imports": { - "#/*": "./apps/ui/src/*" + "#/*": "./src/ui/*" }, "keywords": [ "codex", @@ -113,18 +113,18 @@ "url": "git+https://github.com/ragaeeb/spiracha.git" }, "scripts": { - "build": "bun run typecheck && bun --cwd apps/ui --bun vite build", + "build": "bun run typecheck && bun --bun vite build", "coverage": "bun run coverage:root && bun run coverage:ui", "coverage:root": "bun test --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run ./src/coverage-check.ts root", - "coverage:ui": "vitest run --coverage --config apps/ui/vitest.config.ts && bun run ./src/coverage-check.ts ui", + "coverage:ui": "vitest run --coverage --config vitest.config.ts && bun run ./src/coverage-check.ts ui", "format": "biome check . --write && biome lint . --write", "lint": "biome check .", - "start": "bun --cwd apps/ui --bun vite dev --host 127.0.0.1 --port 3000", - "test:ui": "vitest run --config apps/ui/vitest.config.ts", - "typecheck": "bun run typecheck:root && bun run typecheck:ui", - "typecheck:root": "bunx tsc --noEmit", - "typecheck:ui": "bunx tsc --noEmit -p apps/ui/tsconfig.json", - "ui:preview": "bun --cwd apps/ui --bun vite preview" + "prepublishOnly": "bun run build && bun run test:package", + "start": "bun --bun vite dev --host 127.0.0.1 --port 3000", + "test:package": "bun run ./src/package-smoke.ts", + "test:ui": "vitest run --config vitest.config.ts", + "typecheck": "bunx tsc --noEmit", + "ui:preview": "bun --bun vite preview" }, "type": "module", "version": "2.3.0" diff --git a/apps/ui/public/icon.svg b/public/icon.svg similarity index 100% rename from apps/ui/public/icon.svg rename to public/icon.svg diff --git a/apps/ui/public/manifest.json b/public/manifest.json similarity index 100% rename from apps/ui/public/manifest.json rename to public/manifest.json diff --git a/apps/ui/public/robots.txt b/public/robots.txt similarity index 100% rename from apps/ui/public/robots.txt rename to public/robots.txt diff --git a/src/coverage-check.test.ts b/src/coverage-check.test.ts index e702984..f4c2d11 100644 --- a/src/coverage-check.test.ts +++ b/src/coverage-check.test.ts @@ -6,13 +6,13 @@ describe('coverage check helpers', () => { const summary = summarizeLcovReport( 'ui', [ - 'SF:src/components/theme-toggle.tsx', + 'SF:src/ui/components/theme-toggle.tsx', 'FNF:2', 'FNH:2', 'LF:10', 'LH:9', 'end_of_record', - 'SF:src/components/ui/dialog.tsx', + 'SF:src/ui/components/ui/dialog.tsx', 'FNF:10', 'FNH:0', 'LF:20', @@ -24,7 +24,7 @@ describe('coverage check helpers', () => { expect(summary.lineCoverage).toBe(90); expect(summary.functionCoverage).toBe(100); expect(summary.fileSummaries).toHaveLength(1); - expect(summary.fileSummaries[0]?.filePath).toBe('src/components/theme-toggle.tsx'); + expect(summary.fileSummaries[0]?.filePath).toBe('src/ui/components/theme-toggle.tsx'); }); it('should split lcov records correctly for CRLF line endings and missing trailing newlines', () => { diff --git a/src/coverage-check.ts b/src/coverage-check.ts index 505c4ca..2785977 100644 --- a/src/coverage-check.ts +++ b/src/coverage-check.ts @@ -38,16 +38,16 @@ const COVERAGE_PROFILES: Record = { }, ui: { excludeSubstrings: [ - 'src/components/projects-table.tsx', - 'src/components/ui/', - 'src/integrations/', - 'src/lib/codex-queries.ts', - 'src/lib/codex-server.ts', - 'src/router.tsx', - 'src/routes/', - 'src/routeTree.gen.ts', + 'src/ui/components/projects-table.tsx', + 'src/ui/components/ui/', + 'src/ui/integrations/', + 'src/ui/lib/codex-queries.ts', + 'src/ui/lib/codex-server.ts', + 'src/ui/router.tsx', + 'src/ui/routes/', + 'src/ui/routeTree.gen.ts', ], - lcovPath: 'apps/ui/coverage/lcov.info', + lcovPath: 'coverage/ui/lcov.info', minimumLineCoverage: 90, }, }; diff --git a/src/lib/antigravity-db.test.ts b/src/lib/antigravity-db.test.ts index f090efd..f1d5bcf 100644 --- a/src/lib/antigravity-db.test.ts +++ b/src/lib/antigravity-db.test.ts @@ -492,6 +492,29 @@ describe('antigravity db discovery', () => { expect(conversation?.conversationBytes).toBe(3); }); + it('should ignore empty Antigravity databases recreated without the trajectory schema', async () => { + const root = await makeRoot(); + const orphanedId = '65656565-6565-4656-8656-656565656565'; + const retainedId = '66666666-6666-4666-8666-666666666666'; + const orphanedDatabase = new Database(path.join(root, 'conversations', `${orphanedId}.db`), { + create: true, + }); + orphanedDatabase.close(); + await Bun.write(path.join(root, 'conversations', `${retainedId}.pb`), new Uint8Array([1, 2, 3])); + + const conversations = await listAntigravityConversations([root]); + + expect(conversations.map((conversation) => conversation.conversationId)).toEqual([retainedId]); + }); + + it('should still surface malformed Antigravity trajectory databases', async () => { + const root = await makeRoot(); + const conversationId = '67676767-6767-4676-8676-676767676767'; + await Bun.write(path.join(root, 'conversations', `${conversationId}.db`), 'not a sqlite database'); + + await expect(listAntigravityConversations([root])).rejects.toThrow(/malformed|not a database/iu); + }); + it('should render markdown exports for Antigravity brain artifacts', async () => { const root = await makeRoot(); const conversationId = '55555555-5555-4555-8555-555555555555'; @@ -1028,7 +1051,9 @@ describe('antigravity db discovery', () => { const deletedDatabaseWalPath = `${deletedDatabasePath}-wal`; const deletedTranscriptPath = path.join(deletedLogsDir, 'overview.txt'); const deletedFullTranscriptPath = path.join(deletedLogsDir, 'transcript_full.jsonl'); + const deletedAnnotationPath = path.join(root, 'annotations', `${deletedId}.pbtxt`); await mkdir(deletedLogsDir, { recursive: true }); + await mkdir(path.dirname(deletedAnnotationPath), { recursive: true }); await Bun.write( path.join(root, 'agyhub_summaries_proto.pb'), encodeSummaryIndex([ @@ -1042,6 +1067,7 @@ describe('antigravity db discovery', () => { await Bun.write(deletedDatabaseWalPath, new Uint8Array([8])); await Bun.write(deletedTranscriptPath, '{}\n'); await Bun.write(deletedFullTranscriptPath, '{}\n'); + await Bun.write(deletedAnnotationPath, 'last_user_view_time: { seconds: 1700000000 }\n'); await Bun.write(path.join(deletedArtifactDir, 'artifact.md'), 'Generated artifact.\n'); await Bun.write(path.join(root, 'conversations', `${retainedId}.pb`), new Uint8Array([4, 5])); @@ -1051,6 +1077,7 @@ describe('antigravity db discovery', () => { expect(result.deletedPaths.sort()).toEqual( [ deletedArtifactDir, + deletedAnnotationPath, deletedConversationPath, deletedDatabasePath, deletedDatabaseShmPath, @@ -1065,12 +1092,64 @@ describe('antigravity db discovery', () => { expect(await Bun.file(deletedDatabaseWalPath).exists()).toBe(false); expect(await Bun.file(deletedTranscriptPath).exists()).toBe(false); expect(await Bun.file(deletedFullTranscriptPath).exists()).toBe(false); + expect(await Bun.file(deletedAnnotationPath).exists()).toBe(false); expect(await Bun.file(path.join(deletedArtifactDir, 'artifact.md')).exists()).toBe(false); const conversations = await listAntigravityConversations([root]); expect(conversations.map((conversation) => conversation.conversationId)).toEqual([retainedId]); }); + it('should report incomplete deletion when a recreated conversation database remains', async () => { + const root = await makeRoot(); + const conversationId = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc'; + const databasePath = path.join(root, 'conversations', `${conversationId}.db`); + await Bun.write(databasePath, new Uint8Array([1, 2, 3])); + let recreated = false; + const recreateAfterUnlink = (async () => { + for (let attempt = 0; attempt < 100; attempt += 1) { + if (!(await Bun.file(databasePath).exists())) { + await Bun.write(databasePath, new Uint8Array([4, 5, 6])); + recreated = true; + return; + } + await Bun.sleep(1); + } + })(); + + const result = await deleteAntigravityConversation([root], conversationId); + await recreateAfterUnlink; + + expect(recreated).toBe(true); + expect(result.deletedConversationIds).toEqual([]); + expect(await Bun.file(databasePath).exists()).toBe(true); + expect(result.deletedPaths).toContain(databasePath); + }); + + it('should report incomplete deletion when Antigravity recreates the database after cleanup', async () => { + const root = await makeRoot(); + const conversationId = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd'; + const databasePath = path.join(root, 'conversations', `${conversationId}.db`); + await Bun.write(databasePath, new Uint8Array([1, 2, 3])); + let recreationAttempts = 0; + const recreateAfterUnlink = (async () => { + for (let attempt = 0; attempt < 4; attempt += 1) { + if (!(await Bun.file(databasePath).exists())) { + await Bun.write(databasePath, new Uint8Array([4, 5, 6])); + recreationAttempts += 1; + } + await Bun.sleep(1); + } + })(); + + const result = await deleteAntigravityConversation([root], conversationId); + await recreateAfterUnlink; + + expect(recreationAttempts).toBeGreaterThan(0); + expect(result.deletedConversationIds).toEqual([]); + expect(await Bun.file(databasePath).exists()).toBe(true); + expect(result.deletedPaths).toContain(databasePath); + }); + it('should replace a read-only Antigravity summary index atomically', async () => { const root = await makeRoot(); const deletedId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'; diff --git a/src/lib/antigravity-db.ts b/src/lib/antigravity-db.ts index 90a8356..08e2add 100644 --- a/src/lib/antigravity-db.ts +++ b/src/lib/antigravity-db.ts @@ -116,6 +116,36 @@ const UNKNOWN_WORKSPACE: WorkspaceInfo = { workspaceUri: null, }; +const ANTIGRAVITY_SQLITE_CLEANUP_ATTEMPT_TIMEOUT_MS = 2_500; +const ANTIGRAVITY_SQLITE_CLEANUP_LOCK_POLL_INTERVAL_MS = 25; +const ANTIGRAVITY_SQLITE_CLEANUP_RECREATION_WATCH_MS = 120; +type AntigravityDatabase = { + close: () => void; + exec: (query: string) => void; +}; + +type AntigravityDatabaseConstructor = new (databasePath: string) => AntigravityDatabase; + +let antigravityDatabaseConstructorPromise: Promise | undefined; + +const getAntigravityDatabaseConstructor = async () => { + if (!antigravityDatabaseConstructorPromise) { + antigravityDatabaseConstructorPromise = (async () => { + try { + const importSqlite = new Function('moduleName', 'return import(moduleName)') as ( + specifier: string, + ) => Promise<{ Database: AntigravityDatabaseConstructor }>; + const sqlite = await importSqlite('bun:sqlite'); + return sqlite.Database; + } catch { + return null; + } + })(); + } + + return antigravityDatabaseConstructorPromise; +}; + const decoder = new TextDecoder(); const pathExists = async (target: string): Promise => { @@ -132,6 +162,9 @@ const isFileMissingError = (error: unknown): boolean => { return code === 'ENOENT' || code === 'ENOTDIR'; }; +const isMissingTrajectorySchemaError = (error: unknown): boolean => + error instanceof Error && /^no such table: (?:main\.)?steps$/iu.test(error.message); + const readVarint = (buffer: Uint8Array, start: number, end: number): { next: number; value: number } => { let value = 0; let multiplier = 1; @@ -555,7 +588,7 @@ const readConversationFileCandidate = async ( }, }; } catch (error) { - if (isFileMissingError(error)) { + if (isFileMissingError(error) || isMissingTrajectorySchemaError(error)) { return null; } @@ -1165,6 +1198,7 @@ const existingAntigravityDeletePaths = async (root: string, conversationId: stri const conversationDir = getAntigravityConversationDir(root); const protobufPath = path.join(conversationDir, `${conversationId}.pb`); const databasePath = path.join(conversationDir, `${conversationId}.db`); + const annotationPath = path.join(root, 'annotations', `${conversationId}.pbtxt`); const artifactDir = path.join(getAntigravityBrainDir(root), conversationId); const logsDir = path.join(artifactDir, '.system_generated', 'logs'); const candidates = [ @@ -1172,6 +1206,7 @@ const existingAntigravityDeletePaths = async (root: string, conversationId: stri databasePath, `${databasePath}-shm`, `${databasePath}-wal`, + annotationPath, path.join(logsDir, 'overview.txt'), path.join(logsDir, 'transcript.jsonl'), path.join(logsDir, 'transcript_full.jsonl'), @@ -1181,16 +1216,93 @@ const existingAntigravityDeletePaths = async (root: string, conversationId: stri return candidates.filter((_, index) => exists[index]); }; -export const deleteAntigravityConversation = async ( - roots: string[], - conversationId: string, -): Promise => { - if (!isSafeConversationId(conversationId)) { - return { deletedConversationIds: [], deletedPaths: [] }; +const removeAntigravityConversationPaths = async (root: string, conversationId: string): Promise => { + const conversationDir = getAntigravityConversationDir(root); + await Promise.all([ + rm(path.join(root, 'annotations', `${conversationId}.pbtxt`), { force: true }), + rm(path.join(conversationDir, `${conversationId}.pb`), { force: true }), + rm(path.join(conversationDir, `${conversationId}.db`), { force: true }), + rm(path.join(conversationDir, `${conversationId}.db-shm`), { force: true }), + rm(path.join(conversationDir, `${conversationId}.db-wal`), { force: true }), + ]); + await rm(path.join(getAntigravityBrainDir(root), conversationId), { force: true, recursive: true }); +}; + +const getAntigravityConversationDatabasePath = (root: string, conversationId: string) => + path.join(getAntigravityConversationDir(root), `${conversationId}.db`); + +const tryAcquireAntigravityConversationDbOwner = async (databasePath: string): Promise => { + const Database = await getAntigravityDatabaseConstructor(); + if (!Database) { + return true; + } + + let db: AntigravityDatabase | undefined; + try { + db = new Database(databasePath); + db.exec('PRAGMA busy_timeout = 250'); + db.exec('BEGIN IMMEDIATE'); + db.exec('COMMIT'); + return true; + } catch (error) { + const message = String(error).toLowerCase(); + return !(message.includes('database is locked') || message.includes('database is busy')); + } finally { + db?.close(); } +}; + +const waitForAntigravityConversationDbOwnership = async ( + databasePath: string, + options: { + requireMissingStability?: boolean; + maxWaitMs?: number; + } = {}, +) => { + const { requireMissingStability = false, maxWaitMs = ANTIGRAVITY_SQLITE_CLEANUP_ATTEMPT_TIMEOUT_MS } = options; + const deadline = Date.now() + maxWaitMs; + + while (Date.now() < deadline) { + if (!(await pathExists(databasePath))) { + if (!requireMissingStability) { + return true; + } + await Bun.sleep(ANTIGRAVITY_SQLITE_CLEANUP_LOCK_POLL_INTERVAL_MS); + continue; + } + + if (requireMissingStability) { + return false; + } + + if (await tryAcquireAntigravityConversationDbOwner(databasePath)) { + return true; + } + + await Bun.sleep(ANTIGRAVITY_SQLITE_CLEANUP_LOCK_POLL_INTERVAL_MS); + } + + return requireMissingStability; +}; +const listExistingAntigravityDeletePaths = async (roots: string[], conversationId: string): Promise => { + const grouped = await Promise.all(roots.map((root) => existingAntigravityDeletePaths(root, conversationId))); + return grouped.flat(); +}; + +type AntigravityConversationRemoval = { + conversationRootsWithDatabasePath: Set; + deletedPaths: string[]; + deletedSummary: boolean; +}; + +const removeAntigravityConversationFromRoots = async ( + roots: string[], + conversationId: string, +): Promise => { const deletedPaths: string[] = []; let deletedSummary = false; + const conversationRootsWithDatabasePath = new Set(); for (const root of roots) { deletedSummary = @@ -1198,16 +1310,92 @@ export const deleteAntigravityConversation = async ( deletedSummary; const rootPaths = await existingAntigravityDeletePaths(root, conversationId); + const databasePath = getAntigravityConversationDatabasePath(root, conversationId); + if (rootPaths.includes(databasePath)) { + conversationRootsWithDatabasePath.add(root); + } deletedPaths.push(...rootPaths); + await removeAntigravityConversationPaths(root, conversationId); + } - const conversationDir = getAntigravityConversationDir(root); - await Promise.all([ - rm(path.join(conversationDir, `${conversationId}.pb`), { force: true }), - rm(path.join(conversationDir, `${conversationId}.db`), { force: true }), - rm(path.join(conversationDir, `${conversationId}.db-shm`), { force: true }), - rm(path.join(conversationDir, `${conversationId}.db-wal`), { force: true }), - ]); - await rm(path.join(getAntigravityBrainDir(root), conversationId), { force: true, recursive: true }); + return { + conversationRootsWithDatabasePath, + deletedPaths, + deletedSummary, + }; +}; + +const waitForAntigravityConversationCleanup = async ( + roots: string[], + conversationId: string, + conversationRootsWithDatabasePath: Set, +): Promise => { + let hadOwnershipTimeout = false; + + for (const root of roots) { + const databasePath = getAntigravityConversationDatabasePath(root, conversationId); + if (await pathExists(databasePath)) { + hadOwnershipTimeout = + hadOwnershipTimeout || !(await waitForAntigravityConversationDbOwnership(databasePath)); + } + } + + for (const root of roots) { + await removeConversationFromSummaryIndex(getAntigravitySummaryIndexPath(root), conversationId); + await removeAntigravityConversationPaths(root, conversationId); + } + + for (const root of roots) { + const databasePath = getAntigravityConversationDatabasePath(root, conversationId); + if (await pathExists(databasePath)) { + hadOwnershipTimeout = + hadOwnershipTimeout || + !(await waitForAntigravityConversationDbOwnership(databasePath, { + maxWaitMs: ANTIGRAVITY_SQLITE_CLEANUP_RECREATION_WATCH_MS, + })); + } + } + + for (const root of conversationRootsWithDatabasePath) { + const databasePath = getAntigravityConversationDatabasePath(root, conversationId); + if (!(await pathExists(databasePath))) { + hadOwnershipTimeout = + hadOwnershipTimeout || + !(await waitForAntigravityConversationDbOwnership(databasePath, { + maxWaitMs: ANTIGRAVITY_SQLITE_CLEANUP_RECREATION_WATCH_MS, + requireMissingStability: true, + })); + } + } + + return hadOwnershipTimeout; +}; + +export const deleteAntigravityConversation = async ( + roots: string[], + conversationId: string, +): Promise => { + if (!isSafeConversationId(conversationId)) { + return { deletedConversationIds: [], deletedPaths: [] }; + } + + const removal = await removeAntigravityConversationFromRoots(roots, conversationId); + const { conversationRootsWithDatabasePath, deletedPaths, deletedSummary } = removal; + + if (deletedSummary || deletedPaths.length > 0) { + const hadOwnershipTimeout = await waitForAntigravityConversationCleanup( + roots, + conversationId, + conversationRootsWithDatabasePath, + ); + + const remainingPaths = await listExistingAntigravityDeletePaths(roots, conversationId); + if (hadOwnershipTimeout || remainingPaths.length > 0) { + return { + deletedConversationIds: [], + deletedPaths: [...new Set([...deletedPaths, ...remainingPaths])], + }; + } } return { diff --git a/src/lib/claude-code-db.test.ts b/src/lib/claude-code-db.test.ts index 781801f..39cd18b 100644 --- a/src/lib/claude-code-db.test.ts +++ b/src/lib/claude-code-db.test.ts @@ -35,6 +35,20 @@ const writeSession = async (projectsDir: string, projectDirName: string, session await writeJsonl(path.join(projectDir, `${sessionId}.jsonl`), records); }; +const writeSubagent = async ( + projectsDir: string, + projectDirName: string, + parentSessionId: string, + agentId: string, + records: unknown[], + metadata: Record, +) => { + const subagentsDir = path.join(projectsDir, projectDirName, parentSessionId, 'subagents'); + await mkdir(subagentsDir, { recursive: true }); + await writeJsonl(path.join(subagentsDir, `${agentId}.jsonl`), records); + await Bun.write(path.join(subagentsDir, `${agentId}.meta.json`), JSON.stringify(metadata)); +}; + const buildSessionRecords = (sessionId: string, cwd: string, firstTimestamp = '2026-06-01T10:00:00.000Z') => [ { cwd, @@ -283,6 +297,70 @@ describe('claude code workspace discovery', () => { expect(transcript?.rawEvents).toHaveLength(3); }); + it('should list Claude Code sub-agents beneath their parent with metadata titles and models', async () => { + const projectsDir = await makeTempRoot(); + const projectDirName = '-Users-rhaq-workspace-ushman-corpus'; + const parentSessionId = 'parent-session'; + const agentId = 'agent-a1d79cbf732582863'; + await writeSession( + projectsDir, + projectDirName, + parentSessionId, + buildSessionRecords(parentSessionId, corpusCwd), + ); + await writeSubagent( + projectsDir, + projectDirName, + parentSessionId, + agentId, + [ + { + cwd: corpusCwd, + isSidechain: true, + message: { content: 'Implement the requested issues.', role: 'user' }, + sessionId: parentSessionId, + timestamp: '2026-06-01T10:01:00.000Z', + type: 'user', + uuid: 'subagent-user', + }, + { + cwd: corpusCwd, + isSidechain: true, + message: { content: 'The issues are implemented.', model: 'claude-opus-5', role: 'assistant' }, + parentUuid: 'subagent-user', + sessionId: parentSessionId, + timestamp: '2026-06-01T10:01:01.000Z', + type: 'assistant', + uuid: 'subagent-assistant', + }, + ], + { agentType: 'general-purpose', description: 'Implement fingerprint #100 and #101', model: 'opus' }, + ); + + const sessions = await listClaudeCodeSessionsForGroup( + 'project:-Users-rhaq-workspace-ushman-corpus', + projectsDir, + ); + const childTranscript = await readClaudeCodeSessionTranscript(projectsDir, agentId); + const child = sessions.find((session) => session.sessionId === agentId); + + expect(sessions).toHaveLength(2); + expect(child).toMatchObject({ + hierarchy: { parentSessionId }, + model: 'claude-opus-5', + sessionId: agentId, + title: 'Implement fingerprint #100 and #101', + }); + expect(childTranscript?.session).toMatchObject({ + model: 'claude-opus-5', + sessionId: agentId, + title: 'Implement fingerprint #100 and #101', + }); + expect(sessions.find((session) => session.sessionId === parentSessionId)).toMatchObject({ + hierarchy: { parentSessionId: null }, + }); + }); + it('should count Claude response fragments once and exclude synthetic user rows from message totals', async () => { const projectsDir = await makeTempRoot(); const sessionId = 'session-fragments'; diff --git a/src/lib/claude-code-db.ts b/src/lib/claude-code-db.ts index ec32e95..f27545b 100644 --- a/src/lib/claude-code-db.ts +++ b/src/lib/claude-code-db.ts @@ -39,6 +39,10 @@ const WORKSPACE_KEY_PREFIX = 'project:'; type TranscriptFile = { directoryName: string; filePath: string; + model: string | null; + parentSessionId: string | null; + sessionId: string; + title: string | null; }; type ParsedTranscriptFile = { @@ -80,6 +84,7 @@ type SessionIdentity = { cwd: string | null; firstUserText: string | null; gitBranch: string | null; + isSubagent: boolean; model: string | null; sessionId: string; title: string | null; @@ -480,7 +485,9 @@ const readTitleCandidate = (raw: Record): string | null => { }; const updateIdentityFromRaw = (identity: SessionIdentity, raw: Record) => { - identity.sessionId = asString(raw.sessionId ?? null) ?? identity.sessionId; + if (!identity.isSubagent) { + identity.sessionId = asString(raw.sessionId ?? null) ?? identity.sessionId; + } identity.cwd = identity.cwd ?? asString(raw.cwd ?? null); identity.version = asString(raw.version ?? null) ?? identity.version; identity.gitBranch = asString(raw.gitBranch ?? null) ?? identity.gitBranch; @@ -533,6 +540,7 @@ const toSessionSummary = ( cwd: identity.cwd, filePath: file.filePath, gitBranch: identity.gitBranch, + hierarchy: { parentSessionId: file.parentSessionId }, model: identity.model, sessionId: identity.sessionId, title: getTitle(identity), @@ -617,9 +625,10 @@ const buildTranscriptFromRawEvents = ( cwd: null, firstUserText: null, gitBranch: null, - model: null, - sessionId: path.basename(file.filePath, '.jsonl'), - title: null, + isSubagent: file.parentSessionId !== null, + model: file.model, + sessionId: file.sessionId, + title: file.title, version: null, }; const stats = createEmptyStats(); @@ -686,15 +695,57 @@ const readTranscriptFile = async ( }; }; +const listJsonlPathsRecursively = async (directory: string): Promise => { + const entries = await readDirectoryEntriesIfExists(directory); + const nestedPaths = await mapWithConcurrency( + entries.filter((entry) => entry.isDirectory()), + READ_CONCURRENCY, + (entry) => listJsonlPathsRecursively(path.join(directory, entry.name)), + ); + return [ + ...entries + .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl')) + .map((entry) => path.join(directory, entry.name)), + ...nestedPaths.flat(), + ]; +}; + +const getTranscriptFileIdentity = (projectDir: string, filePath: string) => { + const relativeParts = path.relative(projectDir, filePath).split(path.sep); + const subagentsIndex = relativeParts.lastIndexOf('subagents'); + return { + parentSessionId: subagentsIndex > 0 ? (relativeParts[subagentsIndex - 1] ?? null) : null, + sessionId: path.basename(filePath, '.jsonl'), + }; +}; + +const readSubagentMetadata = async (filePath: string): Promise<{ model: string | null; title: string | null }> => { + const metadataPath = filePath.replace(/\.jsonl$/u, '.meta.json'); + const value = await Bun.file(metadataPath) + .json() + .catch(() => null); + const metadata = asObject(value as JsonValue); + return { + model: asString(metadata?.model ?? null), + title: cleanLabel(asString(metadata?.description ?? null)), + }; +}; + const listTranscriptFilesForProject = async (projectsDir: string, directoryName: string): Promise => { const projectDir = path.join(projectsDir, directoryName); - const entries = await readDirectoryEntriesIfExists(projectDir); - return entries - .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl')) - .map((entry) => ({ + const filePaths = await listJsonlPathsRecursively(projectDir); + return mapWithConcurrency(filePaths, READ_CONCURRENCY, async (filePath) => { + const { parentSessionId, sessionId } = getTranscriptFileIdentity(projectDir, filePath); + const metadata = parentSessionId ? await readSubagentMetadata(filePath) : { model: null, title: null }; + return { directoryName, - filePath: path.join(projectDir, entry.name), - })); + filePath, + model: metadata.model, + parentSessionId, + sessionId, + title: metadata.title, + }; + }); }; const listProjectDirectoryNames = async (projectsDir: string): Promise => { @@ -954,7 +1005,14 @@ const coalesceTranscriptLineage = (lineage: ClaudeCodeSessionTranscript[]): Clau } const transcript = buildTranscriptFromRawEvents( - { directoryName, filePath: root.session.filePath }, + { + directoryName, + filePath: root.session.filePath, + model: root.session.model, + parentSessionId: root.session.hierarchy.parentSessionId, + sessionId: root.session.sessionId, + title: root.session.title, + }, buildLogicalRawEvents(lineage, canonical), canonical.session.lastActiveAtMs, true, diff --git a/src/lib/claude-code-exporter-types.ts b/src/lib/claude-code-exporter-types.ts index 2a9052d..6d8c968 100644 --- a/src/lib/claude-code-exporter-types.ts +++ b/src/lib/claude-code-exporter-types.ts @@ -41,6 +41,10 @@ export type ClaudeCodeWorkspaceGroup = { worktree: string; }; +export type ClaudeCodeSessionHierarchy = { + parentSessionId: string | null; +}; + export type ClaudeCodeSessionSummary = { assistantMessageCount: number; attachmentCount: number; @@ -51,6 +55,7 @@ export type ClaudeCodeSessionSummary = { cwd: string | null; filePath: string; gitBranch: string | null; + hierarchy: ClaudeCodeSessionHierarchy; inputTokens: number; lastActiveAtIso: string | null; lastActiveAtMs: number | null; diff --git a/src/lib/claude-code-transcript.test.ts b/src/lib/claude-code-transcript.test.ts index af8e55b..9579d1d 100644 --- a/src/lib/claude-code-transcript.test.ts +++ b/src/lib/claude-code-transcript.test.ts @@ -78,6 +78,7 @@ const transcript: ClaudeCodeSessionTranscript = { cwd: '/Users/example/workspace/ushman-corpus', filePath: '/Users/example/.claude/projects/-Users-example-workspace-ushman-corpus/session-a.jsonl', gitBranch: 'main', + hierarchy: { parentSessionId: null }, inputTokens: 10, lastActiveAtIso: '2026-06-01T10:00:05.000Z', lastActiveAtMs: 1_780_307_205_000, diff --git a/src/lib/codex-browser-db.test.ts b/src/lib/codex-browser-db.test.ts index 318eac8..82f28cc 100644 --- a/src/lib/codex-browser-db.test.ts +++ b/src/lib/codex-browser-db.test.ts @@ -445,6 +445,74 @@ describe('codex browser db', () => { }); }); + it('should omit internal guardian subagents from user-facing listings while keeping edge-linked children', async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-browser-db-orphan-subagent-test-')); + tempPaths.push(tempRoot); + const fixture = await createCodexBrowserFixture(tempRoot); + const parentThreadId = fixture.threads[0]!.threadId; + const attachedSubagentId = 'attached-subagent'; + const orphanSubagentId = 'orphan-guardian'; + const db = new Database(fixture.dbPath); + const insertThread = db.prepare(` + INSERT INTO threads ( + id, rollout_path, created_at, updated_at, source, model_provider, cwd, title, + sandbox_policy, approval_mode, thread_source + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + insertThread.run( + attachedSubagentId, + path.join(tempRoot, `${attachedSubagentId}.jsonl`), + 1779038000, + 1779038001, + JSON.stringify({ subagent: { thread_spawn: { parent_thread_id: parentThreadId } } }), + 'openai', + '/Users/example/workspace/spiracha', + 'Inspect transcript renderer', + '{}', + 'never', + 'subagent', + ); + insertThread.run( + orphanSubagentId, + path.join(tempRoot, `${orphanSubagentId}.jsonl`), + 1779038002, + 1779038003, + JSON.stringify({ subagent: { other: 'guardian' } }), + 'openai', + '/Users/example/workspace/spiracha', + 'Internal guardian review', + '{}', + 'never', + 'subagent', + ); + db.prepare(` + INSERT INTO thread_spawn_edges (parent_thread_id, child_thread_id, status) + VALUES (?, ?, ?) + `).run(parentThreadId, attachedSubagentId, 'completed'); + db.close(); + + const projectThreads = await listProjectThreads(fixture.dbPath, 'spiracha', { + includeTranscriptStats: false, + }); + const scopedThreads = listScopedThreads(fixture.dbPath, 'spiracha'); + const projects = await listCodexProjects(fixture.dbPath); + const dashboard = await getCodexDashboardSummary(fixture.dbPath); + + expect(projectThreads.map((entry) => entry.thread.id)).toEqual( + expect.arrayContaining([parentThreadId, attachedSubagentId]), + ); + expect(projectThreads.map((entry) => entry.thread.id)).not.toContain(orphanSubagentId); + expect(projectThreads.find((entry) => entry.thread.id === attachedSubagentId)?.hierarchy).toEqual({ + childThreadCount: 0, + parentThreadId, + }); + expect(scopedThreads.map((thread) => thread.id)).not.toContain(orphanSubagentId); + expect(projects.find((project) => project.name === 'spiracha')).toMatchObject({ threadCount: 3 }); + expect(dashboard.totalThreads).toBe(4); + expect(dashboard.recentThreads.map((entry) => entry.thread.id)).not.toContain(orphanSubagentId); + }); + it('should read rollout activity without blocking the synchronous request path', async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'codex-browser-db-async-activity-test-')); tempPaths.push(tempRoot); diff --git a/src/lib/codex-browser-db.ts b/src/lib/codex-browser-db.ts index 49e013e..c5e06dc 100644 --- a/src/lib/codex-browser-db.ts +++ b/src/lib/codex-browser-db.ts @@ -243,6 +243,16 @@ const parseJsonSafely = (value: string | null) => { } }; +const getUserVisibleThreadFilter = (db: Database) => { + const threadColumns = db.query('PRAGMA table_info(threads)').all() as Array<{ name: string }>; + if (!threadColumns.some((column) => column.name === 'thread_source')) { + return '1'; + } + + // Codex stores internal guardian evaluations as subagent rows without user-facing chat entries. + return "NOT (thread_source = 'subagent' AND source LIKE '%guardian%')"; +}; + export const withReadonlyDb = (dbPath: string, callback: (db: Database) => T): T => { return runWithSqliteRetry({ action: () => { @@ -306,10 +316,15 @@ export const resolveCodexThreadDbPath = () => { const readThreads = (dbPath: string, projectName: string | null = null): ThreadRow[] => { return withReadonlyDb(dbPath, (db) => { - const projectFilter = projectName ? `WHERE ${PROJECT_CWD_FILTER}` : ''; + const filters = [getUserVisibleThreadFilter(db)]; + if (projectName) { + filters.push(PROJECT_CWD_FILTER); + } + return db .query( - `SELECT ${THREAD_ROW_COLUMNS} FROM threads ${projectFilter} + `SELECT ${THREAD_ROW_COLUMNS} FROM threads + WHERE ${filters.join(' AND ')} ORDER BY COALESCE(updated_at_ms, updated_at * 1000) DESC, id DESC`, ) .all(...(projectName ? [projectName] : [])) as ThreadRow[]; @@ -1532,7 +1547,8 @@ const readProjectAggregateRows = (db: Database) => { COUNT(*) AS thread_count, COALESCE(SUM(tokens_used), 0) AS total_tokens FROM threads - WHERE typeof(cwd) = 'text' AND TRIM(cwd) <> '' + WHERE ${getUserVisibleThreadFilter(db)} + AND typeof(cwd) = 'text' AND TRIM(cwd) <> '' GROUP BY cwd, model `) .all() as ProjectAggregateRow[]; @@ -1705,6 +1721,7 @@ const readDashboardDatabaseData = (dbPath: string) => { COUNT(*) AS total_threads, COALESCE(SUM(tokens_used), 0) AS total_tokens FROM threads + WHERE ${getUserVisibleThreadFilter(db)} `) .get() as DashboardDatabaseTotals; const recentCandidates = db @@ -1721,7 +1738,8 @@ const readDashboardDatabaseData = (dbPath: string) => { updated_at, updated_at_ms FROM threads - WHERE typeof(cwd) = 'text' AND TRIM(cwd) <> '' + WHERE ${getUserVisibleThreadFilter(db)} + AND typeof(cwd) = 'text' AND TRIM(cwd) <> '' `) .all() as DashboardThreadCandidate[]; const existingTableNames = getExistingTableNames(db); diff --git a/src/lib/cursor-db.test.ts b/src/lib/cursor-db.test.ts index be7eeea..30655f8 100644 --- a/src/lib/cursor-db.test.ts +++ b/src/lib/cursor-db.test.ts @@ -113,6 +113,93 @@ describe('cursor-db workspace discovery', () => { expect(group?.threadCount).toBe(1); }); + it('should group modern Cursor orchestrators and subagents under their multi-root workspace', async () => { + const userDir = await makeUserDir(); + const workspaceFilePath = path.join(userDir, 'multi-root.code-workspace'); + await Bun.write( + workspaceFilePath, + JSON.stringify({ folders: [{ path: '/Users/test/workspace/app' }, { path: '/Users/test/workspace/e2e' }] }), + ); + await createCursorFixture(userDir, { + buckets: [ + { + bucketId: 'multi-root-bucket', + workspace: `file://${workspaceFilePath}`, + }, + ], + composerTableHeaders: [ + { bucketId: 'multi-root-bucket', composerId: 'orchestrator' }, + { + bucketId: 'multi-root-bucket', + composerId: 'subagent', + isSubagent: true, + parentComposerId: 'orchestrator', + }, + { + bucketId: 'multi-root-bucket', + composerId: 'aborted-draft', + isArchived: true, + }, + ], + threads: [ + { + bubbles: [{ bubbleId: 'root-user', text: 'You are the orchestrator and lead...', type: 1 }], + composerId: 'orchestrator', + model: 'claude-fable-5', + name: 'Vendor externalization gap', + reasoningEffort: 'low', + }, + { + bubbles: [ + { + bubbleId: 'child-user', + text: 'You are investigating a parked...', + toolCall: { + name: 'read_file', + rawArgs: '{"path":"/Users/test/workspace/e2e/src/test.ts"}', + }, + type: 1, + }, + ], + composerId: 'subagent', + model: 'cursor-grok-4.5-medium', + name: 'Investigate parked test', + }, + { + bubbles: [], + composerId: 'aborted-draft', + model: 'claude-fable-5', + name: 'Vendor externalization gap', + status: 'aborted', + }, + ], + }); + + const groups = await listCursorWorkspaceGroups(userDir); + const workspace = groups.find((group) => group.key === `workspace:${workspaceFilePath}`); + const threads = await listCursorThreadsForGroup(workspace!, userDir, { includeTranscriptDirs: false }); + + expect(workspace?.threadCount).toBe(2); + expect(threads.every((thread) => thread.workspaceLabel === 'multi-root.code-workspace')).toBe(true); + expect(threads).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + composerId: 'orchestrator', + model: 'claude-fable-5', + parentComposerId: null, + reasoningEffort: 'low', + }), + expect.objectContaining({ + composerId: 'subagent', + model: 'grok-4.5', + parentComposerId: 'orchestrator', + reasoningEffort: 'medium', + }), + ]), + ); + expect(groups.find((group) => group.key === 'folder:/Users/test/workspace/e2e')?.threadCount ?? 0).toBe(0); + }); + it('should match a workspace by folder basename query', async () => { const userDir = await makeUserDir(); await createCursorFixture(userDir, baseSpec()); diff --git a/src/lib/cursor-db.ts b/src/lib/cursor-db.ts index 1256afa..8fed257 100644 --- a/src/lib/cursor-db.ts +++ b/src/lib/cursor-db.ts @@ -29,6 +29,15 @@ type ComposerEntry = Record & { workspaceIdentifier?: { id?: string } | null; }; +type ComposerHeaderRow = { + composerId: string; + workspaceId: string | null; + createdAt: number | null; + lastUpdatedAt: number | null; + isSubagent: number | null; + value: string | null; +}; + export const CURSOR_READONLY_DB_OPEN_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI; // Cursor databases are WAL-mode. A plain read-only open fails once Cursor cleanly shuts down and @@ -165,12 +174,48 @@ const parseCodeWorkspaceFolders = async (workspaceFilePath: string): Promise { + let parsed: ComposerEntry = {}; + try { + parsed = (asObject(JSON.parse(row.value ?? '{}') as JsonValue) ?? {}) as ComposerEntry; + } catch { + parsed = {}; + } + + return { + ...parsed, + composerId: row.composerId, + createdAt: asNumber(parsed.createdAt ?? null) ?? row.createdAt, + isSubagent: row.isSubagent === 1, + lastUpdatedAt: asNumber(parsed.lastUpdatedAt ?? null) ?? row.lastUpdatedAt, + workspaceIdentifier: parsed.workspaceIdentifier ?? (row.workspaceId ? { id: row.workspaceId } : null), + }; +}; + +const readModernComposerHeaders = (db: Database): ComposerEntry[] => { + const modernTable = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'composerHeaders'").get(); + if (!modernTable) { + return []; + } + + const rows = db + .query('SELECT composerId, workspaceId, createdAt, lastUpdatedAt, isSubagent, value FROM composerHeaders') + .all() as ComposerHeaderRow[]; + return rows.map(parseComposerHeaderRow); +}; + export const loadGlobalComposerHeaders = (globalDbPath: string): ComposerEntry[] => { try { const db = openCursorReadonlyDb(globalDbPath); try { - const data = readItemValue<{ allComposers?: ComposerEntry[] }>(db, COMPOSER_HEADERS_KEY); - return data?.allComposers ?? []; + const legacy = readItemValue<{ allComposers?: ComposerEntry[] }>(db, COMPOSER_HEADERS_KEY); + const headersById = new Map(); + for (const header of [...(legacy?.allComposers ?? []), ...readModernComposerHeaders(db)]) { + if (header.composerId) { + headersById.set(header.composerId, header); + } + } + return [...headersById.values()]; } finally { db.close(); } @@ -520,8 +565,16 @@ type GlobalHead = { lastUpdatedAtMs: number | null; mode: string | null; pathHint: string | null; + model: string | null; + reasoningEffort: string | null; + status: string | null; +}; +type HeaderInfo = { + name: string | null; + uriPath: string | null; + bucketId: string | null; + parentComposerId: string | null; }; -type HeaderInfo = { name: string | null; uriPath: string | null; bucketId: string | null }; type BubbleStat = { count: number; bytes: number }; type CursorDiscovery = { @@ -718,8 +771,11 @@ const parseGlobalHead = (value: string | null): GlobalHead => { createdAtMs: null, lastUpdatedAtMs: null, mode: null, + model: null, name: null, pathHint: null, + reasoningEffort: null, + status: null, }; } @@ -730,8 +786,11 @@ const parseGlobalHead = (value: string | null): GlobalHead => { createdAtMs: null, lastUpdatedAtMs: null, mode: null, + model: null, name: null, pathHint: inferFolderFromBlob(value), + reasoningEffort: null, + status: null, }; } @@ -740,17 +799,35 @@ const parseGlobalHead = (value: string | null): GlobalHead => { createdAtMs: null, lastUpdatedAtMs: null, mode: null, + model: null, name: null, pathHint: inferFolderFromBlob(value), + reasoningEffort: null, + status: null, }; } + const modelConfig = asObject(parsed.modelConfig ?? null); + const selectedModels = Array.isArray(modelConfig?.selectedModels) ? modelConfig.selectedModels : []; + const selectedModel = selectedModels + .map((entry) => asObject(entry)) + .find((entry) => asString(entry?.modelId ?? null) === asString(modelConfig?.modelName ?? null)); + const parameters = Array.isArray(selectedModel?.parameters) ? selectedModel.parameters : []; + const reasoningEffort = parameters + .map((parameter) => asObject(parameter)) + .find((parameter) => asString(parameter?.id ?? null) === 'effort'); + const rawModel = asString(modelConfig?.modelName ?? null); + const encodedGrokModel = rawModel?.match(/^cursor-(grok-[\d.]+)-(low|medium|high)$/u); + return { createdAtMs: asNumber(parsed.createdAt ?? null), lastUpdatedAtMs: asNumber(parsed.lastUpdatedAt ?? null), mode: asString(parsed.unifiedMode ?? null), + model: encodedGrokModel?.[1] ?? rawModel, name: asString(parsed.name ?? null), pathHint: inferFolderFromBlob(value), + reasoningEffort: asString(reasoningEffort?.value ?? null) ?? encodedGrokModel?.[2] ?? null, + status: asString(parsed.status ?? null), }; }; @@ -791,9 +868,11 @@ const readHeaderInfo = (globalDbPath: string): Map => { | { id?: string; uri?: { path?: string; fsPath?: string } } | undefined; const uriPath = identifier?.uri?.path ?? identifier?.uri?.fsPath ?? null; + const subagentInfo = asObject(header.subagentInfo ?? null); info.set(header.composerId, { bucketId: identifier?.id ?? null, name: typeof header.name === 'string' ? header.name : null, + parentComposerId: asString(subagentInfo?.parentComposerId ?? null), uriPath: uriPath ? normalizeCursorPath(uriPath) : null, }); } @@ -825,6 +904,10 @@ type ResolvedThread = { groupKey: string; groupLabel: string; bucketId: string | null; + model: string | null; + reasoningEffort: string | null; + parentComposerId: string | null; + status: string | null; }; const findLinkedBucketId = ( @@ -904,8 +987,12 @@ const resolveThreadFolder = ( groupLabel: folder ? path.basename(folder) : 'Unknown project', lastUpdatedAtMs: head?.lastUpdatedAtMs ?? null, mode: head?.mode ?? null, + model: head?.model ?? null, name: head?.name || headerInfo?.name || '(untitled)', + parentComposerId: headerInfo?.parentComposerId ?? null, + reasoningEffort: head?.reasoningEffort ?? null, stat, + status: head?.status ?? null, }; }; @@ -917,7 +1004,10 @@ const toThreadSummary = (resolved: ResolvedThread): CursorThreadSummary => ({ createdAtMs: resolved.createdAtMs, lastUpdatedAtMs: resolved.lastUpdatedAtMs, mode: resolved.mode, + model: resolved.model, name: resolved.name, + parentComposerId: resolved.parentComposerId, + reasoningEffort: resolved.reasoningEffort, transcriptDirs: [], workspaceKey: resolved.groupKey, workspaceLabel: resolved.groupLabel, @@ -931,14 +1021,24 @@ const assembleDiscovery = ( const threadsByKey = new Map(); const lastActiveByKey = new Map(); + const groupLabels = new Map(bucketGroups.map((group) => [group.key, group.label])); for (const thread of resolved) { + if (thread.stat.count === 0 && thread.status === 'aborted') { + continue; + } + // Empty threads with no resolvable workspace are pure noise; keep them out of the catch-all. if (thread.groupKey === UNKNOWN_GROUP_KEY && thread.stat.count === 0) { continue; } const list = threadsByKey.get(thread.groupKey) ?? []; - list.push(toThreadSummary(thread)); + list.push( + toThreadSummary({ + ...thread, + groupLabel: groupLabels.get(thread.groupKey) ?? thread.groupLabel, + }), + ); threadsByKey.set(thread.groupKey, list); lastActiveByKey.set( thread.groupKey, diff --git a/src/lib/cursor-exporter-types.ts b/src/lib/cursor-exporter-types.ts index e1f6762..c34df74 100644 --- a/src/lib/cursor-exporter-types.ts +++ b/src/lib/cursor-exporter-types.ts @@ -107,6 +107,9 @@ export type CursorThreadSummary = { bubbleBytes: number; transcriptDirs: string[]; mode: string | null; + model: string | null; + reasoningEffort: string | null; + parentComposerId: string | null; }; export type CursorBubbleKind = 'user' | 'assistant' | 'unknown'; diff --git a/src/lib/cursor-recovery.test.ts b/src/lib/cursor-recovery.test.ts index 2c6dbd2..642be05 100644 --- a/src/lib/cursor-recovery.test.ts +++ b/src/lib/cursor-recovery.test.ts @@ -130,7 +130,10 @@ describe('pruneCursorThreads', () => { createdAtMs: null, lastUpdatedAtMs: null, mode: null, + model: null, name: 'Unsafe thread', + parentComposerId: null, + reasoningEffort: null, transcriptDirs: [transcriptDir], workspaceKey: '', workspaceLabel: '', diff --git a/src/lib/cursor-recovery.ts b/src/lib/cursor-recovery.ts index a8bfcc4..225d458 100644 --- a/src/lib/cursor-recovery.ts +++ b/src/lib/cursor-recovery.ts @@ -640,7 +640,10 @@ export const collectCursorThreadsForDeletion = async ( createdAtMs: null, lastUpdatedAtMs: null, mode: null, + model: null, name: '', + parentComposerId: null, + reasoningEffort: null, transcriptDirs: await findCursorTranscriptDirs(composerId, userDir), workspaceKey: '', workspaceLabel: '', diff --git a/src/lib/cursor-test-helpers.ts b/src/lib/cursor-test-helpers.ts index a746b14..09cca79 100644 --- a/src/lib/cursor-test-helpers.ts +++ b/src/lib/cursor-test-helpers.ts @@ -25,6 +25,9 @@ export type FixtureThread = { headRichText?: string; headText?: string; unifiedMode?: string; + model?: string; + reasoningEffort?: string; + status?: string; bubbles: FixtureBubble[]; omittedBubbleHeaders?: number; }; @@ -41,6 +44,13 @@ export type CursorFixtureSpec = { buckets: FixtureBucket[]; threads: FixtureThread[]; headerLinks?: Array<{ composerId: string; bucketId: string; uriPath?: string }>; + composerTableHeaders?: Array<{ + composerId: string; + bucketId: string; + isSubagent?: boolean; + isArchived?: boolean; + parentComposerId?: string; + }>; historyEntries?: Array<{ resource: string; timestamps?: number[] }>; }; @@ -89,8 +99,20 @@ const writeGlobalThread = (db: Database, thread: FixtureThread) => { createdAt: thread.createdAt ?? null, fullConversationHeadersOnly: orderedHeaders, lastUpdatedAt: thread.lastUpdatedAt ?? null, + modelConfig: thread.model + ? { + modelName: thread.model, + selectedModels: [ + { + modelId: thread.model, + parameters: thread.reasoningEffort ? [{ id: 'effort', value: thread.reasoningEffort }] : [], + }, + ], + } + : undefined, name: thread.name ?? null, richText: thread.headRichText ?? '', + status: thread.status, text: thread.headText ?? '', totalBubbleHeaderCount: orderedHeaders.length + omitted, unifiedMode: thread.unifiedMode ?? 'agent', @@ -132,6 +154,43 @@ const buildHeaderEntry = (thread: FixtureThread, bucketId: string, uriPath?: str workspaceIdentifier: buildWorkspaceIdentifier(bucketId, uriPath), }); +const writeModernHeaders = ( + db: Database, + links: NonNullable, + threadsById: Map, +) => { + db.exec( + 'CREATE TABLE composerHeaders (composerId TEXT PRIMARY KEY, workspaceId TEXT, createdAt INTEGER, lastUpdatedAt INTEGER, isArchived INTEGER, isSubagent INTEGER, recency INTEGER, checkpointAt INTEGER, value TEXT)', + ); + for (const link of links) { + const thread = threadsById.get(link.composerId); + if (!thread) { + continue; + } + const header = { + ...buildHeaderEntry(thread, link.bucketId), + subagentInfo: link.parentComposerId + ? { + parentComposerId: link.parentComposerId, + rootParentConversationId: link.parentComposerId, + } + : undefined, + }; + db.run( + 'INSERT INTO composerHeaders (composerId, workspaceId, createdAt, lastUpdatedAt, isArchived, isSubagent, recency, checkpointAt, value) VALUES (?, ?, ?, ?, ?, ?, 0, 0, ?)', + [ + link.composerId, + link.bucketId, + thread.createdAt ?? null, + thread.lastUpdatedAt ?? null, + link.isArchived ? 1 : 0, + link.isSubagent ? 1 : 0, + JSON.stringify(header), + ], + ); + } +}; + const writeGlobalDb = async (userDir: string, spec: CursorFixtureSpec) => { const globalDir = path.join(userDir, 'globalStorage'); await mkdir(globalDir, { recursive: true }); @@ -150,6 +209,10 @@ const writeGlobalDb = async (userDir: string, spec: CursorFixtureSpec) => { }) .filter((entry): entry is ReturnType => entry !== null); insertItem(db, 'composer.composerHeaders', { allComposers: headers }); + + if (spec.composerTableHeaders) { + writeModernHeaders(db, spec.composerTableHeaders, threadsById); + } } finally { db.close(); } diff --git a/src/package-manifest.test.ts b/src/package-manifest.test.ts index 10b390e..73dbceb 100644 --- a/src/package-manifest.test.ts +++ b/src/package-manifest.test.ts @@ -82,15 +82,14 @@ describe('package manifest', () => { it('should use one package manifest for root, UI, and direct client workflows', async () => { const manifest = await readPackageManifest(); - expect(await Bun.file(path.join(process.cwd(), 'apps/ui/package.json')).exists()).toBe(false); + expect(await Bun.file(path.join(process.cwd(), 'apps/ui')).exists()).toBe(false); expect(manifest.workspaces).toBeUndefined(); expect(manifest.imports).toEqual({ - '#/*': './apps/ui/src/*', + '#/*': './src/ui/*', }); - expect(manifest.files).not.toContain('apps/ui/package.json'); for (const command of Object.values(manifest.scripts ?? {})) { - expect(command).not.toContain('bun run --cwd apps/ui'); + expect(command).not.toContain('apps/ui'); } }); @@ -101,8 +100,17 @@ describe('package manifest', () => { expect(manifest.devDependencies?.[dependencyName], dependencyName).toBeDefined(); } - expect(manifest.scripts?.['test:ui']).toContain('apps/ui/vitest.config.ts'); - expect(manifest.scripts?.['typecheck:ui']).toContain('apps/ui/tsconfig.json'); + expect(manifest.scripts?.['test:ui']).toBe('vitest run --config vitest.config.ts'); + expect(manifest.scripts?.typecheck).toBe('bunx tsc --noEmit'); + expect(manifest.scripts?.['typecheck:root']).toBeUndefined(); + expect(manifest.scripts?.['typecheck:ui']).toBeUndefined(); + }); + + it('should require the packed bunx smoke test before publishing', async () => { + const manifest = await readPackageManifest(); + + expect(manifest.scripts?.['test:package']).toBe('bun run ./src/package-smoke.ts'); + expect(manifest.scripts?.prepublishOnly).toBe('bun run build && bun run test:package'); }); it('should document every supported source in contributor and UI metadata', async () => { @@ -117,7 +125,7 @@ describe('package manifest', () => { 'MiniMax Code', 'OpenCode', ]; - const documentedFiles = ['README.md', 'AGENTS.md', 'apps/ui/AGENTS.md', 'apps/ui/src/routes/__root.tsx']; + const documentedFiles = ['README.md', 'AGENTS.md', 'src/ui/routes/__root.tsx']; for (const filePath of documentedFiles) { const content = await Bun.file(path.join(process.cwd(), filePath)).text(); @@ -163,8 +171,9 @@ describe('package manifest', () => { expect(output).toMatch(/packed .*package\.json/u); expect(output).toContain('bin/spiracha.ts'); expect(output).toContain('src/client.ts'); - expect(output).toContain('apps/ui/vite.config.ts'); - expect(output).not.toContain('apps/ui/package.json'); + expect(output).toContain('vite.config.ts'); + expect(output).toContain('src/ui/routes/__root.tsx'); + expect(output).not.toContain('apps/ui'); }); it('should keep the package file list free of removed CLI files', async () => { @@ -173,12 +182,12 @@ describe('package manifest', () => { expect(manifest.files).toContain('src/lib/**/*.ts'); expect(manifest.files).toContain('src/client.ts'); expect(manifest.files).toContain('bin/spiracha.ts'); - expect(manifest.files).toContain('apps/ui/src/**/*'); - expect(manifest.files).toContain('apps/ui/public/**/*'); - expect(manifest.files).toContain('apps/ui/vite.config.ts'); - expect(manifest.files).not.toContain('apps/ui/package.json'); - expect(manifest.files).toContain('!apps/ui/src/**/*.vitest.ts'); - expect(manifest.files).toContain('!apps/ui/src/**/*.vitest.tsx'); + expect(manifest.files).toContain('src/ui/**/*'); + expect(manifest.files).toContain('public/**/*'); + expect(manifest.files).toContain('vite.config.ts'); + expect(manifest.files).toContain('tsconfig.json'); + expect(manifest.files).toContain('!src/ui/**/*.vitest.ts'); + expect(manifest.files).toContain('!src/ui/**/*.vitest.tsx'); expect(manifest.files).toContain('!src/lib/**/*.test.ts'); expect(manifest.files).toContain('!src/lib/*-test-helpers.ts'); expect(manifest.files).not.toContain('STABLE_DATA_API.md'); diff --git a/src/package-smoke.test.ts b/src/package-smoke.test.ts new file mode 100644 index 0000000..04c69cc --- /dev/null +++ b/src/package-smoke.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'bun:test'; +import path from 'node:path'; +import { getPackedTarballPath, isPackagedUiHealthyResponse } from './package-smoke'; + +describe('packaged UI smoke helpers', () => { + it('should derive the packed tarball path from package metadata', () => { + expect(getPackedTarballPath('/tmp/spiracha', 'spiracha', '2.3.0')).toBe( + path.join('/tmp/spiracha', 'spiracha-2.3.0.tgz'), + ); + }); + + it('should reject Bun fallback responses', () => { + expect( + isPackagedUiHealthyResponse({ + bodyText: 'Welcome to Bun! To get started, return a Response object.', + contentType: 'text/plain; charset=utf-8', + ok: true, + }), + ).toBe(false); + }); + + it('should accept the Spiracha SSR app shell', () => { + expect( + isPackagedUiHealthyResponse({ + bodyText: 'Spiracha', + contentType: 'text/html; charset=utf-8', + ok: true, + }), + ).toBe(true); + }); +}); diff --git a/src/package-smoke.ts b/src/package-smoke.ts new file mode 100644 index 0000000..a15abb2 --- /dev/null +++ b/src/package-smoke.ts @@ -0,0 +1,181 @@ +#!/usr/bin/env bun + +import { mkdtemp, rm } from 'node:fs/promises'; +import { createServer } from 'node:net'; +import os from 'node:os'; +import path from 'node:path'; + +type PackageManifest = { + name: string; + version: string; +}; + +type PackagedUiProbe = { + bodyText: string; + contentType: string | null; + ok: boolean; +}; + +const HOST = '127.0.0.1'; +const STARTUP_TIMEOUT_MS = 30_000; + +export const getPackedTarballPath = (directory: string, packageName: string, version: string) => + path.join(directory, `${packageName}-${version}.tgz`); + +export const isPackagedUiHealthyResponse = (probe: PackagedUiProbe) => + probe.ok && + probe.contentType?.toLowerCase().includes('text/html') === true && + /]/iu.test(probe.bodyText) && + probe.bodyText.includes('Spiracha') && + !probe.bodyText.includes('Welcome to Bun!'); + +const getAvailablePort = async () => { + const server = createServer(); + server.unref(); + + return new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, HOST, () => { + const address = server.address(); + if (!address || typeof address === 'string') { + server.close(() => reject(new Error('Failed to find a free port for the package smoke test.'))); + return; + } + + server.close((error) => { + if (error) { + reject(error); + return; + } + + resolve(address.port); + }); + }); + }); +}; + +const runCommand = async (argv: string[], cwd: string) => { + const proc = Bun.spawn(argv, { + cwd, + stderr: 'pipe', + stdout: 'pipe', + }); + const stdoutPromise = new Response(proc.stdout).text(); + const stderrPromise = new Response(proc.stderr).text(); + const timeoutId = setTimeout(() => proc.kill(), STARTUP_TIMEOUT_MS); + + try { + const [exitCode, stdoutText, stderrText] = await Promise.all([proc.exited, stdoutPromise, stderrPromise]); + + if (exitCode !== 0) { + throw new Error( + [ + `Command failed with exit code ${exitCode}: ${argv.join(' ')}`, + stdoutText.trim() ? `stdout:\n${stdoutText}` : '', + stderrText.trim() ? `stderr:\n${stderrText}` : '', + ] + .filter(Boolean) + .join('\n\n'), + ); + } + } finally { + clearTimeout(timeoutId); + } +}; + +const waitForServer = async (url: string): Promise => { + const deadline = Date.now() + STARTUP_TIMEOUT_MS; + let lastError = 'no response'; + + while (Date.now() < deadline) { + const remainingMs = Math.max(1, deadline - Date.now()); + const startupSignal = AbortSignal.timeout(remainingMs); + const abortController = new AbortController(); + const timeoutId = setTimeout(() => abortController.abort(), remainingMs); + const signal = AbortSignal.any([startupSignal, abortController.signal]); + + try { + const response = await fetch(url, { signal }); + if (response.ok) { + clearTimeout(timeoutId); + return { + bodyText: await response.text(), + contentType: response.headers.get('content-type'), + ok: response.ok, + }; + } + + lastError = `HTTP ${response.status}`; + } catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } finally { + clearTimeout(timeoutId); + abortController.abort(); + } + + await Bun.sleep(250); + } + + throw new Error(`Timed out waiting for packaged Spiracha at ${url} (${lastError}).`); +}; + +const readPackageManifest = async (cwd: string): Promise => + Bun.file(path.join(cwd, 'package.json')).json(); + +export const runPackagedUiSmokeTest = async (cwd = process.cwd()) => { + const manifest = await readPackageManifest(cwd); + const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'spiracha-package-smoke-')); + const port = await getAvailablePort(); + const packageTgz = getPackedTarballPath(tempDirectory, manifest.name, manifest.version); + + try { + await runCommand([process.execPath, 'pm', 'pack', '--destination', tempDirectory], cwd); + await Bun.write(path.join(tempDirectory, 'package.json'), '{"name":"spiracha-smoke","private":true}\n'); + + const bunx = Bun.which('bunx') ?? 'bunx'; + const proc = Bun.spawn([bunx, '--package', packageTgz, 'spiracha'], { + cwd: tempDirectory, + env: { + ...process.env, + PORT: String(port), + }, + stderr: 'pipe', + stdout: 'pipe', + }); + const stdoutPromise = new Response(proc.stdout).text(); + const stderrPromise = new Response(proc.stderr).text(); + const url = `http://${HOST}:${port}/`; + + try { + const probe = await waitForServer(url); + if (!isPackagedUiHealthyResponse(probe)) { + throw new Error(`Packaged Spiracha returned an unhealthy response at ${url}.`); + } + } catch (error) { + proc.kill('SIGTERM'); + const [stdoutText, stderrText] = await Promise.all([stdoutPromise, stderrPromise]); + throw new Error( + [ + error instanceof Error ? error.message : String(error), + stdoutText.trim() ? `stdout:\n${stdoutText}` : '', + stderrText.trim() ? `stderr:\n${stderrText}` : '', + ] + .filter(Boolean) + .join('\n\n'), + ); + } finally { + proc.kill('SIGTERM'); + await Promise.all([ + proc.exited.catch(() => undefined), + stdoutPromise.catch(() => ''), + stderrPromise.catch(() => ''), + ]); + } + } finally { + await rm(tempDirectory, { force: true, recursive: true }); + } +}; + +if (import.meta.main) { + await runPackagedUiSmokeTest(); +} diff --git a/src/ui-api-server.test.ts b/src/ui-api-server.test.ts index 472a3a3..f1b2188 100644 --- a/src/ui-api-server.test.ts +++ b/src/ui-api-server.test.ts @@ -70,13 +70,15 @@ const fetchWithTimeout = (url: string, init?: RequestInit) => { }; const startUiDevServer = (port: number, env: NodeJS.ProcessEnv) => { - const uiDirectory = path.join(process.cwd(), 'apps', 'ui'); - return Bun.spawn(['bun', '--bun', 'vite', 'dev', '--host', '127.0.0.1', '--port', String(port), '--strictPort'], { - cwd: uiDirectory, - env, - stderr: 'pipe', - stdout: 'pipe', - }); + return Bun.spawn( + [process.execPath, '--bun', 'vite', 'dev', '--host', '127.0.0.1', '--port', String(port), '--strictPort'], + { + cwd: process.cwd(), + env, + stderr: 'pipe', + stdout: 'pipe', + }, + ); }; describe('UI API server routes', () => { diff --git a/apps/ui/src/components/analytics-breakdowns.tsx b/src/ui/components/analytics-breakdowns.tsx similarity index 100% rename from apps/ui/src/components/analytics-breakdowns.tsx rename to src/ui/components/analytics-breakdowns.tsx diff --git a/apps/ui/src/components/analytics-breakdowns.vitest.tsx b/src/ui/components/analytics-breakdowns.vitest.tsx similarity index 100% rename from apps/ui/src/components/analytics-breakdowns.vitest.tsx rename to src/ui/components/analytics-breakdowns.vitest.tsx diff --git a/apps/ui/src/components/antigravity-conversations-table.tsx b/src/ui/components/antigravity-conversations-table.tsx similarity index 100% rename from apps/ui/src/components/antigravity-conversations-table.tsx rename to src/ui/components/antigravity-conversations-table.tsx diff --git a/apps/ui/src/components/antigravity-conversations-table.vitest.tsx b/src/ui/components/antigravity-conversations-table.vitest.tsx similarity index 100% rename from apps/ui/src/components/antigravity-conversations-table.vitest.tsx rename to src/ui/components/antigravity-conversations-table.vitest.tsx diff --git a/apps/ui/src/components/antigravity-keychain-panel.tsx b/src/ui/components/antigravity-keychain-panel.tsx similarity index 100% rename from apps/ui/src/components/antigravity-keychain-panel.tsx rename to src/ui/components/antigravity-keychain-panel.tsx diff --git a/apps/ui/src/components/antigravity-keychain-panel.vitest.tsx b/src/ui/components/antigravity-keychain-panel.vitest.tsx similarity index 100% rename from apps/ui/src/components/antigravity-keychain-panel.vitest.tsx rename to src/ui/components/antigravity-keychain-panel.vitest.tsx diff --git a/apps/ui/src/components/antigravity-workspaces-table.tsx b/src/ui/components/antigravity-workspaces-table.tsx similarity index 100% rename from apps/ui/src/components/antigravity-workspaces-table.tsx rename to src/ui/components/antigravity-workspaces-table.tsx diff --git a/apps/ui/src/components/antigravity-workspaces-table.vitest.tsx b/src/ui/components/antigravity-workspaces-table.vitest.tsx similarity index 100% rename from apps/ui/src/components/antigravity-workspaces-table.vitest.tsx rename to src/ui/components/antigravity-workspaces-table.vitest.tsx diff --git a/apps/ui/src/components/app-shell.tsx b/src/ui/components/app-shell.tsx similarity index 96% rename from apps/ui/src/components/app-shell.tsx rename to src/ui/components/app-shell.tsx index 0f2d8db..1a357c2 100644 --- a/apps/ui/src/components/app-shell.tsx +++ b/src/ui/components/app-shell.tsx @@ -22,7 +22,6 @@ type NavItem = { activePrefixes?: readonly string[]; icon: typeof LayoutDashboard; label: string; - preload?: 'render'; to: string; }; @@ -37,7 +36,6 @@ const integrationNavItems: readonly NavItem[] = [ activePrefixes: ['/antigravity', '/antigravity-conversations'], icon: Sparkles, label: 'Antigravity', - preload: 'render', to: '/antigravity', }, { @@ -50,7 +48,12 @@ const integrationNavItems: readonly NavItem[] = [ { activePrefixes: ['/codex', '/threads'], icon: FolderOpen, label: 'Codex', to: '/codex' }, { activePrefixes: ['/cursor', '/cursor-threads'], icon: SquareTerminal, label: 'Cursor', to: '/cursor' }, { activePrefixes: ['/grok', '/grok-sessions'], icon: Bot, label: 'Grok', to: '/grok' }, - { activePrefixes: ['/kiro', '/kiro-sessions'], icon: BrainCircuit, label: 'Kiro', to: '/kiro' }, + { + activePrefixes: ['/kiro', '/kiro-sessions'], + icon: BrainCircuit, + label: 'Kiro', + to: '/kiro', + }, { activePrefixes: ['/minimax-code', '/minimax-code-sessions'], icon: BrainCircuit, @@ -61,7 +64,6 @@ const integrationNavItems: readonly NavItem[] = [ activePrefixes: ['/opencode', '/opencode-sessions'], icon: Code2, label: 'OpenCode', - preload: 'render', to: '/opencode', }, { activePrefixes: ['/qoder', '/qoder-sessions'], icon: Workflow, label: 'Qoder', to: '/qoder' }, @@ -72,7 +74,7 @@ const isNavItemActive = (pathname: string, item: NavItem) => { return prefixes.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)); }; -const renderNavItem = (pathname: string, item: NavItem) => { +const renderNavItem = (pathname: string, item: NavItem, preload?: 'intent') => { const active = isNavItemActive(pathname, item); const Icon = item.icon; @@ -87,7 +89,8 @@ const renderNavItem = (pathname: string, item: NavItem) => { ? 'bg-[var(--accent-muted)] font-medium text-[var(--accent-foreground)]' : 'text-[var(--muted-foreground)] hover:bg-[var(--panel-secondary)] hover:text-[var(--foreground)]', )} - preload={item.preload} + preload={preload} + preloadDelay={preload ? 0 : undefined} to={item.to} > @@ -180,7 +183,7 @@ export function AppShell({ children }: PropsWithChildren) { diff --git a/apps/ui/src/components/app-shell.vitest.tsx b/src/ui/components/app-shell.vitest.tsx similarity index 91% rename from apps/ui/src/components/app-shell.vitest.tsx rename to src/ui/components/app-shell.vitest.tsx index af33af5..63dc1c8 100644 --- a/apps/ui/src/components/app-shell.vitest.tsx +++ b/src/ui/components/app-shell.vitest.tsx @@ -1,7 +1,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import type { ReactNode } from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import packageJsonRaw from '../../../../package.json?raw'; +import packageJsonRaw from '../../../package.json?raw'; const useRouterStateMock = vi.fn(); const searchQueryMock = vi.fn(); @@ -15,6 +15,7 @@ vi.mock('@tanstack/react-router', () => ({ children, className, preload, + preloadDelay, to, ...props }: { @@ -22,12 +23,13 @@ vi.mock('@tanstack/react-router', () => ({ activeOptions?: { includeSearch?: boolean }; children: ReactNode; className: string; - preload?: 'render'; + preload?: 'intent'; + preloadDelay?: number; to: string; }) => { linkActiveOptionsMock(to, activeOptions); return ( - + {children} ); @@ -72,9 +74,22 @@ describe('AppShell', () => { expect(screen.getByRole('link', { name: /Codex/i }).className).toContain('bg-[var(--accent-muted)]'); expect(screen.getByRole('link', { name: /Codex/i }).getAttribute('aria-current')).toBe('page'); expect(linkActiveOptionsMock).toHaveBeenCalledWith('/codex', { includeSearch: false }); - expect(screen.getByRole('link', { name: 'Antigravity' }).getAttribute('data-preload')).toBe('render'); - expect(screen.getByRole('link', { name: 'OpenCode' }).getAttribute('data-preload')).toBe('render'); - expect(screen.getByRole('link', { name: 'Codex' }).getAttribute('data-preload')).toBeNull(); + for (const integration of [ + 'Antigravity', + 'Claude Code', + 'Cline', + 'Codex', + 'Cursor', + 'Grok', + 'Kiro', + 'MiniMax Code', + 'OpenCode', + 'Qoder', + ]) { + const link = screen.getByRole('link', { name: integration }); + expect(link.getAttribute('data-preload')).toBe('intent'); + expect(link.getAttribute('data-preload-delay')).toBe('0'); + } expect(screen.getByRole('link', { name: /Dashboard/i }).className).toContain( 'hover:bg-[var(--panel-secondary)]', ); diff --git a/apps/ui/src/components/breadcrumbs.tsx b/src/ui/components/breadcrumbs.tsx similarity index 100% rename from apps/ui/src/components/breadcrumbs.tsx rename to src/ui/components/breadcrumbs.tsx diff --git a/apps/ui/src/components/claude-code-sessions-table.tsx b/src/ui/components/claude-code-sessions-table.tsx similarity index 60% rename from apps/ui/src/components/claude-code-sessions-table.tsx rename to src/ui/components/claude-code-sessions-table.tsx index f984061..517cdab 100644 --- a/apps/ui/src/components/claude-code-sessions-table.tsx +++ b/src/ui/components/claude-code-sessions-table.tsx @@ -2,7 +2,7 @@ import type { ClaudeCodeSessionSummary } from '@spiracha/lib/claude-code-exporte import { Link } from '@tanstack/react-router'; import type { SortingState } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table'; -import { Download, MoreHorizontal, Trash2 } from 'lucide-react'; +import { Download, GitFork, MoreHorizontal, Trash2 } from 'lucide-react'; import { useMemo } from 'react'; import { DataTable } from '#/components/data-table'; import { SelectionActionsToolbar } from '#/components/selection-actions-toolbar'; @@ -13,7 +13,8 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '#/components/ui/dropdown-menu'; -import { formatDateTime, formatNumber, formatTokens } from '#/lib/formatters'; +import { formatDateTime, formatModelLabel, formatNumber, formatTokens } from '#/lib/formatters'; +import { cn } from '#/lib/utils'; type ClaudeCodeSessionsTableProps = { onDeleteSession: (session: ClaudeCodeSessionSummary) => void; @@ -23,25 +24,101 @@ type ClaudeCodeSessionsTableProps = { sessions: ClaudeCodeSessionSummary[]; }; -const columnHelper = createColumnHelper(); +type ClaudeCodeSessionTreeNode = ClaudeCodeSessionSummary & { + children: ClaudeCodeSessionTreeNode[]; +}; + +const columnHelper = createColumnHelper(); const defaultSorting: SortingState = [{ desc: true, id: 'lastActive' }]; +const SessionTitleCell = ({ depth, session }: { depth: number; session: ClaudeCodeSessionTreeNode }) => { + const isSubagent = depth > 0; + + return ( +
+
+ {isSubagent ? ( +
+
+ ); +}; + +const getSessionTreeRoots = (sessions: ClaudeCodeSessionSummary[]): ClaudeCodeSessionTreeNode[] => { + const nodesById = new Map(sessions.map((session) => [session.sessionId, { ...session, children: [] }])); + const childIdsByParentId = new Map(); + const rootIds: string[] = []; + + for (const session of sessions) { + const sessionId = session.sessionId; + const parentSessionId = session.hierarchy?.parentSessionId ?? null; + if (!parentSessionId || parentSessionId === sessionId || !nodesById.has(parentSessionId)) { + rootIds.push(sessionId); + continue; + } + + const childIds = childIdsByParentId.get(parentSessionId) ?? []; + childIds.push(sessionId); + childIdsByParentId.set(parentSessionId, childIds); + } + + const roots: ClaudeCodeSessionTreeNode[] = []; + const attachedSessionIds = new Set(); + const attachNode = (sessionId: string, parent: ClaudeCodeSessionTreeNode | null) => { + if (attachedSessionIds.has(sessionId)) { + return; + } + + const node = nodesById.get(sessionId); + if (!node) { + return; + } + + attachedSessionIds.add(sessionId); + if (parent) { + parent.children.push(node); + } else { + roots.push(node); + } + + for (const childSessionId of childIdsByParentId.get(sessionId) ?? []) { + attachNode(childSessionId, node); + } + }; + + for (const sessionId of rootIds) { + attachNode(sessionId, null); + } + for (const session of sessions) { + attachNode(session.sessionId, null); + } + + return roots; +}; + +const withoutChildren = ({ children: _children, ...session }: ClaudeCodeSessionTreeNode): ClaudeCodeSessionSummary => + session; + const columns = ( onDeleteSession: (session: ClaudeCodeSessionSummary) => void, onExportSession: (session: ClaudeCodeSessionSummary) => void, ) => [ columnHelper.accessor('title', { - cell: (info) => ( - -

{info.getValue()}

-

{info.row.original.sessionId}

- - ), + cell: (info) => , header: 'Session', }), columnHelper.accessor('lastActiveAtMs', { @@ -54,7 +131,9 @@ const columns = ( id: 'lastActive', }), columnHelper.accessor('model', { - cell: (info) => {info.getValue() ?? 'unknown'}, + cell: (info) => ( + {info.getValue() ? formatModelLabel(info.getValue()) : 'unknown'} + ), header: 'Model', }), columnHelper.accessor('messageCount', { @@ -93,14 +172,14 @@ const columns = ( onExportSession(info.row.original)} + onClick={() => onExportSession(withoutChildren(info.row.original))} > Export session onDeleteSession(info.row.original)} + onClick={() => onDeleteSession(withoutChildren(info.row.original))} > Delete session @@ -122,14 +201,17 @@ export function ClaudeCodeSessionsTable({ sessions, }: ClaudeCodeSessionsTableProps) { const tableColumns = useMemo(() => columns(onDeleteSession, onExportSession), [onDeleteSession, onExportSession]); + const sessionTreeRoots = useMemo(() => getSessionTreeRoots(sessions), [sessions]); return ( row.sessionId} + getSubRows={(row) => row.children} initialSorting={defaultSorting} renderToolbar={({ clearSelection, selectedRows }) => { const selectedSessionIds = selectedRows.map((row) => row.sessionId); diff --git a/apps/ui/src/components/claude-code-workspaces-table.tsx b/src/ui/components/claude-code-workspaces-table.tsx similarity index 100% rename from apps/ui/src/components/claude-code-workspaces-table.tsx rename to src/ui/components/claude-code-workspaces-table.tsx diff --git a/apps/ui/src/components/cline-tasks-table.tsx b/src/ui/components/cline-tasks-table.tsx similarity index 100% rename from apps/ui/src/components/cline-tasks-table.tsx rename to src/ui/components/cline-tasks-table.tsx diff --git a/apps/ui/src/components/cline-workspaces-table.tsx b/src/ui/components/cline-workspaces-table.tsx similarity index 100% rename from apps/ui/src/components/cline-workspaces-table.tsx rename to src/ui/components/cline-workspaces-table.tsx diff --git a/apps/ui/src/components/cursor-threads-table.tsx b/src/ui/components/cursor-threads-table.tsx similarity index 61% rename from apps/ui/src/components/cursor-threads-table.tsx rename to src/ui/components/cursor-threads-table.tsx index c49a0ff..dfe9e97 100644 --- a/apps/ui/src/components/cursor-threads-table.tsx +++ b/src/ui/components/cursor-threads-table.tsx @@ -2,7 +2,7 @@ import type { CursorThreadSummary } from '@spiracha/lib/cursor-exporter-types'; import { Link } from '@tanstack/react-router'; import type { SortingState } from '@tanstack/react-table'; import { createColumnHelper } from '@tanstack/react-table'; -import { Download, MoreHorizontal, Trash2 } from 'lucide-react'; +import { Download, GitFork, MoreHorizontal, Trash2 } from 'lucide-react'; import { useMemo } from 'react'; import { DataTable } from '#/components/data-table'; import { SelectionActionsToolbar } from '#/components/selection-actions-toolbar'; @@ -13,7 +13,8 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '#/components/ui/dropdown-menu'; -import { formatBytes, formatDateTime, formatNumber } from '#/lib/formatters'; +import { formatBytes, formatDateTime, formatModelLabel, formatNumber } from '#/lib/formatters'; +import { cn } from '#/lib/utils'; type CursorThreadsTableProps = { onDeleteThread: (thread: CursorThreadSummary) => void; @@ -23,28 +24,58 @@ type CursorThreadsTableProps = { threads: CursorThreadSummary[]; }; -const columnHelper = createColumnHelper(); +type CursorThreadTreeNode = CursorThreadSummary & { children: CursorThreadTreeNode[] }; + +const columnHelper = createColumnHelper(); const defaultSorting: SortingState = [{ desc: true, id: 'updatedAt' }]; +const CursorThreadTitleCell = ({ depth, thread }: { depth: number; thread: CursorThreadTreeNode }) => ( +
0 ? 'border-[var(--border)] border-l-2 pl-3' : '')} data-row-depth={depth}> +
+ {depth > 0 ? ( +
+
+); + +const getCursorThreadTreeRoots = (threads: CursorThreadSummary[]): CursorThreadTreeNode[] => { + const nodesById = new Map( + threads.map((thread) => [thread.composerId, { ...thread, children: [] as CursorThreadTreeNode[] }]), + ); + const roots: CursorThreadTreeNode[] = []; + for (const thread of threads) { + const node = nodesById.get(thread.composerId)!; + const parent = thread.parentComposerId ? nodesById.get(thread.parentComposerId) : null; + if (parent && parent !== node) { + parent.children.push(node); + } else { + roots.push(node); + } + } + return roots; +}; + +const withoutChildren = ({ children: _children, ...thread }: CursorThreadTreeNode): CursorThreadSummary => thread; + const columns = ( onDeleteThread: (thread: CursorThreadSummary) => void, onExportThread: (thread: CursorThreadSummary) => void, ) => [ columnHelper.accessor('name', { - cell: (info) => ( - -

{info.getValue()}

-

- {info.row.original.mode ? `${info.row.original.mode} · ` : ''} - {info.row.original.composerId} -

- - ), + cell: (info) => , header: 'Thread', }), columnHelper.accessor('lastUpdatedAtMs', { @@ -69,6 +100,19 @@ const columns = ( cell: (info) => {info.getValue() ?? 'unknown'}, header: 'Mode', }), + columnHelper.accessor('model', { + cell: (info) => ( +
+
{info.getValue() ? formatModelLabel(info.getValue()) : 'unknown'}
+ {info.row.original.reasoningEffort ? ( +
+ {info.row.original.reasoningEffort} reasoning +
+ ) : null} +
+ ), + header: 'Model', + }), columnHelper.accessor('bubbleCount', { cell: (info) => {formatNumber(info.getValue())}, header: 'Messages', @@ -95,14 +139,14 @@ const columns = ( onExportThread(info.row.original)} + onClick={() => onExportThread(withoutChildren(info.row.original))} > Export thread onDeleteThread(info.row.original)} + onClick={() => onDeleteThread(withoutChildren(info.row.original))} > Delete thread @@ -124,14 +168,17 @@ export const CursorThreadsTable = ({ threads, }: CursorThreadsTableProps) => { const tableColumns = useMemo(() => columns(onDeleteThread, onExportThread), [onDeleteThread, onExportThread]); + const threadTreeRoots = useMemo(() => getCursorThreadTreeRoots(threads), [threads]); return ( row.composerId} + getSubRows={(row) => row.children} initialSorting={defaultSorting} renderToolbar={({ clearSelection, selectedRows }) => { const selectedComposerIds = selectedRows.map((row) => row.composerId); diff --git a/apps/ui/src/components/cursor-threads-table.vitest.tsx b/src/ui/components/cursor-threads-table.vitest.tsx similarity index 85% rename from apps/ui/src/components/cursor-threads-table.vitest.tsx rename to src/ui/components/cursor-threads-table.vitest.tsx index 9797964..5418f6d 100644 --- a/apps/ui/src/components/cursor-threads-table.vitest.tsx +++ b/src/ui/components/cursor-threads-table.vitest.tsx @@ -123,7 +123,10 @@ const thread: CursorThreadSummary = { createdAtMs: 1_700_000_000_000, lastUpdatedAtMs: 1_700_000_100_000, mode: 'agent', + model: 'claude-fable-5', name: 'Fix the checkout flow', + parentComposerId: null, + reasoningEffort: 'low', transcriptDirs: [], workspaceKey: 'folder:/Users/user/workspace/demo', workspaceLabel: 'demo', @@ -179,6 +182,35 @@ describe('CursorThreadsTable', () => { expect(onDeleteThreads).toHaveBeenCalledWith(['thread-1', 'thread-2']); }); + it('should nest subagents under their orchestrator and display their models', () => { + render( + , + ); + + expect(screen.getByText('Claude Fable 5')).toBeTruthy(); + expect(screen.getByText('Grok 4.5')).toBeTruthy(); + expect(screen.getByText('medium reasoning')).toBeTruthy(); + expect( + screen.getByText('Investigate parked test').closest('[data-row-depth]')?.getAttribute('data-row-depth'), + ).toBe('1'); + }); + it('should trigger single-thread export and delete actions from the row menu', async () => { const onDeleteThread = vi.fn(); const onExportThread = vi.fn(); diff --git a/apps/ui/src/components/cursor-workspaces-table.tsx b/src/ui/components/cursor-workspaces-table.tsx similarity index 100% rename from apps/ui/src/components/cursor-workspaces-table.tsx rename to src/ui/components/cursor-workspaces-table.tsx diff --git a/apps/ui/src/components/cursor-workspaces-table.vitest.tsx b/src/ui/components/cursor-workspaces-table.vitest.tsx similarity index 100% rename from apps/ui/src/components/cursor-workspaces-table.vitest.tsx rename to src/ui/components/cursor-workspaces-table.vitest.tsx diff --git a/apps/ui/src/components/data-table.tsx b/src/ui/components/data-table.tsx similarity index 100% rename from apps/ui/src/components/data-table.tsx rename to src/ui/components/data-table.tsx diff --git a/apps/ui/src/components/data-table.vitest.tsx b/src/ui/components/data-table.vitest.tsx similarity index 100% rename from apps/ui/src/components/data-table.vitest.tsx rename to src/ui/components/data-table.vitest.tsx diff --git a/apps/ui/src/components/delete-confirm-dialog.tsx b/src/ui/components/delete-confirm-dialog.tsx similarity index 100% rename from apps/ui/src/components/delete-confirm-dialog.tsx rename to src/ui/components/delete-confirm-dialog.tsx diff --git a/apps/ui/src/components/delete-confirm-dialog.vitest.tsx b/src/ui/components/delete-confirm-dialog.vitest.tsx similarity index 100% rename from apps/ui/src/components/delete-confirm-dialog.vitest.tsx rename to src/ui/components/delete-confirm-dialog.vitest.tsx diff --git a/apps/ui/src/components/display-components.vitest.tsx b/src/ui/components/display-components.vitest.tsx similarity index 100% rename from apps/ui/src/components/display-components.vitest.tsx rename to src/ui/components/display-components.vitest.tsx diff --git a/apps/ui/src/components/evidence-lens-editor.tsx b/src/ui/components/evidence-lens-editor.tsx similarity index 100% rename from apps/ui/src/components/evidence-lens-editor.tsx rename to src/ui/components/evidence-lens-editor.tsx diff --git a/apps/ui/src/components/evidence-lens-editor.vitest.tsx b/src/ui/components/evidence-lens-editor.vitest.tsx similarity index 100% rename from apps/ui/src/components/evidence-lens-editor.vitest.tsx rename to src/ui/components/evidence-lens-editor.vitest.tsx diff --git a/apps/ui/src/components/export-dialog.tsx b/src/ui/components/export-dialog.tsx similarity index 100% rename from apps/ui/src/components/export-dialog.tsx rename to src/ui/components/export-dialog.tsx diff --git a/apps/ui/src/components/export-dialog.vitest.tsx b/src/ui/components/export-dialog.vitest.tsx similarity index 100% rename from apps/ui/src/components/export-dialog.vitest.tsx rename to src/ui/components/export-dialog.vitest.tsx diff --git a/apps/ui/src/components/grok-sessions-table.tsx b/src/ui/components/grok-sessions-table.tsx similarity index 100% rename from apps/ui/src/components/grok-sessions-table.tsx rename to src/ui/components/grok-sessions-table.tsx diff --git a/apps/ui/src/components/grok-workspaces-table.tsx b/src/ui/components/grok-workspaces-table.tsx similarity index 100% rename from apps/ui/src/components/grok-workspaces-table.tsx rename to src/ui/components/grok-workspaces-table.tsx diff --git a/apps/ui/src/components/json-panel.tsx b/src/ui/components/json-panel.tsx similarity index 100% rename from apps/ui/src/components/json-panel.tsx rename to src/ui/components/json-panel.tsx diff --git a/apps/ui/src/components/kiro-sessions-table.tsx b/src/ui/components/kiro-sessions-table.tsx similarity index 100% rename from apps/ui/src/components/kiro-sessions-table.tsx rename to src/ui/components/kiro-sessions-table.tsx diff --git a/apps/ui/src/components/kiro-workspaces-table.tsx b/src/ui/components/kiro-workspaces-table.tsx similarity index 100% rename from apps/ui/src/components/kiro-workspaces-table.tsx rename to src/ui/components/kiro-workspaces-table.tsx diff --git a/apps/ui/src/components/list-search-input.tsx b/src/ui/components/list-search-input.tsx similarity index 100% rename from apps/ui/src/components/list-search-input.tsx rename to src/ui/components/list-search-input.tsx diff --git a/apps/ui/src/components/list-search-input.vitest.tsx b/src/ui/components/list-search-input.vitest.tsx similarity index 100% rename from apps/ui/src/components/list-search-input.vitest.tsx rename to src/ui/components/list-search-input.vitest.tsx diff --git a/apps/ui/src/components/loading-panel.tsx b/src/ui/components/loading-panel.tsx similarity index 100% rename from apps/ui/src/components/loading-panel.tsx rename to src/ui/components/loading-panel.tsx diff --git a/apps/ui/src/components/metadata-section.tsx b/src/ui/components/metadata-section.tsx similarity index 100% rename from apps/ui/src/components/metadata-section.tsx rename to src/ui/components/metadata-section.tsx diff --git a/apps/ui/src/components/metric-card.tsx b/src/ui/components/metric-card.tsx similarity index 100% rename from apps/ui/src/components/metric-card.tsx rename to src/ui/components/metric-card.tsx diff --git a/apps/ui/src/components/minimax-code-sessions-table.tsx b/src/ui/components/minimax-code-sessions-table.tsx similarity index 100% rename from apps/ui/src/components/minimax-code-sessions-table.tsx rename to src/ui/components/minimax-code-sessions-table.tsx diff --git a/apps/ui/src/components/minimax-code-tables.vitest.tsx b/src/ui/components/minimax-code-tables.vitest.tsx similarity index 100% rename from apps/ui/src/components/minimax-code-tables.vitest.tsx rename to src/ui/components/minimax-code-tables.vitest.tsx diff --git a/apps/ui/src/components/minimax-code-workspaces-table.tsx b/src/ui/components/minimax-code-workspaces-table.tsx similarity index 100% rename from apps/ui/src/components/minimax-code-workspaces-table.tsx rename to src/ui/components/minimax-code-workspaces-table.tsx diff --git a/apps/ui/src/components/opencode-sessions-table.tsx b/src/ui/components/opencode-sessions-table.tsx similarity index 100% rename from apps/ui/src/components/opencode-sessions-table.tsx rename to src/ui/components/opencode-sessions-table.tsx diff --git a/apps/ui/src/components/opencode-workspaces-table.tsx b/src/ui/components/opencode-workspaces-table.tsx similarity index 100% rename from apps/ui/src/components/opencode-workspaces-table.tsx rename to src/ui/components/opencode-workspaces-table.tsx diff --git a/apps/ui/src/components/page-header.tsx b/src/ui/components/page-header.tsx similarity index 100% rename from apps/ui/src/components/page-header.tsx rename to src/ui/components/page-header.tsx diff --git a/apps/ui/src/components/projects-loading-state.tsx b/src/ui/components/projects-loading-state.tsx similarity index 100% rename from apps/ui/src/components/projects-loading-state.tsx rename to src/ui/components/projects-loading-state.tsx diff --git a/apps/ui/src/components/projects-loading-state.vitest.tsx b/src/ui/components/projects-loading-state.vitest.tsx similarity index 100% rename from apps/ui/src/components/projects-loading-state.vitest.tsx rename to src/ui/components/projects-loading-state.vitest.tsx diff --git a/apps/ui/src/components/projects-table.tsx b/src/ui/components/projects-table.tsx similarity index 100% rename from apps/ui/src/components/projects-table.tsx rename to src/ui/components/projects-table.tsx diff --git a/apps/ui/src/components/projects-table.vitest.tsx b/src/ui/components/projects-table.vitest.tsx similarity index 100% rename from apps/ui/src/components/projects-table.vitest.tsx rename to src/ui/components/projects-table.vitest.tsx diff --git a/apps/ui/src/components/qoder-sessions-table.tsx b/src/ui/components/qoder-sessions-table.tsx similarity index 100% rename from apps/ui/src/components/qoder-sessions-table.tsx rename to src/ui/components/qoder-sessions-table.tsx diff --git a/apps/ui/src/components/qoder-sessions-table.vitest.tsx b/src/ui/components/qoder-sessions-table.vitest.tsx similarity index 100% rename from apps/ui/src/components/qoder-sessions-table.vitest.tsx rename to src/ui/components/qoder-sessions-table.vitest.tsx diff --git a/apps/ui/src/components/qoder-workspaces-table.tsx b/src/ui/components/qoder-workspaces-table.tsx similarity index 100% rename from apps/ui/src/components/qoder-workspaces-table.tsx rename to src/ui/components/qoder-workspaces-table.tsx diff --git a/apps/ui/src/components/recent-threads-list.tsx b/src/ui/components/recent-threads-list.tsx similarity index 100% rename from apps/ui/src/components/recent-threads-list.tsx rename to src/ui/components/recent-threads-list.tsx diff --git a/apps/ui/src/components/recent-threads-list.vitest.tsx b/src/ui/components/recent-threads-list.vitest.tsx similarity index 100% rename from apps/ui/src/components/recent-threads-list.vitest.tsx rename to src/ui/components/recent-threads-list.vitest.tsx diff --git a/apps/ui/src/components/reload-error-panel.tsx b/src/ui/components/reload-error-panel.tsx similarity index 100% rename from apps/ui/src/components/reload-error-panel.tsx rename to src/ui/components/reload-error-panel.tsx diff --git a/apps/ui/src/components/route-error-panel.tsx b/src/ui/components/route-error-panel.tsx similarity index 100% rename from apps/ui/src/components/route-error-panel.tsx rename to src/ui/components/route-error-panel.tsx diff --git a/apps/ui/src/components/route-error-panel.vitest.tsx b/src/ui/components/route-error-panel.vitest.tsx similarity index 100% rename from apps/ui/src/components/route-error-panel.vitest.tsx rename to src/ui/components/route-error-panel.vitest.tsx diff --git a/apps/ui/src/components/selection-actions-toolbar.tsx b/src/ui/components/selection-actions-toolbar.tsx similarity index 100% rename from apps/ui/src/components/selection-actions-toolbar.tsx rename to src/ui/components/selection-actions-toolbar.tsx diff --git a/apps/ui/src/components/selection-actions-toolbar.vitest.tsx b/src/ui/components/selection-actions-toolbar.vitest.tsx similarity index 100% rename from apps/ui/src/components/selection-actions-toolbar.vitest.tsx rename to src/ui/components/selection-actions-toolbar.vitest.tsx diff --git a/apps/ui/src/components/source-tables.vitest.tsx b/src/ui/components/source-tables.vitest.tsx similarity index 86% rename from apps/ui/src/components/source-tables.vitest.tsx rename to src/ui/components/source-tables.vitest.tsx index 7b1a553..631177e 100644 --- a/apps/ui/src/components/source-tables.vitest.tsx +++ b/src/ui/components/source-tables.vitest.tsx @@ -154,7 +154,7 @@ const sessionSpecs: Array<{ }, { Component: ClaudeCodeSessionsTable as unknown as ComponentType, - expectedValues: ['Claude model', '1,234', '2,500 tokens', '1.0.0'], + expectedValues: ['Claude Model', '1,234', '2,500 tokens', '1.0.0'], route: '/claude-code-sessions/claude-session', session: { lastActiveAtMs: 1_700_000_000_000, @@ -261,6 +261,50 @@ describe('source session tables', () => { expect(onDeleteSession).toHaveBeenCalledWith(session); }); } + + it('should render Claude Code sub-agents as nested rows beneath their parent', () => { + const parent = { + ...sessionSpecs[1]!.session, + hierarchy: { parentSessionId: null }, + sessionId: 'parent-session', + title: 'Fingerprint Wave 1 behavioral fixes', + }; + const child = { + ...parent, + hierarchy: { parentSessionId: 'parent-session' }, + model: 'claude-opus-5', + sessionId: 'agent-a1d79cbf732582863', + title: 'Implement fingerprint #100 and #101', + }; + const grandchild = { + ...child, + hierarchy: { parentSessionId: 'agent-a1d79cbf732582863' }, + model: 'claude-opus-5', + sessionId: 'agent-7f8e90e8', + title: 'Follow-up review for #101', + }; + + render( + , + ); + + const childLink = screen.getByRole('link', { name: /Implement fingerprint #100 and #101/ }); + const grandchildLink = screen.getByRole('link', { name: /Follow-up review for #101/ }); + expect(childLink.closest('[data-row-depth="1"]')).toBeTruthy(); + expect(childLink.closest('[data-row-depth="0"]')).toBeNull(); + expect(grandchildLink.closest('[data-row-depth="2"]')).toBeTruthy(); + expect((childLink.closest('[data-row-depth="1"]') as HTMLElement | null)?.style.paddingLeft).toBe('0.75rem'); + expect((grandchildLink.closest('[data-row-depth="2"]') as HTMLElement | null)?.style.paddingLeft).toBe( + '1.5rem', + ); + expect(screen.getAllByText('Claude Opus 5')).toHaveLength(2); + }); }); describe('source workspace tables', () => { diff --git a/apps/ui/src/components/text-document-panel.tsx b/src/ui/components/text-document-panel.tsx similarity index 100% rename from apps/ui/src/components/text-document-panel.tsx rename to src/ui/components/text-document-panel.tsx diff --git a/apps/ui/src/components/theme-toggle.tsx b/src/ui/components/theme-toggle.tsx similarity index 100% rename from apps/ui/src/components/theme-toggle.tsx rename to src/ui/components/theme-toggle.tsx diff --git a/apps/ui/src/components/theme-toggle.vitest.tsx b/src/ui/components/theme-toggle.vitest.tsx similarity index 100% rename from apps/ui/src/components/theme-toggle.vitest.tsx rename to src/ui/components/theme-toggle.vitest.tsx diff --git a/apps/ui/src/components/thread-goals-panel.tsx b/src/ui/components/thread-goals-panel.tsx similarity index 100% rename from apps/ui/src/components/thread-goals-panel.tsx rename to src/ui/components/thread-goals-panel.tsx diff --git a/apps/ui/src/components/thread-goals-panel.vitest.tsx b/src/ui/components/thread-goals-panel.vitest.tsx similarity index 100% rename from apps/ui/src/components/thread-goals-panel.vitest.tsx rename to src/ui/components/thread-goals-panel.vitest.tsx diff --git a/apps/ui/src/components/thread-tools-panel.tsx b/src/ui/components/thread-tools-panel.tsx similarity index 100% rename from apps/ui/src/components/thread-tools-panel.tsx rename to src/ui/components/thread-tools-panel.tsx diff --git a/apps/ui/src/components/thread-tools-panel.vitest.tsx b/src/ui/components/thread-tools-panel.vitest.tsx similarity index 100% rename from apps/ui/src/components/thread-tools-panel.vitest.tsx rename to src/ui/components/thread-tools-panel.vitest.tsx diff --git a/apps/ui/src/components/threads-table.tsx b/src/ui/components/threads-table.tsx similarity index 100% rename from apps/ui/src/components/threads-table.tsx rename to src/ui/components/threads-table.tsx diff --git a/apps/ui/src/components/threads-table.vitest.tsx b/src/ui/components/threads-table.vitest.tsx similarity index 100% rename from apps/ui/src/components/threads-table.vitest.tsx rename to src/ui/components/threads-table.vitest.tsx diff --git a/apps/ui/src/components/transcript-controls.tsx b/src/ui/components/transcript-controls.tsx similarity index 100% rename from apps/ui/src/components/transcript-controls.tsx rename to src/ui/components/transcript-controls.tsx diff --git a/apps/ui/src/components/transcript-controls.vitest.tsx b/src/ui/components/transcript-controls.vitest.tsx similarity index 100% rename from apps/ui/src/components/transcript-controls.vitest.tsx rename to src/ui/components/transcript-controls.vitest.tsx diff --git a/apps/ui/src/components/transcript-search.tsx b/src/ui/components/transcript-search.tsx similarity index 100% rename from apps/ui/src/components/transcript-search.tsx rename to src/ui/components/transcript-search.tsx diff --git a/apps/ui/src/components/transcript-search.vitest.tsx b/src/ui/components/transcript-search.vitest.tsx similarity index 100% rename from apps/ui/src/components/transcript-search.vitest.tsx rename to src/ui/components/transcript-search.vitest.tsx diff --git a/apps/ui/src/components/transcript-view.tsx b/src/ui/components/transcript-view.tsx similarity index 100% rename from apps/ui/src/components/transcript-view.tsx rename to src/ui/components/transcript-view.tsx diff --git a/apps/ui/src/components/transcript-view.vitest.tsx b/src/ui/components/transcript-view.vitest.tsx similarity index 100% rename from apps/ui/src/components/transcript-view.vitest.tsx rename to src/ui/components/transcript-view.vitest.tsx diff --git a/apps/ui/src/components/ui/alert-dialog.tsx b/src/ui/components/ui/alert-dialog.tsx similarity index 100% rename from apps/ui/src/components/ui/alert-dialog.tsx rename to src/ui/components/ui/alert-dialog.tsx diff --git a/apps/ui/src/components/ui/badge.tsx b/src/ui/components/ui/badge.tsx similarity index 100% rename from apps/ui/src/components/ui/badge.tsx rename to src/ui/components/ui/badge.tsx diff --git a/apps/ui/src/components/ui/button.tsx b/src/ui/components/ui/button.tsx similarity index 100% rename from apps/ui/src/components/ui/button.tsx rename to src/ui/components/ui/button.tsx diff --git a/apps/ui/src/components/ui/checkbox.tsx b/src/ui/components/ui/checkbox.tsx similarity index 100% rename from apps/ui/src/components/ui/checkbox.tsx rename to src/ui/components/ui/checkbox.tsx diff --git a/apps/ui/src/components/ui/dialog.tsx b/src/ui/components/ui/dialog.tsx similarity index 100% rename from apps/ui/src/components/ui/dialog.tsx rename to src/ui/components/ui/dialog.tsx diff --git a/apps/ui/src/components/ui/dropdown-menu.tsx b/src/ui/components/ui/dropdown-menu.tsx similarity index 100% rename from apps/ui/src/components/ui/dropdown-menu.tsx rename to src/ui/components/ui/dropdown-menu.tsx diff --git a/apps/ui/src/components/ui/input.tsx b/src/ui/components/ui/input.tsx similarity index 100% rename from apps/ui/src/components/ui/input.tsx rename to src/ui/components/ui/input.tsx diff --git a/apps/ui/src/components/ui/scroll-area.tsx b/src/ui/components/ui/scroll-area.tsx similarity index 100% rename from apps/ui/src/components/ui/scroll-area.tsx rename to src/ui/components/ui/scroll-area.tsx diff --git a/apps/ui/src/components/ui/select.tsx b/src/ui/components/ui/select.tsx similarity index 100% rename from apps/ui/src/components/ui/select.tsx rename to src/ui/components/ui/select.tsx diff --git a/apps/ui/src/components/ui/separator.tsx b/src/ui/components/ui/separator.tsx similarity index 100% rename from apps/ui/src/components/ui/separator.tsx rename to src/ui/components/ui/separator.tsx diff --git a/apps/ui/src/components/ui/sheet.tsx b/src/ui/components/ui/sheet.tsx similarity index 100% rename from apps/ui/src/components/ui/sheet.tsx rename to src/ui/components/ui/sheet.tsx diff --git a/apps/ui/src/components/ui/skeleton.tsx b/src/ui/components/ui/skeleton.tsx similarity index 100% rename from apps/ui/src/components/ui/skeleton.tsx rename to src/ui/components/ui/skeleton.tsx diff --git a/apps/ui/src/components/ui/table.tsx b/src/ui/components/ui/table.tsx similarity index 100% rename from apps/ui/src/components/ui/table.tsx rename to src/ui/components/ui/table.tsx diff --git a/apps/ui/src/components/ui/tabs.tsx b/src/ui/components/ui/tabs.tsx similarity index 100% rename from apps/ui/src/components/ui/tabs.tsx rename to src/ui/components/ui/tabs.tsx diff --git a/apps/ui/src/components/ui/tabs.vitest.tsx b/src/ui/components/ui/tabs.vitest.tsx similarity index 100% rename from apps/ui/src/components/ui/tabs.vitest.tsx rename to src/ui/components/ui/tabs.vitest.tsx diff --git a/apps/ui/src/components/ui/tooltip.tsx b/src/ui/components/ui/tooltip.tsx similarity index 100% rename from apps/ui/src/components/ui/tooltip.tsx rename to src/ui/components/ui/tooltip.tsx diff --git a/apps/ui/src/integrations/tanstack-query/devtools.tsx b/src/ui/integrations/tanstack-query/devtools.tsx similarity index 100% rename from apps/ui/src/integrations/tanstack-query/devtools.tsx rename to src/ui/integrations/tanstack-query/devtools.tsx diff --git a/apps/ui/src/integrations/tanstack-query/root-provider.tsx b/src/ui/integrations/tanstack-query/root-provider.tsx similarity index 100% rename from apps/ui/src/integrations/tanstack-query/root-provider.tsx rename to src/ui/integrations/tanstack-query/root-provider.tsx diff --git a/apps/ui/src/lib/antigravity-conversation-state.ts b/src/ui/lib/antigravity-conversation-state.ts similarity index 100% rename from apps/ui/src/lib/antigravity-conversation-state.ts rename to src/ui/lib/antigravity-conversation-state.ts diff --git a/apps/ui/src/lib/antigravity-conversation-state.vitest.ts b/src/ui/lib/antigravity-conversation-state.vitest.ts similarity index 100% rename from apps/ui/src/lib/antigravity-conversation-state.vitest.ts rename to src/ui/lib/antigravity-conversation-state.vitest.ts diff --git a/apps/ui/src/lib/antigravity-queries.ts b/src/ui/lib/antigravity-queries.ts similarity index 100% rename from apps/ui/src/lib/antigravity-queries.ts rename to src/ui/lib/antigravity-queries.ts diff --git a/apps/ui/src/lib/antigravity-server-browser.vitest.ts b/src/ui/lib/antigravity-server-browser.vitest.ts similarity index 100% rename from apps/ui/src/lib/antigravity-server-browser.vitest.ts rename to src/ui/lib/antigravity-server-browser.vitest.ts diff --git a/apps/ui/src/lib/antigravity-server.ts b/src/ui/lib/antigravity-server.ts similarity index 100% rename from apps/ui/src/lib/antigravity-server.ts rename to src/ui/lib/antigravity-server.ts diff --git a/apps/ui/src/lib/antigravity-server.vitest.ts b/src/ui/lib/antigravity-server.vitest.ts similarity index 100% rename from apps/ui/src/lib/antigravity-server.vitest.ts rename to src/ui/lib/antigravity-server.vitest.ts diff --git a/apps/ui/src/lib/antigravity-transcript-events.vitest.ts b/src/ui/lib/antigravity-transcript-events.vitest.ts similarity index 100% rename from apps/ui/src/lib/antigravity-transcript-events.vitest.ts rename to src/ui/lib/antigravity-transcript-events.vitest.ts diff --git a/apps/ui/src/lib/claude-code-queries.ts b/src/ui/lib/claude-code-queries.ts similarity index 100% rename from apps/ui/src/lib/claude-code-queries.ts rename to src/ui/lib/claude-code-queries.ts diff --git a/apps/ui/src/lib/claude-code-server.ts b/src/ui/lib/claude-code-server.ts similarity index 100% rename from apps/ui/src/lib/claude-code-server.ts rename to src/ui/lib/claude-code-server.ts diff --git a/apps/ui/src/lib/claude-code-server.vitest.ts b/src/ui/lib/claude-code-server.vitest.ts similarity index 99% rename from apps/ui/src/lib/claude-code-server.vitest.ts rename to src/ui/lib/claude-code-server.vitest.ts index f067882..2a8acca 100644 --- a/apps/ui/src/lib/claude-code-server.vitest.ts +++ b/src/ui/lib/claude-code-server.vitest.ts @@ -86,6 +86,7 @@ const buildTranscript = (entryCount: number, filePath = '/tmp/session-large.json cwd: '/workspace/project', filePath, gitBranch: null, + hierarchy: { parentSessionId: null }, inputTokens: 0, lastActiveAtIso: '2026-06-01T11:00:00.000Z', lastActiveAtMs: 2, diff --git a/apps/ui/src/lib/claude-code-transcript-events.ts b/src/ui/lib/claude-code-transcript-events.ts similarity index 100% rename from apps/ui/src/lib/claude-code-transcript-events.ts rename to src/ui/lib/claude-code-transcript-events.ts diff --git a/apps/ui/src/lib/claude-code-transcript-events.vitest.ts b/src/ui/lib/claude-code-transcript-events.vitest.ts similarity index 99% rename from apps/ui/src/lib/claude-code-transcript-events.vitest.ts rename to src/ui/lib/claude-code-transcript-events.vitest.ts index 959fdef..8a4bdd6 100644 --- a/apps/ui/src/lib/claude-code-transcript-events.vitest.ts +++ b/src/ui/lib/claude-code-transcript-events.vitest.ts @@ -67,6 +67,7 @@ const transcript: ClaudeCodeSessionTranscript = { cwd: '/workspace/project', filePath: '/tmp/session-a.jsonl', gitBranch: null, + hierarchy: { parentSessionId: null }, inputTokens: 5, lastActiveAtIso: '2026-06-01T10:00:02.000Z', lastActiveAtMs: 1_780_307_202_000, diff --git a/apps/ui/src/lib/cline-queries.ts b/src/ui/lib/cline-queries.ts similarity index 100% rename from apps/ui/src/lib/cline-queries.ts rename to src/ui/lib/cline-queries.ts diff --git a/apps/ui/src/lib/cline-server.ts b/src/ui/lib/cline-server.ts similarity index 100% rename from apps/ui/src/lib/cline-server.ts rename to src/ui/lib/cline-server.ts diff --git a/apps/ui/src/lib/cline-server.vitest.ts b/src/ui/lib/cline-server.vitest.ts similarity index 100% rename from apps/ui/src/lib/cline-server.vitest.ts rename to src/ui/lib/cline-server.vitest.ts diff --git a/apps/ui/src/lib/cline-transcript-events.ts b/src/ui/lib/cline-transcript-events.ts similarity index 100% rename from apps/ui/src/lib/cline-transcript-events.ts rename to src/ui/lib/cline-transcript-events.ts diff --git a/apps/ui/src/lib/cline-transcript-events.vitest.ts b/src/ui/lib/cline-transcript-events.vitest.ts similarity index 100% rename from apps/ui/src/lib/cline-transcript-events.vitest.ts rename to src/ui/lib/cline-transcript-events.vitest.ts diff --git a/apps/ui/src/lib/codex-queries.ts b/src/ui/lib/codex-queries.ts similarity index 98% rename from apps/ui/src/lib/codex-queries.ts rename to src/ui/lib/codex-queries.ts index 713be8d..5038c1e 100644 --- a/apps/ui/src/lib/codex-queries.ts +++ b/src/ui/lib/codex-queries.ts @@ -48,6 +48,7 @@ export const projectsQueryOptions = () => export const projectThreadsQueryOptions = (project: string) => queryOptions({ + ...explicitThreadRefreshOnly, queryFn: () => listProjectThreadsFn({ data: { project } }), queryKey: ['project-threads', project], retry: retrySqliteQuery, diff --git a/apps/ui/src/lib/codex-queries.vitest.ts b/src/ui/lib/codex-queries.vitest.ts similarity index 92% rename from apps/ui/src/lib/codex-queries.vitest.ts rename to src/ui/lib/codex-queries.vitest.ts index 6bdfd5d..0e2ece6 100644 --- a/apps/ui/src/lib/codex-queries.vitest.ts +++ b/src/ui/lib/codex-queries.vitest.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { + projectThreadsQueryOptions, threadSnapshotQueryOptions, threadTranscriptPreviewQueryOptions, threadTranscriptQueryOptions, @@ -16,6 +17,7 @@ describe('threadSnapshotQueryOptions', () => { it('should not passively refresh thread data while live mode is disabled', () => { const queries = [ + projectThreadsQueryOptions('project-1'), threadSnapshotQueryOptions('thread-1'), threadTranscriptPreviewQueryOptions('thread-1'), threadTranscriptQueryOptions('thread-1'), diff --git a/apps/ui/src/lib/codex-server.ts b/src/ui/lib/codex-server.ts similarity index 100% rename from apps/ui/src/lib/codex-server.ts rename to src/ui/lib/codex-server.ts diff --git a/apps/ui/src/lib/codex-server.vitest.ts b/src/ui/lib/codex-server.vitest.ts similarity index 100% rename from apps/ui/src/lib/codex-server.vitest.ts rename to src/ui/lib/codex-server.vitest.ts diff --git a/apps/ui/src/lib/codex-thread-event-origin.ts b/src/ui/lib/codex-thread-event-origin.ts similarity index 100% rename from apps/ui/src/lib/codex-thread-event-origin.ts rename to src/ui/lib/codex-thread-event-origin.ts diff --git a/apps/ui/src/lib/codex-thread-event-origin.vitest.ts b/src/ui/lib/codex-thread-event-origin.vitest.ts similarity index 100% rename from apps/ui/src/lib/codex-thread-event-origin.vitest.ts rename to src/ui/lib/codex-thread-event-origin.vitest.ts diff --git a/apps/ui/src/lib/codex-thread-live-hub.ts b/src/ui/lib/codex-thread-live-hub.ts similarity index 100% rename from apps/ui/src/lib/codex-thread-live-hub.ts rename to src/ui/lib/codex-thread-live-hub.ts diff --git a/apps/ui/src/lib/codex-thread-live-hub.vitest.ts b/src/ui/lib/codex-thread-live-hub.vitest.ts similarity index 100% rename from apps/ui/src/lib/codex-thread-live-hub.vitest.ts rename to src/ui/lib/codex-thread-live-hub.vitest.ts diff --git a/apps/ui/src/lib/codex-thread-live-types.ts b/src/ui/lib/codex-thread-live-types.ts similarity index 100% rename from apps/ui/src/lib/codex-thread-live-types.ts rename to src/ui/lib/codex-thread-live-types.ts diff --git a/apps/ui/src/lib/codex-thread-live-url.ts b/src/ui/lib/codex-thread-live-url.ts similarity index 100% rename from apps/ui/src/lib/codex-thread-live-url.ts rename to src/ui/lib/codex-thread-live-url.ts diff --git a/apps/ui/src/lib/codex-thread-live.ts b/src/ui/lib/codex-thread-live.ts similarity index 100% rename from apps/ui/src/lib/codex-thread-live.ts rename to src/ui/lib/codex-thread-live.ts diff --git a/apps/ui/src/lib/codex-thread-live.vitest.ts b/src/ui/lib/codex-thread-live.vitest.ts similarity index 100% rename from apps/ui/src/lib/codex-thread-live.vitest.ts rename to src/ui/lib/codex-thread-live.vitest.ts diff --git a/apps/ui/src/lib/codex-thread-live.worker.ts b/src/ui/lib/codex-thread-live.worker.ts similarity index 100% rename from apps/ui/src/lib/codex-thread-live.worker.ts rename to src/ui/lib/codex-thread-live.worker.ts diff --git a/apps/ui/src/lib/cursor-queries.ts b/src/ui/lib/cursor-queries.ts similarity index 100% rename from apps/ui/src/lib/cursor-queries.ts rename to src/ui/lib/cursor-queries.ts diff --git a/apps/ui/src/lib/cursor-server.ts b/src/ui/lib/cursor-server.ts similarity index 100% rename from apps/ui/src/lib/cursor-server.ts rename to src/ui/lib/cursor-server.ts diff --git a/apps/ui/src/lib/cursor-server.vitest.ts b/src/ui/lib/cursor-server.vitest.ts similarity index 99% rename from apps/ui/src/lib/cursor-server.vitest.ts rename to src/ui/lib/cursor-server.vitest.ts index 14f4ca8..a6fbc1d 100644 --- a/apps/ui/src/lib/cursor-server.vitest.ts +++ b/src/ui/lib/cursor-server.vitest.ts @@ -120,7 +120,10 @@ const makeThread = (overrides: Partial = {}): CursorThreadS createdAtMs: 1_700_000_000_000, lastUpdatedAtMs: 1_700_000_100_000, mode: 'agent', + model: null, name: 'Thread one', + parentComposerId: null, + reasoningEffort: null, transcriptDirs: [], workspaceKey: workspaceOne.key, workspaceLabel: workspaceOne.label, diff --git a/apps/ui/src/lib/cursor-transcript-events.ts b/src/ui/lib/cursor-transcript-events.ts similarity index 100% rename from apps/ui/src/lib/cursor-transcript-events.ts rename to src/ui/lib/cursor-transcript-events.ts diff --git a/apps/ui/src/lib/cursor-transcript-events.vitest.ts b/src/ui/lib/cursor-transcript-events.vitest.ts similarity index 98% rename from apps/ui/src/lib/cursor-transcript-events.vitest.ts rename to src/ui/lib/cursor-transcript-events.vitest.ts index 1e3d061..dc0165b 100644 --- a/apps/ui/src/lib/cursor-transcript-events.vitest.ts +++ b/src/ui/lib/cursor-transcript-events.vitest.ts @@ -57,7 +57,7 @@ const buildMultiStepTranscript = (): CursorThreadTranscript => ({ text: 'I will inspect the component first.', thinking: null, toolCall: { - argumentsText: '{"cmd":"sed -n 1,160p apps/ui/src/components/export-dialog.tsx"}', + argumentsText: '{"cmd":"sed -n 1,160p src/ui/components/export-dialog.tsx"}', callId: 'call-1', name: 'exec_command', resultText: 'component source', diff --git a/apps/ui/src/lib/delete-batch.ts b/src/ui/lib/delete-batch.ts similarity index 100% rename from apps/ui/src/lib/delete-batch.ts rename to src/ui/lib/delete-batch.ts diff --git a/apps/ui/src/lib/delete-batch.vitest.ts b/src/ui/lib/delete-batch.vitest.ts similarity index 100% rename from apps/ui/src/lib/delete-batch.vitest.ts rename to src/ui/lib/delete-batch.vitest.ts diff --git a/apps/ui/src/lib/download.ts b/src/ui/lib/download.ts similarity index 100% rename from apps/ui/src/lib/download.ts rename to src/ui/lib/download.ts diff --git a/apps/ui/src/lib/download.vitest.ts b/src/ui/lib/download.vitest.ts similarity index 100% rename from apps/ui/src/lib/download.vitest.ts rename to src/ui/lib/download.vitest.ts diff --git a/apps/ui/src/lib/error-presentation.ts b/src/ui/lib/error-presentation.ts similarity index 100% rename from apps/ui/src/lib/error-presentation.ts rename to src/ui/lib/error-presentation.ts diff --git a/apps/ui/src/lib/error-presentation.vitest.ts b/src/ui/lib/error-presentation.vitest.ts similarity index 100% rename from apps/ui/src/lib/error-presentation.vitest.ts rename to src/ui/lib/error-presentation.vitest.ts diff --git a/apps/ui/src/lib/evidence-export.ts b/src/ui/lib/evidence-export.ts similarity index 100% rename from apps/ui/src/lib/evidence-export.ts rename to src/ui/lib/evidence-export.ts diff --git a/apps/ui/src/lib/export-mutation.ts b/src/ui/lib/export-mutation.ts similarity index 100% rename from apps/ui/src/lib/export-mutation.ts rename to src/ui/lib/export-mutation.ts diff --git a/apps/ui/src/lib/export-mutation.vitest.ts b/src/ui/lib/export-mutation.vitest.ts similarity index 100% rename from apps/ui/src/lib/export-mutation.vitest.ts rename to src/ui/lib/export-mutation.vitest.ts diff --git a/apps/ui/src/lib/export-options.ts b/src/ui/lib/export-options.ts similarity index 100% rename from apps/ui/src/lib/export-options.ts rename to src/ui/lib/export-options.ts diff --git a/apps/ui/src/lib/formatters.ts b/src/ui/lib/formatters.ts similarity index 100% rename from apps/ui/src/lib/formatters.ts rename to src/ui/lib/formatters.ts diff --git a/apps/ui/src/lib/formatters.vitest.ts b/src/ui/lib/formatters.vitest.ts similarity index 100% rename from apps/ui/src/lib/formatters.vitest.ts rename to src/ui/lib/formatters.vitest.ts diff --git a/apps/ui/src/lib/grok-queries.ts b/src/ui/lib/grok-queries.ts similarity index 100% rename from apps/ui/src/lib/grok-queries.ts rename to src/ui/lib/grok-queries.ts diff --git a/apps/ui/src/lib/grok-server.ts b/src/ui/lib/grok-server.ts similarity index 100% rename from apps/ui/src/lib/grok-server.ts rename to src/ui/lib/grok-server.ts diff --git a/apps/ui/src/lib/grok-server.vitest.ts b/src/ui/lib/grok-server.vitest.ts similarity index 100% rename from apps/ui/src/lib/grok-server.vitest.ts rename to src/ui/lib/grok-server.vitest.ts diff --git a/apps/ui/src/lib/grok-transcript-events.ts b/src/ui/lib/grok-transcript-events.ts similarity index 100% rename from apps/ui/src/lib/grok-transcript-events.ts rename to src/ui/lib/grok-transcript-events.ts diff --git a/apps/ui/src/lib/grok-transcript-events.vitest.ts b/src/ui/lib/grok-transcript-events.vitest.ts similarity index 100% rename from apps/ui/src/lib/grok-transcript-events.vitest.ts rename to src/ui/lib/grok-transcript-events.vitest.ts diff --git a/apps/ui/src/lib/kiro-queries.ts b/src/ui/lib/kiro-queries.ts similarity index 100% rename from apps/ui/src/lib/kiro-queries.ts rename to src/ui/lib/kiro-queries.ts diff --git a/apps/ui/src/lib/kiro-server.ts b/src/ui/lib/kiro-server.ts similarity index 100% rename from apps/ui/src/lib/kiro-server.ts rename to src/ui/lib/kiro-server.ts diff --git a/apps/ui/src/lib/kiro-server.vitest.ts b/src/ui/lib/kiro-server.vitest.ts similarity index 100% rename from apps/ui/src/lib/kiro-server.vitest.ts rename to src/ui/lib/kiro-server.vitest.ts diff --git a/apps/ui/src/lib/kiro-transcript-events.ts b/src/ui/lib/kiro-transcript-events.ts similarity index 100% rename from apps/ui/src/lib/kiro-transcript-events.ts rename to src/ui/lib/kiro-transcript-events.ts diff --git a/apps/ui/src/lib/kiro-transcript-events.vitest.ts b/src/ui/lib/kiro-transcript-events.vitest.ts similarity index 100% rename from apps/ui/src/lib/kiro-transcript-events.vitest.ts rename to src/ui/lib/kiro-transcript-events.vitest.ts diff --git a/apps/ui/src/lib/minimax-code-queries.ts b/src/ui/lib/minimax-code-queries.ts similarity index 100% rename from apps/ui/src/lib/minimax-code-queries.ts rename to src/ui/lib/minimax-code-queries.ts diff --git a/apps/ui/src/lib/minimax-code-server.ts b/src/ui/lib/minimax-code-server.ts similarity index 100% rename from apps/ui/src/lib/minimax-code-server.ts rename to src/ui/lib/minimax-code-server.ts diff --git a/apps/ui/src/lib/minimax-code-server.vitest.ts b/src/ui/lib/minimax-code-server.vitest.ts similarity index 100% rename from apps/ui/src/lib/minimax-code-server.vitest.ts rename to src/ui/lib/minimax-code-server.vitest.ts diff --git a/apps/ui/src/lib/minimax-code-transcript-events.ts b/src/ui/lib/minimax-code-transcript-events.ts similarity index 100% rename from apps/ui/src/lib/minimax-code-transcript-events.ts rename to src/ui/lib/minimax-code-transcript-events.ts diff --git a/apps/ui/src/lib/minimax-code-transcript-events.vitest.ts b/src/ui/lib/minimax-code-transcript-events.vitest.ts similarity index 100% rename from apps/ui/src/lib/minimax-code-transcript-events.vitest.ts rename to src/ui/lib/minimax-code-transcript-events.vitest.ts diff --git a/apps/ui/src/lib/mutation-error.ts b/src/ui/lib/mutation-error.ts similarity index 100% rename from apps/ui/src/lib/mutation-error.ts rename to src/ui/lib/mutation-error.ts diff --git a/apps/ui/src/lib/mutation-error.vitest.ts b/src/ui/lib/mutation-error.vitest.ts similarity index 100% rename from apps/ui/src/lib/mutation-error.vitest.ts rename to src/ui/lib/mutation-error.vitest.ts diff --git a/apps/ui/src/lib/opencode-queries.ts b/src/ui/lib/opencode-queries.ts similarity index 100% rename from apps/ui/src/lib/opencode-queries.ts rename to src/ui/lib/opencode-queries.ts diff --git a/apps/ui/src/lib/opencode-server.ts b/src/ui/lib/opencode-server.ts similarity index 100% rename from apps/ui/src/lib/opencode-server.ts rename to src/ui/lib/opencode-server.ts diff --git a/apps/ui/src/lib/opencode-server.vitest.ts b/src/ui/lib/opencode-server.vitest.ts similarity index 100% rename from apps/ui/src/lib/opencode-server.vitest.ts rename to src/ui/lib/opencode-server.vitest.ts diff --git a/apps/ui/src/lib/opencode-transcript-events.ts b/src/ui/lib/opencode-transcript-events.ts similarity index 100% rename from apps/ui/src/lib/opencode-transcript-events.ts rename to src/ui/lib/opencode-transcript-events.ts diff --git a/apps/ui/src/lib/opencode-transcript-events.vitest.ts b/src/ui/lib/opencode-transcript-events.vitest.ts similarity index 100% rename from apps/ui/src/lib/opencode-transcript-events.vitest.ts rename to src/ui/lib/opencode-transcript-events.vitest.ts diff --git a/apps/ui/src/lib/package-metadata.ts b/src/ui/lib/package-metadata.ts similarity index 88% rename from apps/ui/src/lib/package-metadata.ts rename to src/ui/lib/package-metadata.ts index 24729db..a91b37b 100644 --- a/apps/ui/src/lib/package-metadata.ts +++ b/src/ui/lib/package-metadata.ts @@ -1,4 +1,4 @@ -import packageJsonRaw from '../../../../package.json?raw'; +import packageJsonRaw from '../../../package.json?raw'; type PackageMetadata = { homepage: string; diff --git a/apps/ui/src/lib/path-utils.ts b/src/ui/lib/path-utils.ts similarity index 100% rename from apps/ui/src/lib/path-utils.ts rename to src/ui/lib/path-utils.ts diff --git a/apps/ui/src/lib/qoder-queries.ts b/src/ui/lib/qoder-queries.ts similarity index 100% rename from apps/ui/src/lib/qoder-queries.ts rename to src/ui/lib/qoder-queries.ts diff --git a/apps/ui/src/lib/qoder-server.ts b/src/ui/lib/qoder-server.ts similarity index 100% rename from apps/ui/src/lib/qoder-server.ts rename to src/ui/lib/qoder-server.ts diff --git a/apps/ui/src/lib/qoder-server.vitest.ts b/src/ui/lib/qoder-server.vitest.ts similarity index 100% rename from apps/ui/src/lib/qoder-server.vitest.ts rename to src/ui/lib/qoder-server.vitest.ts diff --git a/apps/ui/src/lib/qoder-transcript-events.ts b/src/ui/lib/qoder-transcript-events.ts similarity index 100% rename from apps/ui/src/lib/qoder-transcript-events.ts rename to src/ui/lib/qoder-transcript-events.ts diff --git a/apps/ui/src/lib/qoder-transcript-events.vitest.ts b/src/ui/lib/qoder-transcript-events.vitest.ts similarity index 100% rename from apps/ui/src/lib/qoder-transcript-events.vitest.ts rename to src/ui/lib/qoder-transcript-events.vitest.ts diff --git a/apps/ui/src/lib/route-search.ts b/src/ui/lib/route-search.ts similarity index 100% rename from apps/ui/src/lib/route-search.ts rename to src/ui/lib/route-search.ts diff --git a/apps/ui/src/lib/route-search.vitest.ts b/src/ui/lib/route-search.vitest.ts similarity index 100% rename from apps/ui/src/lib/route-search.vitest.ts rename to src/ui/lib/route-search.vitest.ts diff --git a/apps/ui/src/lib/route-state-reset.tsx b/src/ui/lib/route-state-reset.tsx similarity index 100% rename from apps/ui/src/lib/route-state-reset.tsx rename to src/ui/lib/route-state-reset.tsx diff --git a/apps/ui/src/lib/route-state-reset.vitest.tsx b/src/ui/lib/route-state-reset.vitest.tsx similarity index 100% rename from apps/ui/src/lib/route-state-reset.vitest.tsx rename to src/ui/lib/route-state-reset.vitest.tsx diff --git a/apps/ui/src/lib/settings-server.ts b/src/ui/lib/settings-server.ts similarity index 100% rename from apps/ui/src/lib/settings-server.ts rename to src/ui/lib/settings-server.ts diff --git a/apps/ui/src/lib/settings-server.vitest.ts b/src/ui/lib/settings-server.vitest.ts similarity index 100% rename from apps/ui/src/lib/settings-server.vitest.ts rename to src/ui/lib/settings-server.vitest.ts diff --git a/apps/ui/src/lib/settings-store.tsx b/src/ui/lib/settings-store.tsx similarity index 100% rename from apps/ui/src/lib/settings-store.tsx rename to src/ui/lib/settings-store.tsx diff --git a/apps/ui/src/lib/settings-store.vitest.tsx b/src/ui/lib/settings-store.vitest.tsx similarity index 100% rename from apps/ui/src/lib/settings-store.vitest.tsx rename to src/ui/lib/settings-store.vitest.tsx diff --git a/apps/ui/src/lib/settings.ts b/src/ui/lib/settings.ts similarity index 100% rename from apps/ui/src/lib/settings.ts rename to src/ui/lib/settings.ts diff --git a/apps/ui/src/lib/settings.vitest.ts b/src/ui/lib/settings.vitest.ts similarity index 100% rename from apps/ui/src/lib/settings.vitest.ts rename to src/ui/lib/settings.vitest.ts diff --git a/apps/ui/src/lib/source-queries.vitest.ts b/src/ui/lib/source-queries.vitest.ts similarity index 100% rename from apps/ui/src/lib/source-queries.vitest.ts rename to src/ui/lib/source-queries.vitest.ts diff --git a/apps/ui/src/lib/source-session-export-server.ts b/src/ui/lib/source-session-export-server.ts similarity index 100% rename from apps/ui/src/lib/source-session-export-server.ts rename to src/ui/lib/source-session-export-server.ts diff --git a/apps/ui/src/lib/source-session-export-server.vitest.ts b/src/ui/lib/source-session-export-server.vitest.ts similarity index 100% rename from apps/ui/src/lib/source-session-export-server.vitest.ts rename to src/ui/lib/source-session-export-server.vitest.ts diff --git a/apps/ui/src/lib/text-filter.ts b/src/ui/lib/text-filter.ts similarity index 100% rename from apps/ui/src/lib/text-filter.ts rename to src/ui/lib/text-filter.ts diff --git a/apps/ui/src/lib/text-filter.vitest.ts b/src/ui/lib/text-filter.vitest.ts similarity index 100% rename from apps/ui/src/lib/text-filter.vitest.ts rename to src/ui/lib/text-filter.vitest.ts diff --git a/apps/ui/src/lib/thread-id.ts b/src/ui/lib/thread-id.ts similarity index 100% rename from apps/ui/src/lib/thread-id.ts rename to src/ui/lib/thread-id.ts diff --git a/apps/ui/src/lib/thread-id.vitest.ts b/src/ui/lib/thread-id.vitest.ts similarity index 100% rename from apps/ui/src/lib/thread-id.vitest.ts rename to src/ui/lib/thread-id.vitest.ts diff --git a/apps/ui/src/lib/thread-metadata.ts b/src/ui/lib/thread-metadata.ts similarity index 100% rename from apps/ui/src/lib/thread-metadata.ts rename to src/ui/lib/thread-metadata.ts diff --git a/apps/ui/src/lib/thread-metadata.vitest.ts b/src/ui/lib/thread-metadata.vitest.ts similarity index 100% rename from apps/ui/src/lib/thread-metadata.vitest.ts rename to src/ui/lib/thread-metadata.vitest.ts diff --git a/apps/ui/src/lib/thread-transcript-load.ts b/src/ui/lib/thread-transcript-load.ts similarity index 100% rename from apps/ui/src/lib/thread-transcript-load.ts rename to src/ui/lib/thread-transcript-load.ts diff --git a/apps/ui/src/lib/thread-transcript-load.vitest.ts b/src/ui/lib/thread-transcript-load.vitest.ts similarity index 100% rename from apps/ui/src/lib/thread-transcript-load.vitest.ts rename to src/ui/lib/thread-transcript-load.vitest.ts diff --git a/apps/ui/src/lib/thread-transcript-stats.ts b/src/ui/lib/thread-transcript-stats.ts similarity index 100% rename from apps/ui/src/lib/thread-transcript-stats.ts rename to src/ui/lib/thread-transcript-stats.ts diff --git a/apps/ui/src/lib/utils.ts b/src/ui/lib/utils.ts similarity index 100% rename from apps/ui/src/lib/utils.ts rename to src/ui/lib/utils.ts diff --git a/apps/ui/src/lib/workspace-delete-navigation.ts b/src/ui/lib/workspace-delete-navigation.ts similarity index 100% rename from apps/ui/src/lib/workspace-delete-navigation.ts rename to src/ui/lib/workspace-delete-navigation.ts diff --git a/apps/ui/src/lib/workspace-delete-navigation.vitest.ts b/src/ui/lib/workspace-delete-navigation.vitest.ts similarity index 100% rename from apps/ui/src/lib/workspace-delete-navigation.vitest.ts rename to src/ui/lib/workspace-delete-navigation.vitest.ts diff --git a/apps/ui/src/routeTree.gen.ts b/src/ui/routeTree.gen.ts similarity index 100% rename from apps/ui/src/routeTree.gen.ts rename to src/ui/routeTree.gen.ts index 2640a13..9e61a0c 100644 --- a/apps/ui/src/routeTree.gen.ts +++ b/src/ui/routeTree.gen.ts @@ -9,54 +9,59 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as SettingsRouteImport } from './routes/settings' -import { Route as AnalyticsRouteImport } from './routes/analytics' -import { Route as ThreadIdRouteImport } from './routes/$threadId' import { Route as IndexRouteImport } from './routes/index' -import { Route as QoderIndexRouteImport } from './routes/qoder.index' -import { Route as OpencodeIndexRouteImport } from './routes/opencode.index' -import { Route as MinimaxCodeIndexRouteImport } from './routes/minimax-code.index' -import { Route as KiroIndexRouteImport } from './routes/kiro.index' -import { Route as GrokIndexRouteImport } from './routes/grok.index' -import { Route as CursorIndexRouteImport } from './routes/cursor.index' -import { Route as CodexIndexRouteImport } from './routes/codex.index' -import { Route as ClineIndexRouteImport } from './routes/cline.index' -import { Route as ClaudeCodeIndexRouteImport } from './routes/claude-code.index' +import { Route as ThreadIdRouteImport } from './routes/$threadId' +import { Route as AnalyticsRouteImport } from './routes/analytics' +import { Route as SettingsRouteImport } from './routes/settings' +import { Route as AntigravityConversationsConversationIdRouteImport } from './routes/antigravity-conversations.$conversationId' import { Route as AntigravityIndexRouteImport } from './routes/antigravity.index' -import { Route as ThreadsThreadIdRouteImport } from './routes/threads.$threadId' -import { Route as QoderWorkspaceKeyRouteImport } from './routes/qoder.$workspaceKey' -import { Route as QoderSessionsSessionIdRouteImport } from './routes/qoder-sessions.$sessionId' -import { Route as OpencodeWorkspaceKeyRouteImport } from './routes/opencode.$workspaceKey' -import { Route as OpencodeSessionsSessionIdRouteImport } from './routes/opencode-sessions.$sessionId' -import { Route as MinimaxCodeWorkspaceKeyRouteImport } from './routes/minimax-code.$workspaceKey' -import { Route as MinimaxCodeSessionsSessionIdRouteImport } from './routes/minimax-code-sessions.$sessionId' -import { Route as KiroWorkspaceKeyRouteImport } from './routes/kiro.$workspaceKey' -import { Route as KiroSessionsSessionIdRouteImport } from './routes/kiro-sessions.$sessionId' -import { Route as GrokWorkspaceKeyRouteImport } from './routes/grok.$workspaceKey' -import { Route as GrokSessionsSessionIdRouteImport } from './routes/grok-sessions.$sessionId' -import { Route as CursorWorkspaceKeyRouteImport } from './routes/cursor.$workspaceKey' -import { Route as CursorThreadsComposerIdRouteImport } from './routes/cursor-threads.$composerId' -import { Route as CodexProjectRouteImport } from './routes/codex.$project' -import { Route as ClineWorkspaceKeyRouteImport } from './routes/cline.$workspaceKey' -import { Route as ClineTasksTaskIdRouteImport } from './routes/cline-tasks.$taskId' -import { Route as ClaudeCodeWorkspaceKeyRouteImport } from './routes/claude-code.$workspaceKey' -import { Route as ClaudeCodeSessionsSessionIdRouteImport } from './routes/claude-code-sessions.$sessionId' import { Route as AntigravityWorkspaceKeyRouteImport } from './routes/antigravity.$workspaceKey' -import { Route as AntigravityConversationsConversationIdRouteImport } from './routes/antigravity-conversations.$conversationId' -import { Route as ApiV1SourcesRouteImport } from './routes/api.v1.sources' -import { Route as ApiV1ResolveRouteImport } from './routes/api.v1.resolve' -import { Route as ApiV1ConversationsRouteImport } from './routes/api.v1.conversations' +import { Route as ClaudeCodeSessionsSessionIdRouteImport } from './routes/claude-code-sessions.$sessionId' +import { Route as ClaudeCodeIndexRouteImport } from './routes/claude-code.index' +import { Route as ClaudeCodeWorkspaceKeyRouteImport } from './routes/claude-code.$workspaceKey' +import { Route as ClineTasksTaskIdRouteImport } from './routes/cline-tasks.$taskId' +import { Route as ClineIndexRouteImport } from './routes/cline.index' +import { Route as ClineWorkspaceKeyRouteImport } from './routes/cline.$workspaceKey' +import { Route as CodexIndexRouteImport } from './routes/codex.index' +import { Route as CodexProjectRouteImport } from './routes/codex.$project' +import { Route as CursorThreadsComposerIdRouteImport } from './routes/cursor-threads.$composerId' +import { Route as CursorIndexRouteImport } from './routes/cursor.index' +import { Route as CursorWorkspaceKeyRouteImport } from './routes/cursor.$workspaceKey' +import { Route as GrokSessionsSessionIdRouteImport } from './routes/grok-sessions.$sessionId' +import { Route as GrokIndexRouteImport } from './routes/grok.index' +import { Route as GrokWorkspaceKeyRouteImport } from './routes/grok.$workspaceKey' +import { Route as KiroSessionsSessionIdRouteImport } from './routes/kiro-sessions.$sessionId' +import { Route as KiroIndexRouteImport } from './routes/kiro.index' +import { Route as KiroWorkspaceKeyRouteImport } from './routes/kiro.$workspaceKey' +import { Route as MinimaxCodeSessionsSessionIdRouteImport } from './routes/minimax-code-sessions.$sessionId' +import { Route as MinimaxCodeIndexRouteImport } from './routes/minimax-code.index' +import { Route as MinimaxCodeWorkspaceKeyRouteImport } from './routes/minimax-code.$workspaceKey' +import { Route as OpencodeSessionsSessionIdRouteImport } from './routes/opencode-sessions.$sessionId' +import { Route as OpencodeIndexRouteImport } from './routes/opencode.index' +import { Route as OpencodeWorkspaceKeyRouteImport } from './routes/opencode.$workspaceKey' +import { Route as QoderSessionsSessionIdRouteImport } from './routes/qoder-sessions.$sessionId' +import { Route as QoderIndexRouteImport } from './routes/qoder.index' +import { Route as QoderWorkspaceKeyRouteImport } from './routes/qoder.$workspaceKey' +import { Route as ThreadsThreadIdRouteImport } from './routes/threads.$threadId' import { Route as ApiV1ConversationQueryRouteImport } from './routes/api.v1.conversation-query' -import { Route as ApiV1ConversationsExportRouteImport } from './routes/api.v1.conversations.export' +import { Route as ApiV1ConversationsRouteImport } from './routes/api.v1.conversations' +import { Route as ApiV1ResolveRouteImport } from './routes/api.v1.resolve' +import { Route as ApiV1SourcesRouteImport } from './routes/api.v1.sources' import { Route as ApiV1ConversationsDeleteRouteImport } from './routes/api.v1.conversations.delete' -import { Route as ApiV1ConversationsSourceIdRouteImport } from './routes/api.v1.conversations.$source.$id' +import { Route as ApiV1ConversationsExportRouteImport } from './routes/api.v1.conversations.export' import { Route as ApiV1CodexThreadsEventsRouteImport } from './routes/api.v1.codex.threads.events' -import { Route as ApiV1ConversationsSourceIdExportRouteImport } from './routes/api.v1.conversations.$source.$id.export' +import { Route as ApiV1ConversationsSourceIdRouteImport } from './routes/api.v1.conversations.$source.$id' import { Route as ApiV1ConversationsSourceIdEvidenceRouteImport } from './routes/api.v1.conversations.$source.$id.evidence' +import { Route as ApiV1ConversationsSourceIdExportRouteImport } from './routes/api.v1.conversations.$source.$id.export' -const SettingsRoute = SettingsRouteImport.update({ - id: '/settings', - path: '/settings', +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ThreadIdRoute = ThreadIdRouteImport.update({ + id: '/$threadId', + path: '/$threadId', getParentRoute: () => rootRouteImport, } as any) const AnalyticsRoute = AnalyticsRouteImport.update({ @@ -64,44 +69,56 @@ const AnalyticsRoute = AnalyticsRouteImport.update({ path: '/analytics', getParentRoute: () => rootRouteImport, } as any) -const ThreadIdRoute = ThreadIdRouteImport.update({ - id: '/$threadId', - path: '/$threadId', +const SettingsRoute = SettingsRouteImport.update({ + id: '/settings', + path: '/settings', getParentRoute: () => rootRouteImport, } as any) -const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', +const AntigravityConversationsConversationIdRoute = + AntigravityConversationsConversationIdRouteImport.update({ + id: '/antigravity-conversations/$conversationId', + path: '/antigravity-conversations/$conversationId', + getParentRoute: () => rootRouteImport, + } as any) +const AntigravityIndexRoute = AntigravityIndexRouteImport.update({ + id: '/antigravity/', + path: '/antigravity/', getParentRoute: () => rootRouteImport, } as any) -const QoderIndexRoute = QoderIndexRouteImport.update({ - id: '/qoder/', - path: '/qoder/', +const AntigravityWorkspaceKeyRoute = AntigravityWorkspaceKeyRouteImport.update({ + id: '/antigravity/$workspaceKey', + path: '/antigravity/$workspaceKey', getParentRoute: () => rootRouteImport, } as any) -const OpencodeIndexRoute = OpencodeIndexRouteImport.update({ - id: '/opencode/', - path: '/opencode/', +const ClaudeCodeSessionsSessionIdRoute = + ClaudeCodeSessionsSessionIdRouteImport.update({ + id: '/claude-code-sessions/$sessionId', + path: '/claude-code-sessions/$sessionId', + getParentRoute: () => rootRouteImport, + } as any) +const ClaudeCodeIndexRoute = ClaudeCodeIndexRouteImport.update({ + id: '/claude-code/', + path: '/claude-code/', getParentRoute: () => rootRouteImport, } as any) -const MinimaxCodeIndexRoute = MinimaxCodeIndexRouteImport.update({ - id: '/minimax-code/', - path: '/minimax-code/', +const ClaudeCodeWorkspaceKeyRoute = ClaudeCodeWorkspaceKeyRouteImport.update({ + id: '/claude-code/$workspaceKey', + path: '/claude-code/$workspaceKey', getParentRoute: () => rootRouteImport, } as any) -const KiroIndexRoute = KiroIndexRouteImport.update({ - id: '/kiro/', - path: '/kiro/', +const ClineTasksTaskIdRoute = ClineTasksTaskIdRouteImport.update({ + id: '/cline-tasks/$taskId', + path: '/cline-tasks/$taskId', getParentRoute: () => rootRouteImport, } as any) -const GrokIndexRoute = GrokIndexRouteImport.update({ - id: '/grok/', - path: '/grok/', +const ClineIndexRoute = ClineIndexRouteImport.update({ + id: '/cline/', + path: '/cline/', getParentRoute: () => rootRouteImport, } as any) -const CursorIndexRoute = CursorIndexRouteImport.update({ - id: '/cursor/', - path: '/cursor/', +const ClineWorkspaceKeyRoute = ClineWorkspaceKeyRouteImport.update({ + id: '/cline/$workspaceKey', + path: '/cline/$workspaceKey', getParentRoute: () => rootRouteImport, } as any) const CodexIndexRoute = CodexIndexRouteImport.update({ @@ -109,50 +126,54 @@ const CodexIndexRoute = CodexIndexRouteImport.update({ path: '/codex/', getParentRoute: () => rootRouteImport, } as any) -const ClineIndexRoute = ClineIndexRouteImport.update({ - id: '/cline/', - path: '/cline/', +const CodexProjectRoute = CodexProjectRouteImport.update({ + id: '/codex/$project', + path: '/codex/$project', getParentRoute: () => rootRouteImport, } as any) -const ClaudeCodeIndexRoute = ClaudeCodeIndexRouteImport.update({ - id: '/claude-code/', - path: '/claude-code/', +const CursorThreadsComposerIdRoute = CursorThreadsComposerIdRouteImport.update({ + id: '/cursor-threads/$composerId', + path: '/cursor-threads/$composerId', getParentRoute: () => rootRouteImport, } as any) -const AntigravityIndexRoute = AntigravityIndexRouteImport.update({ - id: '/antigravity/', - path: '/antigravity/', +const CursorIndexRoute = CursorIndexRouteImport.update({ + id: '/cursor/', + path: '/cursor/', getParentRoute: () => rootRouteImport, } as any) -const ThreadsThreadIdRoute = ThreadsThreadIdRouteImport.update({ - id: '/threads/$threadId', - path: '/threads/$threadId', +const CursorWorkspaceKeyRoute = CursorWorkspaceKeyRouteImport.update({ + id: '/cursor/$workspaceKey', + path: '/cursor/$workspaceKey', getParentRoute: () => rootRouteImport, } as any) -const QoderWorkspaceKeyRoute = QoderWorkspaceKeyRouteImport.update({ - id: '/qoder/$workspaceKey', - path: '/qoder/$workspaceKey', +const GrokSessionsSessionIdRoute = GrokSessionsSessionIdRouteImport.update({ + id: '/grok-sessions/$sessionId', + path: '/grok-sessions/$sessionId', getParentRoute: () => rootRouteImport, } as any) -const QoderSessionsSessionIdRoute = QoderSessionsSessionIdRouteImport.update({ - id: '/qoder-sessions/$sessionId', - path: '/qoder-sessions/$sessionId', +const GrokIndexRoute = GrokIndexRouteImport.update({ + id: '/grok/', + path: '/grok/', getParentRoute: () => rootRouteImport, } as any) -const OpencodeWorkspaceKeyRoute = OpencodeWorkspaceKeyRouteImport.update({ - id: '/opencode/$workspaceKey', - path: '/opencode/$workspaceKey', +const GrokWorkspaceKeyRoute = GrokWorkspaceKeyRouteImport.update({ + id: '/grok/$workspaceKey', + path: '/grok/$workspaceKey', getParentRoute: () => rootRouteImport, } as any) -const OpencodeSessionsSessionIdRoute = - OpencodeSessionsSessionIdRouteImport.update({ - id: '/opencode-sessions/$sessionId', - path: '/opencode-sessions/$sessionId', - getParentRoute: () => rootRouteImport, - } as any) -const MinimaxCodeWorkspaceKeyRoute = MinimaxCodeWorkspaceKeyRouteImport.update({ - id: '/minimax-code/$workspaceKey', - path: '/minimax-code/$workspaceKey', +const KiroSessionsSessionIdRoute = KiroSessionsSessionIdRouteImport.update({ + id: '/kiro-sessions/$sessionId', + path: '/kiro-sessions/$sessionId', + getParentRoute: () => rootRouteImport, +} as any) +const KiroIndexRoute = KiroIndexRouteImport.update({ + id: '/kiro/', + path: '/kiro/', + getParentRoute: () => rootRouteImport, +} as any) +const KiroWorkspaceKeyRoute = KiroWorkspaceKeyRouteImport.update({ + id: '/kiro/$workspaceKey', + path: '/kiro/$workspaceKey', getParentRoute: () => rootRouteImport, } as any) const MinimaxCodeSessionsSessionIdRoute = @@ -161,76 +182,60 @@ const MinimaxCodeSessionsSessionIdRoute = path: '/minimax-code-sessions/$sessionId', getParentRoute: () => rootRouteImport, } as any) -const KiroWorkspaceKeyRoute = KiroWorkspaceKeyRouteImport.update({ - id: '/kiro/$workspaceKey', - path: '/kiro/$workspaceKey', - getParentRoute: () => rootRouteImport, -} as any) -const KiroSessionsSessionIdRoute = KiroSessionsSessionIdRouteImport.update({ - id: '/kiro-sessions/$sessionId', - path: '/kiro-sessions/$sessionId', - getParentRoute: () => rootRouteImport, -} as any) -const GrokWorkspaceKeyRoute = GrokWorkspaceKeyRouteImport.update({ - id: '/grok/$workspaceKey', - path: '/grok/$workspaceKey', +const MinimaxCodeIndexRoute = MinimaxCodeIndexRouteImport.update({ + id: '/minimax-code/', + path: '/minimax-code/', getParentRoute: () => rootRouteImport, } as any) -const GrokSessionsSessionIdRoute = GrokSessionsSessionIdRouteImport.update({ - id: '/grok-sessions/$sessionId', - path: '/grok-sessions/$sessionId', +const MinimaxCodeWorkspaceKeyRoute = MinimaxCodeWorkspaceKeyRouteImport.update({ + id: '/minimax-code/$workspaceKey', + path: '/minimax-code/$workspaceKey', getParentRoute: () => rootRouteImport, } as any) -const CursorWorkspaceKeyRoute = CursorWorkspaceKeyRouteImport.update({ - id: '/cursor/$workspaceKey', - path: '/cursor/$workspaceKey', +const OpencodeSessionsSessionIdRoute = + OpencodeSessionsSessionIdRouteImport.update({ + id: '/opencode-sessions/$sessionId', + path: '/opencode-sessions/$sessionId', + getParentRoute: () => rootRouteImport, + } as any) +const OpencodeIndexRoute = OpencodeIndexRouteImport.update({ + id: '/opencode/', + path: '/opencode/', getParentRoute: () => rootRouteImport, } as any) -const CursorThreadsComposerIdRoute = CursorThreadsComposerIdRouteImport.update({ - id: '/cursor-threads/$composerId', - path: '/cursor-threads/$composerId', +const OpencodeWorkspaceKeyRoute = OpencodeWorkspaceKeyRouteImport.update({ + id: '/opencode/$workspaceKey', + path: '/opencode/$workspaceKey', getParentRoute: () => rootRouteImport, } as any) -const CodexProjectRoute = CodexProjectRouteImport.update({ - id: '/codex/$project', - path: '/codex/$project', +const QoderSessionsSessionIdRoute = QoderSessionsSessionIdRouteImport.update({ + id: '/qoder-sessions/$sessionId', + path: '/qoder-sessions/$sessionId', getParentRoute: () => rootRouteImport, } as any) -const ClineWorkspaceKeyRoute = ClineWorkspaceKeyRouteImport.update({ - id: '/cline/$workspaceKey', - path: '/cline/$workspaceKey', +const QoderIndexRoute = QoderIndexRouteImport.update({ + id: '/qoder/', + path: '/qoder/', getParentRoute: () => rootRouteImport, } as any) -const ClineTasksTaskIdRoute = ClineTasksTaskIdRouteImport.update({ - id: '/cline-tasks/$taskId', - path: '/cline-tasks/$taskId', +const QoderWorkspaceKeyRoute = QoderWorkspaceKeyRouteImport.update({ + id: '/qoder/$workspaceKey', + path: '/qoder/$workspaceKey', getParentRoute: () => rootRouteImport, } as any) -const ClaudeCodeWorkspaceKeyRoute = ClaudeCodeWorkspaceKeyRouteImport.update({ - id: '/claude-code/$workspaceKey', - path: '/claude-code/$workspaceKey', +const ThreadsThreadIdRoute = ThreadsThreadIdRouteImport.update({ + id: '/threads/$threadId', + path: '/threads/$threadId', getParentRoute: () => rootRouteImport, } as any) -const ClaudeCodeSessionsSessionIdRoute = - ClaudeCodeSessionsSessionIdRouteImport.update({ - id: '/claude-code-sessions/$sessionId', - path: '/claude-code-sessions/$sessionId', - getParentRoute: () => rootRouteImport, - } as any) -const AntigravityWorkspaceKeyRoute = AntigravityWorkspaceKeyRouteImport.update({ - id: '/antigravity/$workspaceKey', - path: '/antigravity/$workspaceKey', +const ApiV1ConversationQueryRoute = ApiV1ConversationQueryRouteImport.update({ + id: '/api/v1/conversation-query', + path: '/api/v1/conversation-query', getParentRoute: () => rootRouteImport, } as any) -const AntigravityConversationsConversationIdRoute = - AntigravityConversationsConversationIdRouteImport.update({ - id: '/antigravity-conversations/$conversationId', - path: '/antigravity-conversations/$conversationId', - getParentRoute: () => rootRouteImport, - } as any) -const ApiV1SourcesRoute = ApiV1SourcesRouteImport.update({ - id: '/api/v1/sources', - path: '/api/v1/sources', +const ApiV1ConversationsRoute = ApiV1ConversationsRouteImport.update({ + id: '/api/v1/conversations', + path: '/api/v1/conversations', getParentRoute: () => rootRouteImport, } as any) const ApiV1ResolveRoute = ApiV1ResolveRouteImport.update({ @@ -238,32 +243,21 @@ const ApiV1ResolveRoute = ApiV1ResolveRouteImport.update({ path: '/api/v1/resolve', getParentRoute: () => rootRouteImport, } as any) -const ApiV1ConversationsRoute = ApiV1ConversationsRouteImport.update({ - id: '/api/v1/conversations', - path: '/api/v1/conversations', - getParentRoute: () => rootRouteImport, -} as any) -const ApiV1ConversationQueryRoute = ApiV1ConversationQueryRouteImport.update({ - id: '/api/v1/conversation-query', - path: '/api/v1/conversation-query', +const ApiV1SourcesRoute = ApiV1SourcesRouteImport.update({ + id: '/api/v1/sources', + path: '/api/v1/sources', getParentRoute: () => rootRouteImport, } as any) -const ApiV1ConversationsExportRoute = - ApiV1ConversationsExportRouteImport.update({ - id: '/export', - path: '/export', - getParentRoute: () => ApiV1ConversationsRoute, - } as any) const ApiV1ConversationsDeleteRoute = ApiV1ConversationsDeleteRouteImport.update({ id: '/delete', path: '/delete', getParentRoute: () => ApiV1ConversationsRoute, } as any) -const ApiV1ConversationsSourceIdRoute = - ApiV1ConversationsSourceIdRouteImport.update({ - id: '/$source/$id', - path: '/$source/$id', +const ApiV1ConversationsExportRoute = + ApiV1ConversationsExportRouteImport.update({ + id: '/export', + path: '/export', getParentRoute: () => ApiV1ConversationsRoute, } as any) const ApiV1CodexThreadsEventsRoute = ApiV1CodexThreadsEventsRouteImport.update({ @@ -271,11 +265,11 @@ const ApiV1CodexThreadsEventsRoute = ApiV1CodexThreadsEventsRouteImport.update({ path: '/api/v1/codex/threads/events', getParentRoute: () => rootRouteImport, } as any) -const ApiV1ConversationsSourceIdExportRoute = - ApiV1ConversationsSourceIdExportRouteImport.update({ - id: '/export', - path: '/export', - getParentRoute: () => ApiV1ConversationsSourceIdRoute, +const ApiV1ConversationsSourceIdRoute = + ApiV1ConversationsSourceIdRouteImport.update({ + id: '/$source/$id', + path: '/$source/$id', + getParentRoute: () => ApiV1ConversationsRoute, } as any) const ApiV1ConversationsSourceIdEvidenceRoute = ApiV1ConversationsSourceIdEvidenceRouteImport.update({ @@ -283,6 +277,12 @@ const ApiV1ConversationsSourceIdEvidenceRoute = path: '/evidence', getParentRoute: () => ApiV1ConversationsSourceIdRoute, } as any) +const ApiV1ConversationsSourceIdExportRoute = + ApiV1ConversationsSourceIdExportRouteImport.update({ + id: '/export', + path: '/export', + getParentRoute: () => ApiV1ConversationsSourceIdRoute, + } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -608,18 +608,11 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/settings': { - id: '/settings' - path: '/settings' - fullPath: '/settings' - preLoaderRoute: typeof SettingsRouteImport - parentRoute: typeof rootRouteImport - } - '/analytics': { - id: '/analytics' - path: '/analytics' - fullPath: '/analytics' - preLoaderRoute: typeof AnalyticsRouteImport + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } '/$threadId': { @@ -629,60 +622,67 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ThreadIdRouteImport parentRoute: typeof rootRouteImport } - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport + '/analytics': { + id: '/analytics' + path: '/analytics' + fullPath: '/analytics' + preLoaderRoute: typeof AnalyticsRouteImport parentRoute: typeof rootRouteImport } - '/qoder/': { - id: '/qoder/' - path: '/qoder' - fullPath: '/qoder/' - preLoaderRoute: typeof QoderIndexRouteImport + '/settings': { + id: '/settings' + path: '/settings' + fullPath: '/settings' + preLoaderRoute: typeof SettingsRouteImport parentRoute: typeof rootRouteImport } - '/opencode/': { - id: '/opencode/' - path: '/opencode' - fullPath: '/opencode/' - preLoaderRoute: typeof OpencodeIndexRouteImport + '/antigravity-conversations/$conversationId': { + id: '/antigravity-conversations/$conversationId' + path: '/antigravity-conversations/$conversationId' + fullPath: '/antigravity-conversations/$conversationId' + preLoaderRoute: typeof AntigravityConversationsConversationIdRouteImport parentRoute: typeof rootRouteImport } - '/minimax-code/': { - id: '/minimax-code/' - path: '/minimax-code' - fullPath: '/minimax-code/' - preLoaderRoute: typeof MinimaxCodeIndexRouteImport + '/antigravity/': { + id: '/antigravity/' + path: '/antigravity' + fullPath: '/antigravity/' + preLoaderRoute: typeof AntigravityIndexRouteImport parentRoute: typeof rootRouteImport } - '/kiro/': { - id: '/kiro/' - path: '/kiro' - fullPath: '/kiro/' - preLoaderRoute: typeof KiroIndexRouteImport + '/antigravity/$workspaceKey': { + id: '/antigravity/$workspaceKey' + path: '/antigravity/$workspaceKey' + fullPath: '/antigravity/$workspaceKey' + preLoaderRoute: typeof AntigravityWorkspaceKeyRouteImport parentRoute: typeof rootRouteImport } - '/grok/': { - id: '/grok/' - path: '/grok' - fullPath: '/grok/' - preLoaderRoute: typeof GrokIndexRouteImport + '/claude-code-sessions/$sessionId': { + id: '/claude-code-sessions/$sessionId' + path: '/claude-code-sessions/$sessionId' + fullPath: '/claude-code-sessions/$sessionId' + preLoaderRoute: typeof ClaudeCodeSessionsSessionIdRouteImport parentRoute: typeof rootRouteImport } - '/cursor/': { - id: '/cursor/' - path: '/cursor' - fullPath: '/cursor/' - preLoaderRoute: typeof CursorIndexRouteImport + '/claude-code/': { + id: '/claude-code/' + path: '/claude-code' + fullPath: '/claude-code/' + preLoaderRoute: typeof ClaudeCodeIndexRouteImport parentRoute: typeof rootRouteImport } - '/codex/': { - id: '/codex/' - path: '/codex' - fullPath: '/codex/' - preLoaderRoute: typeof CodexIndexRouteImport + '/claude-code/$workspaceKey': { + id: '/claude-code/$workspaceKey' + path: '/claude-code/$workspaceKey' + fullPath: '/claude-code/$workspaceKey' + preLoaderRoute: typeof ClaudeCodeWorkspaceKeyRouteImport + parentRoute: typeof rootRouteImport + } + '/cline-tasks/$taskId': { + id: '/cline-tasks/$taskId' + path: '/cline-tasks/$taskId' + fullPath: '/cline-tasks/$taskId' + preLoaderRoute: typeof ClineTasksTaskIdRouteImport parentRoute: typeof rootRouteImport } '/cline/': { @@ -692,74 +692,67 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ClineIndexRouteImport parentRoute: typeof rootRouteImport } - '/claude-code/': { - id: '/claude-code/' - path: '/claude-code' - fullPath: '/claude-code/' - preLoaderRoute: typeof ClaudeCodeIndexRouteImport - parentRoute: typeof rootRouteImport - } - '/antigravity/': { - id: '/antigravity/' - path: '/antigravity' - fullPath: '/antigravity/' - preLoaderRoute: typeof AntigravityIndexRouteImport + '/cline/$workspaceKey': { + id: '/cline/$workspaceKey' + path: '/cline/$workspaceKey' + fullPath: '/cline/$workspaceKey' + preLoaderRoute: typeof ClineWorkspaceKeyRouteImport parentRoute: typeof rootRouteImport } - '/threads/$threadId': { - id: '/threads/$threadId' - path: '/threads/$threadId' - fullPath: '/threads/$threadId' - preLoaderRoute: typeof ThreadsThreadIdRouteImport + '/codex/': { + id: '/codex/' + path: '/codex' + fullPath: '/codex/' + preLoaderRoute: typeof CodexIndexRouteImport parentRoute: typeof rootRouteImport } - '/qoder/$workspaceKey': { - id: '/qoder/$workspaceKey' - path: '/qoder/$workspaceKey' - fullPath: '/qoder/$workspaceKey' - preLoaderRoute: typeof QoderWorkspaceKeyRouteImport + '/codex/$project': { + id: '/codex/$project' + path: '/codex/$project' + fullPath: '/codex/$project' + preLoaderRoute: typeof CodexProjectRouteImport parentRoute: typeof rootRouteImport } - '/qoder-sessions/$sessionId': { - id: '/qoder-sessions/$sessionId' - path: '/qoder-sessions/$sessionId' - fullPath: '/qoder-sessions/$sessionId' - preLoaderRoute: typeof QoderSessionsSessionIdRouteImport + '/cursor-threads/$composerId': { + id: '/cursor-threads/$composerId' + path: '/cursor-threads/$composerId' + fullPath: '/cursor-threads/$composerId' + preLoaderRoute: typeof CursorThreadsComposerIdRouteImport parentRoute: typeof rootRouteImport } - '/opencode/$workspaceKey': { - id: '/opencode/$workspaceKey' - path: '/opencode/$workspaceKey' - fullPath: '/opencode/$workspaceKey' - preLoaderRoute: typeof OpencodeWorkspaceKeyRouteImport + '/cursor/': { + id: '/cursor/' + path: '/cursor' + fullPath: '/cursor/' + preLoaderRoute: typeof CursorIndexRouteImport parentRoute: typeof rootRouteImport } - '/opencode-sessions/$sessionId': { - id: '/opencode-sessions/$sessionId' - path: '/opencode-sessions/$sessionId' - fullPath: '/opencode-sessions/$sessionId' - preLoaderRoute: typeof OpencodeSessionsSessionIdRouteImport + '/cursor/$workspaceKey': { + id: '/cursor/$workspaceKey' + path: '/cursor/$workspaceKey' + fullPath: '/cursor/$workspaceKey' + preLoaderRoute: typeof CursorWorkspaceKeyRouteImport parentRoute: typeof rootRouteImport } - '/minimax-code/$workspaceKey': { - id: '/minimax-code/$workspaceKey' - path: '/minimax-code/$workspaceKey' - fullPath: '/minimax-code/$workspaceKey' - preLoaderRoute: typeof MinimaxCodeWorkspaceKeyRouteImport + '/grok-sessions/$sessionId': { + id: '/grok-sessions/$sessionId' + path: '/grok-sessions/$sessionId' + fullPath: '/grok-sessions/$sessionId' + preLoaderRoute: typeof GrokSessionsSessionIdRouteImport parentRoute: typeof rootRouteImport } - '/minimax-code-sessions/$sessionId': { - id: '/minimax-code-sessions/$sessionId' - path: '/minimax-code-sessions/$sessionId' - fullPath: '/minimax-code-sessions/$sessionId' - preLoaderRoute: typeof MinimaxCodeSessionsSessionIdRouteImport + '/grok/': { + id: '/grok/' + path: '/grok' + fullPath: '/grok/' + preLoaderRoute: typeof GrokIndexRouteImport parentRoute: typeof rootRouteImport } - '/kiro/$workspaceKey': { - id: '/kiro/$workspaceKey' - path: '/kiro/$workspaceKey' - fullPath: '/kiro/$workspaceKey' - preLoaderRoute: typeof KiroWorkspaceKeyRouteImport + '/grok/$workspaceKey': { + id: '/grok/$workspaceKey' + path: '/grok/$workspaceKey' + fullPath: '/grok/$workspaceKey' + preLoaderRoute: typeof GrokWorkspaceKeyRouteImport parentRoute: typeof rootRouteImport } '/kiro-sessions/$sessionId': { @@ -769,95 +762,95 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof KiroSessionsSessionIdRouteImport parentRoute: typeof rootRouteImport } - '/grok/$workspaceKey': { - id: '/grok/$workspaceKey' - path: '/grok/$workspaceKey' - fullPath: '/grok/$workspaceKey' - preLoaderRoute: typeof GrokWorkspaceKeyRouteImport + '/kiro/': { + id: '/kiro/' + path: '/kiro' + fullPath: '/kiro/' + preLoaderRoute: typeof KiroIndexRouteImport parentRoute: typeof rootRouteImport } - '/grok-sessions/$sessionId': { - id: '/grok-sessions/$sessionId' - path: '/grok-sessions/$sessionId' - fullPath: '/grok-sessions/$sessionId' - preLoaderRoute: typeof GrokSessionsSessionIdRouteImport + '/kiro/$workspaceKey': { + id: '/kiro/$workspaceKey' + path: '/kiro/$workspaceKey' + fullPath: '/kiro/$workspaceKey' + preLoaderRoute: typeof KiroWorkspaceKeyRouteImport parentRoute: typeof rootRouteImport } - '/cursor/$workspaceKey': { - id: '/cursor/$workspaceKey' - path: '/cursor/$workspaceKey' - fullPath: '/cursor/$workspaceKey' - preLoaderRoute: typeof CursorWorkspaceKeyRouteImport + '/minimax-code-sessions/$sessionId': { + id: '/minimax-code-sessions/$sessionId' + path: '/minimax-code-sessions/$sessionId' + fullPath: '/minimax-code-sessions/$sessionId' + preLoaderRoute: typeof MinimaxCodeSessionsSessionIdRouteImport parentRoute: typeof rootRouteImport } - '/cursor-threads/$composerId': { - id: '/cursor-threads/$composerId' - path: '/cursor-threads/$composerId' - fullPath: '/cursor-threads/$composerId' - preLoaderRoute: typeof CursorThreadsComposerIdRouteImport + '/minimax-code/': { + id: '/minimax-code/' + path: '/minimax-code' + fullPath: '/minimax-code/' + preLoaderRoute: typeof MinimaxCodeIndexRouteImport parentRoute: typeof rootRouteImport } - '/codex/$project': { - id: '/codex/$project' - path: '/codex/$project' - fullPath: '/codex/$project' - preLoaderRoute: typeof CodexProjectRouteImport + '/minimax-code/$workspaceKey': { + id: '/minimax-code/$workspaceKey' + path: '/minimax-code/$workspaceKey' + fullPath: '/minimax-code/$workspaceKey' + preLoaderRoute: typeof MinimaxCodeWorkspaceKeyRouteImport parentRoute: typeof rootRouteImport } - '/cline/$workspaceKey': { - id: '/cline/$workspaceKey' - path: '/cline/$workspaceKey' - fullPath: '/cline/$workspaceKey' - preLoaderRoute: typeof ClineWorkspaceKeyRouteImport + '/opencode-sessions/$sessionId': { + id: '/opencode-sessions/$sessionId' + path: '/opencode-sessions/$sessionId' + fullPath: '/opencode-sessions/$sessionId' + preLoaderRoute: typeof OpencodeSessionsSessionIdRouteImport parentRoute: typeof rootRouteImport } - '/cline-tasks/$taskId': { - id: '/cline-tasks/$taskId' - path: '/cline-tasks/$taskId' - fullPath: '/cline-tasks/$taskId' - preLoaderRoute: typeof ClineTasksTaskIdRouteImport + '/opencode/': { + id: '/opencode/' + path: '/opencode' + fullPath: '/opencode/' + preLoaderRoute: typeof OpencodeIndexRouteImport parentRoute: typeof rootRouteImport } - '/claude-code/$workspaceKey': { - id: '/claude-code/$workspaceKey' - path: '/claude-code/$workspaceKey' - fullPath: '/claude-code/$workspaceKey' - preLoaderRoute: typeof ClaudeCodeWorkspaceKeyRouteImport + '/opencode/$workspaceKey': { + id: '/opencode/$workspaceKey' + path: '/opencode/$workspaceKey' + fullPath: '/opencode/$workspaceKey' + preLoaderRoute: typeof OpencodeWorkspaceKeyRouteImport parentRoute: typeof rootRouteImport } - '/claude-code-sessions/$sessionId': { - id: '/claude-code-sessions/$sessionId' - path: '/claude-code-sessions/$sessionId' - fullPath: '/claude-code-sessions/$sessionId' - preLoaderRoute: typeof ClaudeCodeSessionsSessionIdRouteImport + '/qoder-sessions/$sessionId': { + id: '/qoder-sessions/$sessionId' + path: '/qoder-sessions/$sessionId' + fullPath: '/qoder-sessions/$sessionId' + preLoaderRoute: typeof QoderSessionsSessionIdRouteImport parentRoute: typeof rootRouteImport } - '/antigravity/$workspaceKey': { - id: '/antigravity/$workspaceKey' - path: '/antigravity/$workspaceKey' - fullPath: '/antigravity/$workspaceKey' - preLoaderRoute: typeof AntigravityWorkspaceKeyRouteImport + '/qoder/': { + id: '/qoder/' + path: '/qoder' + fullPath: '/qoder/' + preLoaderRoute: typeof QoderIndexRouteImport parentRoute: typeof rootRouteImport } - '/antigravity-conversations/$conversationId': { - id: '/antigravity-conversations/$conversationId' - path: '/antigravity-conversations/$conversationId' - fullPath: '/antigravity-conversations/$conversationId' - preLoaderRoute: typeof AntigravityConversationsConversationIdRouteImport + '/qoder/$workspaceKey': { + id: '/qoder/$workspaceKey' + path: '/qoder/$workspaceKey' + fullPath: '/qoder/$workspaceKey' + preLoaderRoute: typeof QoderWorkspaceKeyRouteImport parentRoute: typeof rootRouteImport } - '/api/v1/sources': { - id: '/api/v1/sources' - path: '/api/v1/sources' - fullPath: '/api/v1/sources' - preLoaderRoute: typeof ApiV1SourcesRouteImport + '/threads/$threadId': { + id: '/threads/$threadId' + path: '/threads/$threadId' + fullPath: '/threads/$threadId' + preLoaderRoute: typeof ThreadsThreadIdRouteImport parentRoute: typeof rootRouteImport } - '/api/v1/resolve': { - id: '/api/v1/resolve' - path: '/api/v1/resolve' - fullPath: '/api/v1/resolve' - preLoaderRoute: typeof ApiV1ResolveRouteImport + '/api/v1/conversation-query': { + id: '/api/v1/conversation-query' + path: '/api/v1/conversation-query' + fullPath: '/api/v1/conversation-query' + preLoaderRoute: typeof ApiV1ConversationQueryRouteImport parentRoute: typeof rootRouteImport } '/api/v1/conversations': { @@ -867,19 +860,19 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiV1ConversationsRouteImport parentRoute: typeof rootRouteImport } - '/api/v1/conversation-query': { - id: '/api/v1/conversation-query' - path: '/api/v1/conversation-query' - fullPath: '/api/v1/conversation-query' - preLoaderRoute: typeof ApiV1ConversationQueryRouteImport + '/api/v1/resolve': { + id: '/api/v1/resolve' + path: '/api/v1/resolve' + fullPath: '/api/v1/resolve' + preLoaderRoute: typeof ApiV1ResolveRouteImport parentRoute: typeof rootRouteImport } - '/api/v1/conversations/export': { - id: '/api/v1/conversations/export' - path: '/export' - fullPath: '/api/v1/conversations/export' - preLoaderRoute: typeof ApiV1ConversationsExportRouteImport - parentRoute: typeof ApiV1ConversationsRoute + '/api/v1/sources': { + id: '/api/v1/sources' + path: '/api/v1/sources' + fullPath: '/api/v1/sources' + preLoaderRoute: typeof ApiV1SourcesRouteImport + parentRoute: typeof rootRouteImport } '/api/v1/conversations/delete': { id: '/api/v1/conversations/delete' @@ -888,11 +881,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiV1ConversationsDeleteRouteImport parentRoute: typeof ApiV1ConversationsRoute } - '/api/v1/conversations/$source/$id': { - id: '/api/v1/conversations/$source/$id' - path: '/$source/$id' - fullPath: '/api/v1/conversations/$source/$id' - preLoaderRoute: typeof ApiV1ConversationsSourceIdRouteImport + '/api/v1/conversations/export': { + id: '/api/v1/conversations/export' + path: '/export' + fullPath: '/api/v1/conversations/export' + preLoaderRoute: typeof ApiV1ConversationsExportRouteImport parentRoute: typeof ApiV1ConversationsRoute } '/api/v1/codex/threads/events': { @@ -902,12 +895,12 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiV1CodexThreadsEventsRouteImport parentRoute: typeof rootRouteImport } - '/api/v1/conversations/$source/$id/export': { - id: '/api/v1/conversations/$source/$id/export' - path: '/export' - fullPath: '/api/v1/conversations/$source/$id/export' - preLoaderRoute: typeof ApiV1ConversationsSourceIdExportRouteImport - parentRoute: typeof ApiV1ConversationsSourceIdRoute + '/api/v1/conversations/$source/$id': { + id: '/api/v1/conversations/$source/$id' + path: '/$source/$id' + fullPath: '/api/v1/conversations/$source/$id' + preLoaderRoute: typeof ApiV1ConversationsSourceIdRouteImport + parentRoute: typeof ApiV1ConversationsRoute } '/api/v1/conversations/$source/$id/evidence': { id: '/api/v1/conversations/$source/$id/evidence' @@ -916,6 +909,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiV1ConversationsSourceIdEvidenceRouteImport parentRoute: typeof ApiV1ConversationsSourceIdRoute } + '/api/v1/conversations/$source/$id/export': { + id: '/api/v1/conversations/$source/$id/export' + path: '/export' + fullPath: '/api/v1/conversations/$source/$id/export' + preLoaderRoute: typeof ApiV1ConversationsSourceIdExportRouteImport + parentRoute: typeof ApiV1ConversationsSourceIdRoute + } } } diff --git a/apps/ui/src/router.tsx b/src/ui/router.tsx similarity index 100% rename from apps/ui/src/router.tsx rename to src/ui/router.tsx diff --git a/apps/ui/src/routes/$threadId.tsx b/src/ui/routes/$threadId.tsx similarity index 100% rename from apps/ui/src/routes/$threadId.tsx rename to src/ui/routes/$threadId.tsx diff --git a/src/ui/routes/-claude-code-index.vitest.tsx b/src/ui/routes/-claude-code-index.vitest.tsx new file mode 100644 index 0000000..7bc6ace --- /dev/null +++ b/src/ui/routes/-claude-code-index.vitest.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import type { ComponentType, ReactNode } from 'react'; +import { expect, it, vi } from 'vitest'; + +vi.mock('@tanstack/react-router', () => ({ + createFileRoute: + () => + (options: unknown): { options: unknown } => ({ options }), + Link: ({ children }: { children: ReactNode }) => children, +})); + +import { Route } from './claude-code.index'; + +it('should immediately show the Claude Code loading state while the initial route is pending', () => { + const routeOptions = (Route as unknown as { options: { pendingComponent?: ComponentType; pendingMs?: number } }) + .options; + const PendingComponent = routeOptions.pendingComponent; + + expect(PendingComponent).toBeTypeOf('function'); + expect(routeOptions.pendingMs).toBe(0); + if (!PendingComponent) { + throw new Error('Expected Claude Code route to define a pending component'); + } + render(); + + expect(screen.getByText('Loading Claude Code')).toBeTruthy(); + expect(screen.getByText('Loading Claude Code workspace and session metadata.')).toBeTruthy(); +}); diff --git a/src/ui/routes/-cursor-index.vitest.tsx b/src/ui/routes/-cursor-index.vitest.tsx new file mode 100644 index 0000000..6484238 --- /dev/null +++ b/src/ui/routes/-cursor-index.vitest.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import type { ComponentType, ReactNode } from 'react'; +import { expect, it, vi } from 'vitest'; + +vi.mock('@tanstack/react-router', () => ({ + createFileRoute: + () => + (options: unknown): { options: unknown } => ({ options }), + Link: ({ children }: { children: ReactNode }) => children, +})); + +import { Route } from './cursor.index'; + +it('should immediately show the Cursor loading state while the initial route is pending', () => { + const routeOptions = (Route as unknown as { options: { pendingComponent?: ComponentType; pendingMs?: number } }) + .options; + const PendingComponent = routeOptions.pendingComponent; + + expect(PendingComponent).toBeTypeOf('function'); + expect(routeOptions.pendingMs).toBe(0); + if (!PendingComponent) { + throw new Error('Expected Cursor route to define a pending component'); + } + render(); + + expect(screen.getByText('Loading Cursor')).toBeTruthy(); + expect(screen.getByText('Loading Cursor workspace and thread metadata.')).toBeTruthy(); +}); diff --git a/src/ui/routes/-grok-index.vitest.tsx b/src/ui/routes/-grok-index.vitest.tsx new file mode 100644 index 0000000..37e5c86 --- /dev/null +++ b/src/ui/routes/-grok-index.vitest.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import type { ComponentType, ReactNode } from 'react'; +import { expect, it, vi } from 'vitest'; + +vi.mock('@tanstack/react-router', () => ({ + createFileRoute: + () => + (options: unknown): { options: unknown } => ({ options }), + Link: ({ children }: { children: ReactNode }) => children, +})); + +import { Route } from './grok.index'; + +it('should immediately show the Grok loading state while the initial route is pending', () => { + const routeOptions = (Route as unknown as { options: { pendingComponent?: ComponentType; pendingMs?: number } }) + .options; + const PendingComponent = routeOptions.pendingComponent; + + expect(PendingComponent).toBeTypeOf('function'); + expect(routeOptions.pendingMs).toBe(0); + if (!PendingComponent) { + throw new Error('Expected Grok route to define a pending component'); + } + render(); + + expect(screen.getByText('Loading Grok')).toBeTruthy(); + expect(screen.getByText('Loading Grok workspace and session metadata.')).toBeTruthy(); +}); diff --git a/src/ui/routes/-kiro-index.vitest.tsx b/src/ui/routes/-kiro-index.vitest.tsx new file mode 100644 index 0000000..39f755e --- /dev/null +++ b/src/ui/routes/-kiro-index.vitest.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import type { ComponentType, ReactNode } from 'react'; +import { expect, it, vi } from 'vitest'; + +vi.mock('@tanstack/react-router', () => ({ + createFileRoute: + () => + (options: unknown): { options: unknown } => ({ options }), + Link: ({ children }: { children: ReactNode }) => children, +})); + +import { Route } from './kiro.index'; + +it('should show a Kiro loading state while the initial workspace route is pending', () => { + const routeOptions = (Route as unknown as { options: { pendingComponent?: ComponentType; pendingMs?: number } }) + .options; + const PendingComponent = routeOptions.pendingComponent; + + expect(PendingComponent).toBeTypeOf('function'); + expect(routeOptions.pendingMs).toBe(0); + if (!PendingComponent) { + throw new Error('Expected Kiro route to define a pending component'); + } + render(); + + expect(screen.getByText('Loading Kiro')).toBeTruthy(); + expect(screen.getByText('Loading Kiro workspace and session metadata.')).toBeTruthy(); +}); diff --git a/src/ui/routes/-minimax-code-index.vitest.tsx b/src/ui/routes/-minimax-code-index.vitest.tsx new file mode 100644 index 0000000..abc9dce --- /dev/null +++ b/src/ui/routes/-minimax-code-index.vitest.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react'; +import type { ComponentType, ReactNode } from 'react'; +import { expect, it, vi } from 'vitest'; + +vi.mock('@tanstack/react-router', () => ({ + createFileRoute: + () => + (options: unknown): { options: unknown } => ({ options }), + Link: ({ children }: { children: ReactNode }) => children, +})); + +import { Route } from './minimax-code.index'; + +it('should immediately show the MiniMax Code loading state while the initial route is pending', () => { + const routeOptions = (Route as unknown as { options: { pendingComponent?: ComponentType; pendingMs?: number } }) + .options; + const PendingComponent = routeOptions.pendingComponent; + + expect(PendingComponent).toBeTypeOf('function'); + expect(routeOptions.pendingMs).toBe(0); + if (!PendingComponent) { + throw new Error('Expected MiniMax Code route to define a pending component'); + } + render(); + + expect(screen.getByText('Loading MiniMax Code')).toBeTruthy(); + expect(screen.getByText('Loading MiniMax Code workspace and session metadata.')).toBeTruthy(); +}); diff --git a/apps/ui/src/routes/__root.tsx b/src/ui/routes/__root.tsx similarity index 88% rename from apps/ui/src/routes/__root.tsx rename to src/ui/routes/__root.tsx index 0648aab..55c585e 100644 --- a/apps/ui/src/routes/__root.tsx +++ b/src/ui/routes/__root.tsx @@ -52,6 +52,19 @@ function RootErrorComponent({ error }: { error: Error }) { ); } +function RootNotFoundComponent() { + return ( +
+
+

Page not found

+

+ The requested Spiracha route does not exist. +

+
+
+ ); +} + export const Route = createRootRouteWithContext()({ component: RootComponent, errorComponent: RootErrorComponent, @@ -90,6 +103,7 @@ export const Route = createRootRouteWithContext()({ ], }), loader: () => getInitialSettingsFn(), + notFoundComponent: RootNotFoundComponent, }); function RootComponent() { diff --git a/apps/ui/src/routes/analytics.tsx b/src/ui/routes/analytics.tsx similarity index 100% rename from apps/ui/src/routes/analytics.tsx rename to src/ui/routes/analytics.tsx diff --git a/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx b/src/ui/routes/antigravity-conversations.$conversationId.tsx similarity index 100% rename from apps/ui/src/routes/antigravity-conversations.$conversationId.tsx rename to src/ui/routes/antigravity-conversations.$conversationId.tsx diff --git a/apps/ui/src/routes/antigravity.$workspaceKey.tsx b/src/ui/routes/antigravity.$workspaceKey.tsx similarity index 100% rename from apps/ui/src/routes/antigravity.$workspaceKey.tsx rename to src/ui/routes/antigravity.$workspaceKey.tsx diff --git a/apps/ui/src/routes/antigravity.index.tsx b/src/ui/routes/antigravity.index.tsx similarity index 100% rename from apps/ui/src/routes/antigravity.index.tsx rename to src/ui/routes/antigravity.index.tsx diff --git a/apps/ui/src/routes/api.v1.codex.threads.events.ts b/src/ui/routes/api.v1.codex.threads.events.ts similarity index 100% rename from apps/ui/src/routes/api.v1.codex.threads.events.ts rename to src/ui/routes/api.v1.codex.threads.events.ts diff --git a/apps/ui/src/routes/api.v1.conversation-query.ts b/src/ui/routes/api.v1.conversation-query.ts similarity index 100% rename from apps/ui/src/routes/api.v1.conversation-query.ts rename to src/ui/routes/api.v1.conversation-query.ts diff --git a/apps/ui/src/routes/api.v1.conversations.$source.$id.evidence.ts b/src/ui/routes/api.v1.conversations.$source.$id.evidence.ts similarity index 100% rename from apps/ui/src/routes/api.v1.conversations.$source.$id.evidence.ts rename to src/ui/routes/api.v1.conversations.$source.$id.evidence.ts diff --git a/apps/ui/src/routes/api.v1.conversations.$source.$id.export.ts b/src/ui/routes/api.v1.conversations.$source.$id.export.ts similarity index 100% rename from apps/ui/src/routes/api.v1.conversations.$source.$id.export.ts rename to src/ui/routes/api.v1.conversations.$source.$id.export.ts diff --git a/apps/ui/src/routes/api.v1.conversations.$source.$id.ts b/src/ui/routes/api.v1.conversations.$source.$id.ts similarity index 100% rename from apps/ui/src/routes/api.v1.conversations.$source.$id.ts rename to src/ui/routes/api.v1.conversations.$source.$id.ts diff --git a/apps/ui/src/routes/api.v1.conversations.delete.ts b/src/ui/routes/api.v1.conversations.delete.ts similarity index 100% rename from apps/ui/src/routes/api.v1.conversations.delete.ts rename to src/ui/routes/api.v1.conversations.delete.ts diff --git a/apps/ui/src/routes/api.v1.conversations.export.ts b/src/ui/routes/api.v1.conversations.export.ts similarity index 100% rename from apps/ui/src/routes/api.v1.conversations.export.ts rename to src/ui/routes/api.v1.conversations.export.ts diff --git a/apps/ui/src/routes/api.v1.conversations.ts b/src/ui/routes/api.v1.conversations.ts similarity index 100% rename from apps/ui/src/routes/api.v1.conversations.ts rename to src/ui/routes/api.v1.conversations.ts diff --git a/apps/ui/src/routes/api.v1.resolve.ts b/src/ui/routes/api.v1.resolve.ts similarity index 100% rename from apps/ui/src/routes/api.v1.resolve.ts rename to src/ui/routes/api.v1.resolve.ts diff --git a/apps/ui/src/routes/api.v1.sources.ts b/src/ui/routes/api.v1.sources.ts similarity index 100% rename from apps/ui/src/routes/api.v1.sources.ts rename to src/ui/routes/api.v1.sources.ts diff --git a/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx b/src/ui/routes/claude-code-sessions.$sessionId.tsx similarity index 98% rename from apps/ui/src/routes/claude-code-sessions.$sessionId.tsx rename to src/ui/routes/claude-code-sessions.$sessionId.tsx index 8f8d720..d2a2668 100644 --- a/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +++ b/src/ui/routes/claude-code-sessions.$sessionId.tsx @@ -35,7 +35,7 @@ import { } from '#/lib/claude-code-transcript-events'; import { downloadTextFile, downloadUrlFile } from '#/lib/download'; import type { ExportDialogOptions } from '#/lib/export-options'; -import { formatDateTime, formatList, formatNumber, formatTokens } from '#/lib/formatters'; +import { formatDateTime, formatList, formatModelLabel, formatNumber, formatTokens } from '#/lib/formatters'; import { getMutationErrorMessage } from '#/lib/mutation-error'; import { applyPathTransforms } from '#/lib/path-utils'; import { @@ -90,7 +90,10 @@ const buildSessionMetadata = (detail: ClaudeCodeSessionTranscript) => [ }, { label: 'Worktree', value: detail.session.worktree }, { label: 'CWD', value: detail.session.cwd }, - { label: 'Model', value: detail.session.model ?? 'unknown' }, + { + label: 'Model', + value: detail.session.model ? formatModelLabel(detail.session.model) : 'unknown', + }, { label: 'Version', value: detail.session.version ?? 'unknown' }, { label: 'Git branch', value: detail.session.gitBranch ?? 'unknown' }, { label: 'Created', value: {formatDateTime(detail.session.createdAtMs)} }, diff --git a/apps/ui/src/routes/claude-code.$workspaceKey.tsx b/src/ui/routes/claude-code.$workspaceKey.tsx similarity index 100% rename from apps/ui/src/routes/claude-code.$workspaceKey.tsx rename to src/ui/routes/claude-code.$workspaceKey.tsx diff --git a/apps/ui/src/routes/claude-code.index.tsx b/src/ui/routes/claude-code.index.tsx similarity index 89% rename from apps/ui/src/routes/claude-code.index.tsx rename to src/ui/routes/claude-code.index.tsx index deb565a..7e64bf2 100644 --- a/apps/ui/src/routes/claude-code.index.tsx +++ b/src/ui/routes/claude-code.index.tsx @@ -3,6 +3,7 @@ import { createFileRoute } from '@tanstack/react-router'; import { useDeferredValue, useState } from 'react'; import { ClaudeCodeWorkspacesTable } from '#/components/claude-code-workspaces-table'; import { ListSearchInput } from '#/components/list-search-input'; +import { LoadingPanel } from '#/components/loading-panel'; import { PageHeader } from '#/components/page-header'; import { RouteErrorPanel } from '#/components/route-error-panel'; import { claudeCodeWorkspacesQueryOptions } from '#/lib/claude-code-queries'; @@ -12,6 +13,10 @@ export const Route = createFileRoute('/claude-code/')({ component: ClaudeCodePage, errorComponent: ClaudeCodeErrorComponent, loader: ({ context }) => context.queryClient.ensureQueryData(claudeCodeWorkspacesQueryOptions()), + pendingComponent: () => ( + + ), + pendingMs: 0, }); function ClaudeCodeErrorComponent({ error }: { error: Error }) { diff --git a/apps/ui/src/routes/cline-tasks.$taskId.tsx b/src/ui/routes/cline-tasks.$taskId.tsx similarity index 100% rename from apps/ui/src/routes/cline-tasks.$taskId.tsx rename to src/ui/routes/cline-tasks.$taskId.tsx diff --git a/apps/ui/src/routes/cline.$workspaceKey.tsx b/src/ui/routes/cline.$workspaceKey.tsx similarity index 100% rename from apps/ui/src/routes/cline.$workspaceKey.tsx rename to src/ui/routes/cline.$workspaceKey.tsx diff --git a/apps/ui/src/routes/cline.index.tsx b/src/ui/routes/cline.index.tsx similarity index 100% rename from apps/ui/src/routes/cline.index.tsx rename to src/ui/routes/cline.index.tsx diff --git a/apps/ui/src/routes/codex.$project.tsx b/src/ui/routes/codex.$project.tsx similarity index 100% rename from apps/ui/src/routes/codex.$project.tsx rename to src/ui/routes/codex.$project.tsx diff --git a/apps/ui/src/routes/codex.index.tsx b/src/ui/routes/codex.index.tsx similarity index 100% rename from apps/ui/src/routes/codex.index.tsx rename to src/ui/routes/codex.index.tsx diff --git a/apps/ui/src/routes/cursor-threads.$composerId.tsx b/src/ui/routes/cursor-threads.$composerId.tsx similarity index 96% rename from apps/ui/src/routes/cursor-threads.$composerId.tsx rename to src/ui/routes/cursor-threads.$composerId.tsx index 97a6e01..1e722c4 100644 --- a/apps/ui/src/routes/cursor-threads.$composerId.tsx +++ b/src/ui/routes/cursor-threads.$composerId.tsx @@ -21,7 +21,7 @@ import { deleteCursorThreadsFn, exportCursorThreadFn, type getCursorThreadDetail import { cursorTranscriptToThreadEvents, getCursorThreadTranscriptStats } from '#/lib/cursor-transcript-events'; import { downloadTextFile, downloadUrlFile } from '#/lib/download'; import type { ExportDialogOptions } from '#/lib/export-options'; -import { formatBytes, formatDateTime, formatList, formatNumber } from '#/lib/formatters'; +import { formatBytes, formatDateTime, formatList, formatModelLabel, formatNumber } from '#/lib/formatters'; import { getTranscriptDisplayState, parseThreadTranscriptSearch, @@ -50,6 +50,8 @@ const buildCursorThreadMetadata = (detail: CursorThreadDetail) => { }, { label: 'Workspace key', value: {detail.thread.workspaceKey} }, { label: 'Mode', value: detail.thread.mode ?? 'unknown' }, + { label: 'Model', value: detail.thread.model ? formatModelLabel(detail.thread.model) : 'unknown' }, + { label: 'Reasoning', value: detail.thread.reasoningEffort ?? 'unknown' }, { label: 'Created', value: {formatDateTime(detail.thread.createdAtMs)}, @@ -252,7 +254,11 @@ const CursorThreadDetailPage = () => {
- + { /> {detail.transcript && transcriptEvents.length > 0 ? ( { return ; }; +const CursorPendingComponent = () => ( + +); + const CursorPage = () => { const queryClient = useQueryClient(); const workspaces = useSuspenseQuery(cursorWorkspacesQueryOptions()).data; @@ -120,4 +125,6 @@ export const Route = createFileRoute('/cursor/')({ component: CursorPage, errorComponent: CursorErrorComponent, loader: ({ context }) => context.queryClient.ensureQueryData(cursorWorkspacesQueryOptions()), + pendingComponent: CursorPendingComponent, + pendingMs: 0, }); diff --git a/apps/ui/src/routes/grok-sessions.$sessionId.tsx b/src/ui/routes/grok-sessions.$sessionId.tsx similarity index 100% rename from apps/ui/src/routes/grok-sessions.$sessionId.tsx rename to src/ui/routes/grok-sessions.$sessionId.tsx diff --git a/apps/ui/src/routes/grok.$workspaceKey.tsx b/src/ui/routes/grok.$workspaceKey.tsx similarity index 100% rename from apps/ui/src/routes/grok.$workspaceKey.tsx rename to src/ui/routes/grok.$workspaceKey.tsx diff --git a/apps/ui/src/routes/grok.index.tsx b/src/ui/routes/grok.index.tsx similarity index 88% rename from apps/ui/src/routes/grok.index.tsx rename to src/ui/routes/grok.index.tsx index dbb533a..54a476f 100644 --- a/apps/ui/src/routes/grok.index.tsx +++ b/src/ui/routes/grok.index.tsx @@ -3,6 +3,7 @@ import { createFileRoute } from '@tanstack/react-router'; import { useDeferredValue, useState } from 'react'; import { GrokWorkspacesTable } from '#/components/grok-workspaces-table'; import { ListSearchInput } from '#/components/list-search-input'; +import { LoadingPanel } from '#/components/loading-panel'; import { PageHeader } from '#/components/page-header'; import { RouteErrorPanel } from '#/components/route-error-panel'; import { grokWorkspacesQueryOptions } from '#/lib/grok-queries'; @@ -12,6 +13,10 @@ const GrokErrorComponent = ({ error }: { error: Error }) => { return ; }; +const GrokPendingComponent = () => ( + +); + const GrokPage = () => { const workspaces = useSuspenseQuery(grokWorkspacesQueryOptions()).data; const [searchInput, setSearchInput] = useState(''); @@ -45,4 +50,6 @@ export const Route = createFileRoute('/grok/')({ component: GrokPage, errorComponent: GrokErrorComponent, loader: ({ context }) => context.queryClient.ensureQueryData(grokWorkspacesQueryOptions()), + pendingComponent: GrokPendingComponent, + pendingMs: 0, }); diff --git a/apps/ui/src/routes/index.tsx b/src/ui/routes/index.tsx similarity index 100% rename from apps/ui/src/routes/index.tsx rename to src/ui/routes/index.tsx diff --git a/apps/ui/src/routes/kiro-sessions.$sessionId.tsx b/src/ui/routes/kiro-sessions.$sessionId.tsx similarity index 100% rename from apps/ui/src/routes/kiro-sessions.$sessionId.tsx rename to src/ui/routes/kiro-sessions.$sessionId.tsx diff --git a/apps/ui/src/routes/kiro.$workspaceKey.tsx b/src/ui/routes/kiro.$workspaceKey.tsx similarity index 100% rename from apps/ui/src/routes/kiro.$workspaceKey.tsx rename to src/ui/routes/kiro.$workspaceKey.tsx diff --git a/apps/ui/src/routes/kiro.index.tsx b/src/ui/routes/kiro.index.tsx similarity index 89% rename from apps/ui/src/routes/kiro.index.tsx rename to src/ui/routes/kiro.index.tsx index 781b199..d730087 100644 --- a/apps/ui/src/routes/kiro.index.tsx +++ b/src/ui/routes/kiro.index.tsx @@ -3,6 +3,7 @@ import { createFileRoute } from '@tanstack/react-router'; import { useDeferredValue, useState } from 'react'; import { KiroWorkspacesTable } from '#/components/kiro-workspaces-table'; import { ListSearchInput } from '#/components/list-search-input'; +import { LoadingPanel } from '#/components/loading-panel'; import { PageHeader } from '#/components/page-header'; import { RouteErrorPanel } from '#/components/route-error-panel'; import { kiroWorkspacesQueryOptions } from '#/lib/kiro-queries'; @@ -45,4 +46,8 @@ export const Route = createFileRoute('/kiro/')({ component: KiroPage, errorComponent: KiroErrorComponent, loader: ({ context }) => context.queryClient.ensureQueryData(kiroWorkspacesQueryOptions()), + pendingComponent: () => ( + + ), + pendingMs: 0, }); diff --git a/apps/ui/src/routes/minimax-code-sessions.$sessionId.tsx b/src/ui/routes/minimax-code-sessions.$sessionId.tsx similarity index 100% rename from apps/ui/src/routes/minimax-code-sessions.$sessionId.tsx rename to src/ui/routes/minimax-code-sessions.$sessionId.tsx diff --git a/apps/ui/src/routes/minimax-code.$workspaceKey.tsx b/src/ui/routes/minimax-code.$workspaceKey.tsx similarity index 100% rename from apps/ui/src/routes/minimax-code.$workspaceKey.tsx rename to src/ui/routes/minimax-code.$workspaceKey.tsx diff --git a/apps/ui/src/routes/minimax-code.index.tsx b/src/ui/routes/minimax-code.index.tsx similarity index 99% rename from apps/ui/src/routes/minimax-code.index.tsx rename to src/ui/routes/minimax-code.index.tsx index ba18361..09af6f5 100644 --- a/apps/ui/src/routes/minimax-code.index.tsx +++ b/src/ui/routes/minimax-code.index.tsx @@ -47,4 +47,5 @@ export const Route = createFileRoute('/minimax-code/')({ pendingComponent: () => ( ), + pendingMs: 0, }); diff --git a/apps/ui/src/routes/opencode-sessions.$sessionId.tsx b/src/ui/routes/opencode-sessions.$sessionId.tsx similarity index 100% rename from apps/ui/src/routes/opencode-sessions.$sessionId.tsx rename to src/ui/routes/opencode-sessions.$sessionId.tsx diff --git a/apps/ui/src/routes/opencode.$workspaceKey.tsx b/src/ui/routes/opencode.$workspaceKey.tsx similarity index 100% rename from apps/ui/src/routes/opencode.$workspaceKey.tsx rename to src/ui/routes/opencode.$workspaceKey.tsx diff --git a/apps/ui/src/routes/opencode.index.tsx b/src/ui/routes/opencode.index.tsx similarity index 100% rename from apps/ui/src/routes/opencode.index.tsx rename to src/ui/routes/opencode.index.tsx diff --git a/apps/ui/src/routes/qoder-sessions.$sessionId.tsx b/src/ui/routes/qoder-sessions.$sessionId.tsx similarity index 100% rename from apps/ui/src/routes/qoder-sessions.$sessionId.tsx rename to src/ui/routes/qoder-sessions.$sessionId.tsx diff --git a/apps/ui/src/routes/qoder.$workspaceKey.tsx b/src/ui/routes/qoder.$workspaceKey.tsx similarity index 100% rename from apps/ui/src/routes/qoder.$workspaceKey.tsx rename to src/ui/routes/qoder.$workspaceKey.tsx diff --git a/apps/ui/src/routes/qoder.index.tsx b/src/ui/routes/qoder.index.tsx similarity index 100% rename from apps/ui/src/routes/qoder.index.tsx rename to src/ui/routes/qoder.index.tsx diff --git a/apps/ui/src/routes/settings.tsx b/src/ui/routes/settings.tsx similarity index 100% rename from apps/ui/src/routes/settings.tsx rename to src/ui/routes/settings.tsx diff --git a/apps/ui/src/routes/threads.$threadId.tsx b/src/ui/routes/threads.$threadId.tsx similarity index 100% rename from apps/ui/src/routes/threads.$threadId.tsx rename to src/ui/routes/threads.$threadId.tsx diff --git a/apps/ui/src/styles.css b/src/ui/styles.css similarity index 100% rename from apps/ui/src/styles.css rename to src/ui/styles.css diff --git a/apps/ui/vitest.setup.ts b/src/ui/vitest.setup.ts similarity index 100% rename from apps/ui/vitest.setup.ts rename to src/ui/vitest.setup.ts diff --git a/tsconfig.json b/tsconfig.json index ab5edf9..4b5e41b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "allowImportingTsExtensions": true, "allowSyntheticDefaultImports": true, "allowUnreachableCode": false, "allowUnusedLabels": false, @@ -9,7 +10,8 @@ "forceConsistentCasingInFileNames": true, "importHelpers": false, "isolatedModules": true, - "lib": ["ESNext", "DOM"], + "jsx": "react-jsx", + "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleDetection": "force", "moduleResolution": "Bundler", @@ -20,7 +22,9 @@ "noUnusedParameters": true, "outDir": "./dist", "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@spiracha/*": ["./src/*"], + "#/*": ["./src/ui/*"] }, "removeComments": false, "rootDir": ".", @@ -29,7 +33,7 @@ "sourceMap": true, "strict": true, "target": "ESNext", - "types": ["bun", "node"], + "types": ["bun", "node", "vite/client"], "verbatimModuleSyntax": true }, "exclude": ["node_modules", "dist"], diff --git a/apps/ui/vite.config.ts b/vite.config.ts similarity index 89% rename from apps/ui/vite.config.ts rename to vite.config.ts index 246ba2d..16a57f8 100644 --- a/apps/ui/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,6 @@ import { createReadStream } from 'node:fs'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import tailwindcss from '@tailwindcss/vite'; import { devtools } from '@tanstack/devtools-vite'; import { tanstackStart } from '@tanstack/react-start/plugin/vite'; @@ -9,9 +10,9 @@ import { buildUiExportContentDisposition, resolveReadableUiExportFileFromRequestPath, UI_EXPORT_URL_PREFIX, -} from '../../src/lib/ui-export-files'; +} from './src/lib/ui-export-files.ts'; -const uiRoot = __dirname; +const appRoot = path.dirname(fileURLToPath(import.meta.url)); const getExportContentType = (filePath: string) => { if (filePath.endsWith('.zip')) { @@ -76,17 +77,17 @@ const spirachaExportFiles = (): Plugin => { }; const config = defineConfig({ - plugins: [spirachaExportFiles(), devtools(), tailwindcss(), tanstackStart(), viteReact()], + plugins: [spirachaExportFiles(), devtools(), tailwindcss(), tanstackStart({ srcDirectory: 'src/ui' }), viteReact()], resolve: { alias: { - '@spiracha': path.resolve(uiRoot, '..', '..', 'src'), + '@spiracha': path.resolve(appRoot, 'src'), }, tsconfigPaths: true, }, - root: uiRoot, + root: appRoot, server: { fs: { - allow: [path.resolve(uiRoot, '..', '..')], + allow: [appRoot], }, }, }); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..d9d6104 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,29 @@ +import path from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '@spiracha': path.resolve(__dirname, 'src'), + '#': path.resolve(__dirname, 'src/ui'), + }, + }, + root: __dirname, + test: { + coverage: { + exclude: ['src/ui/routeTree.gen.ts', 'src/ui/integrations/**', 'src/ui/router.tsx', 'src/ui/routes/**'], + include: ['src/ui/components/**/*.tsx', 'src/ui/lib/**/*.ts', 'src/ui/lib/**/*.tsx'], + provider: 'v8', + reporter: ['text', 'lcov'], + reportsDirectory: 'coverage/ui', + }, + environment: 'jsdom', + environmentOptions: { + jsdom: { + url: 'http://localhost', + }, + }, + include: ['src/ui/**/*.vitest.ts', 'src/ui/**/*.vitest.tsx'], + setupFiles: ['src/ui/vitest.setup.ts'], + }, +});