From bd5239fb7ed802518c004052fdc737c5fc3aba97 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Sat, 28 Feb 2026 08:00:19 +0100 Subject: [PATCH] refactor: slim down claude instructions for lean context and fast iteration (#8136) * refactor: slim down claude instructions for lean context and fast iteration Co-Authored-By: Claude Opus 4.6 * fix: add private and license feature flags to enterprise validation docs Co-Authored-By: Claude Opus 4.6 * feat: add /refine skill for end-of-session doc evolution Co-Authored-By: Claude Opus 4.6 * refactor: remove architecture.md overview doc per research findings General codebase overviews distract agents and trigger unnecessary exploration. Keep only operational docs (validation, enterprise). Co-Authored-By: Claude Opus 4.6 * feat: add autonomous mode doc for bypass permission workflows Covers: plan-first requirement, tmux pane usage for checking backend/frontend logs, manual testing via Playwright MCP, Playwright gotchas, and end-of-task summary expectations. Co-Authored-By: Claude Opus 4.6 * feat: add mermaid, playwright, and asciinema tools to autonomous mode doc Claude should use mmdc for diagrams during planning, playwright CLI for screenshots of frontend changes, and asciinema for terminal recordings of CLI changes. All attached to the PR. Co-Authored-By: Claude Opus 4.6 * fix: use pastebin for screenshot/recording uploads Co-Authored-By: Claude Opus 4.6 * fix: address PR review findings - Remove stale docs/architecture.md reference from /refine skill - Fix script name: ./update-sqlx -> ./update_sqlx.sh - Remove .claude/settings.local.json mention from enterprise doc Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .claude/skills/refine/SKILL.md | 39 ++ .claude/skills/rust-backend/SKILL.md | 486 +++--------------------- .claude/skills/svelte-frontend/SKILL.md | 324 +++------------- CLAUDE.md | 80 ++-- backend/CLAUDE.md | 104 +---- docs/autonomous-mode.md | 94 +++++ docs/enterprise.md | 38 ++ docs/validation.md | 55 +++ frontend/CLAUDE.md | 202 +--------- 9 files changed, 366 insertions(+), 1056 deletions(-) create mode 100644 .claude/skills/refine/SKILL.md create mode 100644 docs/autonomous-mode.md create mode 100644 docs/enterprise.md create mode 100644 docs/validation.md diff --git a/.claude/skills/refine/SKILL.md b/.claude/skills/refine/SKILL.md new file mode 100644 index 0000000000..aaf747cd29 --- /dev/null +++ b/.claude/skills/refine/SKILL.md @@ -0,0 +1,39 @@ +--- +name: refine +user_invocable: true +description: End-of-session reflection. Reviews friction encountered during the session and proposes updates to docs/ to capture lessons learned. +--- + +# Refine Skill + +Reflect on the current session and update documentation with lessons learned. + +## Instructions + +1. **Identify friction**: Review what happened in this session: + - Run `git diff main...HEAD --stat` to see what files were touched + - Think about: what was slow, what failed, what required multiple attempts, what information was missing or hard to find + +2. **Read current docs**: Read the docs that were relevant to this session: + - `docs/validation.md` + - `docs/enterprise.md` + - `docs/autonomous-mode.md` + - Any skills that were invoked + +3. **Propose updates**: For each piece of friction, decide if it warrants a doc update: + - **Missing knowledge**: Information you had to discover that should be documented + - **Wrong guidance**: Instructions that led you astray + - **Missing validation rule**: A check that should be in the validation matrix + - **New pattern**: A codebase pattern worth capturing for next time + +4. **Apply updates**: Edit the relevant `docs/` files. Keep changes minimal and specific — add only what would have saved time this session. + +5. **Report**: Summarize what was added/changed and why. + +## Rules + +- Only add knowledge confirmed by this session — no speculative additions +- Keep docs concise — add a line or two, not a paragraph +- If a whole new doc is needed, create it in `docs/` and add a pointer in `CLAUDE.md` +- Don't update skills unless a coding pattern was genuinely wrong +- Don't add things Claude already knows — only Windmill-specific knowledge diff --git a/.claude/skills/rust-backend/SKILL.md b/.claude/skills/rust-backend/SKILL.md index c2253319a6..f0c52002bc 100644 --- a/.claude/skills/rust-backend/SKILL.md +++ b/.claude/skills/rust-backend/SKILL.md @@ -3,493 +3,105 @@ name: rust-backend description: Rust coding guidelines for the Windmill backend. MUST use when writing or modifying Rust code in the backend directory. --- -# Rust Backend Coding Guidelines +# Windmill Rust Patterns -Apply these patterns when writing or modifying Rust code in the `backend/` directory. - -## Data Structure Design - -Choose between `struct`, `enum`, or `newtype` based on domain needs: - -- Use `enum` for state machines instead of boolean flags or loosely related fields -- Model invariants explicitly using types (e.g., `NonZeroU32`, `Duration`, custom enums) -- Consider ownership of each field: - - Use `&str` vs `String`, slices vs vectors - - Use `Arc` when sharing across threads - - Use `Cow<'a, T>` for flexible ownership - -```rust -// State machine with enum -enum JobState { - Pending { scheduled_for: DateTime }, - Running { started_at: DateTime, worker: String }, - Completed { result: JobResult, duration_ms: i64 }, - Failed { error: String, retries: u32 }, -} - -// Avoid multiple booleans -struct Job { - is_pending: bool, // Don't do this - is_running: bool, - is_completed: bool, -} -``` - -## Impl Block Organization - -Place `impl` blocks immediately below the struct/enum they modify. Group methods logically: - -```rust -struct JobQueue { - jobs: Vec, - capacity: usize, -} - -impl JobQueue { - // Constructors first - pub fn new(capacity: usize) -> Self { ... } - pub fn with_jobs(jobs: Vec) -> Self { ... } - - // Getters - pub fn len(&self) -> usize { ... } - pub fn is_empty(&self) -> bool { ... } - - // Mutation methods - pub fn push(&mut self, job: Job) -> Result<()> { ... } - pub fn pop(&mut self) -> Option { ... } - - // Domain logic - pub fn next_scheduled(&self) -> Option<&Job> { ... } -} -``` - -## Iterator Chains Over For-Loops - -Prefer functional iterator chains (`.filter().map().collect()`) over imperative for-loops: - -```rust -// Preferred -let results: Vec<_> = items - .iter() - .filter(|item| item.is_valid()) - .map(|item| item.transform()) - .collect(); - -// Avoid -let mut results = Vec::new(); -for item in items.iter() { - if item.is_valid() { - results.push(item.transform()); - } -} -``` +Apply these Windmill-specific patterns when writing Rust code in `backend/`. ## Error Handling -Use the `Error` type from `windmill_common::error`. Return `Result` or `JsonResult` for fallible functions: +Use `Error` from `windmill_common::error`. Return `Result` or `JsonResult`: ```rust use windmill_common::error::{Error, Result}; -// Use ? operator for propagation pub async fn get_job(db: &DB, id: Uuid) -> Result { - let job = sqlx::query_as!(Job, "SELECT ... WHERE id = $1", id) + sqlx::query_as!(Job, "SELECT id, workspace_id FROM v2_job WHERE id = $1", id) .fetch_optional(db) .await? .ok_or_else(|| Error::NotFound("job not found".to_string()))?; - Ok(job) } ``` -Prefer `if let` for optional handling. Use `let...else` when early return makes code clearer: +Never panic in library code. Reserve `.unwrap()` for compile-time guarantees. + +## SQLx Patterns + +**Never use `SELECT *`** — always list columns explicitly. Critical for backwards compatibility when workers lag behind API version: ```rust -let Some(config) = get_config() else { - return Err(Error::MissingConfig); -}; +// Correct +sqlx::query_as!(Job, "SELECT id, workspace_id, path FROM v2_job WHERE id = $1", id) + +// Wrong — breaks when columns are added +sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", id) ``` -Never panic in library code. Reserve `.unwrap()` for cases with compile-time guarantees. Keep functions short to help lifetime inference and clarity. - -## Early Returns - -Return early to avoid deep nesting. Handle error cases and edge conditions first: +Use batch operations to avoid N+1: ```rust -// Preferred - early returns -fn process_job(job: Option) -> Result { - let Some(job) = job else { - return Ok(Output::default()); - }; - - if !job.is_valid() { - return Err(Error::InvalidJob); - } - - if job.is_cached() { - return Ok(job.cached_result()); - } - - // Main logic at the end, not nested - execute_job(job) -} - -// Avoid - deep nesting -fn process_job(job: Option) -> Result { - if let Some(job) = job { - if job.is_valid() { - if !job.is_cached() { - execute_job(job) - } else { - Ok(job.cached_result()) - } - } else { - Err(Error::InvalidJob) - } - } else { - Ok(Output::default()) - } -} +// Preferred — single query with IN clause +sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await? ``` -## Variable Shadowing - -Shadow variables instead of creating new names with prefixes: - -```rust -// Preferred -let data = fetch_raw_data(); -let data = parse(data); -let data = validate(data)?; - -// Avoid -let raw_data = fetch_raw_data(); -let parsed_data = parse(raw_data); -let validated_data = validate(parsed_data)?; -``` - -## Minimal Comments - -- No inline comments explaining obvious code -- No TODO/FIXME comments in committed code -- Doc comments (`///`) only on public items -- Let code be self-documenting through clear naming - -## Type Safety - -Use enums over boolean flags for clarity: - -```rust -// Preferred -enum JobStatus { - Pending, - Running, - Completed, -} - -// Avoid -struct Job { - is_running: bool, - is_completed: bool, -} -``` - -## Pattern Matching - -Prefer explicit matching. Use wildcards strategically for fallback cases or ignored fields: - -```rust -// Explicit matching preferred -match status { - JobStatus::Pending => handle_pending(), - JobStatus::Running => handle_running(), - JobStatus::Completed => handle_completed(), -} - -// Wildcards OK for fallback -match result { - Ok(value) => process(value), - Err(_) => return default_value(), -} - -// Wildcards OK for ignoring fields in destructuring -let Point { x, y, .. } = point; -``` - -## Destructuring in Function Signatures - -Destructure structs directly in function parameters: - -```rust -// Preferred -async fn process_job( - Extension(db): Extension, - Path((workspace, job_id)): Path<(String, Uuid)>, - Query(pagination): Query, -) -> Result> { - // ... -} - -// Avoid -async fn process_job( - db_ext: Extension, - path: Path<(String, Uuid)>, - query: Query, -) -> Result> { - let Extension(db) = db_ext; - let Path((workspace, job_id)) = path; - // ... -} -``` - -## Trait Implementations - -Use standard trait implementations to simplify conversions and reduce boilerplate: - -```rust -// Implement From/Into for type conversions -impl From for ApiJob { - fn from(db: DbJob) -> Self { - ApiJob { - id: db.id, - status: db.status.into(), - } - } -} - -// Use TryFrom for fallible conversions -impl TryFrom for JobKind { - type Error = Error; - fn try_from(s: String) -> Result { ... } -} -``` - -Apply `derive` macros to reduce boilerplate: - -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Job { ... } -``` - -## Module Structure - -- Use `pub(crate)` instead of `pub` when possible; expose only what needs exposing -- Keep APIs small and expressive; avoid leaking internal types -- Organize code into modules reflecting ownership and domain boundaries - -```rust -// Prefer restricted visibility -pub(crate) fn internal_helper() { ... } - -// Only pub for external API -pub fn create_job(...) -> Result { ... } -``` - -## Code Navigation - -Always use rust-analyzer LSP for: -- Go to definition -- Find references -- Type information -- Import resolution - -Do not guess at module paths or type definitions. +Use transactions for multi-step operations. Parameterize all queries. ## JSON Handling -Prefer `Box` over `serde_json::Value` when: -- Storing JSON in the database (JSONB columns) -- Passing JSON through without modification -- The JSON structure doesn't need inspection +Prefer `Box` over `serde_json::Value` when storing/passing JSON without inspection: ```rust -// Preferred - avoids parsing/serialization overhead pub struct Job { - pub id: Uuid, pub args: Option>, } - -// Only use Value when you need to inspect/modify JSON -let value: serde_json::Value = serde_json::from_str(&json)?; -if let Some(field) = value.get("field") { - // modify or inspect -} ``` -## Serde Optimizations +Only use `serde_json::Value` when you need to inspect or modify the JSON. -Use serde attributes to optimize serialization: +## Serde Optimizations ```rust #[derive(Serialize, Deserialize)] pub struct Job { - #[serde(rename = "jobId")] - pub id: Uuid, - - #[serde(default)] - pub priority: i32, - #[serde(skip_serializing_if = "Option::is_none")] pub parent_job: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] pub tags: Vec, + #[serde(default)] + pub priority: i32, } ``` -Prefer borrowing for zero-copy deserialization when lifetimes allow: +## Async & Concurrency + +Never block the async runtime. Use `spawn_blocking` for CPU-intensive work: ```rust -#[derive(Deserialize)] -pub struct JobInput<'a> { - #[serde(borrow)] - pub workspace_id: Cow<'a, str>, - - #[serde(borrow)] - pub script_path: &'a str, -} +let result = tokio::task::spawn_blocking(move || expensive_computation(&data)).await?; ``` -## SQLx Patterns +**Mutex selection**: Prefer `std::sync::Mutex` (or `parking_lot::Mutex`) for data protection. Only use `tokio::sync::Mutex` when holding locks across `.await` points. -**Never use `SELECT *`** - always list columns explicitly. This is critical for backwards compatibility when workers run behind the API server version: +Use `tokio::sync::mpsc` (bounded) for channels. Avoid `std::thread::sleep` in async contexts. + +## Module Structure & Visibility + +- Use `pub(crate)` instead of `pub` when possible +- Place new code in the appropriate crate based on functionality +- API endpoints go in `windmill-api/src/` organized by domain +- Shared functionality goes in `windmill-common/src/` + +## Code Navigation + +Always use rust-analyzer LSP for go-to-definition, find-references, and type info. Do not guess at module paths. + +## Axum Handlers + +Destructure extractors directly in function signatures: ```rust -// Preferred - explicit columns -sqlx::query_as!( - Job, - "SELECT id, workspace_id, path, created_at FROM v2_job WHERE id = $1", - job_id -) - -// Avoid - breaks when columns are added -sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", job_id) +async fn process_job( + Extension(db): Extension, + Path((workspace, job_id)): Path<(String, Uuid)>, + Query(pagination): Query, +) -> Result> { ... } ``` - -Use batch operations to minimize round trips: - -```rust -// Preferred - single query with multiple values -sqlx::query!( - "INSERT INTO job_logs (job_id, logs) VALUES ($1, $2), ($3, $4)", - id1, log1, id2, log2 -) - -// Avoid N+1 queries -for id in ids { - sqlx::query!("SELECT ... WHERE id = $1", id).fetch_one(db).await?; -} - -// Preferred - single query with IN clause -sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await? -``` - -Use transactions for multi-step operations and parameterize all queries. - -## Async & Tokio Patterns - -Never block the async runtime. Use `spawn_blocking` for CPU-intensive or blocking I/O: - -```rust -// Preferred - offload blocking work -let result = tokio::task::spawn_blocking(move || { - expensive_computation(&data) -}).await?; - -// Avoid - blocks the runtime -let result = expensive_computation(&data); // Don't do this in async -``` - -Use tokio primitives for sleep and channels: - -```rust -use tokio::sync::mpsc; -use tokio::time::sleep; - -// Avoid in async contexts -use std::thread::sleep; // Blocks the runtime -``` - -Use bounded channels for backpressure: - -```rust -// Preferred - bounded channel prevents overwhelming -let (tx, rx) = tokio::sync::mpsc::channel(100); - -// Be careful with unbounded -let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); -``` - -## Mutex Selection in Async Code - -**Prefer `std::sync::Mutex` (or `parking_lot::Mutex`) over `tokio::sync::Mutex`** for protecting data in async code. The async mutex is more expensive and only needed when holding locks across `.await` points. - -```rust -// Preferred for data protection - std mutex is faster -use std::sync::Mutex; - -struct Cache { - data: Mutex>, -} - -impl Cache { - fn get(&self, key: &str) -> Option { - self.data.lock().unwrap().get(key).cloned() - } - - fn insert(&self, key: String, value: Value) { - self.data.lock().unwrap().insert(key, value); - } -} -``` - -**Use `tokio::sync::Mutex` only when you must hold the lock across `.await` points**, typically for IO resources like database connections: - -```rust -use tokio::sync::Mutex; -use std::sync::Arc; - -// Async mutex for IO resources held across await points -let conn = Arc::new(Mutex::new(db_connection)); - -async fn execute_query(conn: Arc>, query: &str) { - let mut lock = conn.lock().await; - lock.execute(query).await; // Lock held across .await -} -``` - -**Common pattern**: Wrap `Arc>` in a struct with non-async methods that lock internally, keeping lock scope minimal: - -```rust -struct SharedState { - inner: std::sync::Mutex, -} - -impl SharedState { - fn update(&self, value: i32) { - self.inner.lock().unwrap().value = value; - } - - fn get(&self) -> i32 { - self.inner.lock().unwrap().value - } -} -``` - -**Alternative for IO resources**: Spawn a dedicated task to manage the resource and communicate via message passing: - -```rust -let (tx, mut rx) = tokio::sync::mpsc::channel(32); - -tokio::spawn(async move { - while let Some(cmd) = rx.recv().await { - handle_io_command(&mut resource, cmd).await; - } -}); -``` - -## Build & Tooling - -Build speed tips: -- Use `cargo check` during rapid iteration over `cargo build` -- Minimize unnecessary dependencies and feature flags \ No newline at end of file diff --git a/.claude/skills/svelte-frontend/SKILL.md b/.claude/skills/svelte-frontend/SKILL.md index f51d36672a..57cac70302 100644 --- a/.claude/skills/svelte-frontend/SKILL.md +++ b/.claude/skills/svelte-frontend/SKILL.md @@ -3,316 +3,78 @@ name: svelte-frontend description: Svelte coding guidelines for the Windmill frontend. MUST use when writing or modifying code in the frontend directory. --- -# Svelte 5 Best Practices +# Windmill Svelte Patterns -This guide outlines best practices for developing with Svelte 5, incorporating the new Runes API and other modern Svelte features. These rules MUST NOT be applied on svelte 4 files unless explicitly asked to do so. +Apply these Windmill-specific patterns when writing Svelte code in `frontend/`. For general Svelte 5 syntax (runes, snippets, event handling), use the Svelte MCP server. -## Reactivity with Runes +## Windmill UI Components (MUST use) -Svelte 5 introduces Runes for more explicit and flexible reactivity. +Always use Windmill's design-system components. Never use raw HTML elements. -1. **Embrace Runes for State Management**: - * Use `$state` for reactive local component state. - ```svelte - - - - ``` - * Use `$derived` for computed values based on other reactive state. - ```svelte - - -

{count} * 2 = {doubled}

- ``` - * Use `$effect` for side effects that need to run when reactive values change (e.g., logging, manual DOM manipulation, data fetching). Remember `$effect` does not run on the server. - ```svelte - - ``` - -2. **Props with `$props`**: - * Declare component props using `$props()`. This offers better clarity and flexibility compared to `export let`. - ```svelte - - -

Name: {name}

-

Age: {age}

- ``` - * For bindable props, use `$bindable`. - ```svelte - - - - ``` - -## Event Handling - -* **Use direct event attributes**: Svelte 5 moves away from `on:` directives for DOM events. - * **Do**: `` - * **Don't**: `` -* **For component events, prefer callback props**: Instead of `createEventDispatcher`, pass functions as props. - ```svelte - - - -

Message from child: {message}

- - - - - ``` - -## Snippets for Content Projection - -* **Use `{#snippet ...}` and `{@render ...}` instead of slots**: Snippets are more powerful and flexible. - ```svelte - - - - - {#snippet title()} - My Awesome Title - {/snippet} - {#snippet content()} -

Some interesting content here.

- {/snippet} -
- - - - -
-
{@render title()}
-
{@render content()}
-
- ``` -* Default content is passed via the `children` prop (which is a snippet). - ```svelte - - -
- {@render children?.()} -
- ``` - -## Component Design - -1. **Create Small, Reusable Components**: Break down complex UIs into smaller, focused components. Each component should have a single responsibility. This also aids performance by limiting the scope of reactivity updates. -2. **Descriptive Naming**: Use clear and descriptive names for variables, functions, and components. -3. **Minimize Logic in Components**: Move complex business logic to utility functions or services. Keep components focused on presentation and interaction. - -## State Management (Stores) - -1. **Segment Stores**: Avoid a single global store. Create multiple stores, each responsible for a specific piece of global state (e.g., `userStore.js`, `themeStore.js`). This can help limit reactivity updates to only the parts of the UI that depend on specific state segments. -2. **Use Custom Stores for Complex Logic**: For stores with related methods, create custom stores. - ```javascript - // counterStore.js - import { writable } from 'svelte/store'; - - function createCounter() { - const { subscribe, set, update } = writable(0); - - return { - subscribe, - increment: () => update(n => n + 1), - decrement: () => update(n => n - 1), - reset: () => set(0) - }; - } - export const counter = createCounter(); - ``` -3. **Use Context API for Localized State**: For state shared within a component subtree, consider Svelte's context API (`setContext`, `getContext`) instead of global stores when the state doesn't need to be truly global. - -## Performance Optimizations (Svelte 5) - -When generating Svelte 5 code, prioritize frontend performance by applying the following principles: - -### General Svelte 5 Principles - -- **Leverage the Compiler:** Trust Svelte's compiler to generate optimized JavaScript. Avoid manual DOM manipulation (`document.querySelector`, etc.) unless absolutely necessary for integrating third-party libraries that lack Svelte adapters. -- **Keep Components Small and Focused:** Reinforcing from Component Design, smaller components lead to less complex reactivity graphs and more targeted, efficient updates. - -### Reactivity & State Management - -- **Optimize Computations with `$derived`:** Always use `$derived` for computed values that depend on other state. This ensures the computation only runs when its specific dependencies change, avoiding unnecessary work compared to recomputing derived values in `$effect` or less efficient methods. -- **Minimize `$effect` Usage:** Use `$effect` sparingly and only for true side effects that interact with the outside world or non-Svelte state. Avoid putting complex logic or state updates *within* an `$effect` unless those updates are explicitly intended as a reaction to external changes or non-Svelte state. Excessive or complex effects can impact rendering performance. -- **Structure State for Fine-Grained Updates:** Design your `$state` objects or variables such that updates affect only the necessary parts of the UI. Avoid putting too much unrelated state into a single large object that gets frequently updated, as this can potentially trigger broader updates than necessary. Consider normalizing complex, nested state. - -### List Rendering (`{#each}`) - -- **Mandate `key` Attribute:** Always use a `key` attribute (`{#each items as item (item.id)}`) that refers to a unique, stable identifier for each item in a list. This is critical for allowing Svelte to efficiently update, reorder, add, or remove list items without destroying and re-creating unnecessary DOM elements and component instances. - -### Component Loading & Bundling - -- **Implement Lazy Loading/Code Splitting:** For routes, components, or modules that are not immediately needed on page load, use dynamic imports (`import(...)`) to split the code bundle. SvelteKit handles this automatically for routes, but it can be applied manually to components using helper patterns if needed. -- **Be Mindful of Third-Party Libraries:** When incorporating external libraries, import only the necessary functions or components to minimize the final bundle size. Prefer libraries designed to be tree-shakeable. - -### Rendering & DOM - -- **Use CSS for Animations/Transitions:** Prefer CSS animations or transitions where possible for performance. Svelte's built-in `transition:` directive is also highly optimized and should be used for complex state-driven transitions, but simple cases can often use plain CSS. -- **Optimize Image Loading:** Implement best practices for images: use optimized formats (WebP, AVIF), lazy loading (`loading="lazy"`), and responsive images (``, `srcset`) to avoid loading unnecessarily large images. - -### Server-Side Rendering (SSR) & Hydration - -- **Ensure SSR Compatibility:** Write components that can be rendered on the server for faster initial page loads. Avoid relying on browser-specific APIs (like `window` or `document`) in the main ` - -
- Hello -
- ``` -5. **Stay Updated**: Keep Svelte and its related packages up to date to benefit from the latest features, performance improvements, and security fixes. - -## Windmill UI Component Rules (MUST follow) - -Always use Windmill's own design-system components instead of raw HTML elements. Using raw HTML elements produces inconsistent styling and breaks the design language. - -### Icons — use `lucide-svelte` - -**Never** write inline SVGs. Import icons from `lucide-svelte`. - -```svelte - - - -``` - -### Buttons — use ` - - -