mux: Electron + React desktop app for parallel agent workflows; UX must be fast, responsive, predictable.
Minor breaking changes are expected, but critical flows must allow upgrade↔downgrade without friction; skip migrations when breakage is tightly scoped.
For PRs, commits, and public issues, consult the pull-requests skill for attribution footer requirements and workflow conventions.
No free-floating Markdown. User docs live in docs/ (read docs/README.md, add pages to docs.json navigation, use standard Markdown + mermaid). Developer notes belong inline as comments.
For planning artifacts, use the propose_plan tool or inline comments instead of ad-hoc docs.
Do not add new root-level docs without explicit request; during feature work rely on code + tests + inline comments.
External API docs already live inside /tmp/ai-sdk-docs/**.mdx; never browse https://sdk.vercel.ai/docs/ai-sdk-core directly.
Core UX: projects sidebar (left panel), workspace management (local git worktrees or SSH clones), config stored in ~/.mux/config.json.
Fetch bulk data in one IPC call—no O(n) frontend→backend loops.
React Compiler enabled — auto-memoization handles components/hooks; do not add manual React.memo(), useMemo, or useCallback for memoization purposes. Focus instead on fixing unstable object references that the compiler cannot optimize (e.g., new Set() in state setters, inline object literals as props).
useEffect — Before adding effects, consult the react-effects skill. Most effects for derived state, prop resets, or event-triggered logic are anti-patterns.
Package manager: bun only. Use bun install, bun add, bun run (which proxies to Make when relevant). Run bun install if modules/types go missing.
Makefile is source of truth (new commands land there, not package.json).
Primary targets: make dev|start|build|lint|lint-fix|fmt|fmt-check|typecheck|test|test-integration|clean|help.
Codex reviews: if a PR has Codex review comments, address + resolve them, then re-request review by commenting @codex review on the PR. Repeat until ./scripts/check_codex_comments.sh <pr_number> reports none.
Full static-check includes docs link checking via mintlify broken-links.
Prefer self-healing behavior: if corrupted or invalid data exists in persisted state (e.g., chat.jsonl), the system should sanitize or filter it at load/request time rather than failing permanently.
Never let a single malformed line in history brick a workspace—apply defensive filtering in request-building paths so the user can continue working.
When streaming crashes, any incomplete state committed to disk should either be repairable on next load or excluded from provider requests to avoid API validation errors.
Startup-time initialization must never crash the app. Wrap in try-catch, use timeouts, fall back silently.
Never use emoji characters as UI icons or status indicators; emoji rendering varies across platforms and fonts.
Prefer SVG icons (usually from lucide-react) or shared icon components under src/browser/components/icons/.
For tool call headers, use ToolIcon from src/browser/components/tools/shared/ToolPrimitives.tsx.
If a tool/agent provides an emoji string (e.g., status_set or displayStatus), render via EmojiIcon (src/browser/components/icons/EmojiIcon.tsx) instead of rendering the emoji.
If a new emoji appears in tool output, extend EmojiIcon to map it to an SVG icon.
Colors defined in src/browser/styles/globals.css (:root @theme block). Reference via CSS variables (e.g., var(--color-plan-mode)), never hardcode hex values.
Ban as any; rely on discriminated unions, type guards, or authored interfaces.
Use Record<Enum, Value> for exhaustive mappings to catch missing cases.
Apply utility types (Omit, Pick, etc.) to build UI-specific variants of backend types, preventing unnecessary re-renders and clarifying intent.
Let types drive design: prefer discriminated unions for state, minimize runtime checks, and simplify when types feel unwieldy.
Use using declarations (or equivalent disposables) for processes, file handles, etc., to ensure cleanup even on errors.
Centralize magic constants under src/constants/; share them instead of duplicating values across layers.
Never repeat constant values (like keybinds) in comments—they become stale when the constant changes.
Avoid void asyncFn() - fire-and-forget async calls hide race conditions. When state is observable by other code (in-memory cache, event emitters), ensure visibility order matches invariants. If memory and disk must stay in sync, persist before updating memory so observers see consistent state.
Avoid setTimeout for component coordination - racy and fragile; use callbacks or effects.
Keyboard event propagation - React’s e.stopPropagation() only stops synthetic event bubbling; native window listeners still fire. Use stopKeyboardPropagation(e) from @/browser/utils/events to stop both React and native propagation when blocking global handlers (like stream interrupt on Escape).
Prefer self-contained components over utility functions + hook proliferation. A component that takes workspaceId and computes everything internally is better than one that requires 10 props drilled from parent hooks.
Parent components own localStorage interactions; children announce intent only.
Never call localStorage directly — always use usePersistedState/readPersistedState/updatePersistedState helpers. This includes inside useCallback, event handlers, and non-React functions. The helpers handle JSON parsing, error recovery, and cross-component sync.
When a component needs to read persisted state it doesn’t own (to avoid layout flash), use readPersistedState in useState initializer: useState(() => readPersistedState(key, default)).
When multiple components need the same persisted value, use usePersistedState with identical keys and { listener: true } for automatic cross-component sync.
Avoid destructuring props in function signatures; access via props.field to keep rename-friendly code.
Use static import statements at the top; resolve circular dependencies by extracting shared modules, inverting dependencies, or using DI. Dynamic await import() is not an acceptable workaround.
Frontend must never synthesize workspace IDs (e.g., ${project}-${branch} is forbidden). Backend operations that change IDs must return the value; always consume that response.
It is safe to assume that the frontend and backend of the IPC are always in sync.
Freely make breaking changes, and reorganize / cleanup IPC as needed.
Radix Popover portals don’t work in happy-dom — content renders to document.body via portal but happy-dom doesn’t support this properly. Popover content won’t appear in tests.
Use conditional rendering for testability: Components like AgentModePicker use {isOpen && <div>...} instead of Radix Portal. This renders inline and works in happy-dom.
When adding new dropdown/popover components that need tests/ui coverage, prefer the conditional rendering pattern over Radix Portal.
E2E tests (tests/e2e) work with Radix but are slow (~2min startup); reserve for scenarios that truly need real Electron.
Only use validateApiKeys() in tests that actually make AI API calls.