From 7a3cf55b2b5ae49b72ff4580f4fff83fa7e9cfc9 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Tue, 12 May 2026 23:09:27 +0300 Subject: [PATCH 01/25] docs: add terminal mode feature design spec Co-Authored-By: Claude Sonnet 4.6 --- docs/specs/2026-05-12-terminal-mode-design.md | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 docs/specs/2026-05-12-terminal-mode-design.md diff --git a/docs/specs/2026-05-12-terminal-mode-design.md b/docs/specs/2026-05-12-terminal-mode-design.md new file mode 100644 index 0000000..7c9824a --- /dev/null +++ b/docs/specs/2026-05-12-terminal-mode-design.md @@ -0,0 +1,160 @@ +# Terminal Mode Design + +**Date:** 2026-05-12 +**Branch:** feat/terminal +**Status:** Approved + +## Context + +The session view currently shows only a chat interface (MessageList + TerminalDrawer drawer + ChatInput). The TerminalDrawer renders PTY output from the claude CLI spawned in non-interactive, structured-JSON mode (`--print --output-format stream-json`). There is no way to interact with the Claude Code CLI as a real terminal. + +This feature adds a global `session_mode` setting (`chat` | `terminal`) that switches the session view between the existing chat layout and a full terminal experience backed by a proper interactive PTY. + +## Approach + +Separate WebSocket endpoint (`/ws/terminal/:id`) alongside the existing `/ws/session/:id`. The two paths are fully independent — no changes to the existing chat session handler. Sessions carry their mode in the database so resuming always uses the correct endpoint. + +## Architecture + +``` +CHAT MODE TERMINAL MODE +───────────────────────────── ───────────────────────────── +POST /api/sessions POST /api/sessions + → mode: 'chat' stored in DB → mode: 'terminal' stored in DB + ↓ ↓ +WS /ws/session/:id WS /ws/terminal/:id (NEW) + → claude --print → claude (interactive PTY) + --output-format stream-json → raw I/O passthrough, no JSON + → structured message parsing → no parsing + ↓ ↓ +DashboardView DashboardView + → SessionHeader (shared) → SessionHeader (shared) + → MessageList → TerminalSession.tsx (NEW) + → TerminalDrawer (full-height xterm.js) + → ChatInput +``` + +## Backend Changes + +### `backend/src/routes/settings.ts` +Add `session_mode` to DEFAULTS and ALLOWED set: +```typescript +const DEFAULTS = { bypass_permissions: 'true', session_mode: 'chat' } +``` + +### `backend/src/db/schema.ts` +Add `mode` column to sessions table in CREATE TABLE statement: +```sql +mode TEXT NOT NULL DEFAULT 'chat' +``` +Add a migration guard at DB init time: +```sql +ALTER TABLE sessions ADD COLUMN mode TEXT DEFAULT 'chat' +-- guarded by checking existing columns first +``` + +### `backend/src/routes/sessions.ts` +- **POST** `/api/sessions`: read `session_mode` from settings at creation time, store as `mode` on the new session row +- **GET** `/api/sessions`: include `mode` field in each session object returned +- **POST** `/api/sessions/:id/stop`: after existing SessionManager check, also call `terminalManager.stop(id)` to kill a running terminal PTY + +### `backend/src/ws/terminal.ts` (NEW, ~120 lines) + +`TerminalManager` singleton — tracks active terminal PTYs by session ID (mirrors the existing `SessionManager` pattern). + +WebSocket handler for `/ws/terminal/:id`: +- On connect: look up session from DB; spawn `claude` as persistent interactive PTY + - Args: `--dangerously-skip-permissions` when bypass_permissions=true, nothing else + - PTY config: `xterm-256color`, cols=220, rows=50, cwd=session workdir, inherits env +- Messages **in**: + - `{ type: 'input', data: string }` → write to PTY stdin + - `{ type: 'resize', cols: number, rows: number }` → resize PTY +- Messages **out**: + - `{ type: 'output', data: string }` → raw PTY bytes (ANSI preserved) + - `{ type: 'status', state: 'connected' | 'disconnected' }` +- On PTY exit: broadcast `status: disconnected`, mark session `ended_at` in DB +- Idle timeout: 30 minutes (same as chat sessions) + +### `backend/src/server.ts` +Register new WS route: `server.register(terminalWsPlugin)` alongside existing session WS. + +## Frontend Changes + +### `frontend/src/views/SettingsView.tsx` +Add a "Session Mode" toggle row using the same pattern as the `bypass_permissions` toggle. Two options: **Chat** (default) | **Terminal**. POSTs `{ session_mode: 'chat' | 'terminal' }` to `/api/settings` on change. Applies to all new sessions. + +### `frontend/src/context/SessionContext.tsx` +Add `mode: 'chat' | 'terminal'` to `SessionState`. Populate it via: +- `SESSION_CREATED` action (includes mode from global setting at creation time) +- `RESUME_SESSION` action (includes mode from the session's DB record) + +This is the source of truth for which UI to render — not the global setting directly. + +### `frontend/src/hooks/useDashboard.ts` +Extend the existing parallel fetch to also load `GET /api/settings`. Return `defaultSessionMode: 'chat' | 'terminal'` (used only when creating new sessions to pre-populate the mode). + +### `frontend/src/views/DashboardView.tsx` +Two targeted changes: +1. Pass `null` to `useWebSocket` when `state.mode === 'terminal'` to prevent the chat WS from connecting +2. Swap session content based on `state.mode` — SessionHeader is always rendered: +```tsx +{state.mode === 'terminal' + ? + : <> + + + + +} +``` + +### `frontend/src/hooks/useTerminalSession.ts` (NEW) +Mirrors `useWebSocket` structure but targets `/ws/terminal/:id`: +- Connects on mount when `sessionId` is set +- Sends `{ type: 'input', data }` and `{ type: 'resize', cols, rows }` +- On `status: connected` → dispatch `WS_STATE('idle')` to SessionContext +- On `status: disconnected` → dispatch `WS_STATE('disconnected')` +- On `output` → call `onOutput(data)` callback for xterm write +- Reconnect: exponential backoff up to 5 attempts, then 30s polling (same as useWebSocket) + +### `frontend/src/components/TerminalSession.tsx` (NEW) +Full-height xterm.js terminal filling the space between SessionHeader and viewport bottom: +- Same xterm theme and FitAddon config as existing `TerminalDrawer` +- All keystrokes → `type: 'input'` via useTerminalSession +- ResizeObserver on container → `type: 'resize'` +- `onOutput` callback writes raw data to xterm instance +- No MessageList, no ChatInput — the terminal IS the interface + +### `SessionHeader` (unchanged) +Works for both modes. In terminal mode naturally shows fewer stats (no tokens counter, no running spinner — status is connected/idle). Session name, workdir, stop button, new session button, rename all function identically. + +## Session Resumption + +Sessions carry `mode` in the DB. The session list (`GET /api/sessions`) returns `mode` per session. When `DashboardView.handleResume()` is called, it dispatches `RESUME_SESSION` with the session's stored `mode` — this updates `SessionContext.state.mode` and the correct UI renders automatically. The global setting (`defaultSessionMode`) only determines the mode stored when a **new** session is created; it does not affect resumed sessions. + +## Files to Create +- `backend/src/ws/terminal.ts` +- `frontend/src/hooks/useTerminalSession.ts` +- `frontend/src/components/TerminalSession.tsx` + +## Files to Modify +- `backend/src/routes/settings.ts` +- `backend/src/db/schema.ts` (or wherever migrations run) +- `backend/src/routes/sessions.ts` +- `backend/src/server.ts` +- `frontend/src/context/SessionContext.tsx` +- `frontend/src/hooks/useDashboard.ts` +- `frontend/src/views/DashboardView.tsx` +- `frontend/src/views/SettingsView.tsx` + +## Verification + +1. **Settings**: Open `/settings`, confirm new "Session Mode" toggle appears and saves (Chat ↔ Terminal) +2. **Terminal session creation**: Switch to Terminal mode, create a new session — confirm PTY spawns `claude` interactively, full terminal renders in session view +3. **Chat session creation**: Switch back to Chat mode, create a session — confirm existing chat layout is untouched +4. **I/O passthrough**: In terminal mode, type a prompt directly in the terminal, confirm response renders with ANSI formatting +5. **Resize**: Resize browser window, confirm PTY cols/rows update +6. **Stop**: Click Stop in session header while terminal is active — confirm PTY is killed, session marked ended +7. **Session list**: Confirm both chat and terminal sessions appear in the list and resume correctly +8. **Idle timeout**: Verify terminal PTY is killed after 30 min of inactivity (same as chat) +9. **Lint**: `make lint` passes in both packages From c42e79fb612afdf2c5c0b1c9b27171b436be7ca1 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Tue, 12 May 2026 23:17:23 +0300 Subject: [PATCH 02/25] docs: add terminal mode implementation plan Co-Authored-By: Claude Sonnet 4.6 --- docs/plans/2026-05-12-terminal-mode.md | 1029 ++++++++++++++++++++++++ 1 file changed, 1029 insertions(+) create mode 100644 docs/plans/2026-05-12-terminal-mode.md diff --git a/docs/plans/2026-05-12-terminal-mode.md b/docs/plans/2026-05-12-terminal-mode.md new file mode 100644 index 0000000..296b8dc --- /dev/null +++ b/docs/plans/2026-05-12-terminal-mode.md @@ -0,0 +1,1029 @@ +# Terminal Mode Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a global `session_mode` setting (`chat` | `terminal`) that switches the session content area between the existing chat layout and a full interactive PTY terminal backed by a new `/ws/terminal/:id` WebSocket endpoint. + +**Architecture:** A separate `ws/terminal.ts` module handles interactive PTY sessions (no JSON parsing, raw I/O passthrough) alongside the untouched `ws/session.ts`. Sessions carry their mode in the DB; `SessionContext` holds `mode` for the active session, and `DashboardView` renders either `TerminalSession` (new) or the existing chat layout based on it. + +**Tech Stack:** node-pty, better-sqlite3, Fastify WebSocket, xterm.js + FitAddon, React + TypeScript, Tailwind CSS + +--- + +## File Map + +**Create:** +- `backend/src/ws/terminal.ts` — TerminalManager singleton + `/ws/terminal/:id` WS handler +- `frontend/src/hooks/useTerminalSession.ts` — WS hook for `/ws/terminal/:id` +- `frontend/src/components/TerminalSession.tsx` — full-height xterm.js terminal component + +**Modify:** +- `backend/src/db/schema.ts` — add `mode` column migration guard to sessions table +- `backend/src/routes/settings.ts` — add `session_mode` to DEFAULTS/ALLOWED +- `backend/src/routes/sessions.ts` — POST stores mode; stop+delete kill terminal PTY +- `backend/src/server.ts` — register `terminalWsRoutes` +- `frontend/src/context/SessionContext.tsx` — add `mode` to SessionState + actions +- `frontend/src/hooks/useDashboard.ts` — add `mode` to Session type; fetch settings; return `defaultSessionMode` +- `frontend/src/hooks/useWebSocket.ts` — skip connecting when `state.mode === 'terminal'` +- `frontend/src/views/DashboardView.tsx` — dispatch mode on create/resume; swap content area +- `frontend/src/views/SettingsView.tsx` — add Session Mode toggle + +--- + +## Task 1: DB migration + settings key + +**Files:** +- Modify: `backend/src/db/schema.ts` +- Modify: `backend/src/routes/settings.ts` + +- [ ] **Step 1: Add `mode` column migration guard to `schema.ts`** + + In `backend/src/db/schema.ts`, add this block after the existing `working_time_ms` migration guard (after line 69): + + ```typescript + if (!sessionCols.find((c) => c.name === 'mode')) { + db.prepare("ALTER TABLE sessions ADD COLUMN mode TEXT NOT NULL DEFAULT 'chat'").run() + } + ``` + +- [ ] **Step 2: Add `session_mode` to settings DEFAULTS** + + In `backend/src/routes/settings.ts`, replace lines 4-6: + + ```typescript + const DEFAULTS: Record = { + bypass_permissions: 'true', + session_mode: 'chat', + } + ``` + +- [ ] **Step 3: Lint backend** + + ```bash + cd backend && npm run lint + ``` + Expected: no errors. + +- [ ] **Step 4: Commit** + + ```bash + git add backend/src/db/schema.ts backend/src/routes/settings.ts + git commit -m "feat: add session mode DB column and settings key" + ``` + +--- + +## Task 2: Terminal WebSocket handler + +**Files:** +- Create: `backend/src/ws/terminal.ts` + +- [ ] **Step 1: Create `backend/src/ws/terminal.ts`** + + ```typescript + import * as pty from 'node-pty' + import * as os from 'os' + import * as path from 'path' + import type { WebSocket } from 'ws' + import type { FastifyInstance } from 'fastify' + import { db } from '../db/schema' + + const IDLE_TIMEOUT_MS = 30 * 60 * 1000 + + function getBypassPermissions(): boolean { + const row = db + .prepare('SELECT value FROM settings WHERE key = ?') + .get('bypass_permissions') as { value: string } | undefined + return row ? row.value === 'true' : true + } + + class ActiveTerminalSession { + private ptyProc: pty.IPty | null = null + private sockets = new Set() + private idleTimer: NodeJS.Timeout | null = null + + constructor( + readonly id: string, + private readonly workdir: string, + ) { + this.spawnPty() + this.resetIdle() + } + + private spawnPty() { + const claudeBin = process.env.CLAUDE_BIN ?? 'claude' + const bypassPermissions = getBypassPermissions() + const args: string[] = [] + if (bypassPermissions) args.push('--dangerously-skip-permissions') + + const resolvedCwd = this.workdir.startsWith('~') + ? path.join(os.homedir(), this.workdir.slice(1)) + : this.workdir + + try { + this.ptyProc = pty.spawn(claudeBin, args, { + name: 'xterm-256color', + cols: 220, + rows: 50, + cwd: resolvedCwd, + env: { ...process.env } as Record, + }) + } catch (err) { + this.broadcast({ type: 'output', data: `\r\nError starting terminal: ${(err as Error).message}\r\n` }) + this.broadcast({ type: 'status', state: 'disconnected' }) + return + } + + this.ptyProc.onData((data) => { + this.resetIdle() + this.broadcast({ type: 'output', data }) + }) + + this.ptyProc.onExit(() => { + this.ptyProc = null + db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), this.id) + this.broadcast({ type: 'status', state: 'disconnected' }) + if (this.idleTimer) clearTimeout(this.idleTimer) + }) + } + + attach(ws: WebSocket) { + this.sockets.add(ws) + ws.send(JSON.stringify({ type: 'status', state: 'connected' })) + ws.on('close', () => this.sockets.delete(ws)) + } + + writeInput(data: string) { + this.resetIdle() + this.ptyProc?.write(data) + } + + resize(cols: number, rows: number) { + this.ptyProc?.resize(cols, rows) + } + + kill() { + db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), this.id) + if (this.ptyProc) { + this.ptyProc.kill() + this.ptyProc = null + } + if (this.idleTimer) clearTimeout(this.idleTimer) + this.broadcast({ type: 'status', state: 'disconnected' }) + } + + private broadcast(msg: { type: string; data?: string; state?: string }) { + const payload = JSON.stringify(msg) + for (const ws of this.sockets) { + if (ws.readyState === ws.OPEN) ws.send(payload) + } + } + + private resetIdle() { + if (this.idleTimer) clearTimeout(this.idleTimer) + this.idleTimer = setTimeout(() => this.kill(), IDLE_TIMEOUT_MS) + } + } + + class TerminalManager { + private sessions = new Map() + + getOrCreate(id: string, workdir: string): ActiveTerminalSession { + if (!this.sessions.has(id)) { + this.sessions.set(id, new ActiveTerminalSession(id, workdir)) + } + return this.sessions.get(id)! + } + + kill(id: string) { + this.sessions.get(id)?.kill() + this.sessions.delete(id) + } + } + + export const terminalManager = new TerminalManager() + + export async function terminalWsRoutes(fastify: FastifyInstance) { + fastify.get<{ Params: { id: string } }>( + '/ws/terminal/:id', + { websocket: true }, + (socket, req) => { + const { id } = req.params + + const row = db.prepare('SELECT workdir, ended_at FROM sessions WHERE id = ?').get(id) as + | { workdir: string; ended_at: number | null } + | undefined + + if (!row) { + socket.send(JSON.stringify({ type: 'status', state: 'error', data: 'session not found' })) + socket.close() + return + } + + if (row.ended_at !== null) { + db.prepare('UPDATE sessions SET ended_at = NULL WHERE id = ?').run(id) + } + + const session = terminalManager.getOrCreate(id, row.workdir) + session.attach(socket) + + socket.on('message', (raw: Buffer | string) => { + try { + const msg = JSON.parse(raw.toString()) as { + type: string + data?: string + cols?: number + rows?: number + } + if (msg.type === 'input' && msg.data) { + session.writeInput(msg.data) + } else if (msg.type === 'resize' && msg.cols && msg.rows) { + session.resize(msg.cols, msg.rows) + } + } catch { + // ignore malformed frames + } + }) + }, + ) + } + ``` + +- [ ] **Step 2: Lint backend** + + ```bash + cd backend && npm run lint + ``` + Expected: no errors. + +- [ ] **Step 3: Commit** + + ```bash + git add backend/src/ws/terminal.ts + git commit -m "feat: add terminal WS handler and TerminalManager" + ``` + +--- + +## Task 3: Sessions routes + server registration + +**Files:** +- Modify: `backend/src/routes/sessions.ts` +- Modify: `backend/src/server.ts` + +- [ ] **Step 1: Import `terminalManager` in `sessions.ts`** + + Replace the import block at the top of `backend/src/routes/sessions.ts` (lines 1-8): + + ```typescript + import type { FastifyInstance } from 'fastify' + import { v4 as uuidv4 } from 'uuid' + import fs from 'fs' + import os from 'os' + import path from 'path' + import { db } from '../db/schema' + import { sessionManager } from '../ws/session' + import { terminalManager } from '../ws/terminal' + ``` + +- [ ] **Step 2: Store mode in POST `/api/sessions`** + + Replace the POST handler body (lines 45-57): + + ```typescript + fastify.post<{ Body: { workdir: string; name?: string } }>('/api/sessions', async (req, reply) => { + const { workdir, name } = req.body + if (!workdir || typeof workdir !== 'string') { + return reply.status(400).send({ error: 'workdir is required' }) + } + + const modeRow = db + .prepare("SELECT value FROM settings WHERE key = 'session_mode'") + .get() as { value: string } | undefined + const mode = modeRow?.value === 'terminal' ? 'terminal' : 'chat' + + const id = uuidv4() + db.prepare( + 'INSERT INTO sessions (id, workdir, name, mode, started_at) VALUES (?, ?, ?, ?, ?)', + ).run(id, workdir, name?.trim() || null, mode, Date.now()) + + return reply.status(201).send({ sessionId: id }) + }) + ``` + +- [ ] **Step 3: Kill terminal PTY on stop** + + Replace the stop handler body (lines 59-63): + + ```typescript + fastify.post<{ Params: { id: string } }>('/api/sessions/:id/stop', async (req, reply) => { + const { id } = req.params + sessionManager.kill(id) + terminalManager.kill(id) + db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), id) + return reply.send({ ok: true }) + }) + ``` + +- [ ] **Step 4: Kill terminal PTY on delete** + + Replace the delete handler body (lines 94-105): + + ```typescript + fastify.delete<{ Params: { id: string } }>('/api/sessions/:id', async (req, reply) => { + const { id } = req.params + const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(id) + if (!session) { + return reply.status(404).send({ error: 'Session not found' }) + } + sessionManager.kill(id) + terminalManager.kill(id) + db.prepare('DELETE FROM messages WHERE session_id = ?').run(id) + db.prepare('DELETE FROM sessions WHERE id = ?').run(id) + return reply.send({ ok: true }) + }) + ``` + +- [ ] **Step 5: Register `terminalWsRoutes` in `server.ts`** + + In `backend/src/server.ts`, add the import at line 10 (after the existing `sessionWsRoutes` import): + + ```typescript + import { terminalWsRoutes } from './ws/terminal' + ``` + + And register the route after the existing `sessionWsRoutes` registration (after line 36): + + ```typescript + await fastify.register(terminalWsRoutes) + ``` + +- [ ] **Step 6: Lint backend** + + ```bash + cd backend && npm run lint + ``` + Expected: no errors. + +- [ ] **Step 7: Commit** + + ```bash + git add backend/src/routes/sessions.ts backend/src/server.ts + git commit -m "feat: wire terminal WS route and mode into sessions API" + ``` + +--- + +## Task 4: SessionContext mode field + +**Files:** +- Modify: `frontend/src/context/SessionContext.tsx` + +- [ ] **Step 1: Add `mode` to `SessionState`** + + In `frontend/src/context/SessionContext.tsx`, replace the `SessionState` interface (lines 15-26): + + ```typescript + export interface SessionState { + sessionId: string | null + workdir: string | null + name: string | null + model: string | null + mode: 'chat' | 'terminal' + wsState: 'disconnected' | 'connecting' | 'running' | 'idle' | 'error' + messages: Message[] + workingTimeMs: number + runningStartedAt: number | null + totalTokens: number + pendingPermissions: PermissionRequest[] | null + } + ``` + +- [ ] **Step 2: Add `mode` to `SESSION_CREATED` and `RESUME_SESSION` action types** + + Replace the `Action` type definition (lines 28-41): + + ```typescript + type Action = + | { type: 'SESSION_CREATED'; sessionId: string; workdir: string; name?: string; mode: 'chat' | 'terminal' } + | { type: 'SESSION_CLEARED' } + | { type: 'RESUME_SESSION'; id: string; workdir: string; name?: string; mode: 'chat' | 'terminal' } + | { type: 'WS_STATE'; state: SessionState['wsState']; timestamp: number } + | { type: 'MESSAGE_ADDED'; message: Message } + | { type: 'HISTORY_LOADED'; messages: Message[] } + | { type: 'MODEL_SET'; model: string } + | { type: 'SESSION_RENAMED'; name: string | null } + | { type: 'TOKENS_ADDED'; inputTokens: number; outputTokens: number } + | { type: 'STATS_RESTORED'; totalTokens: number; workingTimeMs: number } + | { type: 'PERMISSION_REQUEST'; permissions: PermissionRequest[] } + | { type: 'PERMISSION_CLEARED' } + ``` + +- [ ] **Step 3: Add `mode` to the initial state** + + Replace the `initial` constant (lines 43-53): + + ```typescript + const initial: SessionState = { + sessionId: null, + workdir: null, + name: null, + model: null, + mode: 'chat', + wsState: 'disconnected', + messages: [], + workingTimeMs: 0, + runningStartedAt: null, + totalTokens: 0, + pendingPermissions: null, + } + ``` + +- [ ] **Step 4: Update reducer cases for `SESSION_CREATED` and `RESUME_SESSION`** + + Replace the two reducer cases (lines 57-62): + + ```typescript + case 'SESSION_CREATED': + return { ...state, sessionId: action.sessionId, workdir: action.workdir, name: action.name ?? null, mode: action.mode, messages: [], wsState: 'connecting', workingTimeMs: 0, runningStartedAt: null, pendingPermissions: null } + case 'SESSION_CLEARED': + return { ...initial } + case 'RESUME_SESSION': + return { ...state, sessionId: action.id, workdir: action.workdir, name: action.name ?? null, mode: action.mode, messages: [], wsState: 'connecting', workingTimeMs: 0, runningStartedAt: null, totalTokens: 0, pendingPermissions: null } + ``` + +- [ ] **Step 5: Lint frontend** + + ```bash + cd frontend && npm run lint + ``` + Expected: errors about callers of `SESSION_CREATED`/`RESUME_SESSION` missing `mode` — these will be fixed in Task 9. The important thing is no errors in `SessionContext.tsx` itself. + + > Note: TypeScript errors in `DashboardView.tsx` about missing `mode` property are expected at this stage. They confirm the type change propagated correctly and will be resolved in Task 9. + +- [ ] **Step 6: Commit** + + ```bash + git add frontend/src/context/SessionContext.tsx + git commit -m "feat: add mode field to SessionContext state and actions" + ``` + +--- + +## Task 5: useDashboard — Session type + settings fetch + +**Files:** +- Modify: `frontend/src/hooks/useDashboard.ts` + +- [ ] **Step 1: Add `mode` to the `Session` interface and add settings to the fetch** + + Replace the entire `useDashboard.ts` file: + + ```typescript + import { useState, useEffect, useCallback } from 'react' + import type { AccountInfo } from './useAccount' + import type { UsageData } from './useUsage' + + export interface Session { + id: string + workdir: string + name: string | null + model: string | null + started_at: number + ended_at: number | null + is_active: boolean + message_count: number + mode: 'chat' | 'terminal' + } + + export interface DashboardData { + account: AccountInfo | null + usage: UsageData | null + sessions: Session[] + activeSessions: number + defaultSessionMode: 'chat' | 'terminal' + loading: boolean + error: string | null + } + + export function useDashboard(month?: string): DashboardData & { refresh: () => void } { + const [account, setAccount] = useState(null) + const [usage, setUsage] = useState(null) + const [sessions, setSessions] = useState([]) + const [defaultSessionMode, setDefaultSessionMode] = useState<'chat' | 'terminal'>('chat') + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const target = month ?? new Date().toISOString().slice(0, 7) + + const fetchAll = useCallback(() => { + setLoading(true) + setError(null) + + Promise.all([ + fetch('/api/account').then((r) => r.json() as Promise), + fetch(`/api/usage?month=${target}`).then((r) => r.json() as Promise), + fetch('/api/sessions').then((r) => r.json() as Promise), + fetch('/api/settings').then((r) => r.json() as Promise>), + ]) + .then(([acc, usg, sess, settings]) => { + setAccount(acc) + setUsage(usg) + setSessions(Array.isArray(sess) ? sess : []) + setDefaultSessionMode(settings.session_mode === 'terminal' ? 'terminal' : 'chat') + setLoading(false) + }) + .catch((e: Error) => { + setError(e.message) + setLoading(false) + }) + }, [target]) + + useEffect(() => { + fetchAll() + const timer = setInterval(fetchAll, 60_000) + return () => clearInterval(timer) + }, [fetchAll]) + + const activeSessions = sessions.filter((s) => s.is_active).length + + return { account, usage, sessions, activeSessions, defaultSessionMode, loading, error, refresh: fetchAll } + } + ``` + +- [ ] **Step 2: Lint frontend** + + ```bash + cd frontend && npm run lint + ``` + Expected: same `DashboardView.tsx` errors from Task 4 (still missing `mode` in dispatches). No new errors. + +- [ ] **Step 3: Commit** + + ```bash + git add frontend/src/hooks/useDashboard.ts + git commit -m "feat: add mode to Session type and fetch settings in useDashboard" + ``` + +--- + +## Task 6: useWebSocket mode guard + +**Files:** +- Modify: `frontend/src/hooks/useWebSocket.ts` + +- [ ] **Step 1: Add `state.mode` guard to prevent chat WS connecting in terminal mode** + + In `frontend/src/hooks/useWebSocket.ts`, replace lines 12-14 (the start of the useEffect): + + ```typescript + useEffect(() => { + if (!state.sessionId || state.mode === 'terminal') return + let closed = false + ``` + + Also update the dependency array at line 95 to include `state.mode`: + + ```typescript + }, [state.sessionId, state.mode]) // eslint-disable-line react-hooks/exhaustive-deps + ``` + +- [ ] **Step 2: Lint frontend** + + ```bash + cd frontend && npm run lint + ``` + Expected: same existing `DashboardView.tsx` errors only. No new errors. + +- [ ] **Step 3: Commit** + + ```bash + git add frontend/src/hooks/useWebSocket.ts + git commit -m "feat: skip chat WS connection when session mode is terminal" + ``` + +--- + +## Task 7: useTerminalSession hook + +**Files:** +- Create: `frontend/src/hooks/useTerminalSession.ts` + +- [ ] **Step 1: Create `frontend/src/hooks/useTerminalSession.ts`** + + ```typescript + import { useEffect, useRef, useCallback } from 'react' + import { useSession } from '../context/SessionContext' + + const MAX_RECONNECT_ATTEMPTS = 5 + const BASE_DELAY_MS = 500 + + export function useTerminalSession(onOutput: (data: string) => void) { + const { state, dispatch } = useSession() + const wsRef = useRef(null) + const attemptsRef = useRef(0) + + useEffect(() => { + if (!state.sessionId || state.mode !== 'terminal') return + let closed = false + + function connect() { + const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws' + const host = window.location.host + const ws = new WebSocket(`${protocol}://${host}/ws/terminal/${state.sessionId}`) + wsRef.current = ws + dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'connecting' }) + + ws.onopen = () => { + attemptsRef.current = 0 + } + + ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data as string) as { + type: string + data?: string + state?: string + } + if (msg.type === 'output' && msg.data) { + onOutput(msg.data) + } else if (msg.type === 'status') { + if (msg.state === 'connected') { + dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'idle' }) + } else if (msg.state === 'disconnected' || msg.state === 'error') { + dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'disconnected' }) + } + } + } catch { + // ignore malformed frames + } + } + + ws.onerror = () => dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'error' }) + + ws.onclose = () => { + if (closed) return + if (attemptsRef.current < MAX_RECONNECT_ATTEMPTS) { + const delay = BASE_DELAY_MS * 2 ** attemptsRef.current + attemptsRef.current++ + setTimeout(connect, delay) + } else { + dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'disconnected' }) + setTimeout(() => { + if (!closed) { attemptsRef.current = 0; connect() } + }, 30_000) + } + } + } + + connect() + return () => { + closed = true + attemptsRef.current = 0 + wsRef.current?.close() + wsRef.current = null + } + }, [state.sessionId, state.mode]) // eslint-disable-line react-hooks/exhaustive-deps + + const send = useCallback((payload: object) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify(payload)) + } + }, []) + + return { send } + } + ``` + +- [ ] **Step 2: Lint frontend** + + ```bash + cd frontend && npm run lint + ``` + Expected: same existing `DashboardView.tsx` errors only. + +- [ ] **Step 3: Commit** + + ```bash + git add frontend/src/hooks/useTerminalSession.ts + git commit -m "feat: add useTerminalSession hook for /ws/terminal/:id" + ``` + +--- + +## Task 8: TerminalSession component + +**Files:** +- Create: `frontend/src/components/TerminalSession.tsx` + +- [ ] **Step 1: Create `frontend/src/components/TerminalSession.tsx`** + + ```typescript + import { useEffect, useRef, useCallback } from 'react' + import { Terminal } from '@xterm/xterm' + import { FitAddon } from '@xterm/addon-fit' + import '@xterm/xterm/css/xterm.css' + import { useTerminalSession } from '../hooks/useTerminalSession' + + export default function TerminalSession() { + const containerRef = useRef(null) + const termRef = useRef(null) + const fitRef = useRef(null) + + const onOutput = useCallback((data: string) => { + termRef.current?.write(data) + }, []) + + const { send } = useTerminalSession(onOutput) + + useEffect(() => { + const term = new Terminal({ + theme: { + background: '#050505', + foreground: '#e2e2e2', + cursor: '#d97706', + selectionBackground: '#d9770640', + }, + fontFamily: 'JetBrains Mono, Fira Code, monospace', + fontSize: 12, + lineHeight: 1.4, + cursorBlink: true, + }) + const fit = new FitAddon() + term.loadAddon(fit) + termRef.current = term + fitRef.current = fit + + if (containerRef.current) { + term.open(containerRef.current) + requestAnimationFrame(() => fit.fit()) + term.onResize(({ cols, rows }) => send({ type: 'resize', cols, rows })) + term.onData((data) => send({ type: 'input', data })) + } + + return () => term.dispose() + }, []) // eslint-disable-line react-hooks/exhaustive-deps + + // Re-fit terminal when container dimensions change (window resize, panel resize) + useEffect(() => { + if (!containerRef.current) return + const observer = new ResizeObserver(() => { + requestAnimationFrame(() => fitRef.current?.fit()) + }) + observer.observe(containerRef.current) + return () => observer.disconnect() + }, []) + + return ( +
+
+
+ ) + } + ``` + +- [ ] **Step 2: Lint frontend** + + ```bash + cd frontend && npm run lint + ``` + Expected: same existing `DashboardView.tsx` errors only. No errors in `TerminalSession.tsx`. + +- [ ] **Step 3: Commit** + + ```bash + git add frontend/src/components/TerminalSession.tsx + git commit -m "feat: add full-height TerminalSession component" + ``` + +--- + +## Task 9: DashboardView wiring + +**Files:** +- Modify: `frontend/src/views/DashboardView.tsx` + +- [ ] **Step 1: Import `TerminalSession` and destructure `defaultSessionMode`** + + Add the import at the top of `DashboardView.tsx` (after the existing imports, before line 17): + + ```typescript + import TerminalSession from '../components/TerminalSession' + ``` + + Update the `useDashboard` destructure (line 23) to include `defaultSessionMode`: + + ```typescript + const { account, usage, sessions, activeSessions, loading, refresh, defaultSessionMode } = useDashboard() + ``` + +- [ ] **Step 2: Add `mode` to `handleSessionStart` dispatch** + + Replace `handleSessionStart` (lines 54-59): + + ```typescript + function handleSessionStart(sessionId: string, workdir: string, name: string | null) { + dispatch({ type: 'SESSION_CREATED', sessionId, workdir, ...(name ? { name } : {}), mode: defaultSessionMode }) + if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) + setShowModal(false) + refresh() + } + ``` + +- [ ] **Step 3: Add `mode` to `handleResume` dispatch** + + Replace `handleResume` (lines 97-100): + + ```typescript + function handleResume(session: Session) { + dispatch({ type: 'RESUME_SESSION', id: session.id, workdir: session.workdir, ...(session.name ? { name: session.name } : {}), mode: session.mode ?? 'chat' }) + if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) + } + ``` + +- [ ] **Step 4: Guard the PTY resize registration for chat mode only** + + Replace the resize `useEffect` (lines 47-51): + + ```typescript + useEffect(() => { + if (state.mode === 'terminal') return + terminalRef.current?.sendResize((cols, rows) => { + send({ type: 'resize', cols, rows }) + }) + }, [send, state.mode]) + ``` + +- [ ] **Step 5: Swap session content area based on `state.mode`** + + Replace the inner session layout (lines 162-185) — the block between `
` and its closing tag: + + ```tsx + {state.sessionId ? ( +
+ {state.pendingPermissions && ( + + )} + + {state.mode === 'terminal' ? ( + + ) : ( + <> + + + + + )} +
+ ) : showModal ? ( + + ) : ( + setShowModal(true)} + /> + )} + ``` + +- [ ] **Step 6: Lint frontend — should now be clean** + + ```bash + cd frontend && npm run lint + ``` + Expected: **no errors**. This is the step where all previously expected TypeScript errors resolve. + +- [ ] **Step 7: Commit** + + ```bash + git add frontend/src/views/DashboardView.tsx + git commit -m "feat: wire terminal mode into DashboardView session layout" + ``` + +--- + +## Task 10: SettingsView session mode toggle + +**Files:** +- Modify: `frontend/src/views/SettingsView.tsx` + +- [ ] **Step 1: Add `sessionMode` state and load it from settings** + + In `frontend/src/views/SettingsView.tsx`, add the new state variable after the `bypassPermissions` state (line 9): + + ```typescript + const [sessionMode, setSessionMode] = useState<'chat' | 'terminal'>('chat') + ``` + + In the `useEffect` fetch callback, add parsing for `session_mode` (after the `setBypassPermissions` line): + + ```typescript + .then((data) => { + setBypassPermissions(data.bypass_permissions !== 'false') + setSessionMode(data.session_mode === 'terminal' ? 'terminal' : 'chat') + setSettingsLoaded(true) + }) + ``` + +- [ ] **Step 2: Include `session_mode` in `handleSave`** + + Replace the `await fetch('/api/settings', ...)` call in `handleSave`: + + ```typescript + await fetch('/api/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + bypass_permissions: String(bypassPermissions), + session_mode: sessionMode, + }), + }).catch(() => {}) + ``` + +- [ ] **Step 3: Add the Session Mode toggle UI** + + Add this block in the JSX, after the "Tool Permissions" section and before the Save button: + + ```tsx + {/* Session mode toggle */} +
+ +

+ Chat mode shows a structured conversation with a collapsible terminal drawer. Terminal mode replaces the chat view with a full interactive terminal — interact with Claude Code exactly as you would in your local terminal. +

+ +
+ ``` + +- [ ] **Step 4: Lint frontend** + + ```bash + cd frontend && npm run lint + ``` + Expected: no errors. + +- [ ] **Step 5: Full lint check (both packages)** + + ```bash + make lint + ``` + Expected: no errors in either package. + +- [ ] **Step 6: Commit** + + ```bash + git add frontend/src/views/SettingsView.tsx + git commit -m "feat: add session mode toggle to SettingsView" + ``` + +--- + +## Verification + +Run `make dev` (backend on :9998, frontend on :9999) and verify: + +1. **Settings toggle**: Navigate to `/settings` — confirm "Session Mode" toggle appears alongside Tool Permissions, can be switched between chat/terminal, and saves correctly. + +2. **Chat mode (default)**: With mode=chat, create a new session — confirm existing chat layout (MessageList + TerminalDrawer + ChatInput) renders unchanged. + +3. **Terminal mode**: Switch to terminal mode, create a new session — confirm the session view shows only a full-height xterm.js terminal (no chat input, no message list, no terminal drawer header). + +4. **I/O passthrough**: In terminal mode, type a prompt into the terminal — confirm keystrokes appear in the terminal and Claude Code responds with ANSI-formatted output. + +5. **Resize**: Resize the browser window — confirm the terminal re-fits and PTY cols/rows update (visible when claude CLI redraws its prompt). + +6. **Stop session**: Click "Stop session" in the header while a terminal session is active — confirm PTY is killed, session shows as ended in the sessions list. + +7. **Session list + resume**: Both chat and terminal sessions appear in the sessions list. Resuming a terminal session reconnects to `/ws/terminal/:id`; resuming a chat session reconnects to `/ws/session/:id`. + +8. **Chat mode unaffected**: Switch back to chat mode, confirm the existing chat experience is identical to before. From bed65f97fda844913cbd436dd4370b5746465abf Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Tue, 12 May 2026 23:30:19 +0300 Subject: [PATCH 03/25] feat: add session mode DB column and settings key Co-Authored-By: Claude Sonnet 4.6 --- backend/src/db/schema.ts | 4 ++++ backend/src/routes/settings.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 270ffda..48e9b82 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -69,6 +69,10 @@ function initDb(): Database.Database { db.prepare('ALTER TABLE sessions ADD COLUMN working_time_ms INTEGER NOT NULL DEFAULT 0').run() } + if (!sessionCols.find((c) => c.name === 'mode')) { + db.prepare("ALTER TABLE sessions ADD COLUMN mode TEXT NOT NULL DEFAULT 'chat'").run() + } + db.prepare(` CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index fe6ef6b..1b4a432 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -3,6 +3,7 @@ import { db } from '../db/schema' const DEFAULTS: Record = { bypass_permissions: 'true', + session_mode: 'chat', } const ALLOWED = new Set(Object.keys(DEFAULTS)) From 6ed5e67190f076abb75d1f435a37ae708e68b67f Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Tue, 12 May 2026 23:32:41 +0300 Subject: [PATCH 04/25] feat: add terminal WS handler and TerminalManager Co-Authored-By: Claude Sonnet 4.6 --- backend/src/ws/terminal.ts | 166 +++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 backend/src/ws/terminal.ts diff --git a/backend/src/ws/terminal.ts b/backend/src/ws/terminal.ts new file mode 100644 index 0000000..d224187 --- /dev/null +++ b/backend/src/ws/terminal.ts @@ -0,0 +1,166 @@ +import * as pty from 'node-pty' +import * as os from 'os' +import * as path from 'path' +import type { WebSocket } from 'ws' +import type { FastifyInstance } from 'fastify' +import { db } from '../db/schema' + +const IDLE_TIMEOUT_MS = 30 * 60 * 1000 + +function getBypassPermissions(): boolean { + const row = db + .prepare('SELECT value FROM settings WHERE key = ?') + .get('bypass_permissions') as { value: string } | undefined + return row ? row.value === 'true' : true +} + +class ActiveTerminalSession { + private ptyProc: pty.IPty | null = null + private sockets = new Set() + private idleTimer: NodeJS.Timeout | null = null + + constructor( + readonly id: string, + private readonly workdir: string, + ) { + this.spawnPty() + this.resetIdle() + } + + private spawnPty() { + const claudeBin = process.env.CLAUDE_BIN ?? 'claude' + const bypassPermissions = getBypassPermissions() + const args: string[] = [] + if (bypassPermissions) args.push('--dangerously-skip-permissions') + + const resolvedCwd = this.workdir.startsWith('~') + ? path.join(os.homedir(), this.workdir.slice(1)) + : this.workdir + + try { + this.ptyProc = pty.spawn(claudeBin, args, { + name: 'xterm-256color', + cols: 220, + rows: 50, + cwd: resolvedCwd, + env: { ...process.env } as Record, + }) + } catch (err) { + this.broadcast({ type: 'output', data: `\r\nError starting terminal: ${(err as Error).message}\r\n` }) + this.broadcast({ type: 'status', state: 'disconnected' }) + return + } + + this.ptyProc.onData((data) => { + this.resetIdle() + this.broadcast({ type: 'output', data }) + }) + + this.ptyProc.onExit(() => { + this.ptyProc = null + db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), this.id) + this.broadcast({ type: 'status', state: 'disconnected' }) + if (this.idleTimer) clearTimeout(this.idleTimer) + }) + } + + attach(ws: WebSocket) { + this.sockets.add(ws) + ws.send(JSON.stringify({ type: 'status', state: 'connected' })) + ws.on('close', () => this.sockets.delete(ws)) + } + + writeInput(data: string) { + this.resetIdle() + this.ptyProc?.write(data) + } + + resize(cols: number, rows: number) { + this.ptyProc?.resize(cols, rows) + } + + kill() { + db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), this.id) + if (this.ptyProc) { + this.ptyProc.kill() + this.ptyProc = null + } + if (this.idleTimer) clearTimeout(this.idleTimer) + this.broadcast({ type: 'status', state: 'disconnected' }) + } + + private broadcast(msg: { type: string; data?: string; state?: string }) { + const payload = JSON.stringify(msg) + for (const ws of this.sockets) { + if (ws.readyState === ws.OPEN) ws.send(payload) + } + } + + private resetIdle() { + if (this.idleTimer) clearTimeout(this.idleTimer) + this.idleTimer = setTimeout(() => this.kill(), IDLE_TIMEOUT_MS) + } +} + +class TerminalManager { + private sessions = new Map() + + getOrCreate(id: string, workdir: string): ActiveTerminalSession { + if (!this.sessions.has(id)) { + this.sessions.set(id, new ActiveTerminalSession(id, workdir)) + } + return this.sessions.get(id)! + } + + kill(id: string) { + this.sessions.get(id)?.kill() + this.sessions.delete(id) + } +} + +export const terminalManager = new TerminalManager() + +export async function terminalWsRoutes(fastify: FastifyInstance) { + fastify.get<{ Params: { id: string } }>( + '/ws/terminal/:id', + { websocket: true }, + (socket, req) => { + const { id } = req.params + + const row = db.prepare('SELECT workdir, ended_at FROM sessions WHERE id = ?').get(id) as + | { workdir: string; ended_at: number | null } + | undefined + + if (!row) { + socket.send(JSON.stringify({ type: 'status', state: 'error', data: 'session not found' })) + socket.close() + return + } + + if (row.ended_at !== null) { + db.prepare('UPDATE sessions SET ended_at = NULL WHERE id = ?').run(id) + } + + const session = terminalManager.getOrCreate(id, row.workdir) + session.attach(socket) + + socket.on('message', (raw: Buffer | string) => { + try { + const msg = JSON.parse(raw.toString()) as { + type: string + data?: string + cols?: number + rows?: number + } + if (msg.type === 'input' && msg.data) { + session.writeInput(msg.data) + } else if (msg.type === 'resize' && msg.cols && msg.rows) { + session.resize(msg.cols, msg.rows) + } + } catch { + // ignore malformed frames + } + }) + }, + ) +} From 17d97811855cac01c0dcce3af0e61b3f29800a61 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Tue, 12 May 2026 23:37:20 +0300 Subject: [PATCH 05/25] fix: terminal session spawn error handling and lifecycle cleanup - Store spawn errors and deliver them to the first attaching socket, rather than broadcasting to an empty set - Remove dead sessions from TerminalManager on natural PTY exit via onCleanup callback - Reset idle timer on socket reconnect in attach() - Narrow broadcast() parameter to a typed TerminalServerMessage union Co-Authored-By: Claude Sonnet 4.6 --- backend/src/ws/terminal.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/backend/src/ws/terminal.ts b/backend/src/ws/terminal.ts index d224187..f11af78 100644 --- a/backend/src/ws/terminal.ts +++ b/backend/src/ws/terminal.ts @@ -14,14 +14,20 @@ function getBypassPermissions(): boolean { return row ? row.value === 'true' : true } +type TerminalServerMessage = + | { type: 'output'; data: string } + | { type: 'status'; state: string } + class ActiveTerminalSession { private ptyProc: pty.IPty | null = null private sockets = new Set() private idleTimer: NodeJS.Timeout | null = null + private spawnError: string | null = null constructor( readonly id: string, private readonly workdir: string, + private readonly onCleanup: (id: string) => void, ) { this.spawnPty() this.resetIdle() @@ -46,8 +52,7 @@ class ActiveTerminalSession { env: { ...process.env } as Record, }) } catch (err) { - this.broadcast({ type: 'output', data: `\r\nError starting terminal: ${(err as Error).message}\r\n` }) - this.broadcast({ type: 'status', state: 'disconnected' }) + this.spawnError = (err as Error).message return } @@ -61,11 +66,20 @@ class ActiveTerminalSession { db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), this.id) this.broadcast({ type: 'status', state: 'disconnected' }) if (this.idleTimer) clearTimeout(this.idleTimer) + this.onCleanup(this.id) }) } attach(ws: WebSocket) { this.sockets.add(ws) + this.resetIdle() + if (this.spawnError) { + ws.send(JSON.stringify({ type: 'output', data: `\r\nError starting terminal: ${this.spawnError}\r\n` })) + ws.send(JSON.stringify({ type: 'status', state: 'disconnected' })) + ws.close() + this.sockets.delete(ws) + return + } ws.send(JSON.stringify({ type: 'status', state: 'connected' })) ws.on('close', () => this.sockets.delete(ws)) } @@ -89,7 +103,7 @@ class ActiveTerminalSession { this.broadcast({ type: 'status', state: 'disconnected' }) } - private broadcast(msg: { type: string; data?: string; state?: string }) { + private broadcast(msg: TerminalServerMessage) { const payload = JSON.stringify(msg) for (const ws of this.sockets) { if (ws.readyState === ws.OPEN) ws.send(payload) @@ -107,7 +121,9 @@ class TerminalManager { getOrCreate(id: string, workdir: string): ActiveTerminalSession { if (!this.sessions.has(id)) { - this.sessions.set(id, new ActiveTerminalSession(id, workdir)) + this.sessions.set(id, new ActiveTerminalSession(id, workdir, (cleanupId) => { + this.sessions.delete(cleanupId) + })) } return this.sessions.get(id)! } From 7bd9ecfc3ea703fd383ee0578f21cf1c6b5635bb Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Tue, 12 May 2026 23:40:14 +0300 Subject: [PATCH 06/25] feat: wire terminal WS route and mode into sessions API Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/sessions.ts | 13 ++++++++++--- backend/src/server.ts | 2 ++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index c556342..5be1d98 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -5,6 +5,7 @@ import os from 'os' import path from 'path' import { db } from '../db/schema' import { sessionManager } from '../ws/session' +import { terminalManager } from '../ws/terminal' export async function sessionRoutes(fastify: FastifyInstance) { fastify.get<{ Querystring: { path?: string } }>('/api/directories', async (req, reply) => { @@ -48,10 +49,15 @@ export async function sessionRoutes(fastify: FastifyInstance) { return reply.status(400).send({ error: 'workdir is required' }) } + const modeRow = db + .prepare("SELECT value FROM settings WHERE key = 'session_mode'") + .get() as { value: string } | undefined + const mode = modeRow?.value === 'terminal' ? 'terminal' : 'chat' + const id = uuidv4() db.prepare( - 'INSERT INTO sessions (id, workdir, name, started_at) VALUES (?, ?, ?, ?)', - ).run(id, workdir, name?.trim() || null, Date.now()) + 'INSERT INTO sessions (id, workdir, name, mode, started_at) VALUES (?, ?, ?, ?, ?)', + ).run(id, workdir, name?.trim() || null, mode, Date.now()) return reply.status(201).send({ sessionId: id }) }) @@ -59,6 +65,7 @@ export async function sessionRoutes(fastify: FastifyInstance) { fastify.post<{ Params: { id: string } }>('/api/sessions/:id/stop', async (req, reply) => { const { id } = req.params sessionManager.kill(id) + terminalManager.kill(id) db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), id) return reply.send({ ok: true }) }) @@ -97,8 +104,8 @@ export async function sessionRoutes(fastify: FastifyInstance) { if (!session) { return reply.status(404).send({ error: 'Session not found' }) } - // Kill the session if it's running sessionManager.kill(id) + terminalManager.kill(id) db.prepare('DELETE FROM messages WHERE session_id = ?').run(id) db.prepare('DELETE FROM sessions WHERE id = ?').run(id) return reply.send({ ok: true }) diff --git a/backend/src/server.ts b/backend/src/server.ts index e5f62ae..d83482f 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -8,6 +8,7 @@ import { accountRoutes } from './routes/account' import { usageRoutes } from './routes/usage' import { sessionRoutes } from './routes/sessions' import { sessionWsRoutes } from './ws/session' +import { terminalWsRoutes } from './ws/terminal' import { settingsRoutes } from './routes/settings' // TODO: add bearer token auth — add @fastify/bearer-auth plugin here @@ -34,6 +35,7 @@ async function start() { await fastify.register(usageRoutes) await fastify.register(sessionRoutes) await fastify.register(sessionWsRoutes) + await fastify.register(terminalWsRoutes) await fastify.register(settingsRoutes) fastify.get('/health', async () => ({ status: 'ok' })) From 918244a186df8f6e2256aaf5b133fa898ad7afd4 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Tue, 12 May 2026 23:43:14 +0300 Subject: [PATCH 07/25] feat: add mode field to SessionContext state and actions Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/context/SessionContext.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/context/SessionContext.tsx b/frontend/src/context/SessionContext.tsx index be79469..6a51552 100644 --- a/frontend/src/context/SessionContext.tsx +++ b/frontend/src/context/SessionContext.tsx @@ -17,6 +17,7 @@ export interface SessionState { workdir: string | null name: string | null model: string | null + mode: 'chat' | 'terminal' wsState: 'disconnected' | 'connecting' | 'running' | 'idle' | 'error' messages: Message[] workingTimeMs: number // cumulative ms spent in 'running' state @@ -26,9 +27,9 @@ export interface SessionState { } type Action = - | { type: 'SESSION_CREATED'; sessionId: string; workdir: string; name?: string } + | { type: 'SESSION_CREATED'; sessionId: string; workdir: string; name?: string; mode: 'chat' | 'terminal' } | { type: 'SESSION_CLEARED' } - | { type: 'RESUME_SESSION'; id: string; workdir: string; name?: string } + | { type: 'RESUME_SESSION'; id: string; workdir: string; name?: string; mode: 'chat' | 'terminal' } | { type: 'WS_STATE'; state: SessionState['wsState']; timestamp: number } | { type: 'MESSAGE_ADDED'; message: Message } | { type: 'HISTORY_LOADED'; messages: Message[] } @@ -44,6 +45,7 @@ const initial: SessionState = { workdir: null, name: null, model: null, + mode: 'chat', wsState: 'disconnected', messages: [], workingTimeMs: 0, @@ -55,11 +57,11 @@ const initial: SessionState = { function reducer(state: SessionState, action: Action): SessionState { switch (action.type) { case 'SESSION_CREATED': - return { ...state, sessionId: action.sessionId, workdir: action.workdir, name: action.name ?? null, messages: [], wsState: 'connecting', workingTimeMs: 0, runningStartedAt: null, pendingPermissions: null } + return { ...state, sessionId: action.sessionId, workdir: action.workdir, name: action.name ?? null, mode: action.mode, messages: [], wsState: 'connecting', workingTimeMs: 0, runningStartedAt: null, pendingPermissions: null } case 'SESSION_CLEARED': return { ...initial } case 'RESUME_SESSION': - return { ...state, sessionId: action.id, workdir: action.workdir, name: action.name ?? null, messages: [], wsState: 'connecting', workingTimeMs: 0, runningStartedAt: null, totalTokens: 0, pendingPermissions: null } + return { ...state, sessionId: action.id, workdir: action.workdir, name: action.name ?? null, mode: action.mode, messages: [], wsState: 'connecting', workingTimeMs: 0, runningStartedAt: null, totalTokens: 0, pendingPermissions: null } case 'WS_STATE': { const prev = state.wsState const next = action.state From 46de54e828e73330f9ff9af05d895ee3602a2c61 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 00:00:44 +0300 Subject: [PATCH 08/25] fix: point lint at tsconfig.app.json and stub mode in DashboardView dispatches --- frontend/package.json | 2 +- frontend/src/views/DashboardView.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index a40c49b..fcb154e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "tsc --noEmit", + "lint": "tsc --noEmit --project tsconfig.app.json", "preview": "vite preview" }, "dependencies": { diff --git a/frontend/src/views/DashboardView.tsx b/frontend/src/views/DashboardView.tsx index 4c15492..76e0a18 100644 --- a/frontend/src/views/DashboardView.tsx +++ b/frontend/src/views/DashboardView.tsx @@ -52,7 +52,7 @@ export default function DashboardView() { function handleSessionStart(sessionId: string, workdir: string, name: string | null) { - dispatch({ type: 'SESSION_CREATED', sessionId, workdir, ...(name ? { name } : {}) }) + dispatch({ type: 'SESSION_CREATED', sessionId, workdir, mode: 'chat', ...(name ? { name } : {}) }) if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) setShowModal(false) refresh() @@ -95,7 +95,7 @@ export default function DashboardView() { } function handleResume(session: Session) { - dispatch({ type: 'RESUME_SESSION', id: session.id, workdir: session.workdir, ...(session.name ? { name: session.name } : {}) }) + dispatch({ type: 'RESUME_SESSION', id: session.id, workdir: session.workdir, mode: 'chat', ...(session.name ? { name: session.name } : {}) }) if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) } From 80d0f0daba04ff3b8099ad6e475677b0f5b78248 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 00:01:35 +0300 Subject: [PATCH 09/25] feat: add mode to Session type and fetch settings in useDashboard --- frontend/src/hooks/useDashboard.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/useDashboard.ts b/frontend/src/hooks/useDashboard.ts index 165b73f..0b9d1f0 100644 --- a/frontend/src/hooks/useDashboard.ts +++ b/frontend/src/hooks/useDashboard.ts @@ -11,6 +11,7 @@ export interface Session { ended_at: number | null is_active: boolean message_count: number + mode: 'chat' | 'terminal' } export interface DashboardData { @@ -18,6 +19,7 @@ export interface DashboardData { usage: UsageData | null sessions: Session[] activeSessions: number + defaultSessionMode: 'chat' | 'terminal' loading: boolean error: string | null } @@ -26,6 +28,7 @@ export function useDashboard(month?: string): DashboardData & { refresh: () => v const [account, setAccount] = useState(null) const [usage, setUsage] = useState(null) const [sessions, setSessions] = useState([]) + const [defaultSessionMode, setDefaultSessionMode] = useState<'chat' | 'terminal'>('chat') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -39,11 +42,13 @@ export function useDashboard(month?: string): DashboardData & { refresh: () => v fetch('/api/account').then((r) => r.json() as Promise), fetch(`/api/usage?month=${target}`).then((r) => r.json() as Promise), fetch('/api/sessions').then((r) => r.json() as Promise), + fetch('/api/settings').then((r) => r.json() as Promise>), ]) - .then(([acc, usg, sess]) => { + .then(([acc, usg, sess, settings]) => { setAccount(acc) setUsage(usg) setSessions(Array.isArray(sess) ? sess : []) + setDefaultSessionMode(settings.session_mode === 'terminal' ? 'terminal' : 'chat') setLoading(false) }) .catch((e: Error) => { @@ -60,5 +65,5 @@ export function useDashboard(month?: string): DashboardData & { refresh: () => v const activeSessions = sessions.filter((s) => s.is_active).length - return { account, usage, sessions, activeSessions, loading, error, refresh: fetchAll } + return { account, usage, sessions, activeSessions, defaultSessionMode, loading, error, refresh: fetchAll } } From 315afcdff829cab8b2be16cc72b98ec259177212 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 00:03:18 +0300 Subject: [PATCH 10/25] feat: skip chat WS connection when session mode is terminal --- frontend/src/hooks/useWebSocket.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/useWebSocket.ts b/frontend/src/hooks/useWebSocket.ts index 2bbceff..8e31bf4 100644 --- a/frontend/src/hooks/useWebSocket.ts +++ b/frontend/src/hooks/useWebSocket.ts @@ -10,7 +10,7 @@ export function useWebSocket(onOutput: (data: string) => void) { const attemptsRef = useRef(0) useEffect(() => { - if (!state.sessionId) return + if (!state.sessionId || state.mode === 'terminal') return let closed = false function connect() { @@ -92,7 +92,7 @@ export function useWebSocket(onOutput: (data: string) => void) { wsRef.current?.close() wsRef.current = null } - }, [state.sessionId]) // eslint-disable-line react-hooks/exhaustive-deps + }, [state.sessionId, state.mode]) // eslint-disable-line react-hooks/exhaustive-deps const send = useCallback((payload: object) => { if (wsRef.current?.readyState === WebSocket.OPEN) { From a035fe33753f93515d1bef821081ccc917e1dfad Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 00:05:02 +0300 Subject: [PATCH 11/25] feat: add useTerminalSession hook for /ws/terminal/:id --- frontend/src/hooks/useTerminalSession.ts | 81 ++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 frontend/src/hooks/useTerminalSession.ts diff --git a/frontend/src/hooks/useTerminalSession.ts b/frontend/src/hooks/useTerminalSession.ts new file mode 100644 index 0000000..1c12e54 --- /dev/null +++ b/frontend/src/hooks/useTerminalSession.ts @@ -0,0 +1,81 @@ +import { useEffect, useRef, useCallback } from 'react' +import { useSession } from '../context/SessionContext' + +const MAX_RECONNECT_ATTEMPTS = 5 +const BASE_DELAY_MS = 500 + +export function useTerminalSession(onOutput: (data: string) => void) { + const { state, dispatch } = useSession() + const wsRef = useRef(null) + const attemptsRef = useRef(0) + + useEffect(() => { + if (!state.sessionId || state.mode !== 'terminal') return + let closed = false + + function connect() { + const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws' + const host = window.location.host + const ws = new WebSocket(`${protocol}://${host}/ws/terminal/${state.sessionId}`) + wsRef.current = ws + dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'connecting' }) + + ws.onopen = () => { + attemptsRef.current = 0 + } + + ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data as string) as { + type: string + data?: string + state?: string + } + if (msg.type === 'output' && msg.data) { + onOutput(msg.data) + } else if (msg.type === 'status') { + if (msg.state === 'connected') { + dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'idle' }) + } else if (msg.state === 'disconnected' || msg.state === 'error') { + dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'disconnected' }) + } + } + } catch { + // ignore malformed frames + } + } + + ws.onerror = () => dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'error' }) + + ws.onclose = () => { + if (closed) return + if (attemptsRef.current < MAX_RECONNECT_ATTEMPTS) { + const delay = BASE_DELAY_MS * 2 ** attemptsRef.current + attemptsRef.current++ + setTimeout(connect, delay) + } else { + dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'disconnected' }) + setTimeout(() => { + if (!closed) { attemptsRef.current = 0; connect() } + }, 30_000) + } + } + } + + connect() + return () => { + closed = true + attemptsRef.current = 0 + wsRef.current?.close() + wsRef.current = null + } + }, [state.sessionId, state.mode]) // eslint-disable-line react-hooks/exhaustive-deps + + const send = useCallback((payload: object) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify(payload)) + } + }, []) + + return { send } +} From 62c427cc356cebd44bdff096d0b510fa97d89e34 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 00:07:12 +0300 Subject: [PATCH 12/25] feat: add full-height TerminalSession component --- frontend/src/components/TerminalSession.tsx | 61 +++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 frontend/src/components/TerminalSession.tsx diff --git a/frontend/src/components/TerminalSession.tsx b/frontend/src/components/TerminalSession.tsx new file mode 100644 index 0000000..562bb9f --- /dev/null +++ b/frontend/src/components/TerminalSession.tsx @@ -0,0 +1,61 @@ +import { useEffect, useRef, useCallback } from 'react' +import { Terminal } from '@xterm/xterm' +import { FitAddon } from '@xterm/addon-fit' +import '@xterm/xterm/css/xterm.css' +import { useTerminalSession } from '../hooks/useTerminalSession' + +export default function TerminalSession() { + const containerRef = useRef(null) + const termRef = useRef(null) + const fitRef = useRef(null) + + const onOutput = useCallback((data: string) => { + termRef.current?.write(data) + }, []) + + const { send } = useTerminalSession(onOutput) + + useEffect(() => { + const term = new Terminal({ + theme: { + background: '#050505', + foreground: '#e2e2e2', + cursor: '#d97706', + selectionBackground: '#d9770640', + }, + fontFamily: 'JetBrains Mono, Fira Code, monospace', + fontSize: 12, + lineHeight: 1.4, + cursorBlink: true, + }) + const fit = new FitAddon() + term.loadAddon(fit) + termRef.current = term + fitRef.current = fit + + if (containerRef.current) { + term.open(containerRef.current) + requestAnimationFrame(() => fit.fit()) + term.onResize(({ cols, rows }) => send({ type: 'resize', cols, rows })) + term.onData((data) => send({ type: 'input', data })) + } + + return () => term.dispose() + }, []) // eslint-disable-line react-hooks/exhaustive-deps + + // Re-fit terminal when container dimensions change (window resize, panel resize) + useEffect(() => { + if (!containerRef.current) return + const observer = new ResizeObserver(() => { + requestAnimationFrame(() => fitRef.current?.fit()) + }) + observer.observe(containerRef.current) + return () => observer.disconnect() + }, []) + + return ( +
+
+
+ ) +} From e45c9cc83da5d6505260cd2124fdbb86376ef6b6 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 00:11:21 +0300 Subject: [PATCH 13/25] feat: wire DashboardView for terminal mode rendering (Task 9) --- frontend/src/views/DashboardView.tsx | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/frontend/src/views/DashboardView.tsx b/frontend/src/views/DashboardView.tsx index 76e0a18..b5aeec3 100644 --- a/frontend/src/views/DashboardView.tsx +++ b/frontend/src/views/DashboardView.tsx @@ -13,6 +13,7 @@ import TerminalDrawer, { type TerminalDrawerHandle } from '../components/Termina import NewSessionModal from '../components/NewSessionModal' import UsageChart from '../components/UsageChart' import PermissionDialog from '../components/PermissionDialog' +import TerminalSession from '../components/TerminalSession' export default function DashboardView() { const { state, dispatch } = useSession() @@ -20,7 +21,7 @@ export default function DashboardView() { const [showModal, setShowModal] = useState(false) const [chartOpen, setChartOpen] = useState(false) - const { account, usage, sessions, activeSessions, loading, refresh } = useDashboard() + const { account, usage, sessions, activeSessions, defaultSessionMode, loading, refresh } = useDashboard() // Local sessions state for optimistic deletion const [localSessions, setLocalSessions] = useState(null) const displaySessions = localSessions ?? sessions @@ -45,14 +46,15 @@ export default function DashboardView() { // Register PTY resize callback once the terminal mounts useEffect(() => { + if (state.mode === 'terminal') return terminalRef.current?.sendResize((cols, rows) => { send({ type: 'resize', cols, rows }) }) - }, [send]) + }, [send, state.mode]) function handleSessionStart(sessionId: string, workdir: string, name: string | null) { - dispatch({ type: 'SESSION_CREATED', sessionId, workdir, mode: 'chat', ...(name ? { name } : {}) }) + dispatch({ type: 'SESSION_CREATED', sessionId, workdir, mode: defaultSessionMode, ...(name ? { name } : {}) }) if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) setShowModal(false) refresh() @@ -95,7 +97,7 @@ export default function DashboardView() { } function handleResume(session: Session) { - dispatch({ type: 'RESUME_SESSION', id: session.id, workdir: session.workdir, mode: 'chat', ...(session.name ? { name: session.name } : {}) }) + dispatch({ type: 'RESUME_SESSION', id: session.id, workdir: session.workdir, mode: session.mode ?? 'chat', ...(session.name ? { name: session.name } : {}) }) if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) } @@ -176,12 +178,18 @@ export default function DashboardView() { totalTokens={state.totalTokens} sessionStartedAt={activeSession?.started_at ?? null} /> - - - + {state.mode === 'terminal' ? ( + + ) : ( + <> + + + + + )}
) : showModal ? ( From 9c622f1817878927d378fde94d1fe15431c20b90 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 00:16:36 +0300 Subject: [PATCH 14/25] feat: add session mode toggle to SettingsView (Task 10) --- frontend/src/views/SettingsView.tsx | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/SettingsView.tsx b/frontend/src/views/SettingsView.tsx index 06d29a5..d66e64a 100644 --- a/frontend/src/views/SettingsView.tsx +++ b/frontend/src/views/SettingsView.tsx @@ -7,6 +7,7 @@ const API_KEY = 'dashboard_anthropic_api_key' export default function SettingsView() { const [apiKey, setApiKey] = useState(() => localStorage.getItem(API_KEY) ?? '') const [bypassPermissions, setBypassPermissions] = useState(true) + const [sessionMode, setSessionMode] = useState<'chat' | 'terminal'>('chat') const [saved, setSaved] = useState(false) const [settingsLoaded, setSettingsLoaded] = useState(false) @@ -15,6 +16,7 @@ export default function SettingsView() { .then((r) => r.json() as Promise>) .then((data) => { setBypassPermissions(data.bypass_permissions !== 'false') + setSessionMode(data.session_mode === 'terminal' ? 'terminal' : 'chat') setSettingsLoaded(true) }) .catch(() => setSettingsLoaded(true)) @@ -27,7 +29,10 @@ export default function SettingsView() { await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ bypass_permissions: String(bypassPermissions) }), + body: JSON.stringify({ + bypass_permissions: String(bypassPermissions), + session_mode: sessionMode, + }), }).catch(() => {}) setSaved(true) @@ -85,6 +90,28 @@ export default function SettingsView() {
+ {/* Session mode toggle */} +
+ +

+ Controls the interface for new sessions. Chat uses the structured message view. Terminal opens a full interactive terminal connected directly to Claude Code. +

+ +
+
{/* Main area */} - {state.sessionId ? ( -
- {state.pendingPermissions && ( - - )} - + {state.pendingPermissions && ( + - {state.mode === 'terminal' ? ( - - ) : ( - <> - - - - - )} -
- ) : showModal ? ( - - ) : ( - setShowModal(true)} + )} + + + + + {/* Chat / session-list / modal — shown when not in an active terminal session */} + {(!state.sessionId || state.mode !== 'terminal') && ( + state.sessionId ? ( +
+ {state.pendingPermissions && ( + + )} + + + + +
+ ) : showModal ? ( + + ) : ( + setShowModal(true)} + /> + ) )} ) From 2a850f65f696212dfff51cf443f88dc23d683d73 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 11:47:21 +0300 Subject: [PATCH 20/25] feat: save and replay terminal scrollback on reconnect Backend accumulates up to 256 KB of PTY output per session in a circular scrollback buffer. On reconnect (existing PTY), it sends the buffer as a {type: 'history'} message before {type: 'status', state: 'connected'}. Frontend clears the xterm viewport and replays the history on receipt, restoring the terminal state after a page refresh. Works in tandem with the CSS-toggle approach: navigation-within-app uses the preserved xterm instance; page refresh uses history replay from the backend buffer. Co-Authored-By: Claude Sonnet 4.6 --- backend/src/ws/terminal.ts | 11 +++++++++++ frontend/src/components/TerminalSession.tsx | 10 +++++++++- frontend/src/hooks/useTerminalSession.ts | 4 +++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/backend/src/ws/terminal.ts b/backend/src/ws/terminal.ts index 21a4557..62040a9 100644 --- a/backend/src/ws/terminal.ts +++ b/backend/src/ws/terminal.ts @@ -16,14 +16,18 @@ function getBypassPermissions(): boolean { type TerminalServerMessage = | { type: 'output'; data: string } + | { type: 'history'; data: string } | { type: 'status'; state: string } +const MAX_SCROLLBACK_BYTES = 256 * 1024 + class ActiveTerminalSession { private ptyProc: pty.IPty | null = null private sockets = new Set() private idleTimer: NodeJS.Timeout | null = null private spawnError: string | null = null private spawned = false + private scrollback = '' constructor( readonly id: string, @@ -65,6 +69,10 @@ class ActiveTerminalSession { this.ptyProc.onData((data) => { this.resetIdle() + this.scrollback += data + if (this.scrollback.length > MAX_SCROLLBACK_BYTES) { + this.scrollback = this.scrollback.slice(this.scrollback.length - MAX_SCROLLBACK_BYTES) + } this.broadcast({ type: 'output', data }) }) @@ -86,6 +94,9 @@ class ActiveTerminalSession { // If PTY already running (reconnect), confirm immediately. // If not yet spawned, wait for the first resize message to spawn with correct dimensions. if (this.spawned) { + if (this.scrollback) { + ws.send(JSON.stringify({ type: 'history', data: this.scrollback })) + } ws.send(JSON.stringify({ type: 'status', state: 'connected' })) } } diff --git a/frontend/src/components/TerminalSession.tsx b/frontend/src/components/TerminalSession.tsx index 47d59e4..1ded0e8 100644 --- a/frontend/src/components/TerminalSession.tsx +++ b/frontend/src/components/TerminalSession.tsx @@ -30,7 +30,15 @@ export default function TerminalSession() { }) }, []) - const { send } = useTerminalSession(onOutput, onConnect) + // Replay scrollback from the backend on reconnect (covers page refresh). + // Clears first so history isn't duplicated on top of live content from CSS-toggle sessions. + const onHistory = useCallback((data: string) => { + if (!termRef.current) return + termRef.current.clear() + termRef.current.write(data) + }, []) + + const { send } = useTerminalSession(onOutput, onConnect, onHistory) sendRef.current = send useEffect(() => { diff --git a/frontend/src/hooks/useTerminalSession.ts b/frontend/src/hooks/useTerminalSession.ts index c0d3257..cd674e4 100644 --- a/frontend/src/hooks/useTerminalSession.ts +++ b/frontend/src/hooks/useTerminalSession.ts @@ -4,7 +4,7 @@ import { useSession } from '../context/SessionContext' const MAX_RECONNECT_ATTEMPTS = 5 const BASE_DELAY_MS = 500 -export function useTerminalSession(onOutput: (data: string) => void, onConnect?: () => void) { +export function useTerminalSession(onOutput: (data: string) => void, onConnect?: () => void, onHistory?: (data: string) => void) { const { state, dispatch } = useSession() const wsRef = useRef(null) const attemptsRef = useRef(0) @@ -34,6 +34,8 @@ export function useTerminalSession(onOutput: (data: string) => void, onConnect?: } if (msg.type === 'output' && msg.data) { onOutput(msg.data) + } else if (msg.type === 'history' && msg.data) { + onHistory?.(msg.data) } else if (msg.type === 'status') { if (msg.state === 'connected') { dispatch({ type: 'WS_STATE', timestamp: Date.now(), state: 'idle' }) From fb8a5630fdff5c8a99c0366aa15c6b67081c39df Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 12:34:06 +0300 Subject: [PATCH 21/25] [fix:ui] session header fix --- frontend/src/components/SessionHeader.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/SessionHeader.tsx b/frontend/src/components/SessionHeader.tsx index 75ccf86..e523da9 100644 --- a/frontend/src/components/SessionHeader.tsx +++ b/frontend/src/components/SessionHeader.tsx @@ -76,14 +76,16 @@ export default function SessionHeader({ const [renameSaving, setRenameSaving] = useState(false) useEffect(() => { - if (state.wsState !== 'running') return + const active = state.wsState === 'running' || (state.mode === 'terminal' && state.wsState === 'idle') + if (!active) return const timer = setInterval(() => setTick((t) => t + 1), 1000) return () => clearInterval(timer) - }, [state.wsState]) + }, [state.wsState, state.mode]) const workingMs = - state.workingTimeMs + - (state.runningStartedAt != null ? Date.now() - state.runningStartedAt : 0) + state.mode === 'terminal' && sessionStartedAt != null + ? Date.now() - sessionStartedAt + : state.workingTimeMs + (state.runningStartedAt != null ? Date.now() - state.runningStartedAt : 0) function startRename() { setNameInput(sessionName ?? '') @@ -184,7 +186,9 @@ export default function SessionHeader({ {workingMs > 0 && ( )} - + {state.mode !== 'terminal' && ( + + )}
From eb7262bb713c6e4bf53e4b4ca083667a4029b322 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 12:41:45 +0300 Subject: [PATCH 22/25] [fix:ui] settings page fix --- frontend/src/views/SettingsView.tsx | 51 ++++++++--------------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/frontend/src/views/SettingsView.tsx b/frontend/src/views/SettingsView.tsx index d66e64a..8b99390 100644 --- a/frontend/src/views/SettingsView.tsx +++ b/frontend/src/views/SettingsView.tsx @@ -2,10 +2,7 @@ import { useState, useEffect } from 'react' import { ArrowLeft, Save } from 'lucide-react' import { NavLink } from 'react-router-dom' -const API_KEY = 'dashboard_anthropic_api_key' - export default function SettingsView() { - const [apiKey, setApiKey] = useState(() => localStorage.getItem(API_KEY) ?? '') const [bypassPermissions, setBypassPermissions] = useState(true) const [sessionMode, setSessionMode] = useState<'chat' | 'terminal'>('chat') const [saved, setSaved] = useState(false) @@ -23,9 +20,6 @@ export default function SettingsView() { }, []) async function handleSave() { - if (apiKey) localStorage.setItem(API_KEY, apiKey) - else localStorage.removeItem(API_KEY) - await fetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -51,64 +45,47 @@ export default function SettingsView() {

Settings

- {/* API key */} -
- -

- Used to fetch billing usage totals in the Usage view. Stored in localStorage only. -

- setApiKey(e.target.value)} - placeholder="sk-ant-…" - className="w-full bg-bg-panel border border-border rounded-md px-3 py-2 text-sm text-text-primary placeholder-text-dim focus:outline-none focus:border-accent" - /> -
- - {/* Bypass permissions toggle */} + {/* Session mode toggle */}

- When enabled, Claude Code runs with --dangerously-skip-permissions — the same trust level as running claude directly in your terminal. When disabled, writes outside the workspace are sandboxed. + Controls the interface for new sessions. Chat uses the structured message view. Terminal opens a full interactive terminal connected directly to Claude Code.

- {/* Session mode toggle */} + {/* Bypass permissions toggle */}

- Controls the interface for new sessions. Chat uses the structured message view. Terminal opens a full interactive terminal connected directly to Claude Code. + When enabled, Claude Code runs with --dangerously-skip-permissions — the same trust level as running claude directly in your terminal. When disabled, writes outside the workspace are sandboxed.

From 4879bd1923349efefbe342b615531e617543891e Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 13:07:37 +0300 Subject: [PATCH 23/25] [fix:global] fixes --- .env.example | 3 -- CLAUDE.md | 21 +++++--- README.md | 13 +++-- backend/package.json | 1 - backend/src/db/schema.ts | 4 ++ backend/src/routes/sessions.ts | 16 +++++- backend/src/routes/settings.ts | 2 +- backend/src/services/anthropicApi.ts | 59 ----------------------- backend/src/services/usageCache.ts | 24 +-------- backend/src/ws/terminal.ts | 21 +++++++- docker-compose.yml | 5 +- frontend/src/components/SessionHeader.tsx | 12 ++--- frontend/src/hooks/useDashboard.ts | 2 +- frontend/src/views/DashboardView.tsx | 2 +- 14 files changed, 71 insertions(+), 114 deletions(-) delete mode 100644 backend/src/services/anthropicApi.ts diff --git a/.env.example b/.env.example index c15c0bf..181b273 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,3 @@ -# Anthropic API key for billing usage stats (optional — local logs work without it) -ANTHROPIC_API_KEY= - # Path to the claude binary (defaults to claude from PATH) CLAUDE_BIN=claude diff --git a/CLAUDE.md b/CLAUDE.md index 6942836..4993eb0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,6 @@ cp .env.example backend/.env | Variable | Default | Description | |---|---|---| -| `ANTHROPIC_API_KEY` | — | Optional; enables billing data in Usage view | | `CLAUDE_BIN` | `claude` | Path to the `claude` binary | | `PORT` | `9998` | Backend listen port | | `HOST` | `0.0.0.0` | Backend listen address | @@ -69,22 +68,30 @@ The Makefile's `dev-backend` target and `run.sh` both conditionally set `NODE_EX | Method | Path | Handler | Description | |---|---|---|---| | GET | `/api/account` | `routes/account.ts` | Account info + claude version | -| GET | `/api/usage` | `routes/usage.ts` | Usage stats (local logs + optional billing API) | +| GET | `/api/usage` | `routes/usage.ts` | Usage stats (local logs) | | GET/POST | `/api/sessions` | `routes/sessions.ts` | List sessions, create session | | POST | `/api/sessions/:id/stop` | `routes/sessions.ts` | Stop active session | | GET/POST | `/api/settings` | `routes/settings.ts` | Persistent key-value settings (SQLite-backed) | | GET | `/health` | `server.ts` | Health check | -| WS | `/ws/session/:id` | `ws/session.ts` | PTY I/O over WebSocket | +| WS | `/ws/session/:id` | `ws/session.ts` | Chat PTY I/O over WebSocket | +| WS | `/ws/terminal/:id` | `ws/terminal.ts` | Terminal PTY I/O over WebSocket | +| DELETE | `/api/sessions/:id` | `routes/sessions.ts` | Delete session (DB + `~/.claude/projects/` file) | ### Settings route -`routes/settings.ts` stores app settings in the SQLite `settings` table. Currently the only key is `bypass_permissions` (default `true`). Only keys in the `ALLOWED` set are accepted — add new keys there before using them. +`routes/settings.ts` stores app settings in the SQLite `settings` table. Keys: `bypass_permissions` (default `true`) and `session_mode` (default `'terminal'`). Only keys in the `ALLOWED` set are accepted — add new keys there before using them. + +### Session modes + +Two modes are supported per session, stored in the `sessions.mode` column: +- **`chat`** — structured JSON streaming via `/ws/session/:id`; messages stored in `messages` table +- **`terminal`** — raw PTY via `/ws/terminal/:id`; scrollback persisted in `sessions.terminal_scrollback` (256 KB circular buffer, flushed to SQLite on activity with a 500 ms debounce) ### Frontend data flow - `useDashboard.ts` fetches account + usage + sessions in parallel with a 60s auto-refresh -- `SessionContext.tsx` holds the active session state; `useWebSocket.ts` manages the WS connection with exponential-backoff reconnect -- `TerminalDrawer.tsx` wraps xterm.js and is **never unmounted** — it is CSS-toggled (display: none) to preserve terminal state across view switches +- `SessionContext.tsx` holds the active session state (including `mode`); `useWebSocket.ts` manages the chat WS connection; `useTerminalSession.ts` manages the terminal WS connection +- `TerminalSession.tsx` and `TerminalDrawer.tsx` are **never unmounted** — both are CSS-toggled (`display: none`) to preserve xterm scroll buffer across navigation ### Syntax highlighting @@ -102,7 +109,7 @@ docker-compose up # Dashboard at http://localhost:8080 ``` -`docker-compose.yml` mounts `~/.claude` (read-only) and `~/projects` (read-write) from the host. +`docker-compose.yml` mounts `~/.claude` (read-write — required for session deletion) and `~/projects` (read-write) from the host. ## Systemd service (Linux) diff --git a/README.md b/README.md index 2c44e06..0b067ed 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,16 @@ [![Docker](https://img.shields.io/badge/docker-ghcr.io-blue)](https://github.com/adeotek/claude-code-webui/pkgs/container/claude-code-webui) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) -A browser-based UI for [Claude Code](https://claude.ai/code) — manage sessions, track token usage, and chat with Claude from any device on your network. +A browser-based UI for [Claude Code](https://claude.ai/code) — manage sessions, track token usage, and interact with Claude from any device on your network. ## Features - **Session management** — create, resume, rename, and delete Claude Code sessions -- **Real-time chat** — markdown rendering with syntax-highlighted code blocks -- **Terminal drawer** — integrated xterm.js terminal that persists across view switches -- **Usage dashboard** — daily token usage graph with optional Anthropic billing data +- **Terminal mode** — full interactive xterm.js terminal connected directly to Claude Code (default) +- **Chat mode** — structured message view with markdown rendering and syntax-highlighted code blocks +- **Usage dashboard** — daily token usage graph - **Account overview** — Claude version, authentication status, and model info -- **Persistent history** — session messages and token counts stored in SQLite +- **Persistent history** — session messages, token counts, and terminal scrollback stored in SQLite ## Quick Start @@ -55,7 +55,7 @@ docker-compose up Open **http://localhost:8080**. -`docker-compose.yml` mounts `~/.claude` (read-only) and `~/projects` (read-write) from the host. To build the image locally instead of pulling from the registry: +`docker-compose.yml` mounts `~/.claude` (read-write — required for session deletion) and `~/projects` (read-write) from the host. To build the image locally instead of pulling from the registry: ```bash docker build -t claude-code-webui . @@ -72,7 +72,6 @@ cp .env.example backend/.env | Variable | Default | Description | |---|---|---| -| `ANTHROPIC_API_KEY` | — | Optional — enables billing data in the Usage view | | `CLAUDE_BIN` | `claude` | Path to the `claude` binary | | `PORT` | `9998` | Backend listen port | | `HOST` | `0.0.0.0` | Backend listen address | diff --git a/backend/package.json b/backend/package.json index 7141c17..e3cb551 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,7 +9,6 @@ "lint": "tsc --noEmit" }, "dependencies": { - "@anthropic-ai/sdk": "^0.52.0", "@fastify/cors": "^10.0.1", "@fastify/static": "^9.1.3", "@fastify/websocket": "^11.0.1", diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 48e9b82..2c5cbcb 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -73,6 +73,10 @@ function initDb(): Database.Database { db.prepare("ALTER TABLE sessions ADD COLUMN mode TEXT NOT NULL DEFAULT 'chat'").run() } + if (!sessionCols.find((c) => c.name === 'terminal_scrollback')) { + db.prepare('ALTER TABLE sessions ADD COLUMN terminal_scrollback TEXT').run() + } + db.prepare(` CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index 5be1d98..2c23f4a 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -100,7 +100,9 @@ export async function sessionRoutes(fastify: FastifyInstance) { fastify.delete<{ Params: { id: string } }>('/api/sessions/:id', async (req, reply) => { const { id } = req.params - const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(id) + const session = db + .prepare('SELECT workdir, claude_session_id FROM sessions WHERE id = ?') + .get(id) as { workdir: string; claude_session_id: string | null } | undefined if (!session) { return reply.status(404).send({ error: 'Session not found' }) } @@ -108,6 +110,18 @@ export async function sessionRoutes(fastify: FastifyInstance) { terminalManager.kill(id) db.prepare('DELETE FROM messages WHERE session_id = ?').run(id) db.prepare('DELETE FROM sessions WHERE id = ?').run(id) + + if (session.claude_session_id) { + const absWorkdir = session.workdir.startsWith('~') + ? path.join(os.homedir(), session.workdir.slice(1)) + : session.workdir + const encoded = absWorkdir.replace(/[/.]/g, '-') + const projectDir = path.join(os.homedir(), '.claude', 'projects', encoded) + const base = path.join(projectDir, session.claude_session_id) + try { fs.rmSync(`${base}.jsonl`) } catch { /* already gone */ } + try { fs.rmSync(base, { recursive: true }) } catch { /* already gone or absent */ } + } + return reply.send({ ok: true }) }) } diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index 1b4a432..4beffd3 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -3,7 +3,7 @@ import { db } from '../db/schema' const DEFAULTS: Record = { bypass_permissions: 'true', - session_mode: 'chat', + session_mode: 'terminal', } const ALLOWED = new Set(Object.keys(DEFAULTS)) diff --git a/backend/src/services/anthropicApi.ts b/backend/src/services/anthropicApi.ts deleted file mode 100644 index d143a3c..0000000 --- a/backend/src/services/anthropicApi.ts +++ /dev/null @@ -1,59 +0,0 @@ -import Anthropic from '@anthropic-ai/sdk' -import fs from 'fs' -import path from 'path' -import os from 'os' -import type { DayUsage } from './localLogs' - -let client: Anthropic | null = null - -function resolveApiKey(): string | null { - if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY - - // Fall back to ~/.claude/settings.json env block — where Claude Code users - // typically store ANTHROPIC_API_KEY without it being in the shell environment. - try { - const settingsPath = path.join(os.homedir(), '.claude', 'settings.json') - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as Record - const env = settings.env as Record | undefined - if (env?.ANTHROPIC_API_KEY) return env.ANTHROPIC_API_KEY - } catch { - // settings.json absent or malformed — no key available - } - - return null -} - -function getClient(): Anthropic | null { - const apiKey = resolveApiKey() - if (!apiKey) return null - if (!client) client = new Anthropic({ apiKey }) - return client -} - -export async function fetchApiUsage(month: string): Promise { - const api = getClient() - if (!api) return [] - - try { - const [year, mon] = month.split('-').map(Number) - const startDate = new Date(year, mon - 1, 1) - const endDate = new Date(year, mon, 0) - - // @ts-ignore — usage endpoint may not be in current SDK types - const response = await api.usage.monthly({ - start_time: startDate.toISOString(), - end_time: endDate.toISOString(), - }) - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const items: any[] = response?.data ?? [] - return items.map((item) => ({ - date: String(item.date ?? '').slice(0, 10), - inputTokens: Number(item.input_tokens ?? 0), - outputTokens: Number(item.output_tokens ?? 0), - costUsd: Number(item.cost_usd ?? 0), - })) - } catch { - return [] - } -} diff --git a/backend/src/services/usageCache.ts b/backend/src/services/usageCache.ts index d3616cc..1175f11 100644 --- a/backend/src/services/usageCache.ts +++ b/backend/src/services/usageCache.ts @@ -1,6 +1,5 @@ import { db } from '../db/schema' import { parseLocalUsage, type DayUsage } from './localLogs' -import { fetchApiUsage } from './anthropicApi' import { fetchOAuthUsage, type OAuthUsageData } from './oauthUsage' const CACHE_TTL_MS = 60 * 60 * 1000 // 1 hour @@ -39,13 +38,8 @@ export async function getUsage(month: string): Promise { ) } - const [localDays, apiDays] = await Promise.all([ - Promise.resolve(parseLocalUsage(month)), - fetchApiUsage(month), - ]) - - const merged = mergeDays(localDays, apiDays) - const sources: string[] = ['local', ...(apiDays.length > 0 ? ['api'] : [])] + const merged = parseLocalUsage(month) + const sources: string[] = ['local'] const upsert = db.prepare(` INSERT INTO usage_cache (date, input_tokens, output_tokens, cost_usd, source, cached_at) @@ -75,20 +69,6 @@ export async function getUsage(month: string): Promise { return buildResult(merged, month, sources, rateLimits) } -function mergeDays(local: DayUsage[], api: DayUsage[]): DayUsage[] { - const map = new Map() - for (const d of local) map.set(d.date, { ...d }) - for (const d of api) { - const existing = map.get(d.date) - if (existing) { - // API is authoritative for cost; local provides session-level token detail - existing.costUsd = d.costUsd - } else { - map.set(d.date, { ...d }) - } - } - return [...map.values()].sort((a, b) => a.date.localeCompare(b.date)) -} function buildResult(days: DayUsage[], month: string, sources: string[], rateLimits: OAuthUsageData | null): UsageResult { const sessions = (db diff --git a/backend/src/ws/terminal.ts b/backend/src/ws/terminal.ts index 62040a9..d118468 100644 --- a/backend/src/ws/terminal.ts +++ b/backend/src/ws/terminal.ts @@ -25,6 +25,7 @@ class ActiveTerminalSession { private ptyProc: pty.IPty | null = null private sockets = new Set() private idleTimer: NodeJS.Timeout | null = null + private flushTimer: NodeJS.Timeout | null = null private spawnError: string | null = null private spawned = false private scrollback = '' @@ -33,7 +34,22 @@ class ActiveTerminalSession { readonly id: string, private readonly workdir: string, private readonly onCleanup: (id: string) => void, - ) {} + ) { + const row = db + .prepare('SELECT terminal_scrollback FROM sessions WHERE id = ?') + .get(id) as { terminal_scrollback: string | null } | undefined + this.scrollback = row?.terminal_scrollback ?? '' + } + + private scheduleFlush() { + if (this.flushTimer) clearTimeout(this.flushTimer) + this.flushTimer = setTimeout(() => this.flushScrollback(), 500) + } + + private flushScrollback() { + if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null } + db.prepare('UPDATE sessions SET terminal_scrollback = ? WHERE id = ?').run(this.scrollback, this.id) + } private spawnPty(cols: number, rows: number) { const claudeBin = process.env.CLAUDE_BIN ?? 'claude' @@ -74,10 +90,12 @@ class ActiveTerminalSession { this.scrollback = this.scrollback.slice(this.scrollback.length - MAX_SCROLLBACK_BYTES) } this.broadcast({ type: 'output', data }) + this.scheduleFlush() }) this.ptyProc.onExit(() => { this.ptyProc = null + this.flushScrollback() db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), this.id) this.broadcast({ type: 'status', state: 'disconnected' }) if (this.idleTimer) clearTimeout(this.idleTimer) @@ -115,6 +133,7 @@ class ActiveTerminalSession { } kill() { + this.flushScrollback() db.prepare('UPDATE sessions SET ended_at = ? WHERE id = ?').run(Date.now(), this.id) if (this.ptyProc) { this.ptyProc.kill() diff --git a/docker-compose.yml b/docker-compose.yml index 26b2bd3..4bebfed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,11 +6,10 @@ services: ports: - "8080:9998" environment: - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - CLAUDE_BIN=${CLAUDE_BIN:-claude} volumes: - # Mount Claude Code credentials and session logs (read-only) - - ~/.claude:/root/.claude:ro + # Mount Claude Code credentials and session data (read-write for session deletion) + - ~/.claude:/root/.claude:rw # Mount projects directory for working directory access - ~/projects:/projects:rw restart: unless-stopped diff --git a/frontend/src/components/SessionHeader.tsx b/frontend/src/components/SessionHeader.tsx index e523da9..a2661da 100644 --- a/frontend/src/components/SessionHeader.tsx +++ b/frontend/src/components/SessionHeader.tsx @@ -76,16 +76,14 @@ export default function SessionHeader({ const [renameSaving, setRenameSaving] = useState(false) useEffect(() => { - const active = state.wsState === 'running' || (state.mode === 'terminal' && state.wsState === 'idle') - if (!active) return + if (state.wsState !== 'running') return const timer = setInterval(() => setTick((t) => t + 1), 1000) return () => clearInterval(timer) - }, [state.wsState, state.mode]) + }, [state.wsState]) const workingMs = - state.mode === 'terminal' && sessionStartedAt != null - ? Date.now() - sessionStartedAt - : state.workingTimeMs + (state.runningStartedAt != null ? Date.now() - state.runningStartedAt : 0) + state.workingTimeMs + + (state.runningStartedAt != null ? Date.now() - state.runningStartedAt : 0) function startRename() { setNameInput(sessionName ?? '') @@ -183,7 +181,7 @@ export default function SessionHeader({ {sessionStartedAt != null && ( )} - {workingMs > 0 && ( + {workingMs > 0 && state.mode !== 'terminal' && ( )} {state.mode !== 'terminal' && ( diff --git a/frontend/src/hooks/useDashboard.ts b/frontend/src/hooks/useDashboard.ts index 0b9d1f0..0d098a3 100644 --- a/frontend/src/hooks/useDashboard.ts +++ b/frontend/src/hooks/useDashboard.ts @@ -28,7 +28,7 @@ export function useDashboard(month?: string): DashboardData & { refresh: () => v const [account, setAccount] = useState(null) const [usage, setUsage] = useState(null) const [sessions, setSessions] = useState([]) - const [defaultSessionMode, setDefaultSessionMode] = useState<'chat' | 'terminal'>('chat') + const [defaultSessionMode, setDefaultSessionMode] = useState<'chat' | 'terminal'>('terminal') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) diff --git a/frontend/src/views/DashboardView.tsx b/frontend/src/views/DashboardView.tsx index 38a69ba..89939ab 100644 --- a/frontend/src/views/DashboardView.tsx +++ b/frontend/src/views/DashboardView.tsx @@ -97,7 +97,7 @@ export default function DashboardView() { } function handleResume(session: Session) { - dispatch({ type: 'RESUME_SESSION', id: session.id, workdir: session.workdir, mode: defaultSessionMode, ...(session.name ? { name: session.name } : {}) }) + dispatch({ type: 'RESUME_SESSION', id: session.id, workdir: session.workdir, mode: session.mode, ...(session.name ? { name: session.name } : {}) }) if (account?.model) dispatch({ type: 'MODEL_SET', model: account.model }) } From d7ee017d978bf5acedd38c811221ae6af6508253 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 14:05:33 +0300 Subject: [PATCH 24/25] [feat:tests] packages upgrades and unit tests added --- .github/workflows/claude-code-webui-ci.yml | 8 + Makefile | 6 +- backend/package-lock.json | 1608 ++++++- backend/package.json | 16 +- backend/src/routes/sessions.test.ts | 218 + backend/src/routes/sessions.ts | 4 +- backend/src/routes/settings.test.ts | 88 + backend/src/server.ts | 1 + backend/src/services/localLogs.test.ts | 217 + backend/src/test/db.ts | 38 + backend/tsconfig.json | 4 +- backend/vitest.config.ts | 7 + frontend/package-lock.json | 4150 ++++++++---------- frontend/package.json | 37 +- frontend/postcss.config.js | 6 - frontend/src/components/AssistantMessage.tsx | 3 +- frontend/src/components/SessionHeader.tsx | 28 +- frontend/src/components/SessionList.tsx | 22 +- frontend/src/components/UsageChart.tsx | 2 +- frontend/src/context/SessionContext.test.ts | 241 + frontend/src/context/SessionContext.tsx | 4 +- frontend/src/index.css | 5 +- frontend/src/test/setup.ts | 1 + frontend/src/utils/format.test.ts | 125 + frontend/src/utils/format.ts | 49 + frontend/src/vite-env.d.ts | 1 + frontend/tailwind.config.js | 40 + frontend/vite.config.ts | 13 +- frontend/vitest.config.ts | 9 + 29 files changed, 4404 insertions(+), 2547 deletions(-) create mode 100644 backend/src/routes/sessions.test.ts create mode 100644 backend/src/routes/settings.test.ts create mode 100644 backend/src/services/localLogs.test.ts create mode 100644 backend/src/test/db.ts create mode 100644 backend/vitest.config.ts delete mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/context/SessionContext.test.ts create mode 100644 frontend/src/test/setup.ts create mode 100644 frontend/src/utils/format.test.ts create mode 100644 frontend/src/utils/format.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/vitest.config.ts diff --git a/.github/workflows/claude-code-webui-ci.yml b/.github/workflows/claude-code-webui-ci.yml index 25d4529..904e5a9 100644 --- a/.github/workflows/claude-code-webui-ci.yml +++ b/.github/workflows/claude-code-webui-ci.yml @@ -42,3 +42,11 @@ jobs: - name: Build backend working-directory: backend run: npm run build + + - name: Test backend + working-directory: backend + run: npm test + + - name: Test frontend + working-directory: frontend + run: npm test diff --git a/Makefile b/Makefile index 330a9a0..ee2522d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: dev dev-backend dev-frontend build lint install clean run service-install service-uninstall service-test +.PHONY: dev dev-backend dev-frontend build lint test install clean run service-install service-uninstall service-test # Fedora / RHEL hosts lack the Google Trust Services intermediate CA that # Anthropic's API uses. Set NODE_EXTRA_CA_CERTS only when the bundle exists @@ -23,6 +23,10 @@ lint: cd backend && npm run lint cd frontend && npm run lint +test: + cd backend && npm test + cd frontend && npm test + install: cd backend && npm install cd frontend && npm install diff --git a/backend/package-lock.json b/backend/package-lock.json index f8c51dc..9657200 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -8,31 +8,54 @@ "name": "claude-code-webui-backend", "version": "0.1.0", "dependencies": { - "@anthropic-ai/sdk": "^0.52.0", - "@fastify/cors": "^10.0.1", + "@fastify/cors": "^11.2.0", "@fastify/static": "^9.1.3", "@fastify/websocket": "^11.0.1", - "better-sqlite3": "^11.0.0", + "better-sqlite3": "^12.10.0", "fastify": "^5.2.1", - "node-pty": "^1.1.0", - "uuid": "^11.1.0" + "node-pty": "^1.1.0" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", - "@types/node": "^22.15.0", - "@types/uuid": "^10.0.0", + "@types/node": "^25.7.0", "@types/ws": "^8.18.1", "tsx": "^4.19.4", - "typescript": "^5.7.3" + "typescript": "^6.0.3", + "vitest": "^4.1.6" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.52.0.tgz", - "integrity": "sha512-d4c+fg+xy9e46c8+YnrrgIQR45CZlAi7PwdzIfDXDM6ACxEZli1/fxhURsq30ZpMZy6LvSkr41jGq5aF5TD7rQ==", + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, "license": "MIT", - "bin": { - "anthropic-ai-sdk": "bin/cli" + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -515,9 +538,9 @@ } }, "node_modules/@fastify/cors": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", - "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz", + "integrity": "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==", "funding": [ { "type": "github", @@ -531,7 +554,7 @@ "license": "MIT", "dependencies": { "fastify-plugin": "^5.0.0", - "mnemonist": "0.40.0" + "toad-cache": "^3.7.0" } }, "node_modules/@fastify/error": { @@ -692,6 +715,13 @@ "ws": "^8.16.0" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -701,194 +731,649 @@ "node": ">=8" } }, - "node_modules/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", - "license": "MIT" - }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@types/node": "*" + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@types/node": { - "version": "22.19.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz", - "integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==", + "node_modules/@oxc-project/types": { + "version": "0.129.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", + "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", + "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "license": "MIT" - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", + "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/atomic-sleep": { + "node_modules/@rolldown/binding-freebsd-x64": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", + "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/avvio": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", - "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", + "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", + "cpu": [ + "arm" ], + "dev": true, "license": "MIT", - "dependencies": { - "@fastify/error": "^4.0.0", - "fastq": "^1.17.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", + "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "18 || 20 || >=22" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", + "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", + "cpu": [ + "arm64" ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, + "dev": true, "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", + "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", + "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", + "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "18 || 20 || >=22" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", + "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", + "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", + "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", + "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", + "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", + "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" @@ -908,6 +1393,16 @@ "ieee754": "^1.1.13" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -927,6 +1422,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -1012,6 +1514,13 @@ "once": "^1.4.0" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -1060,6 +1569,16 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1069,6 +1588,16 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1188,6 +1717,24 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1351,42 +1898,303 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, - "node_modules/light-my-request": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", - "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" ], - "license": "BSD-3-Clause", - "dependencies": { - "cookie": "^1.0.1", - "process-warning": "^4.0.0", - "set-cookie-parser": "^2.6.0" + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/light-my-request/node_modules/process-warning": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", - "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" ], - "license": "MIT" + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, "node_modules/lru-cache": { "version": "11.3.6", @@ -1397,6 +2205,16 @@ "node": "20 || >=22" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -1460,13 +2278,23 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "node_modules/mnemonist": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", - "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "obliterator": "^2.0.4" + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/napi-build-utils": { @@ -1503,10 +2331,15 @@ "node-addon-api": "^7.1.0" } }, - "node_modules/obliterator": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", - "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT" }, "node_modules/on-exit-leak-free": { @@ -1543,6 +2376,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1580,6 +2440,35 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -1721,6 +2610,40 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "node_modules/rolldown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", + "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.129.0", + "@rolldown/pluginutils": "1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0", + "@rolldown/binding-darwin-arm64": "1.0.0", + "@rolldown/binding-darwin-x64": "1.0.0", + "@rolldown/binding-freebsd-x64": "1.0.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", + "@rolldown/binding-linux-arm64-gnu": "1.0.0", + "@rolldown/binding-linux-arm64-musl": "1.0.0", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0", + "@rolldown/binding-linux-s390x-gnu": "1.0.0", + "@rolldown/binding-linux-x64-gnu": "1.0.0", + "@rolldown/binding-linux-x64-musl": "1.0.0", + "@rolldown/binding-openharmony-arm64": "1.0.0", + "@rolldown/binding-wasm32-wasi": "1.0.0", + "@rolldown/binding-win32-arm64-msvc": "1.0.0", + "@rolldown/binding-win32-x64-msvc": "1.0.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1812,6 +2735,13 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -1866,6 +2796,16 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1875,6 +2815,13 @@ "node": ">= 10.x" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1884,6 +2831,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", @@ -1948,6 +2902,50 @@ "node": ">=20" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/toad-cache": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", @@ -1966,6 +2964,14 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -1999,9 +3005,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2013,9 +3019,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", "dev": true, "license": "MIT" }, @@ -2025,17 +3031,189 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/uuid": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", - "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "node_modules/vite": { + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", + "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.0", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, "bin": { - "uuid": "dist/esm/bin/uuid" + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } }, "node_modules/wrappy": { diff --git a/backend/package.json b/backend/package.json index e3cb551..ed849a3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,23 +6,23 @@ "dev": "tsx watch --env-file=.env src/server.ts", "build": "tsc -p tsconfig.json", "start": "node dist/server.js", - "lint": "tsc --noEmit" + "lint": "tsc --noEmit", + "test": "vitest run" }, "dependencies": { - "@fastify/cors": "^10.0.1", + "@fastify/cors": "^11.2.0", "@fastify/static": "^9.1.3", "@fastify/websocket": "^11.0.1", - "better-sqlite3": "^11.0.0", + "better-sqlite3": "^12.10.0", "fastify": "^5.2.1", - "node-pty": "^1.1.0", - "uuid": "^11.1.0" + "node-pty": "^1.1.0" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", - "@types/node": "^22.15.0", - "@types/uuid": "^10.0.0", + "@types/node": "^25.7.0", "@types/ws": "^8.18.1", "tsx": "^4.19.4", - "typescript": "^5.7.3" + "typescript": "^6.0.3", + "vitest": "^4.1.6" } } diff --git a/backend/src/routes/sessions.test.ts b/backend/src/routes/sessions.test.ts new file mode 100644 index 0000000..0e17e04 --- /dev/null +++ b/backend/src/routes/sessions.test.ts @@ -0,0 +1,218 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' +import Fastify from 'fastify' +import type { FastifyInstance } from 'fastify' + +vi.mock('../ws/session', () => ({ sessionManager: { kill: vi.fn() } })) +vi.mock('../ws/terminal', () => ({ terminalManager: { kill: vi.fn() } })) +vi.mock('../db/schema', async () => { + const { createTestDb } = await import('../test/db') + return { db: createTestDb() } +}) + +const { db } = await import('../db/schema') +const { sessionManager } = await import('../ws/session') +const { terminalManager } = await import('../ws/terminal') + +describe('sessions routes', () => { + let app: FastifyInstance + + beforeEach(async () => { + db.prepare('DELETE FROM messages').run() + db.prepare('DELETE FROM sessions').run() + db.prepare('DELETE FROM settings').run() + + vi.clearAllMocks() + + app = Fastify() + const { sessionRoutes } = await import('./sessions') + await app.register(sessionRoutes) + await app.ready() + }) + + afterEach(async () => { + await app.close() + }) + + describe('POST /api/sessions', () => { + it('creates a session and returns 201 with a sessionId', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/sessions', + payload: { workdir: '/tmp/test' }, + }) + expect(res.statusCode).toBe(201) + const body = res.json() + expect(body).toHaveProperty('sessionId') + expect(typeof body.sessionId).toBe('string') + expect(body.sessionId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ) + }) + + it('returns 400 if workdir is missing', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/sessions', + payload: {}, + }) + expect(res.statusCode).toBe(400) + }) + + it('uses session_mode from settings when creating session', async () => { + db.prepare("INSERT INTO settings (key, value) VALUES ('session_mode', 'chat')").run() + + const res = await app.inject({ + method: 'POST', + url: '/api/sessions', + payload: { workdir: '/tmp/test' }, + }) + expect(res.statusCode).toBe(201) + const { sessionId } = res.json() + const row = db.prepare('SELECT mode FROM sessions WHERE id = ?').get(sessionId) as { mode: string } + expect(row.mode).toBe('chat') + }) + }) + + describe('GET /api/sessions', () => { + it('returns an empty array when no sessions exist', async () => { + const res = await app.inject({ method: 'GET', url: '/api/sessions' }) + expect(res.statusCode).toBe(200) + expect(res.json()).toEqual([]) + }) + + it('returns sessions after they are created', async () => { + await app.inject({ + method: 'POST', + url: '/api/sessions', + payload: { workdir: '/tmp/proj1' }, + }) + await app.inject({ + method: 'POST', + url: '/api/sessions', + payload: { workdir: '/tmp/proj2' }, + }) + + const res = await app.inject({ method: 'GET', url: '/api/sessions' }) + expect(res.statusCode).toBe(200) + const body = res.json() + expect(Array.isArray(body)).toBe(true) + expect(body.length).toBe(2) + }) + }) + + describe('PATCH /api/sessions/:id', () => { + it('renames a session and returns { ok: true }', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/sessions', + payload: { workdir: '/tmp/test' }, + }) + const { sessionId } = createRes.json() + + const patchRes = await app.inject({ + method: 'PATCH', + url: `/api/sessions/${sessionId}`, + payload: { name: 'My Session' }, + }) + expect(patchRes.statusCode).toBe(200) + expect(patchRes.json()).toEqual({ ok: true }) + + const row = db.prepare('SELECT name FROM sessions WHERE id = ?').get(sessionId) as { name: string } + expect(row.name).toBe('My Session') + }) + + it('returns 404 for unknown session id', async () => { + const res = await app.inject({ + method: 'PATCH', + url: '/api/sessions/non-existent-id', + payload: { name: 'Test' }, + }) + expect(res.statusCode).toBe(404) + }) + }) + + describe('DELETE /api/sessions/:id', () => { + it('deletes a session and returns { ok: true }', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/sessions', + payload: { workdir: '/tmp/test' }, + }) + const { sessionId } = createRes.json() + + const deleteRes = await app.inject({ + method: 'DELETE', + url: `/api/sessions/${sessionId}`, + }) + expect(deleteRes.statusCode).toBe(200) + expect(deleteRes.json()).toEqual({ ok: true }) + + const row = db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId) + expect(row).toBeUndefined() + }) + + it('returns 404 for unknown session id', async () => { + const res = await app.inject({ + method: 'DELETE', + url: '/api/sessions/non-existent-id', + }) + expect(res.statusCode).toBe(404) + }) + + it('calls sessionManager.kill and terminalManager.kill on delete', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/sessions', + payload: { workdir: '/tmp/test' }, + }) + const { sessionId } = createRes.json() + + await app.inject({ + method: 'DELETE', + url: `/api/sessions/${sessionId}`, + }) + + expect(sessionManager.kill).toHaveBeenCalledWith(sessionId) + expect(terminalManager.kill).toHaveBeenCalledWith(sessionId) + }) + + it('handles session with ~ workdir without crashing', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/sessions', + payload: { workdir: '~' }, + }) + const { sessionId } = createRes.json() + + const deleteRes = await app.inject({ + method: 'DELETE', + url: `/api/sessions/${sessionId}`, + }) + expect(deleteRes.statusCode).toBe(200) + expect(deleteRes.json()).toEqual({ ok: true }) + + expect(sessionManager.kill).toHaveBeenCalledWith(sessionId) + }) + }) + + describe('POST /api/sessions/:id/stop', () => { + it('stops a session and returns { ok: true }', async () => { + const createRes = await app.inject({ + method: 'POST', + url: '/api/sessions', + payload: { workdir: '/tmp/test' }, + }) + const { sessionId } = createRes.json() + + const stopRes = await app.inject({ + method: 'POST', + url: `/api/sessions/${sessionId}/stop`, + }) + expect(stopRes.statusCode).toBe(200) + expect(stopRes.json()).toEqual({ ok: true }) + + expect(sessionManager.kill).toHaveBeenCalledWith(sessionId) + expect(terminalManager.kill).toHaveBeenCalledWith(sessionId) + }) + }) +}) diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index 2c23f4a..b43e57d 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -1,5 +1,5 @@ import type { FastifyInstance } from 'fastify' -import { v4 as uuidv4 } from 'uuid' +import { randomUUID } from 'crypto' import fs from 'fs' import os from 'os' import path from 'path' @@ -54,7 +54,7 @@ export async function sessionRoutes(fastify: FastifyInstance) { .get() as { value: string } | undefined const mode = modeRow?.value === 'terminal' ? 'terminal' : 'chat' - const id = uuidv4() + const id = randomUUID() db.prepare( 'INSERT INTO sessions (id, workdir, name, mode, started_at) VALUES (?, ?, ?, ?, ?)', ).run(id, workdir, name?.trim() || null, mode, Date.now()) diff --git a/backend/src/routes/settings.test.ts b/backend/src/routes/settings.test.ts new file mode 100644 index 0000000..d52fcdd --- /dev/null +++ b/backend/src/routes/settings.test.ts @@ -0,0 +1,88 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' +import Fastify from 'fastify' +import type { FastifyInstance } from 'fastify' + +vi.mock('../db/schema', async () => { + const { createTestDb } = await import('../test/db') + return { db: createTestDb() } +}) + +// Import db AFTER mock so we get the test instance +const { db } = await import('../db/schema') + +describe('settings routes', () => { + let app: FastifyInstance + + beforeEach(async () => { + // Reset settings table before each test + db.prepare('DELETE FROM settings').run() + + app = Fastify() + const { settingsRoutes } = await import('./settings') + await app.register(settingsRoutes) + await app.ready() + }) + + afterEach(async () => { + await app.close() + }) + + it('GET /api/settings returns defaults when DB is empty', async () => { + const res = await app.inject({ method: 'GET', url: '/api/settings' }) + expect(res.statusCode).toBe(200) + const body = res.json() + expect(body).toMatchObject({ + bypass_permissions: 'true', + session_mode: 'terminal', + }) + }) + + it('POST /api/settings updates a valid key and GET reflects it', async () => { + const postRes = await app.inject({ + method: 'POST', + url: '/api/settings', + payload: { bypass_permissions: 'false' }, + }) + expect(postRes.statusCode).toBe(200) + expect(postRes.json()).toEqual({ ok: true }) + + const getRes = await app.inject({ method: 'GET', url: '/api/settings' }) + expect(getRes.statusCode).toBe(200) + expect(getRes.json().bypass_permissions).toBe('false') + }) + + it('POST /api/settings with an unknown key returns 400', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/settings', + payload: { unknown_key: 'value' }, + }) + expect(res.statusCode).toBe(400) + expect(res.json()).toMatchObject({ error: 'no valid keys' }) + }) + + it('POST /api/settings with a mix of valid and invalid keys stores valid ones', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/settings', + payload: { bypass_permissions: 'false', bogus_key: 'ignored' }, + }) + // Has at least one valid key → should succeed + expect(res.statusCode).toBe(200) + expect(res.json()).toEqual({ ok: true }) + + const getRes = await app.inject({ method: 'GET', url: '/api/settings' }) + expect(getRes.json().bypass_permissions).toBe('false') + }) + + it('GET /api/settings returns updated session_mode after POST', async () => { + await app.inject({ + method: 'POST', + url: '/api/settings', + payload: { session_mode: 'chat' }, + }) + + const getRes = await app.inject({ method: 'GET', url: '/api/settings' }) + expect(getRes.json().session_mode).toBe('chat') + }) +}) diff --git a/backend/src/server.ts b/backend/src/server.ts index d83482f..91e68af 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -20,6 +20,7 @@ const fastify = Fastify({ logger: true }) async function start() { const devOrigin = process.env.FRONTEND_ORIGIN await fastify.register(cors, { + methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], origin: (origin, cb) => { if (!origin) return cb(null, true) // same-origin / curl / no-CORS try { diff --git a/backend/src/services/localLogs.test.ts b/backend/src/services/localLogs.test.ts new file mode 100644 index 0000000..a8eafde --- /dev/null +++ b/backend/src/services/localLogs.test.ts @@ -0,0 +1,217 @@ +import { vi, describe, it, expect, beforeAll, afterAll } from 'vitest' +import { mkdirSync, writeFileSync, rmSync } from 'fs' +import { join } from 'path' + +const tmpDir = vi.hoisted(() => { + // Use require to avoid hoisting issues with ES module imports + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { mkdtempSync } = require('fs') as typeof import('fs') + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { tmpdir } = require('os') as typeof import('os') + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { join: pathJoin } = require('path') as typeof import('path') + return mkdtempSync(pathJoin(tmpdir(), 'ccw-test-')) +}) + +vi.mock('os', async (importOriginal) => { + const original = await importOriginal() + return { + ...original, + default: { ...original, homedir: () => tmpDir }, + homedir: () => tmpDir, + } +}) + +let parseLocalUsage: (month: string) => import('./localLogs').DayUsage[] + +beforeAll(async () => { + ;({ parseLocalUsage } = await import('./localLogs')) +}) + +afterAll(() => { + try { rmSync(tmpDir, { recursive: true }) } catch { /* ignore */ } +}) + +const PROJECTS_DIR = join(tmpDir, '.claude', 'projects') + +function makeProjectDir(name: string): string { + const dir = join(PROJECTS_DIR, name) + mkdirSync(dir, { recursive: true }) + return dir +} + +function writeJsonl(dir: string, filename: string, lines: object[]): void { + writeFileSync(join(dir, filename), lines.map(l => JSON.stringify(l)).join('\n') + '\n') +} + +describe('parseLocalUsage', () => { + it('returns empty array when projects directory does not exist', () => { + const result = parseLocalUsage('2024-01') + expect(result).toEqual([]) + }) + + it('returns empty array when projects directory is empty', () => { + mkdirSync(PROJECTS_DIR, { recursive: true }) + const result = parseLocalUsage('2024-01') + expect(result).toEqual([]) + }) + + it('aggregates token usage by date for matching month', () => { + const dir = makeProjectDir('proj-a') + writeJsonl(dir, 'session1.jsonl', [ + { + type: 'assistant', + timestamp: '2024-03-15T10:00:00.000Z', + costUSD: 0.01, + message: { usage: { input_tokens: 100, output_tokens: 50 } }, + }, + { + type: 'assistant', + timestamp: '2024-03-15T11:00:00.000Z', + costUSD: 0.02, + message: { usage: { input_tokens: 200, output_tokens: 100 } }, + }, + ]) + + const result = parseLocalUsage('2024-03') + expect(result).toHaveLength(1) + expect(result[0]).toMatchObject({ + date: '2024-03-15', + inputTokens: 300, + outputTokens: 150, + costUsd: expect.closeTo(0.03, 5), + }) + }) + + it('filters out entries not matching the requested month', () => { + const dir = makeProjectDir('proj-b') + writeJsonl(dir, 'session2.jsonl', [ + { + type: 'assistant', + timestamp: '2024-04-01T10:00:00.000Z', + costUSD: 0.05, + message: { usage: { input_tokens: 500, output_tokens: 250 } }, + }, + ]) + + const result = parseLocalUsage('2024-03') + const april = result.find(r => r.date.startsWith('2024-04')) + expect(april).toBeUndefined() + }) + + it('skips non-assistant type entries', () => { + const dir = makeProjectDir('proj-c') + writeJsonl(dir, 'session3.jsonl', [ + { + type: 'user', + timestamp: '2024-05-01T10:00:00.000Z', + message: { usage: { input_tokens: 999, output_tokens: 999 } }, + }, + { + type: 'tool_use', + timestamp: '2024-05-01T10:00:00.000Z', + message: { usage: { input_tokens: 888, output_tokens: 888 } }, + }, + { + type: 'assistant', + timestamp: '2024-05-01T10:00:00.000Z', + costUSD: 0.01, + message: { usage: { input_tokens: 10, output_tokens: 5 } }, + }, + ]) + + const result = parseLocalUsage('2024-05') + expect(result).toHaveLength(1) + expect(result[0].inputTokens).toBe(10) + expect(result[0].outputTokens).toBe(5) + }) + + it('skips malformed/non-JSON lines gracefully', () => { + const dir = makeProjectDir('proj-d') + const content = [ + 'not json at all', + '{broken json', + JSON.stringify({ + type: 'assistant', + timestamp: '2024-06-01T10:00:00.000Z', + costUSD: 0.01, + message: { usage: { input_tokens: 42, output_tokens: 21 } }, + }), + '', + ].join('\n') + writeFileSync(join(dir, 'session4.jsonl'), content) + + const result = parseLocalUsage('2024-06') + expect(result).toHaveLength(1) + expect(result[0].inputTokens).toBe(42) + }) + + it('adds cache_read_input_tokens to inputTokens', () => { + const dir = makeProjectDir('proj-e') + writeJsonl(dir, 'session5.jsonl', [ + { + type: 'assistant', + timestamp: '2024-07-01T10:00:00.000Z', + costUSD: 0.01, + message: { + usage: { + input_tokens: 100, + output_tokens: 50, + cache_read_input_tokens: 200, + }, + }, + }, + ]) + + const result = parseLocalUsage('2024-07') + expect(result).toHaveLength(1) + expect(result[0].inputTokens).toBe(300) + expect(result[0].outputTokens).toBe(50) + }) + + it('aggregates multiple entries across different dates and sorts by date', () => { + const dir = makeProjectDir('proj-f') + writeJsonl(dir, 'session6.jsonl', [ + { + type: 'assistant', + timestamp: '2024-08-03T10:00:00.000Z', + costUSD: 0.01, + message: { usage: { input_tokens: 10, output_tokens: 5 } }, + }, + { + type: 'assistant', + timestamp: '2024-08-01T10:00:00.000Z', + costUSD: 0.02, + message: { usage: { input_tokens: 20, output_tokens: 10 } }, + }, + { + type: 'assistant', + timestamp: '2024-08-03T11:00:00.000Z', + costUSD: 0.01, + message: { usage: { input_tokens: 30, output_tokens: 15 } }, + }, + ]) + + const result = parseLocalUsage('2024-08') + const dates = result.map(r => r.date) + expect(dates).toEqual([...dates].sort()) + const aug03 = result.find(r => r.date === '2024-08-03') + expect(aug03?.inputTokens).toBe(40) + expect(aug03?.outputTokens).toBe(20) + }) + + it('skips entries with missing usage field', () => { + const dir = makeProjectDir('proj-g') + writeJsonl(dir, 'session7.jsonl', [ + { + type: 'assistant', + timestamp: '2024-09-01T10:00:00.000Z', + costUSD: 0.01, + message: {}, + }, + ]) + + const result = parseLocalUsage('2024-09') + expect(result).toHaveLength(0) + }) +}) diff --git a/backend/src/test/db.ts b/backend/src/test/db.ts new file mode 100644 index 0000000..122aecb --- /dev/null +++ b/backend/src/test/db.ts @@ -0,0 +1,38 @@ +import Database from 'better-sqlite3' + +export function createTestDb(): Database.Database { + const db = new Database(':memory:') + db.pragma('journal_mode = WAL') + db.pragma('foreign_keys = ON') + + db.prepare(`CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, workdir TEXT NOT NULL, model TEXT, + name TEXT, mode TEXT NOT NULL DEFAULT 'chat', + started_at INTEGER NOT NULL, ended_at INTEGER, + claude_session_id TEXT, total_tokens INTEGER NOT NULL DEFAULT 0, + working_time_ms INTEGER NOT NULL DEFAULT 0, + terminal_scrollback TEXT + )`).run() + + db.prepare(`CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL REFERENCES sessions(id), + role TEXT NOT NULL CHECK(role IN ('user','assistant')), + content TEXT NOT NULL, tokens INTEGER, created_at INTEGER NOT NULL + )`).run() + + db.prepare(`CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, value TEXT NOT NULL + )`).run() + + db.prepare(`CREATE TABLE IF NOT EXISTS usage_cache ( + date TEXT PRIMARY KEY, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + cost_usd REAL NOT NULL DEFAULT 0, + source TEXT NOT NULL, + cached_at INTEGER NOT NULL + )`).run() + + return db +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 4acf9b4..78e4604 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "node", + "module": "Node16", + "moduleResolution": "node16", "outDir": "./dist", "rootDir": "./src", "strict": true, diff --git a/backend/vitest.config.ts b/backend/vitest.config.ts new file mode 100644 index 0000000..2b1c323 --- /dev/null +++ b/backend/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + }, +}) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5c62291..9045e2e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,190 +8,103 @@ "name": "claude-code-webui-frontend", "version": "0.1.0", "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "lucide-react": "^0.469.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-markdown": "^9.0.1", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "lucide-react": "^1.14.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-markdown": "^10.1.0", "react-router-dom": "^7.1.1", - "react-syntax-highlighter": "^15.6.1", - "recharts": "^2.14.1" + "react-syntax-highlighter": "^16.1.1", + "recharts": "^3.8.1" }, "devDependencies": { + "@tailwindcss/vite": "^4.3.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/node": "^25.7.0", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", - "@vitejs/plugin-react": "^4.3.4", - "autoprefixer": "^10.4.20", + "@vitejs/plugin-react": "^6.0.1", + "jsdom": "^29.1.1", "postcss": "^8.4.49", - "tailwindcss": "^3.4.17", - "typescript": "^5.7.3", - "vite": "^6.0.7" + "tailwindcss": "^4.3.0", + "typescript": "^6.0.3", + "vite": "^8.0.12", + "vitest": "^4.1.6" } }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } + "license": "MIT" }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } + "license": "MIT" }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -202,82 +115,11 @@ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/runtime": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", @@ -287,494 +129,209 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" + "css-tree": "^3.0.0" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "bin": { + "specificity": "bin/cli.js" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT-0", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], + "node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], + "node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, "engines": { - "node": ">=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "engines": { - "node": ">=18" + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, "node_modules/@jridgewell/gen-mapping": { @@ -827,69 +384,92 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@tybys/wasm-util": "^0.10.1" }, - "engines": { - "node": ">= 8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@oxc-project/types": { + "version": "0.129.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", + "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 8" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", - "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", + "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", - "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", "cpu": [ "arm64" ], @@ -897,27 +477,33 @@ "license": "MIT", "optional": true, "os": [ - "android" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", - "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", + "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", - "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", + "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", "cpu": [ "x64" ], @@ -925,209 +511,383 @@ "license": "MIT", "optional": true, "os": [ - "darwin" - ] + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", - "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", + "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", - "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", + "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", - "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", + "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", - "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", + "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", "cpu": [ - "arm" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", - "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", + "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", "cpu": [ - "arm64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", - "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", + "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", - "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", + "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", "cpu": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", - "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", + "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", + "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", + "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", + "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", - "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", - "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", - "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "freebsd" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", - "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", "cpu": [ - "riscv64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", - "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", - "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", - "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", "cpu": [ "x64" ], @@ -1136,12 +896,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", - "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", "cpu": [ "x64" ], @@ -1149,27 +912,46 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" - ] + "linux" + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", - "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "openharmony" - ] + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", - "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", "cpu": [ "arm64" ], @@ -1178,93 +960,147 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", - "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", - "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", - "cpu": [ - "x64" ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", - "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", - "cpu": [ - "x64" - ], + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" } }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.0.0" + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "tslib": "^2.4.0" } }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/d3-array": { @@ -1339,6 +1175,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1388,30 +1231,29 @@ "undici-types": "~7.21.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.28", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", - "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/react-syntax-highlighter": { @@ -1430,6 +1272,12 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@ungap/structured-clone": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", @@ -1437,219 +1285,223 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "@rolldown/pluginutils": "1.0.0-rc.7" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } } }, - "node_modules/@xterm/addon-fit": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", - "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "node_modules/@vitest/expect": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.0.0" + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@xterm/xterm": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "node_modules/@vitest/mocker": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">= 8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "node_modules/@vitest/runner": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "node_modules/@vitest/snapshot": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "node_modules/@vitest/spy": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", + "dev": true, "license": "MIT", "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://opencollective.com/vitest" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.29", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", - "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">=6.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, + "peer": true, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "dequal": "^2.0.3" } }, - "node_modules/camelcase-css": { + "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=12" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001792", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", - "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } }, "node_modules/ccount": { "version": "2.0.1", @@ -1661,6 +1513,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -1701,44 +1563,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1758,16 +1582,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1788,19 +1602,27 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=4" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1928,6 +1750,20 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1945,6 +1781,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -1973,6 +1816,16 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -1986,98 +1839,57 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.353", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", - "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", - "dev": true, - "license": "ISC" + "peer": true }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/enhanced-resolve": { + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", "dev": true, "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, "engines": { - "node": ">= 0.4" + "node": ">=10.13.0" } }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", "license": "MIT", - "engines": { - "node": ">=6" - } + "workspaces": [ + "docs", + "benchmarks" + ] }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", @@ -2089,66 +1901,37 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-equals": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", - "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" + "@types/estree": "^1.0.0" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, + "license": "Apache-2.0", "engines": { - "node": ">= 6" + "node": ">=12.0.0" } }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" }, "node_modules/fault": { "version": "1.0.4", @@ -2163,19 +1946,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -2184,20 +1954,6 @@ "node": ">=0.4.x" } }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2213,57 +1969,21 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } + "license": "ISC" }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "@types/hast": "^3.0.0" }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", - "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -2310,70 +2030,22 @@ } }, "node_modules/hastscript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", - "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "license": "MIT", "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/hastscript/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/hastscript/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/hastscript/node_modules/comma-separated-tokens": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/property-information": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -2389,6 +2061,19 @@ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -2399,6 +2084,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -2425,46 +2130,17 @@ } }, "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "license": "MIT", "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-decimal": { @@ -2477,29 +2153,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-hexadecimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", @@ -2510,16 +2163,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -2532,73 +2175,332 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" + "dev": true, + "license": "MIT", + "peer": true }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "dev": true, "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=6" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" }, "engines": { - "node": ">=6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, "node_modules/longest-streak": { "version": "3.1.0", @@ -2610,18 +2512,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", @@ -2637,24 +2527,45 @@ } }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/lucide-react": { - "version": "0.469.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.469.0.tgz", - "integrity": "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", @@ -2808,15 +2719,12 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } + "license": "CC0-1.0" }, "node_modules/micromark": { "version": "4.0.2", @@ -3260,18 +3168,14 @@ ], "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, "engines": { - "node": ">=8.6" + "node": ">=4" } }, "node_modules/ms": { @@ -3280,18 +3184,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -3302,50 +3194,25 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + ], "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" }, "node_modules/parse-entities": { "version": "4.0.2", @@ -3372,10 +3239,23 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -3387,38 +3267,18 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/postcss": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", @@ -3448,139 +3308,29 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", + "peer": true, "dependencies": { - "postcss-selector-parser": "^6.1.1" + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" }, "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" + "peer": true }, "node_modules/prismjs": { "version": "1.30.0", @@ -3591,23 +3341,6 @@ "node": ">=6" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -3618,62 +3351,48 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6" + } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.6" } }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-markdown": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", - "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -3697,14 +3416,27 @@ "react": ">=18" } }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } } }, "node_modules/react-router": { @@ -3745,225 +3477,101 @@ "react-dom": ">=18" } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "license": "MIT", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "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" - } - }, "node_modules/react-syntax-highlighter": { - "version": "15.6.6", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", - "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.3.1", + "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", - "refractor": "^3.6.0" + "refractor": "^5.0.0" }, - "peerDependencies": { - "react": ">= 0.14.0" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" + "engines": { + "node": ">= 16.20.2" }, "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" + "react": ">= 0.14.0" } }, "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", "license": "MIT", + "workspaces": [ + "www" + ], "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, - "node_modules/refractor": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", - "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", - "license": "MIT", - "dependencies": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/refractor/node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, "license": "MIT", "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">=8" } }, - "node_modules/refractor/node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" }, - "node_modules/refractor/node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "peerDependencies": { + "redux": "^5.0.0" } }, - "node_modules/refractor/node_modules/parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", "license": "MIT", "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/refractor/node_modules/prismjs": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", - "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -3997,133 +3605,81 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { - "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", - "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", + "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.129.0", + "@rolldown/pluginutils": "1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.3", - "@rollup/rollup-android-arm64": "4.60.3", - "@rollup/rollup-darwin-arm64": "4.60.3", - "@rollup/rollup-darwin-x64": "4.60.3", - "@rollup/rollup-freebsd-arm64": "4.60.3", - "@rollup/rollup-freebsd-x64": "4.60.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", - "@rollup/rollup-linux-arm-musleabihf": "4.60.3", - "@rollup/rollup-linux-arm64-gnu": "4.60.3", - "@rollup/rollup-linux-arm64-musl": "4.60.3", - "@rollup/rollup-linux-loong64-gnu": "4.60.3", - "@rollup/rollup-linux-loong64-musl": "4.60.3", - "@rollup/rollup-linux-ppc64-gnu": "4.60.3", - "@rollup/rollup-linux-ppc64-musl": "4.60.3", - "@rollup/rollup-linux-riscv64-gnu": "4.60.3", - "@rollup/rollup-linux-riscv64-musl": "4.60.3", - "@rollup/rollup-linux-s390x-gnu": "4.60.3", - "@rollup/rollup-linux-x64-gnu": "4.60.3", - "@rollup/rollup-linux-x64-musl": "4.60.3", - "@rollup/rollup-openbsd-x64": "4.60.3", - "@rollup/rollup-openharmony-arm64": "4.60.3", - "@rollup/rollup-win32-arm64-msvc": "4.60.3", - "@rollup/rollup-win32-ia32-msvc": "4.60.3", - "@rollup/rollup-win32-x64-gnu": "4.60.3", - "@rollup/rollup-win32-x64-msvc": "4.60.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "@rolldown/binding-android-arm64": "1.0.0", + "@rolldown/binding-darwin-arm64": "1.0.0", + "@rolldown/binding-darwin-x64": "1.0.0", + "@rolldown/binding-freebsd-x64": "1.0.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", + "@rolldown/binding-linux-arm64-gnu": "1.0.0", + "@rolldown/binding-linux-arm64-musl": "1.0.0", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0", + "@rolldown/binding-linux-s390x-gnu": "1.0.0", + "@rolldown/binding-linux-x64-gnu": "1.0.0", + "@rolldown/binding-linux-x64-musl": "1.0.0", + "@rolldown/binding-openharmony-arm64": "1.0.0", + "@rolldown/binding-wasm32-wasi": "1.0.0", + "@rolldown/binding-win32-arm64-msvc": "1.0.0", + "@rolldown/binding-win32-x64-msvc": "1.0.0" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", + "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", "dev": true, "license": "MIT" }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", + "license": "ISC", "dependencies": { - "queue-microtask": "^1.2.2" + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/set-cookie-parser": { "version": "2.7.2", @@ -4131,6 +3687,13 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4151,6 +3714,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -4165,6 +3742,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -4183,109 +3773,57 @@ "inline-style-parser": "0.2.7" } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } + "license": "MIT" }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } + "license": "MIT" }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, "engines": { - "node": ">=0.8" + "node": ">=18" } }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -4321,30 +3859,60 @@ } } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "bin": { + "tldts": "bin/cli.js" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=8.0" + "node": ">=20" } }, "node_modules/trim-lines": { @@ -4367,17 +3935,18 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "Apache-2.0" + "license": "0BSD", + "optional": true }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4388,6 +3957,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", @@ -4482,44 +4061,15 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, "peerDependencies": { - "browserslist": ">= 4.21.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -4549,9 +4099,9 @@ } }, "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -4571,24 +4121,23 @@ } }, "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", + "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "rolldown": "1.0.0", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -4597,14 +4146,15 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "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" @@ -4613,13 +4163,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -4645,52 +4198,177 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", + "node_modules/vitest": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, "engines": { - "node": ">=12.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "picomatch": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, "engines": { - "node": ">=0.4" + "node": ">=8" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, - "license": "ISC" + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" }, "node_modules/zwitch": { "version": "2.0.4", diff --git a/frontend/package.json b/frontend/package.json index fcb154e..9564ae4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,29 +7,34 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "tsc --noEmit --project tsconfig.app.json", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run" }, "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "lucide-react": "^0.469.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-markdown": "^9.0.1", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "lucide-react": "^1.14.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-markdown": "^10.1.0", "react-router-dom": "^7.1.1", - "react-syntax-highlighter": "^15.6.1", - "recharts": "^2.14.1" + "react-syntax-highlighter": "^16.1.1", + "recharts": "^3.8.1" }, "devDependencies": { + "@tailwindcss/vite": "^4.3.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/node": "^25.7.0", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", - "@vitejs/plugin-react": "^4.3.4", - "autoprefixer": "^10.4.20", + "@vitejs/plugin-react": "^6.0.1", + "jsdom": "^29.1.1", "postcss": "^8.4.49", - "tailwindcss": "^3.4.17", - "typescript": "^5.7.3", - "vite": "^6.0.7" + "tailwindcss": "^4.3.0", + "typescript": "^6.0.3", + "vite": "^8.0.12", + "vitest": "^4.1.6" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 2e7af2b..0000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/frontend/src/components/AssistantMessage.tsx b/frontend/src/components/AssistantMessage.tsx index 73542fd..4258014 100644 --- a/frontend/src/components/AssistantMessage.tsx +++ b/frontend/src/components/AssistantMessage.tsx @@ -54,8 +54,8 @@ export default function AssistantMessage({ content }: AssistantMessageProps) { C
+
{content} +
) diff --git a/frontend/src/components/SessionHeader.tsx b/frontend/src/components/SessionHeader.tsx index a2661da..c03ad1e 100644 --- a/frontend/src/components/SessionHeader.tsx +++ b/frontend/src/components/SessionHeader.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react' import { Plus, List, Square, Pencil } from 'lucide-react' import { useSession } from '../context/SessionContext' +import { formatModelName, formatTokens, formatDuration } from '../utils/format' interface SessionHeaderProps { onNewSession: () => void @@ -12,24 +13,6 @@ interface SessionHeaderProps { onRename?: (name: string) => Promise } -function formatModelName(model: string): string { - const s = model.replace(/^claude-/, '').replace(/-\d{8}$/, '') - const m = s.match(/^(opus|sonnet|haiku)-(\d+)-(\d+)/) - if (m) return `${m[1].charAt(0).toUpperCase() + m[1].slice(1)} ${m[2]}.${m[3]}` - const m2 = s.match(/^(\d+)-(?:(\d+)-)?(\w+)$/) - if (m2) { - const family = m2[3].charAt(0).toUpperCase() + m2[3].slice(1) - return m2[2] ? `${family} ${m2[1]}.${m2[2]}` : `${family} ${m2[1]}` - } - return s.charAt(0).toUpperCase() + s.slice(1) -} - -function formatTokens(n: number): string { - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` - if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K` - return String(n) -} - function formatCreatedAt(ts: number): string { const d = new Date(ts) const now = new Date() @@ -39,15 +22,6 @@ function formatCreatedAt(ts: number): string { return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + time } -function formatDuration(ms: number): string { - const totalSec = Math.floor(ms / 1000) - const h = Math.floor(totalSec / 3600) - const m = Math.floor((totalSec % 3600) / 60) - const s = totalSec % 60 - if (h > 0) return `${h}h ${m}m ${s}s` - if (m > 0) return `${m}m ${s}s` - return `${s}s` -} interface StatChipProps { label: string; value: string; valueClass?: string } function StatChip({ label, value, valueClass = 'text-text-secondary' }: StatChipProps) { diff --git a/frontend/src/components/SessionList.tsx b/frontend/src/components/SessionList.tsx index f5bb516..792638a 100644 --- a/frontend/src/components/SessionList.tsx +++ b/frontend/src/components/SessionList.tsx @@ -1,5 +1,6 @@ import { Plus, Play, Square, Trash2 } from 'lucide-react' import type { Session } from '../hooks/useDashboard' +import { formatRelativeTime, lastSegment } from '../utils/format' interface SessionListProps { sessions: Session[] @@ -9,27 +10,6 @@ interface SessionListProps { onNewSession: () => void } -function formatRelativeTime(ts: number): string { - const diffMs = Date.now() - ts - const diffSec = Math.floor(diffMs / 1000) - if (diffSec < 60) return 'just now' - const diffMin = Math.floor(diffSec / 60) - if (diffMin < 60) return `${diffMin}m ago` - const diffH = Math.floor(diffMin / 60) - if (diffH < 24) return `${diffH}h ago` - const diffD = Math.floor(diffH / 24) - if (diffD < 30) return `${diffD} day${diffD === 1 ? '' : 's'} ago` - const diffW = Math.floor(diffD / 7) - if (diffD < 60) return `${diffW} week${diffW === 1 ? '' : 's'} ago` - const diffMo = Math.floor(diffD / 30) - return `${diffMo} month${diffMo === 1 ? '' : 's'} ago` -} - -function lastSegment(path: string): string { - const trimmed = path.replace(/\/$/, '') - const idx = trimmed.lastIndexOf('/') - return idx >= 0 ? trimmed.slice(idx + 1) : trimmed -} async function deleteSession(sessionId: string): Promise { await fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' }) diff --git a/frontend/src/components/UsageChart.tsx b/frontend/src/components/UsageChart.tsx index 4c7bbee..b4afc9e 100644 --- a/frontend/src/components/UsageChart.tsx +++ b/frontend/src/components/UsageChart.tsx @@ -52,7 +52,7 @@ export default function UsageChart({ days }: UsageChartProps) { contentStyle={{ background: '#0f0f0f', border: '1px solid #2a2a2a', borderRadius: 4, fontSize: 11 }} labelStyle={{ color: '#888' }} itemStyle={{ color: '#d97706' }} - formatter={(v: number) => [formatK(v), 'tokens']} + formatter={(v) => [formatK(Number(v ?? 0)), 'tokens']} /> ({ + id, + role: 'user' as const, + content: 'hello', + createdAt: Date.now(), +}) + +describe('SessionContext reducer', () => { + describe('SESSION_CREATED', () => { + it('sets sessionId, mode, clears messages, sets wsState=connecting, resets workingTimeMs', () => { + const state = reducer(initial, { + type: 'SESSION_CREATED', + sessionId: 'abc-123', + workdir: '/tmp/proj', + mode: 'chat', + }) + expect(state.sessionId).toBe('abc-123') + expect(state.workdir).toBe('/tmp/proj') + expect(state.mode).toBe('chat') + expect(state.messages).toEqual([]) + expect(state.wsState).toBe('connecting') + expect(state.workingTimeMs).toBe(0) + expect(state.runningStartedAt).toBeNull() + }) + + it('sets mode to terminal when specified', () => { + const state = reducer(initial, { + type: 'SESSION_CREATED', + sessionId: 'xyz', + workdir: '/tmp', + mode: 'terminal', + }) + expect(state.mode).toBe('terminal') + }) + + it('sets name when provided', () => { + const state = reducer(initial, { + type: 'SESSION_CREATED', + sessionId: 'abc', + workdir: '/tmp', + mode: 'chat', + name: 'My Session', + }) + expect(state.name).toBe('My Session') + }) + + it('sets name to null when not provided', () => { + const state = reducer(initial, { + type: 'SESSION_CREATED', + sessionId: 'abc', + workdir: '/tmp', + mode: 'chat', + }) + expect(state.name).toBeNull() + }) + }) + + describe('SESSION_CLEARED', () => { + it('returns the exact initial state', () => { + const dirty: SessionState = { + ...initial, + sessionId: 'some-id', + messages: [makeMessage()], + totalTokens: 500, + } + const state = reducer(dirty, { type: 'SESSION_CLEARED' }) + expect(state).toEqual(initial) + }) + }) + + describe('RESUME_SESSION', () => { + it('sets id, mode, resets totalTokens to 0, clears messages', () => { + const dirty: SessionState = { + ...initial, + sessionId: 'old-id', + messages: [makeMessage()], + totalTokens: 999, + } + const state = reducer(dirty, { + type: 'RESUME_SESSION', + id: 'new-id', + workdir: '/tmp/new', + mode: 'terminal', + }) + expect(state.sessionId).toBe('new-id') + expect(state.workdir).toBe('/tmp/new') + expect(state.mode).toBe('terminal') + expect(state.totalTokens).toBe(0) + expect(state.messages).toEqual([]) + expect(state.wsState).toBe('connecting') + }) + }) + + describe('WS_STATE', () => { + it('idle → running: sets runningStartedAt to timestamp', () => { + const ts = 1_000_000 + const state = reducer( + { ...initial, wsState: 'idle' }, + { type: 'WS_STATE', state: 'running', timestamp: ts }, + ) + expect(state.wsState).toBe('running') + expect(state.runningStartedAt).toBe(ts) + }) + + it('running → idle: accumulates workingTimeMs and clears runningStartedAt', () => { + const startTs = 1_000_000 + const endTs = 1_005_000 + const intermediate: SessionState = { + ...initial, + wsState: 'running', + runningStartedAt: startTs, + workingTimeMs: 2000, + } + const state = reducer(intermediate, { + type: 'WS_STATE', + state: 'idle', + timestamp: endTs, + }) + expect(state.wsState).toBe('idle') + expect(state.workingTimeMs).toBe(7000) // 2000 + (5000ms elapsed) + expect(state.runningStartedAt).toBeNull() + }) + + it('running → running: no change to timing (stays running)', () => { + const startTs = 1_000_000 + const intermediate: SessionState = { + ...initial, + wsState: 'running', + runningStartedAt: startTs, + workingTimeMs: 1000, + } + const state = reducer(intermediate, { + type: 'WS_STATE', + state: 'running', + timestamp: 1_003_000, + }) + expect(state.wsState).toBe('running') + // runningStartedAt should not change since prev was already running + expect(state.runningStartedAt).toBe(startTs) + expect(state.workingTimeMs).toBe(1000) + }) + }) + + describe('TOKENS_ADDED', () => { + it('adds inputTokens + outputTokens to totalTokens', () => { + const state = reducer( + { ...initial, totalTokens: 100 }, + { type: 'TOKENS_ADDED', inputTokens: 50, outputTokens: 30 }, + ) + expect(state.totalTokens).toBe(180) + }) + + it('handles starting from 0', () => { + const state = reducer(initial, { + type: 'TOKENS_ADDED', + inputTokens: 200, + outputTokens: 100, + }) + expect(state.totalTokens).toBe(300) + }) + }) + + describe('MESSAGE_ADDED', () => { + it('appends message to messages array', () => { + const msg = makeMessage('1') + const state = reducer(initial, { type: 'MESSAGE_ADDED', message: msg }) + expect(state.messages).toHaveLength(1) + expect(state.messages[0]).toBe(msg) + }) + + it('preserves existing messages', () => { + const msg1 = makeMessage('1') + const msg2 = makeMessage('2') + const withOne: SessionState = { ...initial, messages: [msg1] } + const state = reducer(withOne, { type: 'MESSAGE_ADDED', message: msg2 }) + expect(state.messages).toHaveLength(2) + expect(state.messages[0]).toBe(msg1) + expect(state.messages[1]).toBe(msg2) + }) + }) + + describe('HISTORY_LOADED', () => { + it('prepends history messages before current messages', () => { + const existing = makeMessage('existing') + const history1 = makeMessage('h1') + const history2 = makeMessage('h2') + const withExisting: SessionState = { ...initial, messages: [existing] } + const state = reducer(withExisting, { + type: 'HISTORY_LOADED', + messages: [history1, history2], + }) + expect(state.messages).toHaveLength(3) + expect(state.messages[0]).toBe(history1) + expect(state.messages[1]).toBe(history2) + expect(state.messages[2]).toBe(existing) + }) + }) + + describe('MODEL_SET', () => { + it('updates the model field', () => { + const state = reducer(initial, { type: 'MODEL_SET', model: 'claude-sonnet-4-6' }) + expect(state.model).toBe('claude-sonnet-4-6') + }) + }) + + describe('SESSION_RENAMED', () => { + it('updates the name field', () => { + const state = reducer(initial, { type: 'SESSION_RENAMED', name: 'New Name' }) + expect(state.name).toBe('New Name') + }) + + it('sets name to null when null is passed', () => { + const withName: SessionState = { ...initial, name: 'Old Name' } + const state = reducer(withName, { type: 'SESSION_RENAMED', name: null }) + expect(state.name).toBeNull() + }) + }) + + describe('PERMISSION_REQUEST', () => { + it('sets pendingPermissions', () => { + const permissions = [{ tool: 'bash', summary: 'Run a command' }] + const state = reducer(initial, { type: 'PERMISSION_REQUEST', permissions }) + expect(state.pendingPermissions).toEqual(permissions) + }) + }) + + describe('PERMISSION_CLEARED', () => { + it('sets pendingPermissions to null', () => { + const withPending: SessionState = { + ...initial, + pendingPermissions: [{ tool: 'bash', summary: 'test' }], + } + const state = reducer(withPending, { type: 'PERMISSION_CLEARED' }) + expect(state.pendingPermissions).toBeNull() + }) + }) +}) diff --git a/frontend/src/context/SessionContext.tsx b/frontend/src/context/SessionContext.tsx index 6a51552..c0d31e0 100644 --- a/frontend/src/context/SessionContext.tsx +++ b/frontend/src/context/SessionContext.tsx @@ -40,7 +40,7 @@ type Action = | { type: 'PERMISSION_REQUEST'; permissions: PermissionRequest[] } | { type: 'PERMISSION_CLEARED' } -const initial: SessionState = { +export const initial: SessionState = { sessionId: null, workdir: null, name: null, @@ -54,7 +54,7 @@ const initial: SessionState = { pendingPermissions: null, } -function reducer(state: SessionState, action: Action): SessionState { +export function reducer(state: SessionState, action: Action): SessionState { switch (action.type) { case 'SESSION_CREATED': return { ...state, sessionId: action.sessionId, workdir: action.workdir, name: action.name ?? null, mode: action.mode, messages: [], wsState: 'connecting', workingTimeMs: 0, runningStartedAt: null, pendingPermissions: null } diff --git a/frontend/src/index.css b/frontend/src/index.css index 91552a2..2a04823 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,6 +1,5 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@config "../tailwind.config.js"; :root { color-scheme: dark; diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts new file mode 100644 index 0000000..c44951a --- /dev/null +++ b/frontend/src/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom' diff --git a/frontend/src/utils/format.test.ts b/frontend/src/utils/format.test.ts new file mode 100644 index 0000000..43b77d9 --- /dev/null +++ b/frontend/src/utils/format.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { + formatTokens, + formatDuration, + formatModelName, + formatRelativeTime, + lastSegment, +} from './format' + +describe('formatTokens', () => { + it('returns "0" for 0', () => { + expect(formatTokens(0)).toBe('0') + }) + + it('returns "500" for 500', () => { + expect(formatTokens(500)).toBe('500') + }) + + it('returns "1K" for 1000', () => { + expect(formatTokens(1000)).toBe('1K') + }) + + it('returns "2K" for 1500 (rounds)', () => { + expect(formatTokens(1500)).toBe('2K') + }) + + it('returns "1.5M" for 1_500_000', () => { + expect(formatTokens(1_500_000)).toBe('1.5M') + }) +}) + +describe('formatDuration', () => { + it('returns "0s" for 0ms', () => { + expect(formatDuration(0)).toBe('0s') + }) + + it('returns "5s" for 5000ms', () => { + expect(formatDuration(5000)).toBe('5s') + }) + + it('returns "1m 5s" for 65_000ms', () => { + expect(formatDuration(65_000)).toBe('1m 5s') + }) + + it('returns "1h 1m 1s" for 3_661_000ms', () => { + expect(formatDuration(3_661_000)).toBe('1h 1m 1s') + }) +}) + +describe('formatModelName', () => { + it('formats claude-opus-4-7-20251001 as "Opus 4.7"', () => { + expect(formatModelName('claude-opus-4-7-20251001')).toBe('Opus 4.7') + }) + + it('formats claude-sonnet-4-6 as "Sonnet 4.6"', () => { + expect(formatModelName('claude-sonnet-4-6')).toBe('Sonnet 4.6') + }) + + it('formats claude-haiku-4-5 as "Haiku 4.5"', () => { + expect(formatModelName('claude-haiku-4-5')).toBe('Haiku 4.5') + }) + + it('uppercases first char for unknown format', () => { + const result = formatModelName('unknown-model') + expect(result.charAt(0)).toBe(result.charAt(0).toUpperCase()) + }) +}) + +describe('formatRelativeTime', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('returns "just now" for 30s ago', () => { + const now = Date.now() + vi.setSystemTime(now) + expect(formatRelativeTime(now - 30_000)).toBe('just now') + }) + + it('returns "1m ago" for 90s ago', () => { + const now = Date.now() + vi.setSystemTime(now) + expect(formatRelativeTime(now - 90_000)).toBe('1m ago') + }) + + it('returns "2h ago" for 7_200_000ms ago', () => { + const now = Date.now() + vi.setSystemTime(now) + expect(formatRelativeTime(now - 7_200_000)).toBe('2h ago') + }) + + it('returns "2 days ago" for 2 days ago', () => { + const now = Date.now() + vi.setSystemTime(now) + expect(formatRelativeTime(now - 2 * 24 * 60 * 60 * 1000)).toBe('2 days ago') + }) + + it('returns "1 day ago" for exactly 1 day ago', () => { + const now = Date.now() + vi.setSystemTime(now) + expect(formatRelativeTime(now - 24 * 60 * 60 * 1000)).toBe('1 day ago') + }) +}) + +describe('lastSegment', () => { + it('returns "project" for "/home/user/project"', () => { + expect(lastSegment('/home/user/project')).toBe('project') + }) + + it('returns "dir" for "/path/to/dir/"', () => { + expect(lastSegment('/path/to/dir/')).toBe('dir') + }) + + it('returns "app" for "~/work/app"', () => { + expect(lastSegment('~/work/app')).toBe('app') + }) + + it('returns "single" for "/single"', () => { + expect(lastSegment('/single')).toBe('single') + }) +}) diff --git a/frontend/src/utils/format.ts b/frontend/src/utils/format.ts new file mode 100644 index 0000000..78b639f --- /dev/null +++ b/frontend/src/utils/format.ts @@ -0,0 +1,49 @@ +export function formatModelName(model: string): string { + const s = model.replace(/^claude-/, '').replace(/-\d{8}$/, '') + const m = s.match(/^(opus|sonnet|haiku)-(\d+)-(\d+)/) + if (m) return `${m[1].charAt(0).toUpperCase() + m[1].slice(1)} ${m[2]}.${m[3]}` + const m2 = s.match(/^(\d+)-(?:(\d+)-)?(\w+)$/) + if (m2) { + const family = m2[3].charAt(0).toUpperCase() + m2[3].slice(1) + return m2[2] ? `${family} ${m2[1]}.${m2[2]}` : `${family} ${m2[1]}` + } + return s.charAt(0).toUpperCase() + s.slice(1) +} + +export function formatTokens(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` + if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K` + return String(n) +} + +export function formatDuration(ms: number): string { + const totalSec = Math.floor(ms / 1000) + const h = Math.floor(totalSec / 3600) + const m = Math.floor((totalSec % 3600) / 60) + const s = totalSec % 60 + if (h > 0) return `${h}h ${m}m ${s}s` + if (m > 0) return `${m}m ${s}s` + return `${s}s` +} + +export function formatRelativeTime(ts: number): string { + const diffMs = Date.now() - ts + const diffSec = Math.floor(diffMs / 1000) + if (diffSec < 60) return 'just now' + const diffMin = Math.floor(diffSec / 60) + if (diffMin < 60) return `${diffMin}m ago` + const diffH = Math.floor(diffMin / 60) + if (diffH < 24) return `${diffH}h ago` + const diffD = Math.floor(diffH / 24) + if (diffD < 30) return `${diffD} day${diffD === 1 ? '' : 's'} ago` + const diffW = Math.floor(diffD / 7) + if (diffD < 60) return `${diffW} week${diffW === 1 ? '' : 's'} ago` + const diffMo = Math.floor(diffD / 30) + return `${diffMo} month${diffMo === 1 ? '' : 's'} ago` +} + +export function lastSegment(path: string): string { + const trimmed = path.replace(/\/$/, '') + const idx = trimmed.lastIndexOf('/') + return idx >= 0 ? trimmed.slice(idx + 1) : trimmed +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..670cba9 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,40 @@ +export default { + theme: { + extend: { + colors: { + bg: { + base: '#0a0a0a', + surface: '#0f0f0f', + elevated: '#141414', + panel: '#1a1a1a', + }, + border: { + subtle: '#1e1e1e', + DEFAULT: '#2a2a2a', + strong: '#333333', + }, + accent: { + DEFAULT: '#d97706', + hover: '#f59e0b', + muted: '#92400e', + }, + text: { + primary: '#c4c4c4', + secondary: '#aaaaaa', + muted: '#777777', + dim: '#555555', + }, + status: { + green: '#22c55e', + blue: '#93c5fd', + purple: '#c084fc', + red: '#f87171', + }, + }, + fontFamily: { + mono: ['JetBrains Mono', 'Fira Code', 'Cascadia Code', 'monospace'], + }, + }, + }, + plugins: [], +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 29160f5..5c43c02 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' import { hostname as getHostname, networkInterfaces } from 'os' const hostname = getHostname() @@ -8,15 +9,15 @@ const localIps = (Object.values(networkInterfaces()).flat() as { internal: boole .map(iface => iface.address) export default defineConfig({ - plugins: [react()], + plugins: [tailwindcss(), react()], build: { rollupOptions: { output: { - manualChunks: { - 'vendor-react': ['react', 'react-dom', 'react-router-dom'], - 'vendor-xterm': ['@xterm/xterm', '@xterm/addon-fit'], - 'vendor-charts': ['recharts'], - 'vendor-markdown': ['react-markdown', 'react-syntax-highlighter'], + manualChunks(id) { + if (id.includes('react-markdown') || id.includes('react-syntax-highlighter')) return 'vendor-markdown' + if (id.includes('recharts') || id.includes('d3-') || id.includes('victory-')) return 'vendor-charts' + if (id.includes('@xterm')) return 'vendor-xterm' + if (id.includes('react') || id.includes('react-dom') || id.includes('react-router')) return 'vendor-react' }, }, }, diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..34aaebb --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + globals: true, + }, +}) From ea99661d6923b62bdfdfd131c538d172eb569ae3 Mon Sep 17 00:00:00 2001 From: George Benjamin-Schonberger Date: Wed, 13 May 2026 14:35:24 +0300 Subject: [PATCH 25/25] [fix:global] fixes --- backend/tsconfig.json | 2 +- backend/vitest.config.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 78e4604..700f5a4 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -11,5 +11,5 @@ "resolveJsonModule": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "**/*.test.ts"] } diff --git a/backend/vitest.config.ts b/backend/vitest.config.ts index 2b1c323..5b5f2a8 100644 --- a/backend/vitest.config.ts +++ b/backend/vitest.config.ts @@ -3,5 +3,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { environment: 'node', + exclude: ['dist/**', 'node_modules/**'], }, })