The Fully Customizable Desktop Environment for Windows 10/11.
# 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/<module>/
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<Vec<YourType>> {
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<YourManager> = 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<YourType> {
// 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<YourType>,
}
```
Add modifiers before the fn name as needed (order doesn't matter, all are optional):
- `@fallible()` โ the handler returns `crate::Result<T>` (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<T>`).
- `@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<YourType>) 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<Self> {
let token = object.SomeEvent(&TypedEventHandler::new(Self::on_event))?;
Ok(Self { object, token })
}
fn on_event(
_sender: &Option<SomeWinRtObject>,
_args: &Option<SomeArgs>,
) -> 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<Item[]>(() => 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 |
| `<slot>` | `{@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/<widget-name>/`
2. Svelte app: `src/ui/svelte/<widget-name>/`
3. Theme styles: `src/static/themes/default/styles/<widget-name>.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/<widget-name>/`
- 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/<widget-id>.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