Seelen-UI Agent Skills & AI Rules

GitHub

The Fully Customizable Desktop Environment for Windows 10/11.

RAW Rules
4 Discovered Rules โ€ข ~5,107 Estimated Tokens
Quick:
Loading skill content...

AI Agent Protocol & Instructions

AGENTS.md root-instruction ~4,207 tokens
Raw
# 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