See AGENTS.md for the comprehensive development guide (file map, architecture, gotchas, keybindings).
bun run start # Launch
bun test # 92 tests
bunx tsc --noEmit # Type check- Single
useKeyboardin app.tsx — no other component registers key handlers. See AGENTS.md "Single Keyboard Handler". - No
useCallbackwithuseKeyboard— causes stale closures. OpenTUI handles stability via useEffectEvent. - Overlay components are pure renderers — CommandPalette, QuitConfirm, SeekInput, TranscriptUrlInput have NO useKeyboard. Keys handled in app.tsx.
- Hooks before early return —
useTheme()must be called beforeif (!visible) return null. <select>is broken — use<text>elements for all lists.<scrollbox>never getsfocusedprop — it intercepts Enter.- Search gate — when focusedPanel==='search', only Tab/Escape/Ctrl pass. All other keys are for typing.
- Queue is user-controlled — playing from search doesn't add to queue.
- Section model — music vs podcast are separate sections (Ctrl+1/2) with independent views and search routing.
- All colors via
useTheme()— no hardcoded hex values in components.
Two-level: Section (music/podcast) → View (search/queue/library/explore or search/feeds/episodes).
Playback: mpv with JSON IPC over Unix socket. YouTube via yt-dlp. Podcasts prefer YouTube version for synced transcripts.
State: Jotai atoms in src/store/. UI in src/ui/. Providers in src/providers/. Player in src/player/.
See AGENTS.md "When Modifying — Checklist" for the full pre-flight list. Key points:
- New keybinding → app.tsx handler + HelpOverlay + commands.ts
- New overlay → pure renderer, key handling in app.tsx with gate block
- New state → atom in store/, import in app.tsx
- Touches search → test BOTH music and podcast sections
- Touches playback → test queue AND one-off modes