# AGENTS.md Guidance for AI agents working in this repository. Seelen UI is a customizable Windows desktop environment built with: - Rust + Tauri (backend) - TypeScript + React/Preact (frontend) - A monorepo layout with shared libs under `libs/` ## Read First (Non-Negotiable) Build speed / safety: - DO NOT use `cargo build --release` for testing, type-checking, or local iteration. - Prefer `cargo check` for fast Rust validation. - Use `cargo build` (debug) only when you need a binary. Translations: - DO NOT run `npm run translate` during active development. - Add translations manually while iterating; run the translate command only right before a final commit. - NEVER touch non-English i18n files (no adding, editing, translating or deleting keys). Only edit `en` entries / `en.yml`; every other language is handled exclusively by `npm run translate`. Rust locking order (avoid deadlocks): 1. CLI locks 2. DATA locks 3. EVENT locks Rust mutexes: - ALWAYS use `TracedMutex` (`src/background/utils/lock_free/traced_mutex.rs`, imported as `crate::utils::lock_free::TracedMutex`) in the main binary. Never use `parking_lot::Mutex` or `std::sync::Mutex`. It panics with the full chain on deadlocks (same-thread relock or cross-thread cycles) and after a hard timeout on hangs, instead of freezing the app. - Never hold a guard across an unbounded wait (dialogs, user input, network): copy what you need, release it, and lock again afterwards; the hard timeout would crash the app. Serialize such waits with a queue/channel instead. - Never hold a `TracedMutex` guard across an `.await`. Backend architecture rules: - System modules in `src/background/modules/` MUST follow the modern pattern (lazy init + lazy tauri registration). - Business logic must NOT call `emit_to_webviews` directly. - Never write `#[tauri::command]` by hand. Commands are declared once in `libs/core/src/handlers/commands.rs` (`slu_commands_declaration!`) and their Tauri wrappers are generated at compile time; implement the handler body as a plain `impl crate::tauri_handlers::Handlers { pub fn your_command(...) { ... } }` block. - User over system: when the same resource exists both per-user and system-wide (start menu shortcuts, folders, registry keys `HKCU` vs `HKLM`, settings, etc.), always prefer the user one; the system one is only a fallback. WinRT / COM safety: - For WinRT objects with event subscriptions, use wrapper structs with `Drop` for automatic unregistration. - Windows-rs clones `TypedEventHandler` internally: store tokens, not handlers. Design (see `documentation/design-system.md` before touching any UI/styles): - Simple as possible: flat design, standard `data-skin` components, tokens only (`--slu-std-*`, `--color-*`, `--system-accent-*`, `--spacing-*`, `--shadow-*`, `--radius-*`), automatic light/dark via tokens. Nested rounded corners must be concentric (inner radius = outer radius - padding). - Accent ALWAYS = the user's Windows accent (`--system-accent-*` / `--slu-std-ui-*`). The color hue scales (red, green, blue…) are only for indicators that convey meaning (error, warning, success, info), never for accent or decoration. - Widget visuals live in the default theme (`src/static/themes/default/`), not in components, so users can mod them. - No framing `border` (flat): separate with spacing and surface tone; dividers only when needed, 1px in `--slu-std-fg-muted-color`. `outline` is reserved for focus and state indicators (focus ring, active/selected item, drag & drop target, field error), never decoration. - Never redesign, add decorative effects, or invent one-off control styles unless the maintainers explicitly ask. - Colors are never hardcoded and never derived with ad-hoc alphas (use the `--slu-std-ui-*-overlay` vars). Status hues: error = red, warning = yellow, success = green, info = blue. The font is always the system font (no monospace). De-emphasize text with `--slu-std-fg-*` tokens, never `opacity`. Text on accent backgrounds uses `--slu-std-ui-fg-color`. - UX comes first: rules may only be broken for a functional need (legibility, state feedback, affordance), never for looks. Translucency is only allowed on the toolbar, dock (and its preview) and the window manager stack bar. ## Common Commands Initial setup: ```bash npm install && npm run dev ``` Dev / build: - `npm run dev` - Frontend dev workflow - `npm run build:ui` - Build UI bundles - `npm run tauri dev` - Run Tauri in dev mode - `cargo check` - Fast Rust type check - `cargo build` - Debug build Quality (Deno-based): - `deno lint` - `deno fmt` - `npm run type-check` - `npm test` Core library (`libs/core`): - `deno task build` - `deno task build:rs` - Regenerate Rust -> TypeScript bindings - `deno task build:npm` ## Repo Map (Where Things Live) Shared libraries: - `libs/core/` - Core library + Rust-generated TypeScript bindings - `libs/widgets-shared/` - Cross-widget state utilities (includes LazySignal) - `libs/slu-ipc/`, `libs/positioning/`, `libs/widgets-integrity/` Main app: - `src/background/` - Rust backend (modules, native integrations) - `src/service/` - System service components - `src/ui/` - Frontend apps (each subdirectory is an independent app) - examples: `src/ui/settings/`, `src/ui/toolbar/`, `src/ui/launcher/`, `src/ui/window_manager/` ## Frontend Conventions App architecture: - UI apps use a hexagonal-ish layering: `infra/`, `app/`, `domain/`, `shared/`. - Keep boundaries clean: `domain/` is pure logic; `infra/` is UI + integration. Styling: - CSS Modules are the default. - Naming: kebab-case for CSS, camelCase for TS. Internationalization: - All user-visible strings must be i18n. - Translation files live under `i18n/translations/` (YAML). ## Backend: System Modules (Modern Pattern) All modules in `src/background/modules/` follow this pattern: - `application.rs` owns the singleton manager and emits internal events. - `infrastructure.rs` (or `handlers.rs`) owns Tauri commands and bridges internal events -> webviews. - Tauri event registration happens lazily on first command access (via `Once`). Suggested layout: ``` src/background/modules// mod.rs application.rs infrastructure.rs # or handlers.rs domain.rs # optional ``` Minimal pattern (infrastructure side): ```rust use std::sync::Once; use seelen_core::handlers::SeelenEvent; use crate::{app::emit_to_webviews, error::Result}; use super::{YourEvent, YourManager}; fn get_manager() -> &'static YourManager { static REGISTER: Once = Once::new(); REGISTER.call_once(|| { YourManager::subscribe(|_event: YourEvent| { // Keep this small and side-effect focused. if let Ok(data) = crate::tauri_handlers::Handlers::get_your_data() { emit_to_webviews(SeelenEvent::YourDataChanged, data); } }); }); YourManager::instance() } impl crate::tauri_handlers::Handlers { pub fn get_your_data() -> Result> { let manager = get_manager(); Ok(manager.get_data()) } } ``` Handler methods are plain functions, not Tauri commands — no `#[tauri::command]` attribute, no manual `async`/return-type plumbing. The wrapper that Tauri actually invokes is generated from the declaration in `libs/core/src/handlers/commands.rs` (see below) and calls `Handlers::your_command(...)` for you. Minimal pattern (application side): ```rust use std::sync::LazyLock; pub struct YourManager { // fields } #[derive(Debug, Clone)] pub enum YourEvent { DataChanged, } event_manager!(YourManager, YourEvent); impl YourManager { fn new() -> Self { Self { /* init */ } } pub fn instance() -> &'static Self { static MANAGER: LazyLock = LazyLock::new(|| { let mut m = YourManager::new(); m.init().log_error(); m }); &MANAGER } fn init(&mut self) -> Result<()> { self.setup_listeners()?; Ok(()) } fn setup_listeners(&mut self) -> Result<()> { // Listen to OS signals; emit internal YourEvent::* (not webview events) Ok(()) } pub fn get_data(&self) -> Vec { // return data vec![] } } ``` When adding a new backend feature exposed to the UI, update `libs/core`: 1. Declare the command in `libs/core/src/handlers/commands.rs`. The macro generates the `#[tauri::command]` wrapper, the TS binding, and the `tauri::generate_handler![]` entry — do not write any of that by hand. ```rust slu_commands_declaration! { GetYourData = get_your_data() -> Vec, } ``` Add modifiers before the fn name as needed (order doesn't matter, all are optional): - `@fallible()` — the handler returns `crate::Result` (fails); omit it when the handler can never fail and just returns `T` directly. Mirror the return type in the declaration either way (write `-> T`, not `-> Result`). - `@async()` — the handler is an `async fn` and gets `.await`ed by the generated wrapper. - `@webview()` — the generated wrapper injects a `tauri::WebviewWindow` as the first argument (e.g. for `request_widget_permission` checks). ```rust slu_commands_declaration! { SetYourData = @async() @fallible() set_your_data(value: YourType), OpenYourFile = @webview() @fallible() open_your_file(path: String), } ``` Then implement each handler as a plain method on `Handlers` in the owning module's `infrastructure.rs` (see the "Backend: System Modules" pattern above) — never annotate it with `#[tauri::command]` yourself. 2. `libs/core/src/handlers/events.rs` ```rust slu_events_declaration! { YourDataChanged(Vec) as "your-module::data-changed", } ``` 3. Regenerate bindings: `cd libs/core && deno task build:rs` ## WinRT Wrapper Pattern (Automatic Cleanup) Use wrappers for WinRT objects that register events. Rules: - Store event tokens (WinRT tokens are often `i64`). - Do NOT store `TypedEventHandler` values in struct fields. - Implement `Drop` to unregister events. Example: ```rust pub struct WinRtWrapper { pub object: SomeWinRtObject, token: i64, } impl WinRtWrapper { pub fn create(object: SomeWinRtObject) -> Result { let token = object.SomeEvent(&TypedEventHandler::new(Self::on_event))?; Ok(Self { object, token }) } fn on_event( _sender: &Option, _args: &Option, ) -> windows_core::Result<()> { Ok(()) } } impl Drop for WinRtWrapper { fn drop(&mut self) { self.object.RemoveSomeEvent(self.token).log_error(); } } ``` ## Shared State: LazySignal (Cross-Widget) Use `LazySignal` (in `libs/widgets-shared/`) when state is: - fetched asynchronously (invoke/system APIs) - updated by async events - shared across widgets/webviews Critical usage pattern: 1. Create lazy signal with async initializer. 2. Register event listeners first (they may fire immediately). 3. Call `.init()` last; it must not overwrite a value set by an event. Example: ```ts import { lazySignal } from "libs/widgets-shared/LazySignal"; import { invoke, SeelenCommand, SeelenEvent, subscribe } from "@seelen-ui/lib"; const $data = lazySignal(async () => { return await invoke(SeelenCommand.GetYourData); }); subscribe(SeelenEvent.YourDataChanged, (event) => { $data.value = event.payload; }); await $data.init(); ``` ## Svelte: State Encapsulation (Non-Negotiable) Never export `$state` variables directly from a module. Always wrap them in a class with getters and export a single instance of that class. This applies to both raw `$state` and reactive wrappers like `lazyRune`. **Bad:** ```ts export let count = $state(0); export const items = lazyRune(() => invoke(SomeCommand)); ``` **Good:** ```ts const items = lazyRune(() => invoke(SomeCommand)); let count = $state(0); class MyState { get items() { return items; } get count() { return count; } } export const myState = new MyState(); ``` State and reactive values live at module scope; the class only exposes getters (and setters when mutation is needed). ## Svelte: Only Svelte 5+ APIs (Non-Negotiable) All Svelte code in this repo targets **Svelte 5**. Never write Svelte 4 / legacy patterns: | Forbidden (legacy) | Required (Svelte 5+) | | ------------------------------- | ------------------------------------- | | `use:action` for DOM attachment | `{@attach fn}` | | `export let prop` | `let { prop } = $props()` | | `$:` reactive statements | `$derived` / `$effect` | | `createEventDispatcher` | callback props | | `` | `{@render children()}` with `Snippet` | When attaching imperative DOM integrations (e.g. `@dnd-kit/svelte` sortable), always use `{@attach sortable.attach}` directly — never wrap it in a `use:action` helper. ## Creating Svelte Widgets (High-Level) Seelen UI supports standalone Svelte widgets. Prefer following existing widget patterns; do not invent new build plumbing. Typical pieces: 1. Static widget definition: `src/static/widgets//` 2. Svelte app: `src/ui/svelte//` 3. Theme styles: `src/static/themes/default/styles/.scss` 4. i18n: translations (and keep the translation workflow rule) 5. Optional Rust backend integration (use the modern module pattern) Widget checklist: - Static metadata and HTML exist under `src/static/widgets//` - Svelte entry mounts into `#root` and calls `Widget.getCurrent().init(...)` - Shared, event-driven state uses LazySignal - Styling uses existing CSS variables; avoid global class conflicts Shared styling for widgets: - Use `data-skin` attributes for common control styling (buttons, inputs) to avoid class collisions. ## Rust File Layout (Item Order) Rust files must always declare their items in this order (after the `use` imports): 1. Constants (`const` / `static`) 2. Main struct, together with the events it emits (event enum + `event_manager!`) 3. Main struct / events `impl` blocks 4. Sub structs/enums used by the main struct, top-down (a type goes before the types it uses). Every `impl` goes immediately after its type, and only then the next type: `struct A` > `impl A` > `struct B` > `impl B`. 5. Utility (free) functions 6. `#[cfg(test)] mod tests` Reference: `src/background/modules/notifications/wpn_service.rs`. ## Rust Types: Tagged Enums (Serde) Avoid tuple variants for internally tagged enums. Bad: ```rust #[serde(tag = "type")] pub enum Action { WithData(String), } ``` Good: ```rust #[serde(tag = "type")] pub enum Action { WithData { data: String }, } ``` ## Testing Expectations - Prefer quick feedback loops (`cargo check`, `npm run type-check`, `deno lint`). - Keep changes scoped; add tests when behavior changes. - Runtime logs can be read directly (no need to ask the user for them): `%LOCALAPPDATA%\com.seelen.seelen-ui\logs\Seelen UI.log` (service: `SLU Service.log` in the same folder; previous session in `*.log.bak`). ## Documentation & Custom Resources User/developer-facing docs live under `documentation/`. Always check there before asking how something works: - `FEATURES.md` — Full feature reference: every widget, setting, shortcut, and system capability - `design-system.md` — Design manifesto and contract: flat design, tokens, light/dark, `data-skin` components, rules for widget markup and theme styles - `resource-guidelines.md` — Shared concepts: resource IDs, YAML `!include`/`!extend`, i18n, `slu` CLI - `widget-guidelines.md` / `widget-js-api.md` — How to build a custom widget: the resource guideline plus the runtime JS API (`init`/`ready`, `invoke`/`subscribe`) - `theme-guidelines.md` — How to create a theme - `plugin-guidelines.md`, `toolbar-plugins.md`, `dock-plugins.md`, `wm-layouts.md` — Plugins: flat per-widget extension files, with the shared guideline plus per-target-widget schemas (toolbar, dock, window manager layouts) The three external resource types and their required files: | Type | Minimum files | | ------ | ------------------------------------------------------- | | Widget | `metadata.yml`, `i18n/` | | Theme | `metadata.yml`, `styles/.scss` | | Plugin | `metadata.yml`, payload files referenced via `!include` | All resource IDs follow `@username/resource-name`. Dev workflow with the `slu` CLI (Seelen UI must be running): ```bash slu resource load theme ./my-theme # live-load without bundling slu resource load widget ./my-widget slu resource unload theme ./my-theme slu resource bundle theme ./my-theme # produce .yaml for marketplace ``` Built-in resources (used as reference implementations): - `src/static/widgets/` — built-in widget definitions - `src/static/themes/` — built-in themes - `src/static/plugins/` — built-in toolbar plugins