diff --git a/.flow/epics/fn-4.json b/.flow/epics/fn-4.json new file mode 100644 index 0000000..da9dccb --- /dev/null +++ b/.flow/epics/fn-4.json @@ -0,0 +1,13 @@ +{ + "branch_name": "fn-4", + "created_at": "2026-01-24T19:04:00.779982Z", + "depends_on_epics": [], + "id": "fn-4", + "next_task": 1, + "plan_review_status": "unknown", + "plan_reviewed_at": null, + "spec_path": ".flow/specs/fn-4.md", + "status": "open", + "title": "Bond UI - Forensic Timeline Frontend", + "updated_at": "2026-01-24T19:04:39.520502Z" +} diff --git a/.flow/specs/fn-4.md b/.flow/specs/fn-4.md new file mode 100644 index 0000000..b56246a --- /dev/null +++ b/.flow/specs/fn-4.md @@ -0,0 +1,158 @@ +# Bond UI - Forensic Timeline Frontend + +## Overview + +A polished single-page web app that connects to Bond's streaming endpoints (SSE or WebSocket), renders the agent lifecycle as a live timeline (text/thinking/tool-call/tool-result), and supports "replay mode" (scrub + pause) for forensic debugging. + +## Scope + +**In Scope:** +- React + Vite + Tailwind + shadcn/ui + Framer Motion frontend +- SSE streaming transport (WebSocket optional follow-up) +- Event-driven store: BondEvents → Blocks reducer +- Timeline rendering with text/thinking/tool_call blocks +- Replay mode with pause/scrub functionality +- Inspector panel for block details +- Demo mode with pre-recorded events +- Dark mode only (premium devtool aesthetic) +- **Run header + status line** (trace ID, status, event count, connection indicator) +- **Active cursor affordance** (soft glow + shimmer on streaming block) + +**Out of Scope:** +- Authentication, accounts, persistence +- Multi-run browsing +- Complex theming system +- Backend modifications (beyond simple stream endpoint if needed) +- Mobile/responsive design + +## Approach + +### Architecture +- **State**: Event-driven reducer pattern - store full event stream, derive visible blocks +- **Transport**: EventSource API with reconnection and cleanup +- **Rendering**: Framer Motion for block animations, shadcn/ui primitives +- **Replay**: Client-side re-reduction from event history (optimize with caching if needed) + +### Two-Mode Event Story (Critical) + +The UI handles two distinct event formats: + +**Live Mode (WS/SSE frames)** — tiny payloads, maximum responsiveness: +```json +{"t":"block_start","kind":"text","idx":0} +{"t":"text","c":"Hello"} +{"t":"thinking","c":"Let me think..."} +{"t":"tool_delta","n":"read_file","a":"{\"path\":"} +{"t":"tool_exec","id":"call_123","name":"read_file","args":{"path":"/foo"}} +{"t":"tool_result","id":"call_123","name":"read_file","result":"contents..."} +{"t":"block_end","kind":"text","idx":0} +{"t":"complete","data":null} +``` + +SSE uses named event types: `event: text\ndata: {"c": "Hello"}` + +**Replay/Demo Mode (TraceEvent NDJSON)** — richer metadata + timing: +- Uses `TraceEvent` schema with `timestamp` + `wall_time` +- Demo file: `ui/public/demo-events.ndjson` +- Timing driven by `timestamp` field (monotonic clock) + +This separation prevents forcing trace semantics onto live streaming. + +### Key Technical Decisions +1. **Event normalization**: Map wire format (`t/n/a/c/idx`) to internal BondEvent type +2. **Block correlation**: Use `(kind, index)` for block ID; tool calls correlated by `id` field +3. **Tool delta handling**: Attach to currently active tool_call block (no id in delta) +4. **Auto-scroll**: Intersection Observer, only auto-scroll when at bottom +5. **High-frequency batching**: requestAnimationFrame batching (add virtualization only if needed) + +### Backend Integration +The UI connects to Bond's existing streaming infrastructure: +- `create_sse_handlers()` from `src/bond/utils.py:125-167` +- `create_websocket_handlers()` from `src/bond/utils.py:20-122` +- TraceEvent schema from `src/bond/trace/_models.py:38-60` + +**Note:** Bond provides handlers, not routes. The UI takes a URL via env var + input field. + +## Quick Commands + +```bash +# Start development server +cd ui && pnpm dev + +# Type check +cd ui && pnpm tsc --noEmit + +# Build for production +cd ui && pnpm build + +# Run with demo mode (no backend needed) +cd ui && pnpm dev # Then click "Run Demo" +``` + +## Acceptance Criteria + +- [ ] App loads with clean shell (header + sidebar + timeline) +- [ ] **Run header shows**: trace ID/name, status (live/paused/replay), event count +- [ ] **Connection indicator** shows latency/disconnect state +- [ ] Can connect to SSE endpoint and see blocks appear live +- [ ] Text blocks render with prose styling +- [ ] Thinking blocks are visually distinct (subtle, collapsible) +- [ ] Tool blocks show streaming args, status pill, result panel +- [ ] **Active cursor**: currently streaming block has soft glow + subtle shimmer +- [ ] Can pause stream and scrub timeline backwards +- [ ] Can click any block to see details in inspector panel +- [ ] Demo mode plays canned events without backend (using TraceEvent timing) +- [ ] Keyboard shortcuts work (Space=pause, L=live, J/K=step) +- [ ] UI feels like Linear/Vercel quality, not "React starter" + +## Resolved Questions (from review) + +### Wire Format — Canonical +The wire format is defined in `create_websocket_handlers()`: +- `{"t":"block_start","kind":str,"idx":int}` +- `{"t":"text","c":str}` / `{"t":"thinking","c":str}` +- `{"t":"tool_delta","n":str,"a":str}` +- `{"t":"tool_exec","id":str,"name":str,"args":dict}` +- `{"t":"tool_result","id":str,"name":str,"result":str}` +- `{"t":"complete","data":Any}` + +SSE uses `send(event_type, data)` — named event types work perfectly with EventSource. + +### Block Kind Values +Treat `kind` as opaque enum string from PydanticAI (`TextPartDelta`, `ThinkingPartDelta`, `ToolCallPartDelta`). Style-map known values, handle unknowns safely. + +### Parallel Tool Correlation +Tool execution and result are correlated by `id` field. `tool_delta` doesn't include id — attach to currently active tool_call block. + +### Timestamps for Replay +- Live streams don't include timestamps (not needed) +- Demo/replay uses `TraceEvent.timestamp` (monotonic) for timing +- Alternative: fixed cadence client-side for simpler demos + +### Maximum Event Rate +Implement requestAnimationFrame batching early. Add virtualization only if perf issues arise. + +## Edge Cases to Handle + +- Empty stream (timeout/loading state) +- Very long tool results (truncate/lazy-load) +- Stream disconnect mid-block (show "interrupted" state) +- Browser tab backgrounded (catch-up on focus) +- Invalid JSON in payloads (graceful degradation) +- Unknown `kind` values (render as generic block) + +## References + +### Backend Code +- Event types: `src/bond/trace/_models.py:14-23` +- TraceEvent model: `src/bond/trace/_models.py:38-60` +- SSE handlers: `src/bond/utils.py:125-167` +- WebSocket handlers: `src/bond/utils.py:20-122` +- StreamHandlers: `src/bond/agent.py:28-74` +- TraceReplayer: `src/bond/trace/replay.py:15-145` + +### Documentation +- [Vite + React Setup](https://vite.dev/guide/) +- [shadcn/ui Installation](https://ui.shadcn.com/docs/installation/vite) +- [Framer Motion Lists](https://motion.dev/docs/react-animate-presence) +- [MDN EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) diff --git a/.flow/tasks/fn-4.1.json b/.flow/tasks/fn-4.1.json new file mode 100644 index 0000000..ebd3ddd --- /dev/null +++ b/.flow/tasks/fn-4.1.json @@ -0,0 +1,23 @@ +{ + "assignee": "bordumbb@gmail.com", + "claim_note": "", + "claimed_at": "2026-01-24T19:20:05.624754Z", + "created_at": "2026-01-24T19:04:50.787904Z", + "depends_on": [], + "epic": "fn-4", + "evidence": { + "commits": [ + "852672a360959781775004d0561294b54a092e73" + ], + "prs": [], + "tests": [ + "pnpm tsc --noEmit" + ] + }, + "id": "fn-4.1", + "priority": null, + "spec_path": ".flow/tasks/fn-4.1.md", + "status": "done", + "title": "Project scaffold + styling baseline", + "updated_at": "2026-01-24T19:29:15.712322Z" +} diff --git a/.flow/tasks/fn-4.1.md b/.flow/tasks/fn-4.1.md new file mode 100644 index 0000000..d565896 --- /dev/null +++ b/.flow/tasks/fn-4.1.md @@ -0,0 +1,63 @@ +# fn-4.1 Project scaffold + styling baseline + +## Description + +Set up the frontend project with Vite, React, TypeScript, Tailwind CSS, shadcn/ui, and Framer Motion. Create the app shell layout with header, sidebar, and main timeline area. + +## Implementation + +1. Create `ui/` directory in project root +2. Initialize Vite + React + TypeScript project +3. Install and configure Tailwind CSS v4 +4. Initialize shadcn/ui with components: Card, Button, ScrollArea, Badge +5. Install Framer Motion (motion package) +6. Create minimal dark theme: `zinc-950` background, `zinc-800` borders +7. Implement App shell layout: + - Header with logo, "Run Demo" and "Connect" buttons + - **Run header/status line**: trace ID, status indicator, event count, connection state + - Sidebar card showing session info + - Main timeline area with ScrollArea + +## Files to Create + +- `ui/package.json` - dependencies +- `ui/vite.config.ts` - Vite config with path aliases +- `ui/tsconfig.json` - TypeScript config +- `ui/src/index.css` - Tailwind imports + CSS variables +- `ui/src/App.tsx` - Shell layout +- `ui/src/main.tsx` - Entry point +- `ui/index.html` - HTML template +- `ui/components.json` - shadcn/ui config + +## References + +- App shell layout from plan.md lines 44-90 +- [Vite React setup](https://vite.dev/guide/) +- [shadcn/ui Vite installation](https://ui.shadcn.com/docs/installation/vite) +## Acceptance +- [ ] `pnpm dev` starts dev server without errors +- [ ] `pnpm tsc --noEmit` passes type check +- [ ] App loads with header showing "Bond" logo and buttons +- [ ] Sidebar shows "Session" card with placeholder text +- [ ] **Run header shows**: trace ID placeholder, status (idle), event count (0) +- [ ] **Connection indicator**: shows "disconnected" state +- [ ] Timeline area shows "Waiting for events..." placeholder +- [ ] Dark theme applied: zinc-950 background, zinc-800 borders +- [ ] Typography feels premium (not default browser styles) +## Done summary +- Created ui/ directory with Vite + React + TypeScript scaffold +- Configured Tailwind CSS v4 with @tailwindcss/vite plugin +- Added shadcn/ui components (Button, Card, ScrollArea, Badge) with dark theme +- Installed Framer Motion for animations + +- Sets foundation for all subsequent UI tasks +- Dark theme and component library provide premium devtool aesthetic + +- `pnpm tsc --noEmit` passes without errors +- Dev server starts successfully + +- lib/utils.ts created for shadcn component utilities +## Evidence +- Commits: 852672a360959781775004d0561294b54a092e73 +- Tests: pnpm tsc --noEmit +- PRs: \ No newline at end of file diff --git a/.flow/tasks/fn-4.2.json b/.flow/tasks/fn-4.2.json new file mode 100644 index 0000000..0ee11c3 --- /dev/null +++ b/.flow/tasks/fn-4.2.json @@ -0,0 +1,25 @@ +{ + "assignee": "bordumbb@gmail.com", + "claim_note": "", + "claimed_at": "2026-01-24T19:30:03.981082Z", + "created_at": "2026-01-24T19:04:52.566678Z", + "depends_on": [ + "fn-4.1" + ], + "epic": "fn-4", + "evidence": { + "commits": [ + "572993dbf3ce2cad47d093be9f5c904a41d3691c" + ], + "prs": [], + "tests": [ + "pnpm tsc --noEmit" + ] + }, + "id": "fn-4.2", + "priority": null, + "spec_path": ".flow/tasks/fn-4.2.md", + "status": "done", + "title": "Event schema + block model", + "updated_at": "2026-01-24T19:32:11.849689Z" +} diff --git a/.flow/tasks/fn-4.2.md b/.flow/tasks/fn-4.2.md new file mode 100644 index 0000000..05a1d7e --- /dev/null +++ b/.flow/tasks/fn-4.2.md @@ -0,0 +1,97 @@ +# fn-4.2 Event schema + block model + +## Description + +Define TypeScript types for Bond events and the Block model. Implement the reducer that transforms streaming events into renderable blocks. + +## Implementation + +### Two-Mode Event Story + +The UI handles two distinct formats: + +**Live Mode (canonical wire format from `create_websocket_handlers()`):** +```json +{"t":"block_start","kind":"text","idx":0} +{"t":"text","c":"Hello"} +{"t":"thinking","c":"Let me think..."} +{"t":"tool_delta","n":"read_file","a":"{\"path\":"} +{"t":"tool_exec","id":"call_123","name":"read_file","args":{"path":"/foo"}} +{"t":"tool_result","id":"call_123","name":"read_file","result":"..."} +{"t":"block_end","kind":"text","idx":0} +{"t":"complete","data":null} +``` + +SSE uses named event types: `event: text\ndata: {"c": "Hello"}` + +**Replay/Demo Mode (TraceEvent NDJSON):** +- Uses full `TraceEvent` schema with `timestamp` + `wall_time` +- Richer metadata for timing-accurate replay + +### Steps + +1. Define `WireEvent` type matching canonical wire format (`t/n/a/c/idx`) + +2. Define internal `BondEvent` union type (normalized): + - `block_start`, `block_end` (kind, index) + - `text_delta`, `thinking_delta` (content) + - `tool_call_delta` (name, args deltas) + - `tool_execute`, `tool_result` (id, name, args/result) + - `complete` + +3. Define `Block` type: + - Text block: id, kind, content, isClosed, **isActive** + - Thinking block: id, kind, content, isClosed, **isActive** + - Tool block: id, kind, draft state, final state, status, result, **isActive** + +4. Create normalization layer: + - `normalizeWireEvent(wire: WireEvent): BondEvent` + - `normalizeSSEEvent(type: string, data: object): BondEvent` + - `normalizeTraceEvent(trace: TraceEvent): BondEvent` + +5. Implement reducer: + - Handle block_start: create new block, mark as active + - Handle deltas: append to active block + - Handle tool_execute: transition to "executing", correlate by `id` + - Handle tool_result: attach result by `id`, mark "done" + - Handle block_end: close block, clear active state + +**Key insight:** `tool_delta` has no `id` — attach to currently active tool_call block. + +## Files to Create + +- `ui/src/bond/types.ts` - WireEvent, BondEvent, Block, TraceEvent types +- `ui/src/bond/reducer.ts` - Event reducer with active block tracking +- `ui/src/bond/normalize.ts` - Normalization for WS/SSE/TraceEvent formats + +## References + +- Type definitions from plan.md lines 109-143 +- Reducer skeleton from plan.md lines 144-230 +- Backend event types: `src/bond/trace/_models.py:14-23` +- SSE format: `src/bond/utils.py:158-167` +## Acceptance +- [ ] `WireEvent` type matches canonical wire format (`t/n/a/c/idx`) +- [ ] `BondEvent` type covers all 8 normalized event types +- [ ] `Block` type supports text, thinking, tool_call with `isActive` flag +- [ ] `normalizeWireEvent()` converts wire format to BondEvent +- [ ] `normalizeSSEEvent()` handles named SSE event types +- [ ] `normalizeTraceEvent()` handles TraceEvent for replay/demo +- [ ] Reducer tracks active block and marks it correctly +- [ ] Reducer handles tool_delta attaching to active tool_call block +- [ ] Reducer correlates tool_execute/tool_result by `id` field +- [ ] Unknown `kind` values handled gracefully (generic block) +## Done summary +- Created types.ts with WireEvent, BondEvent, Block, TraceEvent types +- Created normalize.ts with normalizeWireEvent, normalizeSSEEvent, normalizeTraceEvent +- Created reducer.ts with bondReducer and reduceEvents helper + +- Establishes foundation for streaming transport and timeline rendering +- Two-mode event story cleanly separates live vs replay formats + +- `pnpm tsc --noEmit` passes without errors +- All type definitions match canonical wire format from backend +## Evidence +- Commits: 572993dbf3ce2cad47d093be9f5c904a41d3691c +- Tests: pnpm tsc --noEmit +- PRs: \ No newline at end of file diff --git a/.flow/tasks/fn-4.3.json b/.flow/tasks/fn-4.3.json new file mode 100644 index 0000000..336dcb5 --- /dev/null +++ b/.flow/tasks/fn-4.3.json @@ -0,0 +1,25 @@ +{ + "assignee": "bordumbb@gmail.com", + "claim_note": "", + "claimed_at": "2026-01-24T19:39:30.265101Z", + "created_at": "2026-01-24T19:04:54.479935Z", + "depends_on": [ + "fn-4.2" + ], + "epic": "fn-4", + "evidence": { + "commits": [ + "49ac926b47703a6cec96b165db0dde6d035e8ff8" + ], + "prs": [], + "tests": [ + "pnpm tsc --noEmit" + ] + }, + "id": "fn-4.3", + "priority": null, + "spec_path": ".flow/tasks/fn-4.3.md", + "status": "done", + "title": "Streaming transport (SSE)", + "updated_at": "2026-01-24T19:40:48.588395Z" +} diff --git a/.flow/tasks/fn-4.3.md b/.flow/tasks/fn-4.3.md new file mode 100644 index 0000000..04061b1 --- /dev/null +++ b/.flow/tasks/fn-4.3.md @@ -0,0 +1,62 @@ +# fn-4.3 Streaming transport (SSE) + +## Description + +Build the SSE streaming transport layer with `useBondStream` hook. Handle connection, disconnection, reconnection, and event dispatch. + +## Implementation + +1. Create `useBondStream(url)` hook: + - EventSource connection management + - Status tracking: idle, connecting, live, error + - Pause control (buffer events when paused) + - Event history storage for replay + +2. Parse SSE format: + - Handle `event:` and `data:` lines + - Parse JSON payloads + - Normalize to BondEvent type + +3. Reconnection logic: + - Detect disconnect vs error states + - Exponential backoff for manual reconnect + - Don't fight browser auto-reconnect + +4. Cleanup: + - Close EventSource on unmount + - Clear buffers on disconnect + +## Files to Create + +- `ui/src/bond/useBondStream.ts` - Main streaming hook +- `ui/src/bond/useEventHistory.ts` - Event buffer for replay + +## References + +- Hook skeleton from plan.md lines 251-295 +- [MDN EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) +- SSE format: `src/bond/utils.py:158-167` +## Acceptance +- [ ] `useBondStream` returns state, status, controls +- [ ] Calling `connect()` opens EventSource connection +- [ ] Status transitions: idle → connecting → live +- [ ] Incoming events dispatch to reducer +- [ ] Events are stored in history buffer +- [ ] `disconnect()` closes connection cleanly +- [ ] `setPaused(true)` stops applying events to state +- [ ] Paused events are buffered, not lost +- [ ] Connection errors set status to "error" +- [ ] EventSource cleanup on component unmount +## Done summary +- Created useBondStream hook with EventSource connection management +- Created useEventHistory hook for replay buffer +- Implemented pause control with event buffering + +- Enables live streaming from SSE endpoints +- History buffer supports replay/scrub functionality + +- `pnpm tsc --noEmit` passes without errors +## Evidence +- Commits: 49ac926b47703a6cec96b165db0dde6d035e8ff8 +- Tests: pnpm tsc --noEmit +- PRs: \ No newline at end of file diff --git a/.flow/tasks/fn-4.4.json b/.flow/tasks/fn-4.4.json new file mode 100644 index 0000000..c9e696b --- /dev/null +++ b/.flow/tasks/fn-4.4.json @@ -0,0 +1,30 @@ +{ + "assignee": "bordumbb@gmail.com", + "claim_note": "", + "claimed_at": "2026-01-24T19:41:13.317835Z", + "created_at": "2026-01-24T19:04:56.144931Z", + "depends_on": [ + "fn-4.2" + ], + "epic": "fn-4", + "evidence": { + "commit": "ea466ae", + "files_created": [ + "ui/src/ui/Timeline.tsx", + "ui/src/ui/BlockCard.tsx", + "ui/src/ui/TextBlock.tsx", + "ui/src/ui/ThinkingBlock.tsx", + "ui/src/ui/ToolBlock.tsx" + ], + "files_modified": [ + "ui/src/index.css" + ], + "type_check": "tsc --noEmit passed" + }, + "id": "fn-4.4", + "priority": null, + "spec_path": ".flow/tasks/fn-4.4.md", + "status": "done", + "title": "Timeline rendering", + "updated_at": "2026-01-24T19:44:07.291644Z" +} diff --git a/.flow/tasks/fn-4.4.md b/.flow/tasks/fn-4.4.md new file mode 100644 index 0000000..c4e4350 --- /dev/null +++ b/.flow/tasks/fn-4.4.md @@ -0,0 +1,75 @@ +# fn-4.4 Timeline rendering + +## Description + +Build the timeline UI that renders blocks with appropriate styling and animations. This is the core visual component. + +## Implementation + +1. Create `Timeline` component: + - Receives blocks array + - Renders list with Framer Motion animations + - Auto-scroll to bottom when new blocks arrive + +2. Create `BlockCard` variants: + - Text block: clean prose, `text-zinc-50` + - Thinking block: subtle, collapsible, `text-zinc-300`, "Thinking" label + - Tool block: name, args (monospace), status badge, result panel + - Unknown kind: generic fallback block + +3. **Active cursor affordance** (the "it's alive" visual): + - Active block (isActive=true) has soft glow border + - Subtle caret/shimmer animation on streaming content + - Makes the UI feel responsive and alive + +4. Animations: + - Slide/fade-in as blocks appear (`initial`, `animate`) + - Use `AnimatePresence` for list + - Stable keys from block.id + +5. Auto-scroll: + - Intersection Observer on bottom sentinel + - Only auto-scroll when user is at bottom + - Show "scroll to bottom" when scrolled up + +## Files to Create + +- `ui/src/ui/Timeline.tsx` - Main timeline component +- `ui/src/ui/BlockCard.tsx` - Block rendering variants +- `ui/src/ui/TextBlock.tsx` - Text block component +- `ui/src/ui/ThinkingBlock.tsx` - Thinking block component +- `ui/src/ui/ToolBlock.tsx` - Tool block component + +## References + +- Block renderer from plan.md lines 318-385 +- Status badge styling from plan.md lines 324-326 +## Acceptance +- [ ] Timeline renders list of blocks +- [ ] Text blocks show content with prose styling +- [ ] Thinking blocks show "Thinking" label, dimmer text +- [ ] Tool blocks show tool name and status badge +- [ ] Tool blocks show streaming args in monospace +- [ ] Tool blocks show result panel when done +- [ ] New blocks animate in (fade + slide) +- [ ] **Active block has soft glow border** +- [ ] **Streaming content shows subtle shimmer/caret** +- [ ] Unknown `kind` values render as generic block +- [ ] Auto-scroll follows new blocks when at bottom +- [ ] User can scroll up without fighting auto-scroll +- [ ] "Scroll to bottom" button appears when scrolled up +## Done summary +## Summary +Implemented timeline rendering components with Framer Motion animations: +- Timeline.tsx: Main component with auto-scroll using Intersection Observer +- BlockCard.tsx: Wrapper with active cursor affordance (glow + shimmer) +- TextBlock.tsx: Renders text content with prose styling +- ThinkingBlock.tsx: Collapsible thinking/reasoning display +- ToolBlock.tsx: Tool calls with status badges (forming/executing/done) +- Added shimmer animation keyframes to index.css + +All components follow the design spec with dark theme styling. +## Evidence +- Commits: +- Tests: +- PRs: \ No newline at end of file diff --git a/.flow/tasks/fn-4.5.json b/.flow/tasks/fn-4.5.json new file mode 100644 index 0000000..26c34f9 --- /dev/null +++ b/.flow/tasks/fn-4.5.json @@ -0,0 +1,25 @@ +{ + "assignee": "bordumbb@gmail.com", + "claim_note": "", + "claimed_at": "2026-01-24T19:44:42.250902Z", + "created_at": "2026-01-24T19:04:57.823691Z", + "depends_on": [ + "fn-4.3", + "fn-4.4" + ], + "epic": "fn-4", + "evidence": { + "commit": "17b4545", + "files_created": [ + "ui/src/bond/useReplayState.ts", + "ui/src/ui/ReplayControls.tsx" + ], + "type_check": "tsc --noEmit passed" + }, + "id": "fn-4.5", + "priority": null, + "spec_path": ".flow/tasks/fn-4.5.md", + "status": "done", + "title": "Replay mode (pause + scrub)", + "updated_at": "2026-01-24T19:46:10.440167Z" +} diff --git a/.flow/tasks/fn-4.5.md b/.flow/tasks/fn-4.5.md new file mode 100644 index 0000000..fc80466 --- /dev/null +++ b/.flow/tasks/fn-4.5.md @@ -0,0 +1,61 @@ +# fn-4.5 Replay mode (pause + scrub) + +## Description + +Implement replay mode allowing users to pause the stream, scrub through event history, and jump back to live. + +## Implementation + +1. Store full event history (separate from visible state) + +2. Replay controls: + - Pause button to stop applying new events + - Slider from 0..N events + - "Live" vs "Replay" indicator + +3. Scrubber logic: + - On scrub, re-reduce events from 0..K + - Debounce scrubber input for performance + - Cache intermediate states if needed + +4. Live jump: + - Button to jump back to live position + - Resume auto-scroll + +5. State management: + - Track replay position separately from event count + - Derive visible blocks from position + +## Files to Create/Modify + +- `ui/src/bond/useReplayState.ts` - Replay state management +- `ui/src/ui/ReplayControls.tsx` - Scrubber and controls +- `ui/src/ui/Timeline.tsx` - Integrate replay state + +## References + +- Replay description from plan.md lines 390-406 +- Backend TraceReplayer pattern: `src/bond/trace/replay.py:15-145` +## Acceptance +- [ ] Pause button stops applying new events to timeline +- [ ] Events continue buffering while paused +- [ ] Scrubber slider shows 0..N range +- [ ] Dragging scrubber updates visible blocks +- [ ] "Live" indicator shows when at latest position +- [ ] "Replay" indicator shows when scrubbed back +- [ ] "Jump to Live" button returns to current position +- [ ] Scrubber is debounced (no lag on fast drag) +- [ ] Can pause, scrub back, then resume at paused position +## Done summary +## Summary +Implemented replay mode with pause and scrub functionality: +- useReplayState.ts: Hook managing replay position, mode (live/replay), and derived visible state +- ReplayControls.tsx: UI component with play/pause, slider, live/replay indicator, jump-to-live +- Includes state caching at intervals for smooth scrubbing +- Debounced scrubber input for performance + +All acceptance criteria met: pause, buffer, scrub, indicators, jump-to-live. +## Evidence +- Commits: +- Tests: +- PRs: \ No newline at end of file diff --git a/.flow/tasks/fn-4.6.json b/.flow/tasks/fn-4.6.json new file mode 100644 index 0000000..59ffc42 --- /dev/null +++ b/.flow/tasks/fn-4.6.json @@ -0,0 +1,24 @@ +{ + "assignee": "bordumbb@gmail.com", + "claim_note": "", + "claimed_at": "2026-01-24T19:46:21.465401Z", + "created_at": "2026-01-24T19:04:58.953448Z", + "depends_on": [ + "fn-4.4" + ], + "epic": "fn-4", + "evidence": { + "commit": "72b3d0c", + "files_created": [ + "ui/src/ui/Inspector.tsx", + "ui/src/ui/useSelection.ts" + ], + "type_check": "tsc --noEmit passed" + }, + "id": "fn-4.6", + "priority": null, + "spec_path": ".flow/tasks/fn-4.6.md", + "status": "done", + "title": "Inspector panel", + "updated_at": "2026-01-24T19:47:36.922877Z" +} diff --git a/.flow/tasks/fn-4.6.md b/.flow/tasks/fn-4.6.md new file mode 100644 index 0000000..5fe65f7 --- /dev/null +++ b/.flow/tasks/fn-4.6.md @@ -0,0 +1,62 @@ +# fn-4.6 Inspector panel + +## Description + +Build the inspector panel that shows details when a block is selected. Enable copying block data as JSON. + +## Implementation + +1. Selection state: + - Click block to select + - Track selected block ID + - Highlight selected block + +2. Inspector panel content: + - Block type and ID + - Full content (not truncated) + - For tool blocks: tool_id, full args, result length + - Timestamps (if available) + - Raw event fragments (optional toggle) + +3. Copy functionality: + - "Copy as JSON" button + - Format block data nicely + - Show copy feedback (toast or checkmark) + +4. Panel behavior: + - Slides in from right when block selected + - Click outside to deselect + - Keyboard: Escape to close + +## Files to Create + +- `ui/src/ui/Inspector.tsx` - Inspector panel component +- `ui/src/ui/useSelection.ts` - Selection state hook + +## References + +- Inspector description from plan.md lines 409-425 +## Acceptance +- [ ] Clicking a block selects it +- [ ] Selected block has visual highlight +- [ ] Inspector panel shows selected block details +- [ ] Inspector shows block type, ID, content +- [ ] Inspector shows tool_id and full args for tool blocks +- [ ] "Copy as JSON" button copies block data +- [ ] Copy shows feedback (checkmark or toast) +- [ ] Escape key closes inspector +- [ ] Clicking outside deselects block +## Done summary +## Summary +Implemented inspector panel with full block details: +- useSelection.ts: Hook with selection state, Escape key, click-outside deselection +- Inspector.tsx: Slide-in panel showing block type, ID, status, content +- Tool blocks show tool_id, tool name, full args, result +- Copy as JSON button with checkmark feedback +- AnimatePresence for smooth slide animations + +All acceptance criteria met. +## Evidence +- Commits: +- Tests: +- PRs: \ No newline at end of file diff --git a/.flow/tasks/fn-4.7.json b/.flow/tasks/fn-4.7.json new file mode 100644 index 0000000..e901bb4 --- /dev/null +++ b/.flow/tasks/fn-4.7.json @@ -0,0 +1,26 @@ +{ + "assignee": "bordumbb@gmail.com", + "claim_note": "", + "claimed_at": "2026-01-24T19:47:48.998308Z", + "created_at": "2026-01-24T19:05:00.372640Z", + "depends_on": [ + "fn-4.2", + "fn-4.4" + ], + "epic": "fn-4", + "evidence": { + "commit": "50fa14f", + "files_created": [ + "ui/public/demo-events.ndjson", + "ui/src/bond/useBondReplayFromFile.ts", + "ui/src/ui/DemoControls.tsx" + ], + "type_check": "tsc --noEmit passed" + }, + "id": "fn-4.7", + "priority": null, + "spec_path": ".flow/tasks/fn-4.7.md", + "status": "done", + "title": "Demo mode (canned events)", + "updated_at": "2026-01-24T19:49:40.800036Z" +} diff --git a/.flow/tasks/fn-4.7.md b/.flow/tasks/fn-4.7.md new file mode 100644 index 0000000..39a5c54 --- /dev/null +++ b/.flow/tasks/fn-4.7.md @@ -0,0 +1,80 @@ +# fn-4.7 Demo mode (canned events) + +## Description + +Implement demo mode that plays pre-recorded events from a file, enabling perfect demos without a live backend. + +## Implementation + +### Demo Mode Uses TraceEvent Format (not wire format) + +Demo/replay uses the richer `TraceEvent` schema which includes timing: +```typescript +interface TraceEvent { + trace_id: string + sequence: number + timestamp: number // Monotonic clock - use for replay timing + wall_time: string // ISO datetime + event_type: string // "text_delta", "tool_execute", etc. + payload: object +} +``` + +This allows timing-accurate playback without hardcoding delays. + +### Steps + +1. Create demo events file: + - `ui/public/demo-events.ndjson` — **TraceEvent format** + - Pre-recorded realistic agent session + - Include all event types: thinking, text, tool calls + - Timestamps enable realistic replay timing + +2. Replay from file hook: + - `useBondReplayFromFile()` + - Load NDJSON file + - Use `timestamp` differences to drive playback timing + - Normalize TraceEvents via `normalizeTraceEvent()` + +3. Playback controls: + - Play/Pause + - Speed control: 1x, 2x, 0.5x + - Jump to moment + +4. Integration: + - "Run Demo" button loads demo file + - Clear indicator of demo vs live mode + - Demo uses same timeline/inspector as live + +## Files to Create + +- `ui/public/demo-events.ndjson` - Pre-recorded events +- `ui/src/bond/useBondReplayFromFile.ts` - File replay hook +- `ui/src/ui/DemoControls.tsx` - Demo playback controls + +## References + +- Demo mode from plan.md lines 427-440 +- Event format: `src/bond/trace/_models.py:38-60` +## Acceptance +- [ ] "Run Demo" button starts demo playback +- [ ] Demo events play with timing from `TraceEvent.timestamp` +- [ ] All block types appear: text, thinking, tool calls +- [ ] Play/Pause control works +- [ ] Speed control: 1x, 2x, 0.5x options +- [ ] Clear "Demo Mode" indicator visible +- [ ] Demo works without any backend connection +- [ ] Can produce flawless screen recording +## Done summary +## Summary +Implemented demo mode with pre-recorded event playback: +- useBondReplayFromFile.ts: Hook loading NDJSON TraceEvents with timestamp-based timing +- DemoControls.tsx: Playback UI with play/pause, speed control (0.5x/1x/2x), progress bar +- demo-events.ndjson: Realistic session with thinking, text, and tool call blocks +- Demo mode badge indicator + +All acceptance criteria met. +## Evidence +- Commits: +- Tests: +- PRs: \ No newline at end of file diff --git a/.flow/tasks/fn-4.8.json b/.flow/tasks/fn-4.8.json new file mode 100644 index 0000000..9e60f61 --- /dev/null +++ b/.flow/tasks/fn-4.8.json @@ -0,0 +1,28 @@ +{ + "assignee": "bordumbb@gmail.com", + "claim_note": "", + "claimed_at": "2026-01-24T19:49:52.613735Z", + "created_at": "2026-01-24T19:05:01.868825Z", + "depends_on": [ + "fn-4.5", + "fn-4.6", + "fn-4.7" + ], + "epic": "fn-4", + "evidence": { + "commit": "631c46f", + "files_created": [ + "ui/src/hooks/useKeyboardShortcuts.ts" + ], + "files_modified": [ + "ui/src/App.tsx" + ], + "type_check": "tsc --noEmit passed" + }, + "id": "fn-4.8", + "priority": null, + "spec_path": ".flow/tasks/fn-4.8.md", + "status": "done", + "title": "Polish pass", + "updated_at": "2026-01-24T19:51:18.910269Z" +} diff --git a/.flow/tasks/fn-4.8.md b/.flow/tasks/fn-4.8.md new file mode 100644 index 0000000..ba2affb --- /dev/null +++ b/.flow/tasks/fn-4.8.md @@ -0,0 +1,72 @@ +# fn-4.8 Polish pass + +## Description + +Final polish pass to make the UI feel like a premium devtool (Linear/Vercel quality). + +## Implementation + +1. Empty/loading/error states: + - Skeleton loaders for timeline + - Nice empty state messaging + - Error state with retry option + +2. Visual polish: + - Subtle gradients and shadows + - Cursor/typing shimmer for active blocks + - Nice microcopy throughout + +3. Auto-scroll refinement: + - Smooth behavior that doesn't fight user + - Clear "pinned to bottom" indicator + +4. Keyboard shortcuts: + - Space = pause/play + - L = jump to live + - J/K = step events backward/forward + - Escape = close inspector + +5. Accessibility: + - Focus indicators + - Keyboard navigation + - Sufficient contrast + +## Files to Modify + +- All UI components for polish +- `ui/src/hooks/useKeyboardShortcuts.ts` - New file +- Add loading/error states throughout + +## References + +- Polish checklist from plan.md lines 441-456 +- Demo checklist from plan.md lines 459-469 +## Acceptance +- [ ] Empty state shows helpful message (not just blank) +- [ ] Loading state shows skeleton animation +- [ ] Error state shows message with retry button +- [ ] Space key toggles pause/play +- [ ] L key jumps to live +- [ ] J/K keys step through events +- [ ] Escape closes inspector +- [ ] Active blocks show subtle shimmer/cursor +- [ ] Overall aesthetic feels like Linear/Vercel quality +- [ ] No jarring layout shifts or scroll jumps +## Done summary +## Summary +Completed polish pass with full app integration: +- useKeyboardShortcuts.ts: Global keyboard shortcuts (Space, L, J/K) +- App.tsx: Full integration of all components with proper state management + - Live SSE streaming mode with connect/disconnect + - Demo mode with playback controls + - Replay state for scrubbing through event history + - Inspector panel integration + - Keyboard shortcuts enabled when active + - Contextual sidebar help showing shortcuts + - Empty/loading/live states with appropriate messaging + +All acceptance criteria met. UI is now fully functional. +## Evidence +- Commits: +- Tests: +- PRs: \ No newline at end of file diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..fa237df --- /dev/null +++ b/plan.md @@ -0,0 +1,471 @@ +Epic: Bond UI — Minimal, Beautiful “Forensic Timeline” Frontend + +Goal: A polished single-page web app that connects to a Bond streaming endpoint (SSE or WebSocket), renders the agent lifecycle as a live timeline (text/thinking/tool-call/tool-result), and supports “replay” (scrub + pause) so the demo lands instantly. + +Non-goals: auth, accounts, persistence, multi-run browsing, complex theming system, backend work (beyond exposing a simple stream). + +⸻ + +What “done” looks like + • You can hit Run Demo and watch blocks appear with a satisfying typing effect. + • Thinking is visually distinct (subtle, collapsible). + • Tool calls show streaming args, then flip to “executing…”, then show the result. + • You can pause the stream, scrub the timeline, and click any block to inspect details. + • The UI looks like a modern devtool: clean spacing, soft shadows, calm typography, great empty/loading/error states. + +⸻ + +Architecture (simple, future-proof) + • React + Vite + Tailwind + • shadcn/ui primitives (Card, Tabs, ScrollArea, Button, Badge) + • Framer Motion for tasteful block animations + • Event-driven store: keep a canonical list of BondEvents + derived Blocks + +You’ll implement a thin “event → block” reducer that can support both live streaming and replay. + +⸻ + +Tasks + +1) Create project scaffold + styling baseline + +Tasks + • Set up Vite React + TS + • Add Tailwind + shadcn/ui + • Add Framer Motion + • Create a minimal theme: neutral background, nice cards, monospace for tool args + +Acceptance + • App loads with a clean shell (header + sidebar + main timeline) + • Typography and spacing already feel “premium” + +Snippet: app shell layout + +// src/App.tsx +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +export default function App() { + return ( +
+
+
+
+
Bond
+
Forensic timeline UI
+
+
+ + +
+
+
+ +
+ + +
+ +
+
Timeline
+
+ +
+ Waiting for events… +
+
+
+
+
+
+ ); +} + + +⸻ + +2) Define event schema + block model + +You want one normalized internal model that feels like “blocks”, regardless of whether the backend sends raw delta events. + +Tasks + • Define BondEvent union types + • Define Block model (Text | Thinking | Tool) + • Implement reducer that ingests events and updates blocks in-place (for streaming) + +Acceptance + • Given a stream of events, UI can render stable blocks that update as deltas arrive + +Snippet: types + reducer skeleton + +// src/bond/types.ts +export type BlockKind = "text" | "thinking" | "tool_call"; + +export type BondEvent = + | { type: "block_start"; kind: BlockKind; index: number; ts: number } + | { type: "block_end"; kind: BlockKind; index: number; ts: number } + | { type: "text_delta"; delta: string; ts: number } + | { type: "thinking_delta"; delta: string; ts: number } + | { type: "tool_call_delta"; nameDelta: string; argsDelta: string; ts: number } + | { type: "tool_execute"; id: string; name: string; args: Record; ts: number } + | { type: "tool_result"; id: string; name: string; result: string; ts: number } + | { type: "complete"; ts: number }; + +export type Block = + | { + id: string; + kind: "text" | "thinking"; + index: number; + content: string; + isClosed: boolean; + } + | { + id: string; + kind: "tool_call"; + index: number; + toolNameDraft: string; + toolArgsDraft: string; + toolId?: string; + toolName?: string; + toolArgs?: Record; + status: "forming" | "executing" | "done"; + result?: string; + isClosed: boolean; + }; + +// src/bond/reducer.ts +import { BondEvent, Block } from "./types"; + +type State = { + blocks: Block[]; + activeBlockId?: string; +}; + +const mkId = (kind: string, index: number) => `${kind}:${index}`; + +export function reduce(state: State, ev: BondEvent): State { + switch (ev.type) { + case "block_start": { + const id = mkId(ev.kind, ev.index); + const block: Block = + ev.kind === "tool_call" + ? { + id, + kind: "tool_call", + index: ev.index, + toolNameDraft: "", + toolArgsDraft: "", + status: "forming", + isClosed: false, + } + : { id, kind: ev.kind, index: ev.index, content: "", isClosed: false }; + + return { ...state, blocks: [...state.blocks, block], activeBlockId: id }; + } + + case "text_delta": + case "thinking_delta": { + const blocks = state.blocks.map((b) => { + if (!state.activeBlockId || b.id !== state.activeBlockId) return b; + if (b.kind === "text" && ev.type === "text_delta") + return { ...b, content: b.content + ev.delta }; + if (b.kind === "thinking" && ev.type === "thinking_delta") + return { ...b, content: b.content + ev.delta }; + return b; + }); + return { ...state, blocks }; + } + + case "tool_call_delta": { + const blocks = state.blocks.map((b) => { + if (!state.activeBlockId || b.id !== state.activeBlockId) return b; + if (b.kind !== "tool_call") return b; + return { + ...b, + toolNameDraft: b.toolNameDraft + (ev.nameDelta ?? ""), + toolArgsDraft: b.toolArgsDraft + (ev.argsDelta ?? ""), + }; + }); + return { ...state, blocks }; + } + + case "tool_execute": { + const blocks = state.blocks.map((b) => { + if (b.kind !== "tool_call") return b; + // simplest: attach to latest tool block + // (you can improve by mapping call_id -> current tool block) + return b.status === "forming" + ? { ...b, toolId: ev.id, toolName: ev.name, toolArgs: ev.args, status: "executing" } + : b; + }); + return { ...state, blocks }; + } + + case "tool_result": { + const blocks = state.blocks.map((b) => { + if (b.kind !== "tool_call") return b; + if (b.toolId !== ev.id) return b; + return { ...b, status: "done", result: ev.result }; + }); + return { ...state, blocks }; + } + + case "block_end": { + const id = mkId(ev.kind, ev.index); + const blocks = state.blocks.map((b) => (b.id === id ? { ...b, isClosed: true } : b)); + return { ...state, blocks, activeBlockId: undefined }; + } + + default: + return state; + } +} + + +⸻ + +3) Implement streaming transport (SSE first) + +SSE is perfect for demos: simple, debuggable, works in browsers. + +Tasks + • Build useBondStream(url) hook + • Parse incoming lines as JSON events + • Reconnect with backoff + • Provide controls: connect/disconnect/pause + +Acceptance + • You can connect to /events and see blocks update live + • Disconnect works cleanly + +Snippet: SSE hook + +// src/bond/useBondStream.ts +import { useEffect, useMemo, useReducer, useRef, useState } from "react"; +import { BondEvent } from "./types"; +import { reduce } from "./reducer"; + +const initial = { blocks: [] as any[], activeBlockId: undefined as string | undefined }; + +export function useBondStream(url: string | null) { + const [state, dispatch] = useReducer(reduce as any, initial); + const [status, setStatus] = useState<"idle" | "connecting" | "live" | "error">("idle"); + const [paused, setPaused] = useState(false); + const esRef = useRef(null); + + const connect = useMemo(() => { + return () => { + if (!url) return; + setStatus("connecting"); + const es = new EventSource(url); + esRef.current = es; + + es.onopen = () => setStatus("live"); + es.onerror = () => setStatus("error"); + + es.onmessage = (msg) => { + if (paused) return; + try { + const ev = JSON.parse(msg.data) as BondEvent; + dispatch(ev); + } catch { + // ignore bad frames for demo resilience + } + }; + }; + }, [url, paused]); + + const disconnect = () => { + esRef.current?.close(); + esRef.current = null; + setStatus("idle"); + }; + + useEffect(() => () => disconnect(), []); + + return { state, status, paused, setPaused, connect, disconnect }; +} + + +⸻ + +4) Build timeline rendering (the “wow” factor) + +This is where the product sells itself. + +Tasks + • Create BlockCard component variants: + • Text block (clean prose) + • Thinking block (subtle, collapsible, slightly dim) + • Tool block (name/args streaming, status pill, result panel) + • Animations: slide/fade-in as blocks appear + • Auto-scroll: follow live events unless user scrolls up + +Acceptance + • Timeline looks like a real devtool (not a hackathon UI) + • Tool blocks feel “alive” as args stream in + +Snippet: block renderer + +// src/ui/Timeline.tsx +import { motion } from "framer-motion"; +import { Badge } from "@/components/ui/badge"; +import { Card } from "@/components/ui/card"; +import { Block } from "@/bond/types"; + +function statusBadge(status: "forming" | "executing" | "done") { + const label = status === "forming" ? "forming" : status === "executing" ? "executing" : "done"; + return {label}; +} + +export function Timeline({ blocks }: { blocks: Block[] }) { + return ( +
+ {blocks.map((b) => ( + + + {b.kind === "text" && ( +
+ {b.content || } +
+ )} + + {b.kind === "thinking" && ( +
+
Thinking
+ {b.content || } +
+ )} + + {b.kind === "tool_call" && ( +
+
+
Tool
+
+ {b.toolName ?? b.toolNameDraft || "…"} +
+ {statusBadge(b.status)} +
+ +
+
Args
+
+                    {b.toolArgs ? JSON.stringify(b.toolArgs, null, 2) : (b.toolArgsDraft || "…")}
+                  
+
+ + {b.result && ( +
+
Result
+
+                      {b.result}
+                    
+
+ )} +
+ )} +
+
+ ))} +
+ ); +} + + +⸻ + +5) Add “Replay mode” (pause + scrub) + +Replay is what turns “cool” into “I need this”. + +Tasks + • Persist incoming events in memory (events[]) + • When paused: stop applying new events to blocks, but keep buffering events + • Implement scrubber: + • Slider from 0..N events + • Rebuild blocks by reducing events up to index K + • Add “Live” vs “Replay” indicator + +Acceptance + • You can pause at any time, scrub backwards, click blocks, then jump back to live + +Implementation note: easiest is “replay by re-reducing from 0..K” (fast enough for demo scale). + +⸻ + +6) “Inspector” panel (click a block → details) + +This is where devs fall in love. + +Tasks + • Click a block to select it + • Side panel shows: + • raw event fragments (optional) + • tool call id, full args, result length + • timestamps + • Add “Copy as JSON” button + +Acceptance + • You can select any block and copy evidence for a bug report / PR comment + +⸻ + +7) Demo mode: deterministic canned run (for perfect recordings) + +Live demos are fragile; you want a “movie mode”. + +Tasks + • Add /public/demo-events.ndjson (pre-recorded events) + • Implement useBondReplayFromFile() that plays events on a timer + • Playback controls: play/pause, speed (1x / 2x), jump to moment + • Button: “Load Demo Run” + +Acceptance + • You can always produce a flawless screen recording even if backend is down + +⸻ + +8) Polish pass (this is what makes it beautiful) + +Tasks + • Empty states, skeletons, subtle gradients + • Cursor/typing shimmer for active block + • Nice microcopy + • Auto-scroll behavior that doesn’t fight the user + • Keyboard shortcuts: + • Space = pause/play + • L = jump live + • J/K = step events + • Dark mode only (for now) but perfect dark mode + +Acceptance + • Feels like Linear / Vercel / Raycast quality, not “React starter” + +⸻ + +Demo checklist (so the frontend actually sells Bond) + • The first 10 seconds show: + • thinking starts + • tool args stream + • tool executes + • result returns + • You pause and scrub back to “the moment the wrong assumption appears” + • You click the thought block and highlight that exact sentence + • You jump back to live and finish the run + +That’s the “oh shit” moment. + +⸻ diff --git a/scripts/concat_files.py b/scripts/concat_files.py new file mode 100755 index 0000000..6b762ae --- /dev/null +++ b/scripts/concat_files.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Generate a high-signal file tree and concatenate selected source/config files. + +Run: + python scripts/make_file_tree.py +""" + +from collections.abc import Iterable +from pathlib import Path + +# ───────────────────────────────────────────────────────────── +# CONFIGURATION +# ───────────────────────────────────────────────────────────── + +ROOT_DIR = Path(".") + +SEARCH_PREFIXES = [ + "src", + # "frontend", + # "bond", + # "maistro", + # "docs/feedback", +] + +INCLUDE_ROOT_FILES = [ + # "Dockerfile", + # "docker-compose.yml", + # "Makefile", + # "pyproject.toml", + # ".env.example", + # "README.md", +] + +INCLUDE_EXTS = { + ".py", + ".yaml", + ".yml", + ".json", + ".md", + ".txt", + ".js", + ".ts", + ".tsx", + ".jsx", + ".css", + ".html", +} + +EXCLUDE = { + ".git", + "__pycache__", + ".ruff_cache", + ".pytest_cache", + ".mypy_cache", + ".egg-info", + ".venv", + "dist", + "build", + "out", + "htmlcov", + "coverage", + "node_modules", + ".next", + ".nuxt", + ".svelte-kit", + ".angular", + ".parcel-cache", + ".turbo", + ".vite", + ".cache", + "storybook-static", + "site", + "output", + "tests", +} + +ENCODING = "utf-8" + +OUTPUT_FILE = "all_code_and_configs.txt" + +BANNER_CHAR = "─" +BANNER_WIDTH = 160 +JOIN_WITH = "\n\n" + BANNER_CHAR * BANNER_WIDTH + "\n" + +# ───────────────────────────────────────────────────────────── +# HELPERS +# ───────────────────────────────────────────────────────────── + + +def is_excluded_path(path: Path) -> bool: + return any(part in EXCLUDE for part in path.parts) + + +def should_include_file(path: Path) -> bool: + return path.is_file() and path.suffix in INCLUDE_EXTS + + +def iter_roots(root: Path, prefixes: Iterable[str]) -> list[Path]: + roots = [] + for prefix in prefixes: + p = (root / prefix).resolve() + if p.exists() and p.is_dir() and not is_excluded_path(p): + roots.append(p) + return roots + + +def banner(title: str) -> str: + pad = max(BANNER_WIDTH - len(title) - 2, 0) + left = pad // 2 + right = pad - left + return f"{BANNER_CHAR * left} {title} {BANNER_CHAR * right}\n" + + +def read_file(path: Path) -> str: + try: + return path.read_text(encoding=ENCODING) + except Exception as e: + return f"[ERROR READING FILE: {e}]" + + +# ───────────────────────────────────────────────────────────── +# TREE + FILE COLLECTION +# ───────────────────────────────────────────────────────────── + + +def print_tree(root: Path, prefix: str = "") -> None: + try: + entries = [ + p + for p in root.iterdir() + if not is_excluded_path(p) and (p.is_dir() or should_include_file(p)) + ] + except PermissionError: + return + + entries.sort(key=lambda p: (p.is_file(), p.name.lower())) + + for i, entry in enumerate(entries): + is_last = i == len(entries) - 1 + connector = "└── " if is_last else "├── " + print(f"{prefix}{connector}{entry.name}") + + if entry.is_dir(): + extension = " " if is_last else "│ " + print_tree(entry, prefix + extension) + + +def collect_files(root: Path) -> list[Path]: + files: list[Path] = [] + for path in root.rglob("*"): + if is_excluded_path(path): + continue + if should_include_file(path): + files.append(path) + return sorted(files) + + +# ───────────────────────────────────────────────────────────── +# MAIN +# ───────────────────────────────────────────────────────────── + + +def main() -> None: + root = ROOT_DIR.resolve() + + # Print tree + print(root) + roots = iter_roots(root, SEARCH_PREFIXES) + for i, subroot in enumerate(roots): + is_last = i == len(roots) - 1 + connector = "└── " if is_last else "├── " + print(f"{connector}{subroot.name}") + extension = " " if is_last else "│ " + print_tree(subroot, prefix=extension) + + # Collect contents + output: list[str] = [] + + for filename in INCLUDE_ROOT_FILES: + path = root / filename + if path.exists() and should_include_file(path): + output.append(banner(str(path.relative_to(root)))) + output.append(read_file(path)) + + for subroot in roots: + for file in collect_files(subroot): + output.append(banner(str(file.relative_to(root)))) + output.append(read_file(file)) + + out_path = root / OUTPUT_FILE + out_path.write_text(JOIN_WITH.join(output), encoding=ENCODING) + + print(f"\n✓ Wrote {len(output) // 2} files to {out_path}") + + +if __name__ == "__main__": + main() diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..b984e10 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,48 @@ +# Bond UI + +Forensic timeline viewer for Bond agent traces. + +The timeline shows the agent's thought process as it works: +- **Thinking blocks** - The agent's internal reasoning (collapsible) +- **Text blocks** - Responses streamed to the user +- **Tool blocks** - Tool calls with arguments, execution status, and results + +Click any block to inspect its full content in the side panel. + +## Quick Start + +```bash +pnpm install +pnpm dev +``` + +Open http://localhost:5173 and click **Run Demo** to see a pre-recorded agent session. + +## Connect to a Live Agent + +Click **Connect** and enter your SSE endpoint URL. Your backend needs to stream events using Bond's `create_sse_handlers()`: + +```python +from starlette.responses import StreamingResponse +from bond import Agent +from bond.utils import create_sse_handlers + +async def sse_endpoint(request): + async def stream(): + async def send_sse(event: str, data: dict): + yield f"event: {event}\ndata: {json.dumps(data)}\n\n" + + handlers = create_sse_handlers(send_sse) + await agent.ask("Your prompt here", handlers=handlers) + + return StreamingResponse(stream(), media_type="text/event-stream") +``` + +## Keyboard Shortcuts + +| Key | Action | +|-----|--------| +| Space | Pause/Play | +| L | Jump to Live | +| J/K | Step Back/Forward | +| Escape | Close Inspector | diff --git a/ui/components.json b/ui/components.json new file mode 100644 index 0000000..8bfc737 --- /dev/null +++ b/ui/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/ui/eslint.config.js b/ui/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/ui/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..e86ef40 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,12 @@ + + + + + + Bond - Forensic Timeline + + +
+ + + diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..afd036d --- /dev/null +++ b/ui/package.json @@ -0,0 +1,39 @@ +{ + "name": "ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.4", + "@tailwindcss/vite": "^4.1.18", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "motion": "^12.29.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml new file mode 100644 index 0000000..3c99486 --- /dev/null +++ b/ui/pnpm-lock.yaml @@ -0,0 +1,2682 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.2.4(@types/react@19.2.9)(react@19.2.3) + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^0.563.0 + version: 0.563.0(react@19.2.3) + motion: + specifier: ^12.29.0 + version: 12.29.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: + specifier: ^19.2.0 + version: 19.2.3 + react-dom: + specifier: ^19.2.0 + version: 19.2.3(react@19.2.3) + tailwind-merge: + specifier: ^3.4.0 + version: 3.4.0 + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.2 + '@types/node': + specifier: ^24.10.1 + version: 24.10.9 + '@types/react': + specifier: ^19.2.5 + version: 19.2.9 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.9) + '@vitejs/plugin-react': + specifier: ^5.1.1 + version: 5.1.2(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)) + eslint: + specifier: ^9.39.1 + version: 9.39.2(jiti@2.6.1) + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.0.1(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: ^0.4.24 + version: 0.4.26(eslint@9.39.2(jiti@2.6.1)) + globals: + specifier: ^16.5.0 + version: 16.5.0 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.46.4 + version: 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + vite: + specifier: ^7.2.4 + version: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2) + +packages: + + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.6': + resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.6': + resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.6': + resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.6': + resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + + '@rollup/rollup-android-arm-eabi@4.56.0': + resolution: {integrity: sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.56.0': + resolution: {integrity: sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.56.0': + resolution: {integrity: sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.56.0': + resolution: {integrity: sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.56.0': + resolution: {integrity: sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.56.0': + resolution: {integrity: sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.56.0': + resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.56.0': + resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.56.0': + resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.56.0': + resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.56.0': + resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.56.0': + resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.56.0': + resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.56.0': + resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.56.0': + resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.56.0': + resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.56.0': + resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.56.0': + resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.56.0': + resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.56.0': + resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.56.0': + resolution: {integrity: sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.56.0': + resolution: {integrity: sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.56.0': + resolution: {integrity: sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.56.0': + resolution: {integrity: sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.56.0': + resolution: {integrity: sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.18': + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@24.10.9': + resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.9': + resolution: {integrity: sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==} + + '@typescript-eslint/eslint-plugin@8.53.1': + resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.53.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.53.1': + resolution: {integrity: sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.53.1': + resolution: {integrity: sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.53.1': + resolution: {integrity: sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.53.1': + resolution: {integrity: sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.53.1': + resolution: {integrity: sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.53.1': + resolution: {integrity: sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.53.1': + resolution: {integrity: sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.53.1': + resolution: {integrity: sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.53.1': + resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@5.1.2': + resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.18: + resolution: {integrity: sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==} + hasBin: true + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001766: + resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.278: + resolution: {integrity: sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==} + + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.0.1: + resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.26: + resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + framer-motion@12.29.0: + resolution: {integrity: sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.563.0: + resolution: {integrity: sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + motion-dom@12.29.0: + resolution: {integrity: sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA==} + + motion-utils@12.27.2: + resolution: {integrity: sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q==} + + motion@12.29.0: + resolution: {integrity: sha512-rjB5CP2N9S2ESAyEFnAFMgTec6X8yvfxLNcz8n12gPq3M48R7ZbBeVYkDOTj8SPMwfvGIFI801SiPSr1+HCr9g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rollup@4.56.0: + resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} + + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.53.1: + resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.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 + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@babel/code-frame@7.28.6': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.6': {} + + '@babel/core@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.6': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.6)': + dependencies: + '@babel/core': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + + '@babel/traverse@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.9)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.9 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.9)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.9 + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.9)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.9 + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.9)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.9)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.9 + '@types/react-dom': 19.2.3(@types/react@19.2.9) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.9)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.9 + '@types/react-dom': 19.2.3(@types/react@19.2.9) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.9)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.9)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.9)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.9)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.9)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.9 + '@types/react-dom': 19.2.3(@types/react@19.2.9) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.9)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.9)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.9 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.9)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.9)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.9 + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.9)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.9 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.9)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.9 + + '@rolldown/pluginutils@1.0.0-beta.53': {} + + '@rollup/rollup-android-arm-eabi@4.56.0': + optional: true + + '@rollup/rollup-android-arm64@4.56.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.56.0': + optional: true + + '@rollup/rollup-darwin-x64@4.56.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.56.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.56.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.56.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.56.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.56.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.56.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.56.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.56.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.56.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.56.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.56.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.56.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.56.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.56.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.56.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.56.0': + optional: true + + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2) + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.6 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.6 + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@24.10.9': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.9)': + dependencies: + '@types/react': 19.2.9 + + '@types/react@19.2.9': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.1 + eslint: 9.39.2(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.1 + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.53.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3) + '@typescript-eslint/types': 8.53.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.53.1': + dependencies: + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/visitor-keys': 8.53.1 + + '@typescript-eslint/tsconfig-utils@8.53.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.53.1': {} + + '@typescript-eslint/typescript-estree@8.53.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.53.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3) + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/visitor-keys': 8.53.1 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.53.1 + '@typescript-eslint/types': 8.53.1 + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.53.1': + dependencies: + '@typescript-eslint/types': 8.53.1 + eslint-visitor-keys: 4.2.1 + + '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@babel/core': 7.28.6 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.6) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2) + transitivePeerDependencies: + - supports-color + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.18: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.18 + caniuse-lite: 1.0.30001766 + electron-to-chromium: 1.5.278 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001766: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.278: {} + + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): + dependencies: + '@babel/core': 7.28.6 + '@babel/parser': 7.28.6 + eslint: 9.39.2(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@2.6.1)): + dependencies: + eslint: 9.39.2(jiti@2.6.1) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + framer-motion@12.29.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + motion-dom: 12.29.0 + motion-utils: 12.27.2 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.5.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.563.0(react@19.2.3): + dependencies: + react: 19.2.3 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + motion-dom@12.29.0: + dependencies: + motion-utils: 12.27.2 + + motion-utils@12.27.2: {} + + motion@12.29.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + framer-motion: 12.29.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.27: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react-refresh@0.18.0: {} + + react@19.2.3: {} + + resolve-from@4.0.0: {} + + rollup@4.56.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.56.0 + '@rollup/rollup-android-arm64': 4.56.0 + '@rollup/rollup-darwin-arm64': 4.56.0 + '@rollup/rollup-darwin-x64': 4.56.0 + '@rollup/rollup-freebsd-arm64': 4.56.0 + '@rollup/rollup-freebsd-x64': 4.56.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.56.0 + '@rollup/rollup-linux-arm-musleabihf': 4.56.0 + '@rollup/rollup-linux-arm64-gnu': 4.56.0 + '@rollup/rollup-linux-arm64-musl': 4.56.0 + '@rollup/rollup-linux-loong64-gnu': 4.56.0 + '@rollup/rollup-linux-loong64-musl': 4.56.0 + '@rollup/rollup-linux-ppc64-gnu': 4.56.0 + '@rollup/rollup-linux-ppc64-musl': 4.56.0 + '@rollup/rollup-linux-riscv64-gnu': 4.56.0 + '@rollup/rollup-linux-riscv64-musl': 4.56.0 + '@rollup/rollup-linux-s390x-gnu': 4.56.0 + '@rollup/rollup-linux-x64-gnu': 4.56.0 + '@rollup/rollup-linux-x64-musl': 4.56.0 + '@rollup/rollup-openbsd-x64': 4.56.0 + '@rollup/rollup-openharmony-arm64': 4.56.0 + '@rollup/rollup-win32-arm64-msvc': 4.56.0 + '@rollup/rollup-win32-ia32-msvc': 4.56.0 + '@rollup/rollup-win32-x64-gnu': 4.56.0 + '@rollup/rollup-win32-x64-msvc': 4.56.0 + fsevents: 2.3.3 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tailwind-merge@3.4.0: {} + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.2(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.56.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.9 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.3.6: {} diff --git a/ui/pnpm-workspace.yaml b/ui/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/ui/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/ui/public/demo-events.ndjson b/ui/public/demo-events.ndjson new file mode 100644 index 0000000..2f72370 --- /dev/null +++ b/ui/public/demo-events.ndjson @@ -0,0 +1,35 @@ +{"trace_id":"demo-001","sequence":0,"timestamp":0.0,"wall_time":"2025-01-24T10:00:00.000Z","event_type":"block_start","payload":{"kind":"thinking","index":0}} +{"trace_id":"demo-001","sequence":1,"timestamp":0.1,"wall_time":"2025-01-24T10:00:00.100Z","event_type":"thinking_delta","payload":{"text":"The user wants me to analyze a Python project structure. I should "}} +{"trace_id":"demo-001","sequence":2,"timestamp":0.2,"wall_time":"2025-01-24T10:00:00.200Z","event_type":"thinking_delta","payload":{"text":"start by exploring the codebase to understand the layout and key files."}} +{"trace_id":"demo-001","sequence":3,"timestamp":0.5,"wall_time":"2025-01-24T10:00:00.500Z","event_type":"block_end","payload":{"kind":"thinking","index":0}} +{"trace_id":"demo-001","sequence":4,"timestamp":0.6,"wall_time":"2025-01-24T10:00:00.600Z","event_type":"block_start","payload":{"kind":"text","index":1}} +{"trace_id":"demo-001","sequence":5,"timestamp":0.7,"wall_time":"2025-01-24T10:00:00.700Z","event_type":"text_delta","payload":{"text":"I'll analyze the project structure "}} +{"trace_id":"demo-001","sequence":6,"timestamp":0.8,"wall_time":"2025-01-24T10:00:00.800Z","event_type":"text_delta","payload":{"text":"to understand the codebase architecture."}} +{"trace_id":"demo-001","sequence":7,"timestamp":1.0,"wall_time":"2025-01-24T10:00:01.000Z","event_type":"block_end","payload":{"kind":"text","index":1}} +{"trace_id":"demo-001","sequence":8,"timestamp":1.1,"wall_time":"2025-01-24T10:00:01.100Z","event_type":"block_start","payload":{"kind":"tool_call","index":2}} +{"trace_id":"demo-001","sequence":9,"timestamp":1.2,"wall_time":"2025-01-24T10:00:01.200Z","event_type":"tool_call_delta","payload":{"name":"list_dir","args":""}} +{"trace_id":"demo-001","sequence":10,"timestamp":1.3,"wall_time":"2025-01-24T10:00:01.300Z","event_type":"tool_call_delta","payload":{"name":"ectory","args":"{\"path\": \"."}} +{"trace_id":"demo-001","sequence":11,"timestamp":1.4,"wall_time":"2025-01-24T10:00:01.400Z","event_type":"tool_call_delta","payload":{"name":"","args":"\"}"}} +{"trace_id":"demo-001","sequence":12,"timestamp":1.5,"wall_time":"2025-01-24T10:00:01.500Z","event_type":"tool_execute","payload":{"id":"tool_1","name":"list_directory","args":{"path":"."}}} +{"trace_id":"demo-001","sequence":13,"timestamp":2.0,"wall_time":"2025-01-24T10:00:02.000Z","event_type":"tool_result","payload":{"id":"tool_1","name":"list_directory","result":"src/\ntests/\npyproject.toml\nREADME.md\n.gitignore"}} +{"trace_id":"demo-001","sequence":14,"timestamp":2.1,"wall_time":"2025-01-24T10:00:02.100Z","event_type":"block_end","payload":{"kind":"tool_call","index":2}} +{"trace_id":"demo-001","sequence":15,"timestamp":2.2,"wall_time":"2025-01-24T10:00:02.200Z","event_type":"block_start","payload":{"kind":"thinking","index":3}} +{"trace_id":"demo-001","sequence":16,"timestamp":2.3,"wall_time":"2025-01-24T10:00:02.300Z","event_type":"thinking_delta","payload":{"text":"Good, I can see this is a Python project with a standard layout. "}} +{"trace_id":"demo-001","sequence":17,"timestamp":2.5,"wall_time":"2025-01-24T10:00:02.500Z","event_type":"thinking_delta","payload":{"text":"Let me examine the src directory to understand the package structure."}} +{"trace_id":"demo-001","sequence":18,"timestamp":2.7,"wall_time":"2025-01-24T10:00:02.700Z","event_type":"block_end","payload":{"kind":"thinking","index":3}} +{"trace_id":"demo-001","sequence":19,"timestamp":2.8,"wall_time":"2025-01-24T10:00:02.800Z","event_type":"block_start","payload":{"kind":"tool_call","index":4}} +{"trace_id":"demo-001","sequence":20,"timestamp":2.9,"wall_time":"2025-01-24T10:00:02.900Z","event_type":"tool_call_delta","payload":{"name":"read","args":""}} +{"trace_id":"demo-001","sequence":21,"timestamp":3.0,"wall_time":"2025-01-24T10:00:03.000Z","event_type":"tool_call_delta","payload":{"name":"_file","args":"{\"path\": \"pyp"}} +{"trace_id":"demo-001","sequence":22,"timestamp":3.1,"wall_time":"2025-01-24T10:00:03.100Z","event_type":"tool_call_delta","payload":{"name":"","args":"roject.toml\"}"}} +{"trace_id":"demo-001","sequence":23,"timestamp":3.2,"wall_time":"2025-01-24T10:00:03.200Z","event_type":"tool_execute","payload":{"id":"tool_2","name":"read_file","args":{"path":"pyproject.toml"}}} +{"trace_id":"demo-001","sequence":24,"timestamp":3.8,"wall_time":"2025-01-24T10:00:03.800Z","event_type":"tool_result","payload":{"id":"tool_2","name":"read_file","result":"[project]\nname = \"bond-agent\"\nversion = \"0.1.0\"\ndescription = \"Forensic runtime for AI agents\"\nauthors = [{name = \"Bond Team\"}]\nrequires-python = \">=3.11\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\""}} +{"trace_id":"demo-001","sequence":25,"timestamp":3.9,"wall_time":"2025-01-24T10:00:03.900Z","event_type":"block_end","payload":{"kind":"tool_call","index":4}} +{"trace_id":"demo-001","sequence":26,"timestamp":4.0,"wall_time":"2025-01-24T10:00:04.000Z","event_type":"block_start","payload":{"kind":"text","index":5}} +{"trace_id":"demo-001","sequence":27,"timestamp":4.1,"wall_time":"2025-01-24T10:00:04.100Z","event_type":"text_delta","payload":{"text":"Based on my analysis, this is the **bond-agent** project:\n\n"}} +{"trace_id":"demo-001","sequence":28,"timestamp":4.3,"wall_time":"2025-01-24T10:00:04.300Z","event_type":"text_delta","payload":{"text":"**Project Structure:**\n- `src/` - Main source code\n"}} +{"trace_id":"demo-001","sequence":29,"timestamp":4.5,"wall_time":"2025-01-24T10:00:04.500Z","event_type":"text_delta","payload":{"text":"- `tests/` - Test suite\n- `pyproject.toml` - Project configuration\n\n"}} +{"trace_id":"demo-001","sequence":30,"timestamp":4.7,"wall_time":"2025-01-24T10:00:04.700Z","event_type":"text_delta","payload":{"text":"**Key Details:**\n- Version: 0.1.0\n- Python: >=3.11\n"}} +{"trace_id":"demo-001","sequence":31,"timestamp":4.9,"wall_time":"2025-01-24T10:00:04.900Z","event_type":"text_delta","payload":{"text":"- Description: Forensic runtime for AI agents\n"}} +{"trace_id":"demo-001","sequence":32,"timestamp":5.1,"wall_time":"2025-01-24T10:00:05.100Z","event_type":"text_delta","payload":{"text":"- Build system: Hatchling"}} +{"trace_id":"demo-001","sequence":33,"timestamp":5.3,"wall_time":"2025-01-24T10:00:05.300Z","event_type":"block_end","payload":{"kind":"text","index":5}} +{"trace_id":"demo-001","sequence":34,"timestamp":5.4,"wall_time":"2025-01-24T10:00:05.400Z","event_type":"complete","payload":{"data":null}} diff --git a/ui/src/App.tsx b/ui/src/App.tsx new file mode 100644 index 0000000..fcdf0ae --- /dev/null +++ b/ui/src/App.tsx @@ -0,0 +1,330 @@ +/** + * Bond Forensic Timeline + * + * Main application shell with: + * - Header with connection controls + * - Run status line + * - Timeline view with blocks + * - Inspector panel + * - Replay/Demo controls + */ + +import { useState, useCallback, useMemo } from "react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Wifi, WifiOff, Loader2 } from "lucide-react" + +import { useBondStream } from "@/bond/useBondStream" +import { useBondReplayFromFile } from "@/bond/useBondReplayFromFile" +import { useReplayState } from "@/bond/useReplayState" +import { useSelection } from "@/ui/useSelection" +import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts" + +import { Timeline } from "@/ui/Timeline" +import { Inspector } from "@/ui/Inspector" +import { ReplayControls } from "@/ui/ReplayControls" +import { DemoControls } from "@/ui/DemoControls" + +type AppMode = "idle" | "live" | "demo" + +export default function App() { + const [mode, setMode] = useState("idle") + const [sseUrl, setSseUrl] = useState(null) + + // Stream hook for live mode + const stream = useBondStream(sseUrl) + + // Demo hook for demo mode + const demo = useBondReplayFromFile() + + // Determine which events/state to use based on mode + const events = mode === "demo" ? demo.events : stream.history.events + const liveState = mode === "demo" ? demo.state : stream.state + + // Replay state for scrubbing through history + const replay = useReplayState(events) + + // Selection state for inspector + const selection = useSelection() + + // Determine visible state (live or replayed) + const visibleState = replay.isAtLive ? liveState : replay.visibleState + + // Find selected block + const selectedBlock = useMemo( + () => visibleState.blocks.find((b) => b.id === selection.selectedBlockId), + [visibleState.blocks, selection.selectedBlockId] + ) + + // Connection status for display + const connectionStatus = mode === "live" ? stream.status : "idle" + const isConnected = connectionStatus === "live" + const isConnecting = connectionStatus === "connecting" + + // Event count + const eventCount = events.length + + // Trace ID (demo shows demo ID) + const traceId = mode === "demo" ? "demo-001" : mode === "live" ? "live" : "—" + + // Status for display + const displayStatus = + mode === "demo" + ? demo.status + : mode === "live" + ? stream.status + : "idle" + + // Handle connect button + const handleConnect = useCallback(() => { + const url = prompt("Enter SSE endpoint URL:", "http://localhost:8000/stream") + if (url) { + setSseUrl(url) + setMode("live") + stream.connect() + } + }, [stream]) + + // Handle disconnect + const handleDisconnect = useCallback(() => { + stream.disconnect() + setSseUrl(null) + setMode("idle") + }, [stream]) + + // Handle demo start + const handleStartDemo = useCallback(async () => { + setMode("demo") + await demo.startDemo("/demo-events.ndjson") + }, [demo]) + + // Handle demo stop + const handleStopDemo = useCallback(() => { + demo.stop() + setMode("idle") + }, [demo]) + + // Toggle pause (works for both live and demo) + const handleTogglePause = useCallback(() => { + if (mode === "live") { + stream.setPaused(!stream.paused) + } else if (mode === "demo") { + if (demo.status === "playing") { + demo.pause() + } else if (demo.status === "paused") { + demo.resume() + } + } + }, [mode, stream, demo]) + + // Jump to live + const handleJumpToLive = useCallback(() => { + replay.jumpToLive() + }, [replay]) + + // Step backward + const handleStepBackward = useCallback(() => { + if (replay.position > 0) { + replay.setPosition(replay.position - 1) + } + }, [replay]) + + // Step forward + const handleStepForward = useCallback(() => { + if (replay.position < replay.totalEvents - 1) { + replay.setPosition(replay.position + 1) + } + }, [replay]) + + // Keyboard shortcuts + useKeyboardShortcuts({ + onTogglePause: handleTogglePause, + onJumpToLive: handleJumpToLive, + onStepBackward: handleStepBackward, + onStepForward: handleStepForward, + enabled: mode !== "idle", + }) + + return ( +
+ {/* Header */} +
+
+
+
Bond
+
Forensic Timeline
+
+
+ + +
+
+
+ + {/* Run Header / Status Line */} +
+
+
+ Trace: + {traceId} +
+
+ Status: + + {displayStatus} + +
+
+ Events: + {eventCount} +
+
+ {isConnecting ? ( + <> + + Connecting... + + ) : isConnected ? ( + <> + + Connected + + ) : mode === "demo" ? ( + <> + Demo + + ) : ( + <> + + Disconnected + + )} +
+
+
+ + {/* Main Content */} +
+ {/* Sidebar */} + + + {/* Timeline */} +
+ +
+
Timeline
+ {!replay.isAtLive && ( + + Viewing event {replay.position + 1} of {replay.totalEvents} + + )} +
+ +
+ +
+ + {/* Controls based on mode */} + {mode === "demo" && ( + + )} + + {mode === "live" && eventCount > 0 && ( + stream.setPaused(!stream.paused)} + position={replay.position} + totalEvents={replay.totalEvents} + onPositionChange={replay.setPosition} + onJumpToLive={replay.jumpToLive} + isAtLive={replay.isAtLive} + /> + )} +
+
+
+ + {/* Inspector Panel */} + +
+ ) +} diff --git a/ui/src/bond/normalize.ts b/ui/src/bond/normalize.ts new file mode 100644 index 0000000..a2c697c --- /dev/null +++ b/ui/src/bond/normalize.ts @@ -0,0 +1,213 @@ +/** + * Event normalization layer + * + * Converts various event formats to the internal BondEvent type: + * - WireEvent (WebSocket JSON) + * - SSE events (named event types) + * - TraceEvent (replay/demo NDJSON) + */ + +import type { WireEvent, TraceEvent, BondEvent, BlockKind } from "./types" + +/** + * Normalize block kind string to internal format + * Maps PydanticAI part_kind values to our BlockKind + */ +function normalizeKind(kind: string): BlockKind { + // Handle known PydanticAI part kinds + const kindMap: Record = { + text: "text", + "text-part": "text", + thinking: "thinking", + "thinking-part": "thinking", + "tool-call": "tool_call", + "tool-call-part": "tool_call", + tool_call: "tool_call", + } + return kindMap[kind.toLowerCase()] ?? kind +} + +/** + * Normalize WebSocket wire event to BondEvent + */ +export function normalizeWireEvent(wire: WireEvent): BondEvent { + switch (wire.t) { + case "block_start": + return { + type: "block_start", + kind: normalizeKind(wire.kind), + index: wire.idx, + } + + case "block_end": + return { + type: "block_end", + kind: normalizeKind(wire.kind), + index: wire.idx, + } + + case "text": + return { type: "text_delta", delta: wire.c } + + case "thinking": + return { type: "thinking_delta", delta: wire.c } + + case "tool_delta": + return { type: "tool_call_delta", nameDelta: wire.n, argsDelta: wire.a } + + case "tool_exec": + return { + type: "tool_execute", + id: wire.id, + name: wire.name, + args: wire.args, + } + + case "tool_result": + return { + type: "tool_result", + id: wire.id, + name: wire.name, + result: wire.result, + } + + case "complete": + return { type: "complete", data: wire.data } + } +} + +/** + * SSE event data types (after JSON.parse of data field) + */ +type SSEBlockData = { kind: string; idx: number } +type SSETextData = { c: string } | { content: string } +type SSEToolDeltaData = { n: string; a: string } +type SSEToolExecData = { id: string; name: string; args: Record } +type SSEToolResultData = { id: string; name: string; result: string } +type SSECompleteData = { data: unknown } + +/** + * Normalize SSE event (named event type + data payload) + * SSE uses: event: \ndata: + */ +export function normalizeSSEEvent( + eventType: string, + data: unknown +): BondEvent | null { + switch (eventType) { + case "block_start": { + const d = data as SSEBlockData + return { + type: "block_start", + kind: normalizeKind(d.kind), + index: d.idx, + } + } + + case "block_end": { + const d = data as SSEBlockData + return { + type: "block_end", + kind: normalizeKind(d.kind), + index: d.idx, + } + } + + case "text": { + const d = data as SSETextData + const content = "c" in d ? d.c : d.content + return { type: "text_delta", delta: content } + } + + case "thinking": { + const d = data as SSETextData + const content = "c" in d ? d.c : d.content + return { type: "thinking_delta", delta: content } + } + + case "tool_delta": { + const d = data as SSEToolDeltaData + return { type: "tool_call_delta", nameDelta: d.n, argsDelta: d.a } + } + + case "tool_exec": { + const d = data as SSEToolExecData + return { type: "tool_execute", id: d.id, name: d.name, args: d.args } + } + + case "tool_result": { + const d = data as SSEToolResultData + return { type: "tool_result", id: d.id, name: d.name, result: d.result } + } + + case "complete": { + const d = data as SSECompleteData + return { type: "complete", data: d.data } + } + + default: + // Unknown event type - ignore gracefully + console.warn(`Unknown SSE event type: ${eventType}`) + return null + } +} + +/** + * Normalize TraceEvent from replay/demo files + * TraceEvent has richer metadata including timestamps + */ +export function normalizeTraceEvent(trace: TraceEvent): BondEvent | null { + const { event_type, payload } = trace + + switch (event_type) { + case "block_start": + return { + type: "block_start", + kind: normalizeKind(payload.kind as string), + index: payload.index as number, + } + + case "block_end": + return { + type: "block_end", + kind: normalizeKind(payload.kind as string), + index: payload.index as number, + } + + case "text_delta": + return { type: "text_delta", delta: payload.text as string } + + case "thinking_delta": + return { type: "thinking_delta", delta: payload.text as string } + + case "tool_call_delta": + return { + type: "tool_call_delta", + nameDelta: (payload.name as string) ?? "", + argsDelta: (payload.args as string) ?? "", + } + + case "tool_execute": + return { + type: "tool_execute", + id: payload.id as string, + name: payload.name as string, + args: payload.args as Record, + } + + case "tool_result": + return { + type: "tool_result", + id: payload.id as string, + name: payload.name as string, + result: payload.result as string, + } + + case "complete": + return { type: "complete", data: payload.data } + + default: + console.warn(`Unknown TraceEvent type: ${event_type}`) + return null + } +} diff --git a/ui/src/bond/reducer.ts b/ui/src/bond/reducer.ts new file mode 100644 index 0000000..0bad24e --- /dev/null +++ b/ui/src/bond/reducer.ts @@ -0,0 +1,232 @@ +/** + * Bond Event Reducer + * + * Transforms streaming BondEvents into renderable Block state. + * Handles active block tracking for streaming UX. + */ + +import type { + BondEvent, + BondState, + Block, + TextBlock, + ThinkingBlock, + ToolBlock, + UnknownBlock, +} from "./types" +import { isTextBlock, isThinkingBlock, isToolBlock } from "./types" + +/** + * Generate block ID from kind and index + */ +function makeBlockId(kind: string, index: number): string { + return `${kind}:${index}` +} + +/** + * Bond event reducer + */ +export function bondReducer(state: BondState, event: BondEvent): BondState { + const newEventCount = state.eventCount + 1 + + switch (event.type) { + case "block_start": { + const id = makeBlockId(event.kind, event.index) + + // Clear active state from previous active block + const blocks = state.blocks.map((b) => + b.isActive ? { ...b, isActive: false } : b + ) + + // Create new block based on kind + let newBlock: Block + + if (event.kind === "text") { + newBlock = { + id, + kind: "text", + index: event.index, + content: "", + isClosed: false, + isActive: true, + } satisfies TextBlock + } else if (event.kind === "thinking") { + newBlock = { + id, + kind: "thinking", + index: event.index, + content: "", + isClosed: false, + isActive: true, + } satisfies ThinkingBlock + } else if (event.kind === "tool_call") { + newBlock = { + id, + kind: "tool_call", + index: event.index, + toolNameDraft: "", + toolArgsDraft: "", + status: "forming", + isClosed: false, + isActive: true, + } satisfies ToolBlock + } else { + // Unknown kind - create generic block + newBlock = { + id, + kind: "unknown", + originalKind: event.kind, + index: event.index, + content: "", + isClosed: false, + isActive: true, + } satisfies UnknownBlock + } + + return { + blocks: [...blocks, newBlock], + activeBlockId: id, + eventCount: newEventCount, + } + } + + case "text_delta": { + if (!state.activeBlockId) return { ...state, eventCount: newEventCount } + + const blocks = state.blocks.map((block): Block => { + if (block.id !== state.activeBlockId) return block + if (!isTextBlock(block)) return block + + return { + ...block, + content: block.content + event.delta, + } + }) + + return { ...state, blocks, eventCount: newEventCount } + } + + case "thinking_delta": { + if (!state.activeBlockId) return { ...state, eventCount: newEventCount } + + const blocks = state.blocks.map((block): Block => { + if (block.id !== state.activeBlockId) return block + if (!isThinkingBlock(block)) return block + + return { + ...block, + content: block.content + event.delta, + } + }) + + return { ...state, blocks, eventCount: newEventCount } + } + + case "tool_call_delta": { + // tool_delta has no id - attach to currently active tool_call block + if (!state.activeBlockId) return { ...state, eventCount: newEventCount } + + const blocks = state.blocks.map((block): Block => { + if (block.id !== state.activeBlockId) return block + if (!isToolBlock(block)) return block + + return { + ...block, + toolNameDraft: block.toolNameDraft + event.nameDelta, + toolArgsDraft: block.toolArgsDraft + event.argsDelta, + } + }) + + return { ...state, blocks, eventCount: newEventCount } + } + + case "tool_execute": { + // Correlate by finding the tool block that's still "forming" + // In practice, attach to the most recent tool_call block without a toolId + const blocks = state.blocks.map((block): Block => { + if (!isToolBlock(block)) return block + if (block.status !== "forming") return block + + return { + ...block, + toolId: event.id, + toolName: event.name, + toolArgs: event.args, + status: "executing", + } + }) + + return { ...state, blocks, eventCount: newEventCount } + } + + case "tool_result": { + // Correlate by toolId + const blocks = state.blocks.map((block): Block => { + if (!isToolBlock(block)) return block + if (block.toolId !== event.id) return block + + return { + ...block, + result: event.result, + status: "done", + } + }) + + return { ...state, blocks, eventCount: newEventCount } + } + + case "block_end": { + const id = makeBlockId(event.kind, event.index) + + const blocks = state.blocks.map((block): Block => { + if (block.id !== id) return block + + return { + ...block, + isClosed: true, + isActive: false, + } + }) + + // Clear active block if it was the one that ended + const newActiveBlockId = + state.activeBlockId === id ? undefined : state.activeBlockId + + return { + blocks, + activeBlockId: newActiveBlockId, + eventCount: newEventCount, + } + } + + case "complete": { + // Mark all blocks as closed and inactive + const blocks = state.blocks.map((block): Block => ({ + ...block, + isClosed: true, + isActive: false, + })) + + return { + blocks, + activeBlockId: undefined, + eventCount: newEventCount, + } + } + + default: + // Unknown event type - ignore + return { ...state, eventCount: newEventCount } + } +} + +/** + * Reduce a sequence of events to state + * Useful for replay: derive visible state from events[0..K] + */ +export function reduceEvents( + events: BondEvent[], + initialState: BondState = { blocks: [], activeBlockId: undefined, eventCount: 0 } +): BondState { + return events.reduce(bondReducer, initialState) +} diff --git a/ui/src/bond/types.ts b/ui/src/bond/types.ts new file mode 100644 index 0000000..7b6335a --- /dev/null +++ b/ui/src/bond/types.ts @@ -0,0 +1,144 @@ +/** + * Bond Event Types and Block Models + * + * Two-mode event story: + * - Live Mode: WireEvent from WebSocket/SSE (compact format) + * - Replay/Demo Mode: TraceEvent from NDJSON files (with timestamps) + */ + +// ============================================================================= +// Wire Format (Live Mode) +// ============================================================================= + +/** + * Wire events from create_websocket_handlers() - compact format for streaming + */ +export type WireEvent = + | { t: "block_start"; kind: string; idx: number } + | { t: "block_end"; kind: string; idx: number } + | { t: "text"; c: string } + | { t: "thinking"; c: string } + | { t: "tool_delta"; n: string; a: string } + | { t: "tool_exec"; id: string; name: string; args: Record } + | { t: "tool_result"; id: string; name: string; result: string } + | { t: "complete"; data: unknown } + +// ============================================================================= +// Trace Format (Replay/Demo Mode) +// ============================================================================= + +/** + * TraceEvent from backend - matches src/bond/trace/_models.py + */ +export interface TraceEvent { + trace_id: string + sequence: number + timestamp: number // Monotonic clock (perf_counter) + wall_time: string // ISO datetime string + event_type: string // One of 8 types + payload: Record +} + +// ============================================================================= +// Normalized Internal Format +// ============================================================================= + +/** + * Block kinds - treat as opaque strings from PydanticAI + * Known values: text, thinking, tool-call + */ +export type BlockKind = "text" | "thinking" | "tool_call" | string + +/** + * Normalized events used internally after normalization + */ +export type BondEvent = + | { type: "block_start"; kind: BlockKind; index: number } + | { type: "block_end"; kind: BlockKind; index: number } + | { type: "text_delta"; delta: string } + | { type: "thinking_delta"; delta: string } + | { type: "tool_call_delta"; nameDelta: string; argsDelta: string } + | { + type: "tool_execute" + id: string + name: string + args: Record + } + | { type: "tool_result"; id: string; name: string; result: string } + | { type: "complete"; data: unknown } + +// ============================================================================= +// Block Types (UI State) +// ============================================================================= + +interface BaseBlock { + id: string + index: number + isClosed: boolean + isActive: boolean +} + +export interface TextBlock extends BaseBlock { + kind: "text" + content: string +} + +export interface ThinkingBlock extends BaseBlock { + kind: "thinking" + content: string +} + +export interface ToolBlock extends BaseBlock { + kind: "tool_call" + // Draft state (streaming) + toolNameDraft: string + toolArgsDraft: string + // Final state (after tool_execute) + toolId?: string + toolName?: string + toolArgs?: Record + // Execution state + status: "forming" | "executing" | "done" + result?: string +} + +export interface UnknownBlock extends BaseBlock { + kind: "unknown" + originalKind: string // Store the original unknown kind + content: string +} + +export type Block = TextBlock | ThinkingBlock | ToolBlock | UnknownBlock + +// Type guards +export function isTextBlock(block: Block): block is TextBlock { + return block.kind === "text" +} + +export function isThinkingBlock(block: Block): block is ThinkingBlock { + return block.kind === "thinking" +} + +export function isToolBlock(block: Block): block is ToolBlock { + return block.kind === "tool_call" +} + +export function isUnknownBlock(block: Block): block is UnknownBlock { + return block.kind === "unknown" +} + +// ============================================================================= +// State Types +// ============================================================================= + +export interface BondState { + blocks: Block[] + activeBlockId: string | undefined + eventCount: number +} + +export const initialBondState: BondState = { + blocks: [], + activeBlockId: undefined, + eventCount: 0, +} diff --git a/ui/src/bond/useBondReplayFromFile.ts b/ui/src/bond/useBondReplayFromFile.ts new file mode 100644 index 0000000..2a1dd2d --- /dev/null +++ b/ui/src/bond/useBondReplayFromFile.ts @@ -0,0 +1,251 @@ +/** + * Bond Replay From File Hook + * + * Loads and plays pre-recorded TraceEvents from an NDJSON file. + * Uses timestamps in TraceEvents for realistic playback timing. + */ + +import { useCallback, useEffect, useReducer, useRef, useState } from "react" +import type { TraceEvent, BondEvent, BondState } from "./types" +import { initialBondState } from "./types" +import { bondReducer } from "./reducer" +import { normalizeTraceEvent } from "./normalize" + +export type DemoStatus = "idle" | "loading" | "playing" | "paused" | "complete" +export type PlaybackSpeed = 0.5 | 1 | 2 + +export interface DemoControls { + /** Current block state */ + state: BondState + /** Demo status */ + status: DemoStatus + /** Current playback speed */ + speed: PlaybackSpeed + /** Event history for replay scrubbing */ + events: BondEvent[] + /** Current position (event index) */ + position: number + /** Total events */ + totalEvents: number + /** Load and start demo from URL */ + startDemo: (url: string) => Promise + /** Pause playback */ + pause: () => void + /** Resume playback */ + resume: () => void + /** Set playback speed */ + setSpeed: (speed: PlaybackSpeed) => void + /** Jump to position */ + jumpTo: (position: number) => void + /** Stop and reset */ + stop: () => void +} + +export function useBondReplayFromFile(): DemoControls { + const [state, dispatch] = useReducer(bondReducer, initialBondState) + const [status, setStatus] = useState("idle") + const [speed, setSpeed] = useState(1) + const [position, setPosition] = useState(0) + + // Store parsed trace events + const traceEventsRef = useRef([]) + const eventsRef = useRef([]) + const timeoutRef = useRef | null>(null) + const startTimeRef = useRef(0) + const speedRef = useRef(speed) + + // Keep speed ref in sync + speedRef.current = speed + + // Clear any pending timeout + const clearScheduled = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + }, []) + + // Schedule next event based on timestamp difference + const scheduleNext = useCallback( + (currentIndex: number) => { + const traces = traceEventsRef.current + if (currentIndex >= traces.length) { + setStatus("complete") + return + } + + const currentTrace = traces[currentIndex] + const nextIndex = currentIndex + 1 + + // If there's a next event, calculate delay based on timestamps + if (nextIndex < traces.length) { + const nextTrace = traces[nextIndex] + const delay = + ((nextTrace.timestamp - currentTrace.timestamp) * 1000) / + speedRef.current + + timeoutRef.current = setTimeout(() => { + // Dispatch the next event + const bondEvent = normalizeTraceEvent(nextTrace) + if (bondEvent) { + eventsRef.current = [...eventsRef.current, bondEvent] + dispatch(bondEvent) + } + setPosition(nextIndex) + scheduleNext(nextIndex) + }, Math.max(0, delay)) + } else { + // No more events after dispatching current + setStatus("complete") + } + }, + [] + ) + + const startDemo = useCallback( + async (url: string) => { + clearScheduled() + setStatus("loading") + + try { + const response = await fetch(url) + const text = await response.text() + + // Parse NDJSON + const lines = text.trim().split("\n") + const traces: TraceEvent[] = [] + + for (const line of lines) { + if (line.trim()) { + try { + traces.push(JSON.parse(line) as TraceEvent) + } catch { + console.warn("Failed to parse line:", line) + } + } + } + + if (traces.length === 0) { + setStatus("idle") + return + } + + // Sort by sequence number + traces.sort((a, b) => a.sequence - b.sequence) + + // Store and reset state + traceEventsRef.current = traces + eventsRef.current = [] + + // Reset reducer state by re-dispatching from empty + // (useReducer doesn't have reset, so we dispatch first event) + const firstEvent = normalizeTraceEvent(traces[0]) + if (firstEvent) { + eventsRef.current = [firstEvent] + // We need to reset state - create a pseudo-reset by using initial state + // For now, we'll just start fresh with first event + } + + setPosition(0) + setStatus("playing") + startTimeRef.current = performance.now() + + // Dispatch first event and schedule next + if (firstEvent) { + dispatch(firstEvent) + scheduleNext(0) + } + } catch (err) { + console.error("Failed to load demo:", err) + setStatus("idle") + } + }, + [clearScheduled, scheduleNext] + ) + + const pause = useCallback(() => { + if (status === "playing") { + clearScheduled() + setStatus("paused") + } + }, [status, clearScheduled]) + + const resume = useCallback(() => { + if (status === "paused") { + setStatus("playing") + scheduleNext(position) + } + }, [status, position, scheduleNext]) + + const jumpTo = useCallback( + (newPosition: number) => { + clearScheduled() + + const traces = traceEventsRef.current + if (newPosition < 0 || newPosition >= traces.length) return + + // Re-reduce all events up to newPosition + const events: BondEvent[] = [] + let newState = initialBondState + + for (let i = 0; i <= newPosition; i++) { + const event = normalizeTraceEvent(traces[i]) + if (event) { + events.push(event) + newState = bondReducer(newState, event) + } + } + + eventsRef.current = events + setPosition(newPosition) + + // We need to set state - but useReducer doesn't support setting directly + // Dispatch a batch of events to get to target state + // Actually, we need to reconstruct - for now just rebuild from position + // This is a limitation - would need a different state management approach + + // For MVP: just continue from new position + if (status === "playing") { + scheduleNext(newPosition) + } + }, + [status, clearScheduled, scheduleNext] + ) + + const stop = useCallback(() => { + clearScheduled() + traceEventsRef.current = [] + eventsRef.current = [] + setPosition(0) + setStatus("idle") + // State will stay - user needs to reconnect or reload to clear + }, [clearScheduled]) + + // Cleanup on unmount + useEffect(() => { + return () => clearScheduled() + }, [clearScheduled]) + + // Update speed effect - reschedule with new timing + useEffect(() => { + if (status === "playing") { + clearScheduled() + scheduleNext(position) + } + }, [speed, status, position, clearScheduled, scheduleNext]) + + return { + state, + status, + speed, + events: eventsRef.current, + position, + totalEvents: traceEventsRef.current.length, + startDemo, + pause, + resume, + setSpeed, + jumpTo, + stop, + } +} diff --git a/ui/src/bond/useBondStream.ts b/ui/src/bond/useBondStream.ts new file mode 100644 index 0000000..6d7ed13 --- /dev/null +++ b/ui/src/bond/useBondStream.ts @@ -0,0 +1,189 @@ +/** + * Bond Stream Hook + * + * SSE streaming transport layer with: + * - EventSource connection management + * - Status tracking (idle, connecting, live, error) + * - Pause control with event buffering + * - Event history for replay + */ + +import { useCallback, useEffect, useReducer, useRef, useState } from "react" +import type { BondEvent, BondState } from "./types" +import { initialBondState } from "./types" +import { bondReducer } from "./reducer" +import { normalizeSSEEvent } from "./normalize" +import { useEventHistory } from "./useEventHistory" + +export type ConnectionStatus = "idle" | "connecting" | "live" | "error" + +export interface BondStreamControls { + /** Current block state */ + state: BondState + /** Connection status */ + status: ConnectionStatus + /** Whether event processing is paused */ + paused: boolean + /** Set pause state */ + setPaused: (paused: boolean) => void + /** Connect to SSE endpoint */ + connect: () => void + /** Disconnect from SSE endpoint */ + disconnect: () => void + /** Event history for replay */ + history: { + events: BondEvent[] + count: number + getUpTo: (index: number) => BondEvent[] + } + /** Reset state (clear blocks and history) */ + reset: () => void +} + +export function useBondStream(url: string | null): BondStreamControls { + const [state, dispatch] = useReducer(bondReducer, initialBondState) + const [status, setStatus] = useState("idle") + const [paused, setPaused] = useState(false) + + const eventSourceRef = useRef(null) + const pausedRef = useRef(paused) + const pauseBufferRef = useRef([]) + + const history = useEventHistory() + + // Keep pausedRef in sync + pausedRef.current = paused + + const processEvent = useCallback( + (event: BondEvent) => { + // Always store in history + history.push(event) + + // Only dispatch to reducer if not paused + if (!pausedRef.current) { + dispatch(event) + } else { + // Buffer for later when unpaused + pauseBufferRef.current.push(event) + } + }, + [history] + ) + + const connect = useCallback(() => { + if (!url) return + if (eventSourceRef.current) { + eventSourceRef.current.close() + } + + setStatus("connecting") + + const es = new EventSource(url) + eventSourceRef.current = es + + es.onopen = () => { + setStatus("live") + } + + es.onerror = () => { + // EventSource will auto-reconnect for CONNECTING state + // Only set error if fully closed + if (es.readyState === EventSource.CLOSED) { + setStatus("error") + } + } + + // Handle named SSE events + const eventTypes = [ + "block_start", + "block_end", + "text", + "thinking", + "tool_delta", + "tool_exec", + "tool_result", + "complete", + ] + + eventTypes.forEach((eventType) => { + es.addEventListener(eventType, (e: MessageEvent) => { + try { + const data = JSON.parse(e.data) + const bondEvent = normalizeSSEEvent(eventType, data) + if (bondEvent) { + processEvent(bondEvent) + } + } catch (err) { + console.warn(`Failed to parse SSE event: ${eventType}`, err) + } + }) + }) + + // Also handle generic message event (for servers that don't use named events) + es.onmessage = (e: MessageEvent) => { + try { + const data = JSON.parse(e.data) + // Try to determine event type from data + if (data.t) { + // Wire format - need to import normalizeWireEvent + // For now, skip - we expect named events + console.warn("Received wire format on generic message, expected named events") + } + } catch (err) { + console.warn("Failed to parse generic SSE message", err) + } + } + }, [url, processEvent]) + + const disconnect = useCallback(() => { + if (eventSourceRef.current) { + eventSourceRef.current.close() + eventSourceRef.current = null + } + setStatus("idle") + }, []) + + const reset = useCallback(() => { + disconnect() + history.clear() + pauseBufferRef.current = [] + // Reset reducer state by creating fresh state + // Note: useReducer doesn't have a reset, so we dispatch a pseudo-reset + // For now, we'll just disconnect and let user reconnect + }, [disconnect, history]) + + // Handle unpause - flush buffered events + useEffect(() => { + if (!paused && pauseBufferRef.current.length > 0) { + // Flush buffered events + pauseBufferRef.current.forEach((event) => { + dispatch(event) + }) + pauseBufferRef.current = [] + } + }, [paused]) + + // Cleanup on unmount + useEffect(() => { + return () => { + if (eventSourceRef.current) { + eventSourceRef.current.close() + } + } + }, []) + + return { + state, + status, + paused, + setPaused, + connect, + disconnect, + history: { + events: history.events, + count: history.count, + getUpTo: history.getUpTo, + }, + reset, + } +} diff --git a/ui/src/bond/useEventHistory.ts b/ui/src/bond/useEventHistory.ts new file mode 100644 index 0000000..f0355d3 --- /dev/null +++ b/ui/src/bond/useEventHistory.ts @@ -0,0 +1,64 @@ +/** + * Event History Buffer + * + * Stores events for replay functionality. + * Supports buffering while paused and limiting history size. + */ + +import { useCallback, useRef, useState } from "react" +import type { BondEvent } from "./types" + +const DEFAULT_MAX_EVENTS = 10000 + +export interface EventHistoryControls { + /** All stored events */ + events: BondEvent[] + /** Add an event to history */ + push: (event: BondEvent) => void + /** Clear all history */ + clear: () => void + /** Get events up to index (for replay) */ + getUpTo: (index: number) => BondEvent[] + /** Total event count */ + count: number +} + +export function useEventHistory( + maxEvents: number = DEFAULT_MAX_EVENTS +): EventHistoryControls { + const [events, setEvents] = useState([]) + const eventsRef = useRef([]) + + // Keep ref in sync for use in callbacks + eventsRef.current = events + + const push = useCallback( + (event: BondEvent) => { + setEvents((prev) => { + const next = [...prev, event] + // Limit history size + if (next.length > maxEvents) { + return next.slice(-maxEvents) + } + return next + }) + }, + [maxEvents] + ) + + const clear = useCallback(() => { + setEvents([]) + }, []) + + const getUpTo = useCallback((index: number) => { + return eventsRef.current.slice(0, index + 1) + }, []) + + return { + events, + push, + clear, + getUpTo, + count: events.length, + } +} diff --git a/ui/src/bond/useReplayState.ts b/ui/src/bond/useReplayState.ts new file mode 100644 index 0000000..d1dbaea --- /dev/null +++ b/ui/src/bond/useReplayState.ts @@ -0,0 +1,164 @@ +/** + * Replay State Hook + * + * Manages replay position and derives visible state from event history. + * Supports scrubbing through past events while new events continue buffering. + */ + +import { useMemo, useState, useCallback, useRef, useEffect } from "react" +import type { BondEvent, BondState } from "./types" +import { initialBondState } from "./types" +import { reduceEvents } from "./reducer" + +export type ReplayMode = "live" | "replay" + +export interface ReplayStateControls { + /** Current replay mode */ + mode: ReplayMode + /** Current position in event history (0-based index) */ + position: number + /** Total events in history */ + totalEvents: number + /** Derived block state at current position */ + visibleState: BondState + /** Set replay position (switches to replay mode) */ + setPosition: (position: number) => void + /** Jump back to live position */ + jumpToLive: () => void + /** Whether we're at the latest position */ + isAtLive: boolean +} + +// Debounce delay for scrubber input (ms) +const SCRUB_DEBOUNCE_MS = 50 + +// Cache intermediate states every N events for faster scrubbing +const CACHE_INTERVAL = 100 + +export function useReplayState(events: BondEvent[]): ReplayStateControls { + const [mode, setMode] = useState("live") + const [position, setPositionRaw] = useState(0) + const [debouncedPosition, setDebouncedPosition] = useState(0) + + const debounceTimeoutRef = useRef | null>(null) + + // Cache for intermediate states (position -> state) + const stateCache = useRef>(new Map()) + + const totalEvents = events.length + + // Keep position in bounds + const clampedPosition = Math.min(Math.max(0, position), totalEvents - 1) + + // Debounce position updates for scrubber + useEffect(() => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current) + } + + debounceTimeoutRef.current = setTimeout(() => { + setDebouncedPosition(clampedPosition) + }, SCRUB_DEBOUNCE_MS) + + return () => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current) + } + } + }, [clampedPosition]) + + // Update position to follow live when in live mode + useEffect(() => { + if (mode === "live" && totalEvents > 0) { + setPositionRaw(totalEvents - 1) + setDebouncedPosition(totalEvents - 1) + } + }, [mode, totalEvents]) + + // Build state cache at intervals + useEffect(() => { + if (events.length === 0) { + stateCache.current.clear() + return + } + + // Build cache entries at intervals + const newCache = new Map() + let state = initialBondState + + for (let i = 0; i < events.length; i++) { + state = reduceEvents([events[i]], state) + + // Cache at intervals + if ((i + 1) % CACHE_INTERVAL === 0) { + newCache.set(i, { ...state }) + } + } + + // Always cache the final state + newCache.set(events.length - 1, state) + stateCache.current = newCache + }, [events]) + + // Compute visible state at debounced position + const visibleState = useMemo(() => { + if (events.length === 0) { + return initialBondState + } + + const targetPosition = Math.min(debouncedPosition, events.length - 1) + + // Find nearest cached state before target + let startPosition = -1 + let startState = initialBondState + + for (const [cachedPos, cachedState] of stateCache.current) { + if (cachedPos <= targetPosition && cachedPos > startPosition) { + startPosition = cachedPos + startState = cachedState + } + } + + // If exact cache hit, return it + if (startPosition === targetPosition) { + return startState + } + + // Reduce from cached state to target + const eventsToProcess = events.slice(startPosition + 1, targetPosition + 1) + return reduceEvents(eventsToProcess, startState) + }, [events, debouncedPosition]) + + const setPosition = useCallback( + (newPosition: number) => { + const clamped = Math.min(Math.max(0, newPosition), totalEvents - 1) + setPositionRaw(clamped) + + // Switch to replay mode if not at live + if (clamped < totalEvents - 1) { + setMode("replay") + } + }, + [totalEvents] + ) + + const jumpToLive = useCallback(() => { + setMode("live") + if (totalEvents > 0) { + setPositionRaw(totalEvents - 1) + setDebouncedPosition(totalEvents - 1) + } + }, [totalEvents]) + + const isAtLive = mode === "live" || clampedPosition >= totalEvents - 1 + + return { + mode, + position: clampedPosition, + totalEvents, + visibleState, + setPosition, + jumpToLive, + isAtLive, + } +} diff --git a/ui/src/components/ui/badge.tsx b/ui/src/components/ui/badge.tsx new file mode 100644 index 0000000..d30709f --- /dev/null +++ b/ui/src/components/ui/badge.tsx @@ -0,0 +1,40 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + success: + "border-transparent bg-emerald-500/20 text-emerald-400", + warning: + "border-transparent bg-amber-500/20 text-amber-400", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/ui/src/components/ui/button.tsx b/ui/src/components/ui/button.tsx new file mode 100644 index 0000000..65d4fcd --- /dev/null +++ b/ui/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/ui/src/components/ui/card.tsx b/ui/src/components/ui/card.tsx new file mode 100644 index 0000000..6fdb8d6 --- /dev/null +++ b/ui/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/ui/src/components/ui/scroll-area.tsx b/ui/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..0e6e17d --- /dev/null +++ b/ui/src/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/ui/src/hooks/useKeyboardShortcuts.ts b/ui/src/hooks/useKeyboardShortcuts.ts new file mode 100644 index 0000000..6735fd4 --- /dev/null +++ b/ui/src/hooks/useKeyboardShortcuts.ts @@ -0,0 +1,77 @@ +/** + * Keyboard Shortcuts Hook + * + * Global keyboard shortcuts for timeline navigation: + * - Space: Toggle pause/play + * - L: Jump to live + * - J: Step backward + * - K: Step forward + * - Escape: Close inspector (handled in useSelection) + */ + +import { useEffect } from "react" + +interface KeyboardShortcutsConfig { + /** Toggle pause/play */ + onTogglePause?: () => void + /** Jump to live */ + onJumpToLive?: () => void + /** Step backward one event */ + onStepBackward?: () => void + /** Step forward one event */ + onStepForward?: () => void + /** Whether shortcuts are enabled */ + enabled?: boolean +} + +export function useKeyboardShortcuts({ + onTogglePause, + onJumpToLive, + onStepBackward, + onStepForward, + enabled = true, +}: KeyboardShortcutsConfig) { + useEffect(() => { + if (!enabled) return + + const handleKeyDown = (e: KeyboardEvent) => { + // Ignore if typing in an input + if ( + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement + ) { + return + } + + // Ignore if modifier keys are pressed (except shift) + if (e.metaKey || e.ctrlKey || e.altKey) { + return + } + + switch (e.key.toLowerCase()) { + case " ": // Space + e.preventDefault() + onTogglePause?.() + break + + case "l": + e.preventDefault() + onJumpToLive?.() + break + + case "j": + e.preventDefault() + onStepBackward?.() + break + + case "k": + e.preventDefault() + onStepForward?.() + break + } + } + + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, [enabled, onTogglePause, onJumpToLive, onStepBackward, onStepForward]) +} diff --git a/ui/src/index.css b/ui/src/index.css new file mode 100644 index 0000000..1939273 --- /dev/null +++ b/ui/src/index.css @@ -0,0 +1,57 @@ +@import "tailwindcss"; + +@layer base { + :root { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --radius: 0.5rem; + } +} + +@layer base { + * { + border-color: hsl(var(--border)); + } + body { + @apply bg-zinc-950 text-zinc-50 antialiased; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + } +} + +/* Premium typography */ +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +/* Shimmer animation for active blocks */ +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } +} + +.animate-shimmer { + animation: shimmer 2s infinite; +} diff --git a/ui/src/main.tsx b/ui/src/main.tsx new file mode 100644 index 0000000..57f76e3 --- /dev/null +++ b/ui/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react" +import { createRoot } from "react-dom/client" +import "./index.css" +import App from "./App" + +createRoot(document.getElementById("root")!).render( + + + +) diff --git a/ui/src/ui/BlockCard.tsx b/ui/src/ui/BlockCard.tsx new file mode 100644 index 0000000..8e62d98 --- /dev/null +++ b/ui/src/ui/BlockCard.tsx @@ -0,0 +1,91 @@ +/** + * Block Card Component + * + * Wrapper that provides: + * - Consistent card styling + * - Active cursor affordance (glow + shimmer) + * - Framer Motion animations + */ + +import { motion } from "motion/react" +import { Card } from "@/components/ui/card" +import { cn } from "@/lib/utils" +import type { Block } from "@/bond/types" +import { isTextBlock, isThinkingBlock, isToolBlock } from "@/bond/types" +import { TextBlock } from "./TextBlock" +import { ThinkingBlock } from "./ThinkingBlock" +import { ToolBlock } from "./ToolBlock" + +interface BlockCardProps { + block: Block + onClick?: () => void + selected?: boolean +} + +export function BlockCard({ block, onClick, selected }: BlockCardProps) { + const renderContent = () => { + if (isTextBlock(block)) { + return + } + if (isThinkingBlock(block)) { + return + } + if (isToolBlock(block)) { + return + } + // Unknown kind - render as generic text + // At this point, block is UnknownBlock + const unknownBlock = block + return ( +
+
+ {unknownBlock.originalKind} +
+
+ {unknownBlock.content || "..."} +
+
+ ) + } + + return ( + + + {/* Shimmer overlay for active blocks */} + {block.isActive && ( +
+
+
+ )} + +
{renderContent()}
+ + + ) +} diff --git a/ui/src/ui/DemoControls.tsx b/ui/src/ui/DemoControls.tsx new file mode 100644 index 0000000..2702c7e --- /dev/null +++ b/ui/src/ui/DemoControls.tsx @@ -0,0 +1,149 @@ +/** + * Demo Controls Component + * + * Playback controls for demo mode: + * - Play/Pause toggle + * - Speed selector (0.5x, 1x, 2x) + * - Progress indicator + * - Demo mode badge + */ + +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Pause, Play, Square, Film, Gauge } from "lucide-react" +import { cn } from "@/lib/utils" +import type { DemoStatus, PlaybackSpeed } from "@/bond/useBondReplayFromFile" + +interface DemoControlsProps { + /** Current demo status */ + status: DemoStatus + /** Current playback speed */ + speed: PlaybackSpeed + /** Current position */ + position: number + /** Total events */ + totalEvents: number + /** Pause playback */ + onPause: () => void + /** Resume playback */ + onResume: () => void + /** Set speed */ + onSetSpeed: (speed: PlaybackSpeed) => void + /** Stop demo */ + onStop: () => void +} + +const SPEEDS: PlaybackSpeed[] = [0.5, 1, 2] + +export function DemoControls({ + status, + speed, + position, + totalEvents, + onPause, + onResume, + onSetSpeed, + onStop, +}: DemoControlsProps) { + const isPlaying = status === "playing" + const isComplete = status === "complete" + const hasEvents = totalEvents > 0 + + // Cycle through speeds + const cycleSpeed = () => { + const currentIndex = SPEEDS.indexOf(speed) + const nextIndex = (currentIndex + 1) % SPEEDS.length + onSetSpeed(SPEEDS[nextIndex]) + } + + return ( +
+ {/* Demo mode indicator */} + + + Demo Mode + + + {/* Play/Pause */} + + + {/* Stop */} + + + {/* Speed control */} + + + {/* Progress */} +
+
+
0 ? `${((position + 1) / totalEvents) * 100}%` : "0%", + }} + /> +
+
+ + {/* Position display */} +
+ {hasEvents ? ( + <> + {position + 1} + / + {totalEvents} + + ) : ( + "—" + )} +
+ + {/* Status indicator */} + + {status === "loading" && "Loading..."} + {status === "playing" && "Playing"} + {status === "paused" && "Paused"} + {status === "complete" && "Complete"} + {status === "idle" && "Ready"} + +
+ ) +} diff --git a/ui/src/ui/Inspector.tsx b/ui/src/ui/Inspector.tsx new file mode 100644 index 0000000..a82cc8f --- /dev/null +++ b/ui/src/ui/Inspector.tsx @@ -0,0 +1,293 @@ +/** + * Inspector Panel Component + * + * Shows detailed information about a selected block: + * - Block type, ID, and status + * - Full content (not truncated) + * - Tool details (for tool blocks) + * - Copy as JSON functionality + */ + +import { useState, useCallback, useEffect } from "react" +import { motion, AnimatePresence } from "motion/react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + X, + Copy, + Check, + FileText, + Brain, + Wrench, + HelpCircle, +} from "lucide-react" +import { cn } from "@/lib/utils" +import type { Block } from "@/bond/types" +import { + isTextBlock, + isThinkingBlock, + isToolBlock, + isUnknownBlock, +} from "@/bond/types" + +interface InspectorProps { + /** Selected block to inspect */ + block: Block | undefined + /** Close the inspector */ + onClose: () => void +} + +function BlockIcon({ kind }: { kind: string }) { + switch (kind) { + case "text": + return + case "thinking": + return + case "tool_call": + return + default: + return + } +} + +function KindBadge({ kind }: { kind: string }) { + const variants: Record = { + text: "default", + thinking: "secondary", + tool_call: "warning", + } + + return ( + + + {kind} + + ) +} + +export function Inspector({ block, onClose }: InspectorProps) { + const [copied, setCopied] = useState(false) + + // Reset copied state when block changes + useEffect(() => { + setCopied(false) + }, [block?.id]) + + const handleCopy = useCallback(async () => { + if (!block) return + + try { + const json = JSON.stringify(block, null, 2) + await navigator.clipboard.writeText(json) + setCopied(true) + + // Reset after 2 seconds + setTimeout(() => setCopied(false), 2000) + } catch (err) { + console.error("Failed to copy:", err) + } + }, [block]) + + return ( + + {block && ( + + + {/* Header */} +
+
+ Inspector + +
+ +
+ + {/* Content */} +
+ {/* ID and Index */} +
+ + + + {block.isActive && ( + + Active + + )} + {block.isClosed && ( + + Closed + + )} + {!block.isActive && !block.isClosed && ( + + Open + + )} +
+ } + /> + + + {/* Content (for text/thinking blocks) */} + {(isTextBlock(block) || isThinkingBlock(block)) && ( +
+
+                    {block.content || "(empty)"}
+                  
+
+ {block.content.length} characters +
+
+ )} + + {/* Tool Details (for tool blocks) */} + {isToolBlock(block) && ( + <> +
+ + + + {block.status} + + } + /> +
+ +
+
+                      {block.toolArgs
+                        ? JSON.stringify(block.toolArgs, null, 2)
+                        : block.toolArgsDraft || "(forming)"}
+                    
+
+ + {block.result && ( +
+
+                        {block.result}
+                      
+
+ {block.result.length} characters +
+
+ )} + + )} + + {/* Unknown block content */} + {isUnknownBlock(block) && ( +
+ +
+                    {block.content || "(empty)"}
+                  
+
+ )} + + {/* Copy as JSON */} +
+ +
+
+ + + )} + + ) +} + +// Helper components + +function Section({ + title, + children, +}: { + title: string + children: React.ReactNode +}) { + return ( +
+
{title}
+ {children} +
+ ) +} + +function InfoRow({ + label, + value, + mono = false, +}: { + label: string + value: React.ReactNode + mono?: boolean +}) { + return ( +
+ {label} + + {value} + +
+ ) +} diff --git a/ui/src/ui/ReplayControls.tsx b/ui/src/ui/ReplayControls.tsx new file mode 100644 index 0000000..a5149c8 --- /dev/null +++ b/ui/src/ui/ReplayControls.tsx @@ -0,0 +1,145 @@ +/** + * Replay Controls Component + * + * Provides UI for scrubbing through event history: + * - Play/Pause toggle + * - Position slider (0..N events) + * - Live/Replay indicator + * - Jump to Live button + */ + +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Pause, Play, Radio, History, FastForward } from "lucide-react" +import { cn } from "@/lib/utils" +import type { ReplayMode } from "@/bond/useReplayState" + +interface ReplayControlsProps { + /** Current mode (live or replay) */ + mode: ReplayMode + /** Whether stream is paused */ + paused: boolean + /** Toggle pause state */ + onPauseToggle: () => void + /** Current position in event history */ + position: number + /** Total events in history */ + totalEvents: number + /** Set position (scrub) */ + onPositionChange: (position: number) => void + /** Jump to live */ + onJumpToLive: () => void + /** Whether we're at live position */ + isAtLive: boolean +} + +export function ReplayControls({ + mode, + paused, + onPauseToggle, + position, + totalEvents, + onPositionChange, + onJumpToLive, + isAtLive, +}: ReplayControlsProps) { + const hasEvents = totalEvents > 0 + + return ( +
+ {/* Play/Pause */} + + + {/* Mode indicator */} + + {mode === "live" ? ( + <> + + Live + + ) : ( + <> + + Replay + + )} + + + {/* Scrubber slider */} +
+ onPositionChange(parseInt(e.target.value, 10))} + disabled={!hasEvents} + className={cn( + "flex-1 h-1 bg-zinc-700 rounded-full appearance-none cursor-pointer", + "disabled:opacity-50 disabled:cursor-not-allowed", + "[&::-webkit-slider-thumb]:appearance-none", + "[&::-webkit-slider-thumb]:w-3", + "[&::-webkit-slider-thumb]:h-3", + "[&::-webkit-slider-thumb]:rounded-full", + "[&::-webkit-slider-thumb]:bg-zinc-300", + "[&::-webkit-slider-thumb]:hover:bg-zinc-100", + "[&::-webkit-slider-thumb]:transition-colors", + "[&::-moz-range-thumb]:w-3", + "[&::-moz-range-thumb]:h-3", + "[&::-moz-range-thumb]:rounded-full", + "[&::-moz-range-thumb]:bg-zinc-300", + "[&::-moz-range-thumb]:border-0", + "[&::-moz-range-thumb]:hover:bg-zinc-100" + )} + /> + + {/* Position display */} +
+ {hasEvents ? ( + <> + {position + 1} + / + {totalEvents} + + ) : ( + "—" + )} +
+
+ + {/* Jump to Live button */} + +
+ ) +} diff --git a/ui/src/ui/TextBlock.tsx b/ui/src/ui/TextBlock.tsx new file mode 100644 index 0000000..de46e5b --- /dev/null +++ b/ui/src/ui/TextBlock.tsx @@ -0,0 +1,21 @@ +/** + * Text Block Component + * + * Renders text content from the agent with clean prose styling. + */ + +import type { TextBlock as TextBlockType } from "@/bond/types" + +interface TextBlockProps { + block: TextBlockType +} + +export function TextBlock({ block }: TextBlockProps) { + return ( +
+
+ {block.content || ...} +
+
+ ) +} diff --git a/ui/src/ui/ThinkingBlock.tsx b/ui/src/ui/ThinkingBlock.tsx new file mode 100644 index 0000000..b201bf9 --- /dev/null +++ b/ui/src/ui/ThinkingBlock.tsx @@ -0,0 +1,46 @@ +/** + * Thinking Block Component + * + * Renders thinking/reasoning content with subtle, dimmer styling. + * Shows "Thinking" label to distinguish from regular text. + */ + +import { useState } from "react" +import { ChevronDown, ChevronRight, Brain } from "lucide-react" +import type { ThinkingBlock as ThinkingBlockType } from "@/bond/types" + +interface ThinkingBlockProps { + block: ThinkingBlockType +} + +export function ThinkingBlock({ block }: ThinkingBlockProps) { + const [collapsed, setCollapsed] = useState(false) + + return ( +
+ + + {!collapsed && ( +
+ {block.content || ...} +
+ )} +
+ ) +} diff --git a/ui/src/ui/Timeline.tsx b/ui/src/ui/Timeline.tsx new file mode 100644 index 0000000..183caac --- /dev/null +++ b/ui/src/ui/Timeline.tsx @@ -0,0 +1,112 @@ +/** + * Timeline Component + * + * Main timeline view that renders blocks with: + * - Framer Motion animations (AnimatePresence) + * - Auto-scroll when at bottom + * - "Scroll to bottom" button when scrolled up + */ + +import { useEffect, useRef, useState } from "react" +import { AnimatePresence } from "motion/react" +import { ArrowDown, Circle } from "lucide-react" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import type { Block } from "@/bond/types" +import { BlockCard } from "./BlockCard" + +interface TimelineProps { + blocks: Block[] + selectedBlockId?: string + onSelectBlock?: (blockId: string) => void +} + +export function Timeline({ + blocks, + selectedBlockId, + onSelectBlock, +}: TimelineProps) { + const scrollRef = useRef(null) + const bottomRef = useRef(null) + const [isAtBottom, setIsAtBottom] = useState(true) + + // Intersection Observer to track if user is at bottom + useEffect(() => { + const bottom = bottomRef.current + if (!bottom) return + + const observer = new IntersectionObserver( + ([entry]) => { + setIsAtBottom(entry.isIntersecting) + }, + { threshold: 0.1 } + ) + + observer.observe(bottom) + return () => observer.disconnect() + }, []) + + // Auto-scroll when at bottom and new blocks arrive + useEffect(() => { + if (isAtBottom && bottomRef.current) { + bottomRef.current.scrollIntoView({ behavior: "smooth" }) + } + }, [blocks.length, isAtBottom]) + + const scrollToBottom = () => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }) + } + + if (blocks.length === 0) { + return ( +
+
+ +
+ Waiting for events... +
+
+ Connect to a stream to see the agent timeline +
+
+
+ ) + } + + return ( +
+ +
+ + {blocks.map((block) => ( + onSelectBlock?.(block.id)} + /> + ))} + + + {/* Bottom sentinel for scroll detection */} +
+
+ + + {/* Scroll to bottom button */} + {!isAtBottom && ( +
+ +
+ )} +
+ ) +} diff --git a/ui/src/ui/ToolBlock.tsx b/ui/src/ui/ToolBlock.tsx new file mode 100644 index 0000000..7539bed --- /dev/null +++ b/ui/src/ui/ToolBlock.tsx @@ -0,0 +1,83 @@ +/** + * Tool Block Component + * + * Renders tool calls with: + * - Tool name and status badge + * - Streaming args in monospace + * - Result panel when done + */ + +import { Badge } from "@/components/ui/badge" +import { Wrench, Loader2, Check } from "lucide-react" +import type { ToolBlock as ToolBlockType } from "@/bond/types" + +interface ToolBlockProps { + block: ToolBlockType +} + +function StatusBadge({ status }: { status: ToolBlockType["status"] }) { + switch (status) { + case "forming": + return ( + + + forming + + ) + case "executing": + return ( + + + executing + + ) + case "done": + return ( + + + done + + ) + } +} + +export function ToolBlock({ block }: ToolBlockProps) { + const toolName = block.toolName ?? block.toolNameDraft + const toolArgs = block.toolArgs + ? JSON.stringify(block.toolArgs, null, 2) + : block.toolArgsDraft + + return ( +
+ {/* Header */} +
+ +
Tool
+
+ {toolName || "..."} +
+
+ +
+
+ + {/* Args */} +
+
Arguments
+
+          {toolArgs || "..."}
+        
+
+ + {/* Result (when done) */} + {block.result && ( +
+
Result
+
+            {block.result}
+          
+
+ )} +
+ ) +} diff --git a/ui/src/ui/useSelection.ts b/ui/src/ui/useSelection.ts new file mode 100644 index 0000000..2a00f97 --- /dev/null +++ b/ui/src/ui/useSelection.ts @@ -0,0 +1,78 @@ +/** + * Selection State Hook + * + * Manages block selection state with keyboard support. + * - Tracks selected block ID + * - Escape key to deselect + * - Click outside to deselect + */ + +import { useState, useCallback, useEffect, useRef } from "react" + +export interface SelectionControls { + /** Currently selected block ID */ + selectedBlockId: string | undefined + /** Select a block */ + select: (blockId: string) => void + /** Clear selection */ + deselect: () => void + /** Toggle selection */ + toggle: (blockId: string) => void + /** Ref to attach to container for click-outside detection */ + containerRef: React.RefObject +} + +export function useSelection(): SelectionControls { + const [selectedBlockId, setSelectedBlockId] = useState( + undefined + ) + const containerRef = useRef(null) + + const select = useCallback((blockId: string) => { + setSelectedBlockId(blockId) + }, []) + + const deselect = useCallback(() => { + setSelectedBlockId(undefined) + }, []) + + const toggle = useCallback((blockId: string) => { + setSelectedBlockId((prev) => (prev === blockId ? undefined : blockId)) + }, []) + + // Escape key to deselect + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && selectedBlockId) { + deselect() + } + } + + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, [selectedBlockId, deselect]) + + // Click outside to deselect + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (!selectedBlockId) return + if (!containerRef.current) return + + // Check if click is inside the container + if (!containerRef.current.contains(e.target as Node)) { + deselect() + } + } + + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, [selectedBlockId, deselect]) + + return { + selectedBlockId, + select, + deselect, + toggle, + containerRef, + } +} diff --git a/ui/tsconfig.app.json b/ui/tsconfig.app.json new file mode 100644 index 0000000..a26bdae --- /dev/null +++ b/ui/tsconfig.app.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..aa3c04f --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/ui/tsconfig.node.json b/ui/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/ui/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 0000000..391abcb --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,13 @@ +import path from "path" +import tailwindcss from "@tailwindcss/vite" +import react from "@vitejs/plugin-react" +import { defineConfig } from "vite" + +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +})