RuleStack

Configs

Stacks

Compare

Diff

RuleStack

Configs

Stacks

Compare

Diff

Read API

RuleStack

Configs

Stacks

Compare

Diff

Read API

Configs/kestra-io/kestra/diff

Two files, one repository

kestra-io/kestra ships 1 format across 2 indexed files. The question worth asking is whether the second one says anything the first does not.

A · AGENTS.md · 2837 wordsB · ui/AGENTS.md · 4095 words
What each file covers, counted
DimensionSharedOnly in AOnly in BOverlap
Sections060290%
Commands03300%
Section tags76344%

What each file covers

Sections

0 shared · 60 only in A · 29 only in B
  • − Coding Agent Guidelines for Kestra Open Source Edition
  • − Project
  • − Tech Stack
  • − Critical Code Patterns
  • − Dependency Injection
  • − Class Structure
  • − Annotations
  • − Error Handling
  • − Java Language Features
  • − Naming Conventions
  • − File Organization
  • − Utility Classes
  • − Enums
  • − Documentation
  • − Webserver Constraints
  • − Worker Constraints
  • − Executor Constraints
  • − Testing Guidelines
  • − Java Tests
  • − Frontend Tests
  • − UI Design System
  • − Frontend Code Style (Vue 3)
  • − Build Commands
  • − Java Backend
  • − Clean build
  • − Full build (includes tests)
  • − Build without tests (faster)
  • − Test Commands
  • − Run all tests (excludes flaky tests)
  • − Run only unit tests (fastest)
  • − Run integration tests
  • − Run flaky tests (separate from build)
  • − Run tests for specific module
  • − Run single test class
  • − Run single test method
  • − After running tests: generate a markdown summary of failures only
  • − Frontend (UI)
  • − Install dependencies
  • − Development server
  • − Type checking
  • − Build for production
  • − Run tests
  • − Linting
  • − Storybook
  • − Development Workflow
  • − Running Locally
  • − Start databases with Docker Compose
  • − Stop databases with Docker Compose
  • − Worktree setup
  • − Security Considerations
  • − Performance Best Practices
  • − Troubleshooting
  • − Module Structure
  • − Pull request guidelines
  • − Issue guidelines
  • − UI Translations
  • − Checking for missing translations
  • − Editing English strings
  • − Adding or regenerating translations
  • − Conflicts in `fingerprints.json`
  • + UI Design System Guidelines
  • + What this is, in plain terms
  • + Golden rules (non-negotiable)
  • + Best practices for keeping the design system healthy
  • + Before you write code
  • + While you write code
  • + When extending the design system
  • + When reviewing a UI PR
  • + Accessibility
  • + Internationalization
  • + Loading, empty, and error states
  • + Data tables & pagination state
  • + The deep-watch / computed-spread trap
  • + Unsaved input in modals (discard guard)
  • + Icons
  • + Performance
  • + Testing UI
  • + Deprecation contract
  • + Anti-patterns (do not write these)
  • + Components
  • + Basic / Layout
  • + Feedback
  • + Form
  • + Data Display
  • + Charts
  • + Navigation
  • + Utilities (import from the design system)
  • + Composables
  • + Design tokens

Commands

0 shared · 33 only in A · 0 only in B
  • − ./gradlew clean
  • − ./gradlew build
  • − ./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel
  • − ./gradlew test
  • − ./gradlew unitTest
  • − ./gradlew integrationTest
  • − ./gradlew flakyTest
  • − ./gradlew :core:test
  • − ./gradlew :module-name:test --tests "ClassName"
  • − ./gradlew :module-name:test --tests "ClassName.methodName"
  • − npx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)
  • − npm install
  • − npm run dev
  • − npm run check:types
  • − npm run build
  • − npm run test:all
  • − npm run test:unit
  • − npm run test:storybook
  • − npm run test:e2e
  • − npm run lint
  • − npm run test:lint
  • − npm run storybook
  • − npm run build-storybook
  • − docker compose -f docker-compose-ci.yml up
  • − docker compose -f docker-compose-ci.yml down
  • − git checkout --ours ui/src/translations/*.json ui/scripts/translations/fingerprints*.json
  • − npm run translations:check
  • − gh issue create --title …
  • − gh issue edit <number> --type Bug
  • − gh issue edit <number> --type Task|Feature|Epic
  • − gh api /orgs/kestra-io/issue-types
  • − npm run translations:generate
  • − task

Section tags

7 shared · 6 only in A · 3 only in B
  • − setup
  • − build
  • − types
  • − security
  • − dependencies
  • − agent-behaviour
  • + lint-format
  • + api
  • + do-not
  •   test
  •   code-style
  •   architecture
  •   git-pr
  •   ui
  •   performance
  •   docs

Line diff

+318 added−341 removed100 unchanged22.7% identical
kestra-io/kestra · AGENTS.md
@@ −1 @@
1# Coding Agent Guidelines for Kestra Open Source Edition
2 
3This document provides essential information for AI coding agents working on the Kestra codebase.
4 
5**IMPORTANT — READ FIRST**
6 
7- **Act as a Senior Software Engineer and Software Architect.** Approach software development with:
8 - **Pragmatism**: Favor simple solutions over clever ones
9 - **Skepticism**: Question decisions that could cause technical debt or scalability issues
10 - **Efficiency**: Only challenge when it genuinely matters
11- **Think before coding**: explicitly state assumptions, compare alternatives, and justify choices.
12- **Simplicity first (KISS)**: overengineering and "gas factories" are strictly forbidden.
13- **Surgical changes only**: touch **only** what is strictly necessary to achieve the goal.
14- **Goal-driven execution**: define what success looks like *before* writing the first line of code.
15- **Preserve existing comments**: never delete any existing comment **unless** you are improving its clarity or usefulness.
16- **Keep comments short and only where they earn their place**: a comment you write should be **one sentence**, or two at most when the *why* genuinely needs it (a non-obvious constraint, a workaround, a subtle ordering or concurrency requirement). Do **not** comment obvious code — no restating what the next line plainly says (`// increment the counter`), no narrating a self-explanatory getter, loop, or well-named call. If the code is readable, the comment is noise; if it isn't, prefer making the code clearer over explaining it.
17- **Write clear, maintainable, and well-documented code**
18- **Build & test are mandatory**
19 
20## Project
21 
22Monorepo built with Java (backend) and Vue (frontend), using Gradle as the build system.
 
 
23 
24## Tech Stack
25- **Backend:** Java 25, Micronaut Framework, Lombok
26- **Frontend:** Vue 3, TypeScript, Vite, Element Plus, Pinia
27- **Build:** Gradle 8.x with multi-project structure (77 submodules)
28- **Testing:** JUnit 5, Mockito, AssertJ, Vitest, Playwright
29 
30## Critical Code Patterns
31 
32### Dependency Injection
33 
34**DO**: Use constructor injection with final fields.
35 
36```java
37@Singleton
38public class MyService {
39 private final SomeDependency dependency;
40 
41 @Inject
42 public MyService(SomeDependency dependency) {
43 this.dependency = Objects.requireNonNull(dependency);
44 }
45}
46```
 
 
 
 
47 
48**DON'T**: Use field injection (`@Inject` on fields directly). Always prefer constructor injection.
49 
50### Class Structure
51 
52```java
53// 1. Package declaration and imports
54// 2. Class-level annotations (@Slf4j, @Singleton, etc.)
55// 3. Class declaration with Javadoc
56// 4. Static constants (UPPER_SNAKE_CASE)
57// 5. Injected fields (@Inject)
58// 6. Constructors
59// 7. Public methods
60// 8. Protected methods
61// 9. Private methods
62// 10. Inner classes/records
63```
64 
65### Annotations
66- **Micronaut:** `@Singleton`, `@Inject`, `@Controller`, `@Replaces`, `@Requires`
67- **Validation:** `@Valid`, `@NotNull`, `@Nullable`
68- **Lombok:** `@Slf4j`, `@Getter`, `@NoArgsConstructor`, `@AllArgsConstructor`
69- Use `@Builder` for complex object creation
70 
71### Error Handling
72 
73**DO**:
74- Use specific exception types — extend `KestraException` or `KestraRuntimeException`
75- Use `Optional<T>` for potentially absent returned values
76- Return empty collections (e.g., `List.of()`, `Collections.emptyList()`) for absent values
77- Use try-with-resources for resource management
78- Log errors before re-throwing: `log.error("message", exception)`
79- Write exception messages as plain, complete sentences that state the fact and the actionable detail — build them with `String.formatted()`/`String.format()`, not string concatenation or em dashes, e.g. `"Cannot acquire lock on asset '%s': already locked by '%s' until %s.".formatted(id, owner, until)`
80 
81**DON'T**: Use generic `Exception`. Don't return null for collections. Don't write terse or telegraphic exception messages (e.g. dropping articles/verbs) or string-concatenate message parts.
82 
83### Java Language Features
84- Use java records for simple data carriers
 
 
 
85 
86### Naming Conventions
87- Follow Java naming-convention best practices for Classes, Methods, Variables, Constants.
88- Boolean methods: Start with `is`, `has`, `should`, `can` (e.g., `isReadOnly()`).
89 
90### File Organization
91- Use 4-space indentation (configured in .editorconfig)
92- UTF-8 encoding with LF line endings
93- No trailing whitespace
94 
95### Utility Classes
96* Mark utility classes as `final` with a private constructor
97* Use static methods only
98* Use existing utility classes (e.g., `ListUtils`, `MapUtils`) instead of creating new ones (`io.kestra.core.utils.*`)
 
 
 
 
 
 
99 
100**MANDATORY — never hand-roll Pebble delimiter detection.** Pebble has two block delimiter pairs — print blocks (`{{ ... }}`) and execute/statement blocks (`{% ... %}`) — and code that only checks for `{{`/`}}` silently misses `{%`/`%}` blocks. Use `io.kestra.core.utils.PebbleUtil` (`containsOpeningBlockDelimiter`, `startsWithOpeningBlockDelimiter`, `endsWithClosingBlockDelimiter`, `openingBlockDelimiters()`/`closingBlockDelimiters()`) instead of writing a new delimiter regex or literal — it derives the delimiter pairs from Pebble's own `Syntax.Builder` defaults, so it never drifts from what Pebble actually parses.
101 
102### Enums
103- Use enums for fixed sets of constants, including internal fields not exposed over the API — prefer a typed enum over a raw `String`/`int` whenever the value is drawn from a closed set of known cases, even if the set may only ever have a couple of members
104- Use `@JsonValue` for custom serialization if needed
105- Use `UNKNOWN` enum value for unknown cases in deserialization
106- Compare Constants From The Left (a.k.a., Yoda conditions)
107- Use a static `fromString` method for case-insensitive lookups using `Enums` class.
108 
109e.g.:
110```java
111public enum MyEnum {
112 VALUE_ONE,
113 VALUE_TWO,
114 UNKNOWN;
115 
116 @JsonCreator
117 public static ResourceType fromString(final String value) {
118 return Enums.getForNameIgnoreCase(value, MyEnum.class, UNKNOWN);
119 }
120}
121```
122 
123### Documentation
124- Javadoc for all public classes and methods - be concise
125- Use `@param`, `@return`, `@throws` appropriately
126- Use `{@inheritDoc}` for inherited methods
127- Include usage examples for complex methods
128 
129## Webserver Constraints
130- Put classes used by only controllers in the webserver module (not core)
131- No business code/rule inside controllers - instead use a Service class
132- All APIs must return a valid JSON object
133- APIs should not return a response being a JSON array which cannot be evolved in a backwards-compatible way
134- Unit tests must assert that a user can only access a given API if authorized to do so, and that access is denied otherwise
135- APIs must be documented with OpenAPI annotations
136- Use DTOs for requests/responses
137- Always validate input parameters with `@Valid`
138- Use `@ExecuteOn(TaskExecutors.IO)` for blocking operations
139- Return meaningful error responses in controllers
140 
141## Worker Constraints
142- Never depend on repositories for code called by the workers - instead use MetaStore/StateStore facades
 
 
143 
144## Executor Constraints
145- Run the `H2RunnerTest` whenever you update part of the executor
146 
147## Testing Guidelines
148 
149### Java Tests
150 
151**DO**:
152- Place tests in same package structure as source code
153- Simple unit test with mocks over complex integration tests when possible
154- Add // Given-When-Then comments for clarity
155- Test method naming: `should<ExpectedBehavior>When<ConditionOrAction>` (also `...Given<Input>`, `...For<Condition>`, `...If<Condition>`), e.g. `shouldThrowExceptionWhenDividingByZero()`
156- Use `@MicronautTest` for tests that require Micronaut beans
157- Use `@KestraTest` for tests that require running Kestra services (e.g., Executor, Scheduler)
158-
159```java
160@KestraTest
161class ServiceTest {
162 @Inject
163 private ServiceClass service;
164 
165 @Test
166 void shouldPerformActionWhenCondition() {
167 // Given (setup)
168 
169 // When (action)
 
 
 
 
 
 
 
170 
171 // Then (assertions)
172 assertThat(result).isNotNull();
173 }
174}
175```
176 
177**DON'T**: Use Nested classes for test organization. Avoid complex test hierarchies.
178 
179**Assertions:**
180- Use AssertJ: `assertThat().isEqualTo()`, `assertThat().isNotNull()`, `assertThatThrownBy()`, `assertThatObject()`
181- Prefer descriptive assertion methods
182- Use `@MockBean` for mocking dependencies
 
 
 
183 
184**Test Categories:**
185- Unit tests: Fast, isolated, no external dependencies
186- Integration tests: Test component interaction, use `@Tag("integration")`
187- Flaky tests: Use `@Tag("flaky")` for unreliable tests
 
188 
189### Frontend Tests
190- Unit tests with Vitest and `@vue/test-utils`
191- E2E tests with Playwright
192- Storybook component tests
193- Use JSdom environment for DOM testing
194- **Prefer Storybook component tests over Vitest unit tests whenever possible** — components render through their real story setup (props, slots, design-system deps) instead of being stubbed out, catching regressions unit mocks miss. Fall back to a Vitest unit test only when the logic under test isn't component-rendering behavior (e.g. a pure helper/composable) or no story exists and adding one isn't practical.
195 
196## UI Design System
197 
198The full UI design-system rules, component catalogue, token reference, and frontend best practices live in [ui/AGENTS.md](ui/AGENTS.md). That file is auto-loaded by AI coding agents whenever work happens under `ui/` in OSS or `ui-ee/` in Enterprise edition, and should be consulted (and kept up to date) for any frontend change.
199 
200@ui/AGENTS.md
201 
202## Frontend Code Style (Vue 3)
203 
204**File Organization:**
205- Use 2-space indentation for Vue, JSON, YAML, CSS
206- Use 4-space indentation for JavaScript/TypeScript
207- Follow Vue 3 Composition API patterns
208- Organize imports: Vue/framework → third-party → local modules
 
 
 
 
209 
210**Naming Conventions:**
211- Components: `PascalCase` files (e.g., `MyComponent.vue`)
212- Variables/functions: `camelCase`
213- Constants: `UPPER_SNAKE_CASE`
214- CSS classes: Follow Element Plus conventions
215 
216**TypeScript:**
217- Use strict TypeScript configuration
218- Prefer type definitions over `any`
219- Use interfaces for object shapes
220- Use enums for fixed sets of values
 
 
 
221 
222## Build Commands
223 
224### Java Backend
225 
226```bash
227# Clean build
228./gradlew clean
229 
230# Full build (includes tests)
231./gradlew build
 
232 
233# Build without tests (faster)
234./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel
 
235```
236 
237### Test Commands
 
 
 
238 
239```bash
240# Run all tests (excludes flaky tests)
241./gradlew test
 
 
242 
243# Run only unit tests (fastest)
244./gradlew unitTest
245 
246# Run integration tests
247./gradlew integrationTest
 
248 
249# Run flaky tests (separate from build)
250./gradlew flakyTest
251 
252# Run tests for specific module
253./gradlew :core:test
 
 
 
254 
255# Run single test class
256./gradlew :module-name:test --tests "ClassName"
257 
258# Run single test method
259./gradlew :module-name:test --tests "ClassName.methodName"
 
 
260 
261# After running tests: generate a markdown summary of failures only
262npx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)
263```
264 
265### Frontend (UI)
266 
267```bash
268cd ui
 
 
269 
270# Install dependencies
271npm install
272 
273# Development server
274npm run dev
275 
276# Type checking
277npm run check:types
278 
279# Build for production
280npm run build
281 
282# Run tests
283npm run test:all # All tests with coverage
284npm run test:unit # Unit tests only
285npm run test:storybook # Storybook tests
286npm run test:e2e # End-to-end tests
287 
288# Linting
289npm run lint # Fix linting issues
290npm run test:lint # Check linting only
291 
292# Storybook
293npm run storybook # Development
294npm run build-storybook # Build
295```
296 
297## Development Workflow
298 
299### Running Locally
300 
3011. **Start/stop backends:**
302```bash
303# Start databases with Docker Compose
304docker compose -f docker-compose-ci.yml up
305 
306# Stop databases with Docker Compose
307docker compose -f docker-compose-ci.yml down
308```
309 
3102. **Access application:** http://localhost:8080
311 
312### Worktree setup
313 
314When working in an EE worktree (detected by: the working directory is under a `worktrees/` directory):
315```bash
316dev-tools/setup-worktree.sh ../worktrees/foo
 
 
317```
318This copies the gitignored `cli/src/main/resources/application-*.yml` files from the main checkout into the worktree. Without this step Kestra cannot boot in the worktree. The script is idempotent — safe to re-run.
319 
320### Security Considerations
321- Use tenant isolation for multi-tenant features
322- Implement proper authorization with `@HasAnyPermission`
323- Handle secrets securely (never log sensitive data)
324 
325### Performance Best Practices
326- Implement pagination for large datasets
327- Use streaming for large file operations
328- Cache frequently accessed data appropriately
329- Initialize collections with the expected size to avoid resizing overhead
330 
331## Troubleshooting
 
 
 
 
 
 
 
 
 
332 
333**Common Issues:**
334- **Build failures:** Run `./gradlew clean` and retry
335- **Test failures:** Check for service dependencies (Docker containers)
336- **Frontend issues:** Ensure Node.js version matches package.json requirements
337 
338**Debugging:**
339- Use IDE debugging with remote JVM debugging
340- Use Micronaut's built-in health endpoints
341- Enable debug logging: `--logging.level.io.kestra=DEBUG`
342- Use JUnit and Vitest reports for test failures
 
 
 
 
 
 
343 
344## Module Structure
345 
346**Core Modules:**
347- `cli` - Command Line Interface
348- `core` - Core functionality
349- `webserver` - Web server
350- `ui` - Vue 3 frontend application
351- `executor` - The component responsible for managing execution state
352- `scheduler` - The component responsible for scheduling polling and schedule triggers
353- `worker` - The component that executes tasks and manages worker instances
354- `worker-controller` - The component that manages worker instances and job distribution
355- `indexer` - The component responsible for indexing executions
356- `plateform` - provides the Platform Bill of Materials (BOM) for dependency management
 
 
 
 
 
357 
358**Queuing Layer:**
359- `queue` - Core API for queue implementations
360- `queue-jdbc` - JDBC-based queue implementation
361 
362**Data Layer:**
363- `jdbc-*` - Database implementations (H2, Postgres, MySQL)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364 
365**Testing Modules:**
366- `tests` - Common test utilities and base classes
367- `jmh-benchmark` - JMH benchmarks for performance testing
368 
369**Key Patterns:**
370- Repository pattern for data access
371- Service layer for business logic
372- Controller layer for HTTP endpoints
373- Builder pattern for object construction (often with Lombok `@Builder`)
374 
375## Pull request guidelines
376- Always add tests, keep your branch rebased instead of merged, and adhere to the commit message recommendations from https://www.conventionalcommits.org/en/v1.0.0.
377- Use types: chore, feat, fix, refactor, test, docs, build
378- Use scopes: apps, assets, core, dashboards, deps, design-system, executions, flows, iam, namespaces, plugins, secrets, storage, scheduler, system, tasks, tenants, tests, topology, triggers, variables, version, worker
379 
380## Issue guidelines
381- **Classify an issue with its GitHub issue type, not a `kind/*` label.** The `kind/bug` label is retired — do not add it. Set the type instead: `gh issue create --title … ` followed by `gh issue edit <number> --type Bug`, or `gh issue edit <number> --type Task|Feature|Epic`. Available types are `Task`, `Bug`, `Feature` and `Epic` (list them with `gh api /orgs/kestra-io/issue-types`).
382- **Do add the `area/*` labels** — `area/frontend`, `area/backend`, `area/devops`, `area/docs`, `area/plugin`, `area/qa`, `area/analytics` — since those drive routing and are still in use.
383- Leave triage labels such as `kind/cooldown` to `kestrabot`; it applies them automatically on new issues.
 
 
 
 
 
384 
385This document should be updated as the codebase evolves. When in doubt, follow existing patterns in the codebase and maintain consistency with established conventions.
386 
387## UI Translations
 
 
 
 
 
 
 
 
 
 
388 
389**MANDATORY — never hardcode user-facing strings.** Every label, button, tooltip, placeholder, dialog/section title, table-column header, and toast/confirm message rendered to the user MUST go through vue-i18n: `t("key")` (or `:label`/`:tooltip` bindings) in components, and `<i18n-t keypath="...">` with named slots when the string embeds markup or a component (e.g. a `<code>` fragment). Never write a literal user-facing string in a template, a `:tooltip`/`:label` attribute, or a `toast.*` call. Reuse existing generic keys (`cancel`, `delete`, `edit`, `save`, `add`, `id`, `description`, `namespace`, `revision`, …) instead of duplicating them; put feature-specific strings under one namespaced object (e.g. `"reusableInputs": { … }`). After adding keys to `en.json`, propagate them to every language (translation generation script) so the missing-keys check stays clean — a key present only in `en.json` fails the check.
390 
391Translation files live in `ui/src/translations/`. There is one JSON file per language code (e.g. `de.json`, `fr.json`) plus the source `en.json`.
 
 
 
392 
393### Checking for missing translations
394 
395Run the check script from the `ui/` directory:
396 
397```bash
398cd ui && npm run translations:check
399```
400 
401A clean run reports `No missing keys.`, `No extra keys.` and `No stale keys.` for every language. Anything listed must be fixed before merging — the same check runs as a PR gate.
402 
403> **Enterprise Edition:** EE-only keys live in `ui-ee/src/translations/ee_translations/en.json` and are checked separately — run `npm run translations:check` in `ui-ee` as well (see `kestra-ee/AGENTS.md` → "Frontend i18n").
 
 
 
 
 
 
 
 
404 
405### Editing English strings
406 
407**Changing an existing English value is a translation change.** Every key carries a fingerprint of the English text its translations were generated from, so editing `en.json` — even just the capitalisation — marks that key stale in all twelve languages and fails `translations:check` until it is regenerated. Run `npm run translations:generate` and commit the result alongside your change.
408 
409This is deliberate: before it existed, edited values were never propagated, and a rename of "SuperAdmin" to "Superadmin" sat un-translated in eleven locales for a year (kestra-io/kestra#10656).
 
 
 
 
 
410 
411### Adding or regenerating translations
412 
413Prefer `npm run translations:generate` (needs `GEMINI_API_KEY`); it fills missing keys and re-translates stale ones on its own, with no flag to remember. Pass `true` to force a full re-translation of everything.
414 
415If you must write a translation by hand:
416 
4171. Identify gaps by running `npm run translations:check`.
4182. Follow these translation rules (mirroring `ui/scripts/translations/generateTranslations.ts`, the generator shared by OSS and EE):
419 - **Reserved English terms — never translate:** `kv store`, `namespace`, `tenant`, `flow`, `subflow`, `task`, `log`, `blueprint`, `id`, `trigger`, `label`, `key`, `value`, `input`, `output`, `port`, `worker`, `backfill`, `healthcheck`, `min`, `max`.
420 - **ALL-CAPS status labels stay in English:** `WARNING`, `FAILED`, `SUCCESS`, `PAUSED`, `RUNNING`, etc.
421 - **Preserve `{placeholder}` variables** exactly — vue-i18n uses a **single** pair of braces. Do not translate the name inside the braces, do not rename it, and never write `{{placeholder}}`: double braces are a compile error (`Not allowed nest placeholder`) and make `t()` throw at render time. Each translation must carry exactly the same placeholders as the English source — no invented ones, none dropped.
422 - **Use natural UI terminology** — avoid false friends or overly literal translations (e.g. German: Execution → Ausführung, Theme → Modus, State → Zustand).
4233. Insert the translated keys into the correct position in the target language JSON, mirroring the key order of `en.json`.
4244. Re-run `npm run translations:check` to confirm everything is clean before committing.
425 
426The tooling itself lives in `ui/scripts/translations/` and is shared with EE, which keeps only thin entry points. Rules live in `.mjs` so the dependency-free PR gate can apply them; file IO and orchestration stay in `.ts`.
427 
428### Conflicts in `fingerprints.json`
429 
430Two branches that both touch `en.json` will both regenerate `ui/scripts/translations/fingerprints.json`, so it conflicts often. **Never hand-merge the hashes and never pick a side** — a hash says "this English text is what the twelve translations were generated from", so choosing the wrong one silently marks a drifted key as current and the drift becomes invisible again.
431 
432Resolve it the same way as a `kestra-sdk` conflict — regenerate:
433 
434```bash
435git checkout --ours ui/src/translations/*.json ui/scripts/translations/fingerprints*.json
436cd ui && npm run translations:generate # fills whatever the other branch added
437npm run translations:check # must report no missing / extra / stale keys
438```
439 
440`en.json` itself normally merges cleanly, since branches usually add different keys; it is the generated files that collide.
441 
kestra-io/kestra · ui/AGENTS.md
@@ +1 @@
1# UI Design System Guidelines
2 
3Scope: this file applies to everything under `ui/`. AI coding agents (Claude Code, Cursor, etc.) load this file automatically when working in this directory; humans should treat it as the source of truth for frontend conventions in Kestra.
4 
5The Kestra design system lives at [ui/packages/design-system/](packages/design-system/) and is the **single source of truth** for every visual element of the product — colors, fonts, spacing, buttons, forms, dialogs, tables, charts, and so on. Anything rendered to a user must come from it.
6 
7## What this is, in plain terms
 
 
 
 
 
 
 
 
 
 
 
8 
9Think of the design system as the product's **visual vocabulary**:
10 
11- A short list of agreed-upon **colors**, **fonts**, and **spacings** (called *design tokens*).
12- A library of pre-built **components** (`KsButton`, `KsTable`, `KsDialog`, …) that already use those tokens.
13- A guarantee that anything built from these pieces will look right in **light mode and dark mode**, follow accessibility rules, and stay visually consistent with the rest of Kestra.
14 
15If a screen feels "off-brand," looks broken in dark mode, or every page styles the same control differently, it's almost always because someone bypassed the design system. The rules below exist to prevent that.
 
 
 
 
16 
17Under the hood, the design system wraps Element Plus under the `kel` namespace and globally registers every component with a `Ks*` prefix. You should almost never `import` from `element-plus` directly in `ui/src/`.
18 
19> **Note on `@kestra-io/ui-libs`:** The codebase may still contain imports from `@kestra-io/ui-libs`, the previous shared component library. That repository is sunsetting — all components have been migrated here into `ui/packages/`. Do not add new imports from `@kestra-io/ui-libs`; use `Ks*` components from the design system instead.
20 
21## Golden rules (non-negotiable)
22 
23These rules are what keep the UI maintainable as it grows. Treat any deviation as a bug.
 
 
 
24 
251. **Use a `Ks*` component if one exists.** Check the tables below before writing anything custom or importing from `element-plus`. New screens that mix `<el-button>` and `<KsButton>` are a regression.
262. **Colors come from `--ks-*` tokens. Always.** No hex codes, no `rgb(...)`, no Element Plus tokens (`--el-*`), no Bootstrap variables, no SCSS color variables in component code. If the token you need does not exist, talk to design and add it to `ks-theme-light.scss` / `ks-theme-dark.scss` / `ks-theme-dark-2.scss` — do not pick a one-off color.
273. **Typography comes from `KsText` or typography tokens.** Use `<KsText>` (with `size`, `type`, `tag`, `truncated`, `lineClamp`) for body copy. For headings or one-off needs, use the `$font-family-*` and `$font-size-*` SCSS variables only inside the design-system package — feature code should not redefine them.
284. **No `:deep()` selectors.** Reaching into a child component's internals breaks encapsulation and silently shatters when the design system is upgraded. If you need to style something inside a `Ks*` component, add a prop, a slot, or a CSS variable to the component upstream.
295. **No SCSS variables (`$...`) in feature components.** Use `var(--ks-*)` CSS custom properties inside `<style>` blocks. SCSS variables don't react to dark mode, can't be overridden at runtime, and bind your component to a specific theme. SCSS variables are only acceptable inside `ui/packages/design-system/` itself, in mixins, or for math at build time.
306. **No magic numbers for theme values.** Spacing, radii, font sizes, and shadows must reference tokens or design-system SCSS variables — never `padding: 13px`, never `border-radius: 6px`. For spacing (`padding`/`margin`/`gap`), reach for the `--ks-spacing-*` scale first (`--ks-spacing-1` = 0.25rem, `-2` = 0.5rem, `-3` = 0.75rem, `-4` = 1rem, `-5` = 1.5rem, `-6` = 2rem, `-7` = 2.5rem, `-8` = 3rem, `-10` = 4rem, `-12` = 5rem, `-16` = 6rem; declared in [`ks-tokens.scss`](packages/design-system/src/assets/styles/ks-tokens.scss)). Only fall back to a raw `rem` value when no token fits — never a hardcoded `px` value (`margin: 0 24px` → `margin: 0 var(--ks-spacing-5)`).
317. **Never override Element Plus classes directly.** Don't write `.el-button { ... }` in feature code. If a `Ks*` component is missing a behavior, extend the component in the design system instead of patching CSS at the call site.
328. **Don't fork — extend.** If a `Ks*` component is *almost* what you need, add a prop or a slot to the component in `ui/packages/design-system/`. Copy-pasting the component into your feature folder is forbidden.
339. **Every new `Ks*` component needs a Storybook story and a unit test.** Stories double as living documentation for design and product reviewers.
3410. **i18n keys live with the design system component**, not inside feature code, when they belong to the component (e.g. `KsEmpty`, `KsDurationPicker`). Register them via `registerDesignSystemI18n`.
35 
36## Best practices for keeping the design system healthy
37 
38A design system rots fast if it's treated as a one-time deliverable. Apply these rules every time you touch UI code or review a UI PR.
39 
40### Before you write code
 
 
 
 
 
 
 
 
 
 
 
41 
42- Search the component tables and Storybook first. The most common waste in this codebase is rebuilding something that already exists.
43- If you can't find what you need, ask: is this a *missing component* (fix it in the DS) or a *missing prop on an existing component* (extend the DS)? Almost never the answer "build it locally."
44- For anything visible to a user, check both light and dark mode in Storybook before merging.
 
 
45 
46### While you write code
47 
48- Build screens by *composing* `Ks*` components. A new feature should read like a list of design-system blocks plus business logic — not a wall of custom CSS.
49- Keep the style **inside** the SFC. `<style scoped src="./x.scss">` is valid and `scoped` still applies, but an external file separates the CSS from the markup it describes for no gain, and it is one more file to open. Block order is `template`, `script`, `style`, enforced by `vue/block-order` in `ui/eslint.config.js`.
50- Keep `<style>` blocks small. If a component file has more than ~50 lines of CSS, you probably need a new prop, a new slot, or a new `Ks*` component.
51- Prefer `scoped` styles and rely on design tokens for theming. If you find yourself writing `:deep(.el-...)`, stop — it's a signal the design system needs to expose something.
52- Write each CSS class selector as a full literal — never construct it with SCSS `&` nesting (`&__row`, `&--active`). Constructed selectors can't be found by search and devtools can't jump from a class to its rule. With `scoped` styles, BEM-style namespacing is redundant anyway: use flat, hyphenated names (`.label-input-row`, not `.label-input { &__row }`).
53- Use semantic tokens, not raw colors. `var(--ks-text-link)` communicates intent; `var(--ks-text-blue-500)` does not exist for a reason.
54- Co-locate component-specific tokens (e.g. `--ks-card-shadow`) in the component's SCSS, but always derive them from semantic tokens.
55 
56### When extending the design system
57 
58- Only expose props that are actually used somewhere in the codebase. Speculative props rot.
59- Mirror Element Plus prop names where possible — predictability is a feature.
60- Pass `v-bind="$attrs"` and forward slots so wrappers don't trap consumer extension points.
61- Add the new component or prop to the relevant table in this file, plus a Storybook story and a unit test, in the same PR.
62- Document tokens in code comments next to where they're declared in `ks-theme-*.scss`. The `scripts/generate-palette.mjs` file is auto-generated — don't hand-edit it.
63 
64### When reviewing a UI PR
 
 
65 
66Reject (or ask to fix) anything that:
 
 
 
67 
68- Imports from `element-plus` directly into `ui/src/`.
69- Uses a hex code, `rgb(...)`, `--el-*`, or `--bs-*` for color.
70- Uses `:deep()` to reach into a `Ks*` or `el-*` component.
71- Hardcodes pixel values for padding, margin, radii, font sizes, or shadows.
72- Adds a CSS class that overrides `.el-...` selectors.
73- Duplicates a component that already exists in the design system.
74- Adds a `Ks*` component without a Storybook story or test.
75- Mounts `KsDataTable` without binding `:currentPage` / `:pageSize` (or `v-model:currentPage` / `v-model:pageSize`) — pagination is controlled; see "Data tables & pagination state".
76- Watches a `computed` that returns a fresh object (spread / `{...}`) with `{deep: true}` — that fires on every dependency change regardless of content. See "The deep-watch / computed-spread trap".
77- Adds a modal/drawer where the user enters data without guarding accidental dismissal — see "Unsaved input in modals (discard guard)".
78 
79### Accessibility
80 
81- Every icon-only `KsIconButton` must have an accessible label (`aria-label` or `title`). Screen readers do not see the icon glyph.
82- Never convey state with color alone — pair status colors with an icon (`KsExecutionStatus` already does this) or a text label.
83- Use semantic HTML inside slots: real `<button>`, `<a>`, `<label>`, headings in document order. Don't fake interactivity with `<div @click>`.
84- `KsDialog`, `KsDrawer`, `KsPopover` already manage focus trap and `Escape`-to-close — don't reimplement these in feature code.
85- Keep tab order logical; rely on the DOM order rather than `tabindex` hacks.
86- Color contrast comes for free as long as you use `--ks-text-*` against `--ks-background-*` pairings. If you mix-and-match, verify with the browser inspector.
87 
88### Internationalization
 
 
 
 
 
89 
90- No hardcoded user-facing strings. Always go through i18n.
91- **In `<template>`, always use the global `$t(...)`** — never the `t` from `useI18n()`. Only call `useI18n()` (`const {t} = useI18n()`) when you need `t` in `<script>` (computed labels, toasts, etc.); if a component needs i18n **only** in its template, use `$t` and don't import `useI18n` at all.
92- Use `<i18n-t>` for plurals and interpolation — never string-concatenate.
93- Format dates and times via `dateUtils` (which respects `TIMEZONE_STORAGE_KEY` and `DATE_FORMAT_STORAGE_KEY`); format durations via `durationUtils.humanDuration()`. Don't reach for `Intl.DateTimeFormat` directly.
94- Strings owned by a `Ks*` component live in the design system's locale files and are registered via `registerDesignSystemI18n`. Strings owned by a feature live in that feature's locale files.
 
95 
96### Loading, empty, and error states
 
 
 
 
97 
98Every async surface must render all four states. "Happy path only" is a bug.
 
 
 
 
 
 
 
 
 
 
99 
100- **Loading:** `KsSkeleton` for content placeholders; `vKsLoading` directive for sections that already have layout; `KsLoading` component for full-page or container-level spinners.
101- **Empty:** `KsEmpty` with an action where possible — never a blank screen.
102- **Error:** `KsAlert type="error"` with retry affordance, or `KsMessage` for transient errors.
103- **Success / data:** the actual content.
104 
105### Data tables & pagination state
 
106 
107`KsDataTable` is a **fully controlled component** for pagination. `props.currentPage` and `props.pageSize` are the single source of truth — the component holds no internal page mirror. The parent owns the state, binds it (URL or local ref), and the component reacts.
108 
109**The contract:**
110 
111- Bind `:currentPage` / `:pageSize` (one-way) OR use `v-model:currentPage` / `v-model:pageSize` (two-way).
112- Listen to `@page-changed` (or rely on `@update:currentPage`/`@update:pageSize` via v-model) and propagate the change to the bound state — typically a `router.push({...route.query, page: String(page), size: String(size)})`.
113- The component watches `[currentPage, pageSize]` and re-fires `loadData` automatically when either prop changes. Do **not** call `dataTable.reload()` from the parent in response to a page click — the prop change handles it.
114- `resetAndReload()` emits `update:currentPage(1)` and `page-changed`; if the page was already 1 it just reloads. Useful from a filter-change watcher to bounce back to page 1 + re-fetch.
 
 
 
 
 
 
 
 
 
115 
116**URL-driven pattern** — the default for top-level list pages (Logs, Flows, Executions, KV, Secrets, Triggers, FlowsSearch, Blueprints):
 
 
117 
118```vue
119<KsDataTable
120 :loadData="loadData"
121 :currentPage="urlPage"
122 :pageSize="urlSize"
123 :total="store.total"
124 @page-changed="({page, size}) => router.push({query: {...route.query, page: String(page), size: String(size)}})"
125/>
126 
127<script setup>
128const urlPage = computed(() => Number(route.query.page) || 1)
129const urlSize = computed(() => Number(route.query.size) || 25)
130</script>
131```
132 
133**Local-state pattern** — for embedded tables that should not appear in the URL (MetricsTable, side-panel views):
134 
135```vue
136<KsDataTable
137 v-model:currentPage="currentPage"
138 v-model:pageSize="pageSize"
139 :loadData="loadData"
140 :total="..."
141/>
142 
143<script setup>
144const currentPage = ref(1)
145const pageSize = ref(25)
146</script>
147```
148 
149**Never** maintain a separate `internalPage` / `pageNumber` ref *and* bind the prop to a different value — that re-introduces the drift bug (URL says page 2, UI shows page 1) that this contract exists to prevent.
 
 
 
 
 
150 
151### The deep-watch / computed-spread trap
152 
153A `computed` that returns a fresh object (via spread or `{...}`) returns a new reference on every evaluation. Watching it with `{deep: true}` does **not** add structural equality — `deep: true` enables deep dependency tracking; the equality check at the top is still `Object.is`. The callback therefore fires on every dependency change, even when the content is unchanged.
154 
155This was the root cause of the logs pagination bug: the watcher reset the page to 1 on every `route.query` mutation, including page-only updates from the user clicking the pagination itself.
156 
157**Don't:**
158 
159```ts
160const filterQuery = computed(() => {
161 const {page: _p, size: _s, sort: _so, ...filters} = route.query
162 return filters // new object reference on every route.query change
163})
164watch(filterQuery, () => dataTable.value?.resetAndReload(), {deep: true})
165// Fires on every route.query change — page clicks, sort clicks, anything —
166// and bounces the user back to page 1.
167```
168 
169**Do:**
 
 
 
 
170 
171```ts
172const filterQueryKey = computed(() => {
173 const {page: _p, size: _s, sort: _so, ...filters} = route.query
174 return JSON.stringify(filters) // stable string — same content, same value
175})
176watch(filterQueryKey, () => dataTable.value?.resetAndReload())
177// Fires only when filter content actually changes.
178```
179 
180The general rule: **if you find yourself reaching for `{deep: true}` on a computed source, the source should probably return a primitive (string / number) instead of an object.** Strings compare by value; references compare by identity. Picking the right primitive is the fix.
181 
182### Unsaved input in modals (discard guard)
183 
184Any modal/drawer where the user **enters data** must not silently lose it on an accidental dismissal. Use the shared `useDiscardGuard` composable — never reimplement the confirm-before-discard logic per modal.
 
 
185 
186```ts
187// ui/src/composables/useDiscardGuard.ts (import path is relative to your component)
188import {useDiscardGuard} from "../../composables/useDiscardGuard"
189 
190// isDirty: true when there is unsaved input worth a prompt
191const {guardedClose} = useDiscardGuard(() => /* isDirty */, {message: t("...")}) // message optional; defaults to "discard changes confirmation"
192const beforeClose = (done: () => void) => guardedClose(() => { reset(); done() })
193```
194 
195```vue
196<KsDialog :beforeClose="beforeClose" ... />
197<KsDrawer :beforeClose="beforeClose" ... />
198```
199 
200Rules:
201- **Guard only *accidental* close paths** — overlay click, `Escape`, the `X`. These all go through `beforeClose`. Explicit **Cancel / Save** buttons set `v-model = false` directly and **must not** be guarded (the user already expressed intent; a prompt there is friction). Note: a programmatic `v-model = false` does **not** trigger `beforeClose` (Element Plus only calls it for user-initiated closes), which is exactly why Cancel/Save bypass it.
202- **`isDirty` is per-modal.** Compare current input against a baseline captured on open (`JSON.stringify` snapshot), or "any meaningful input"; **ignore empty rows** (e.g. a blank label/tag row is not dirty). Reset dirty-relevant state on open so a reopen starts clean.
203- **`KsDialog` and `KsDrawer` both expose a `beforeClose` prop** with signature `(done) => void` — call `done()` to proceed with closing. (Element Plus's `ElDrawer.beforeClose` is a prop, not an event; `KsDrawer` forwards it.)
204- **Don't guard** read-only viewers, action/confirmation dialogs, or ephemeral forms that reset on every open.
205 
206### Icons
 
207 
208- All icons come from [`vue-material-design-icons`](https://github.com/robcresswell/vue-material-design-icons) via `<KsIcon>` (or `<KsIconButton>` for clickable icons).
209- Never inline raw SVG, font-icon classes, or emoji as UI state. If a needed icon is missing, propose adding it to the DS rather than dropping an SVG into a feature folder.
210- Pass `name` (the kebab-case Material name); size and color come from props or the surrounding token context — don't override with inline `style`.
211 
212### Performance
 
213 
214- Lazy-load heavy surfaces: `KsEchart`, `KsLine`, `KsBar`, `KsPie`, `KsGraph`, `KsMarkdown`, code-editor surfaces. Use `defineAsyncComponent` or route-level code splitting.
215- Prefer `v-show` for frequent toggles (tabs, filters), `v-if` for rare/heavy mounts (modals, big tables).
216- Pass stable `key` props in lists. Avoid index-based keys when items have IDs.
217- Don't render giant tables without `KsDataTable`'s pagination/virtualization — server-side paging is the default for anything that can grow.
218- Watch out for `watch(..., { deep: true })` and `computed` with object identity — they often re-run more than you expect.
219 
220### Testing UI
 
221 
222- Unit tests with **Vitest** + `@vue/test-utils`, colocated next to the component.
223- Use `data-test="..."` selectors for E2E tests with **Playwright**. Never select on `.el-*` or `.ks-*` class names — those are not stable contracts and will break on Element Plus / DS upgrades.
224- Storybook stories cover: each variant prop, dark mode, edge cases (empty content, very long text, error state). A `*.stories.ts` file with one default story is not enough.
225- Visual regressions caught in Storybook are cheaper to fix than caught in production.
226 
227### Deprecation contract
 
 
228 
229When retiring a `Ks*` component, prop, or token:
230 
2311. Mark with a `@deprecated` JSDoc tag *and* a one-line replacement path: `@deprecated since 0.x — use <KsNewThing> instead`.
2322. Keep it working for at least one minor release; add a `console.warn` in dev mode if the cost is reasonable.
2333. Migrate all callers in the same release where feasible — don't leave half-migrations.
2344. Only delete after the deprecation window. A silent removal breaks downstream EE / plugin code.
235 
236## Anti-patterns (do not write these)
 
237 
238```vue
239<!-- Wrong: raw element-plus, hex color, :deep, SCSS variable in feature code -->
240<template>
241 <el-button class="my-btn">Save</el-button>
242</template>
243<style lang="scss" scoped>
244 .my-btn {
245 background: #8405ff;
246 font-size: $font-size-md;
247 }
248 :deep(.el-button__text) { color: white; }
249</style>
 
 
 
 
 
 
 
 
 
 
250```
251 
252```vue
253<!-- Right: Ks component, semantic tokens, no deep selector, i18n -->
254<template>
255 <KsButton type="primary">{{ t("save") }}</KsButton>
256</template>
257<style lang="scss" scoped>
258 /* Almost always: no custom CSS is needed at all. */
259</style>
 
 
 
260```
261 
262If your `<style>` block needs to exist:
263 
264```scss
265/* Right: --ks-* tokens, no SCSS vars in feature code, no :deep */
266.my-feature {
267 background: var(--ks-bg-surface);
268 color: var(--ks-text-primary);
269 border: 1px solid var(--ks-border-primary);
270}
271```
 
272 
273## Components
 
 
 
274 
275### Basic / Layout
 
 
 
 
276 
277| Component | Purpose |
278|-----------|---------|
279| `KsButton` / `KsButtonGroup` | Primary action button and grouped buttons |
280| `KsIcon` / `KsIconButton` | Material Design icon display; icon-only button (always with `aria-label`) |
281| `KsLink` | Styled hyperlink |
282| `KsText` | Typography wrapper — preferred over raw `<span>` / `<p>` for theme-aware text |
283| `KsScrollbar` | Custom-styled scrollbar wrapper |
284| `KsContainer` / `KsHeader` / `KsMain` | Page layout shell |
285| `KsRow` / `KsCol` | Responsive grid |
286| `KsSplitter` / `KsSplitterPanel` | Resizable split-pane layout |
287 
288### Feedback
 
 
 
289 
290| Component | Purpose |
291|-----------|---------|
292| `KsAlert` | Alert banner for messages and status feedback |
293| `KsDialog` | Modal dialog (handles focus trap + Escape) |
294| `KsDrawer` | Side drawer / panel |
295| `KsTooltip` | Hover tooltip |
296| `KsPopover` | Popover for contextual content |
297| `KsLoading` (`vKsLoading`) | Loading spinner directive |
298| `KsMessage` | Toast notification service |
299| `KsNotification` | Notification service |
300| `KsMessageBox` | Confirmation dialog service |
301 
302### Form
303 
304| Component | Purpose |
305|-----------|---------|
306| `KsInput` / `KsPassword` | Text and password inputs |
307| `KsInputNumber` | Numeric input with increment / decrement |
308| `KsSelect` / `KsOption` / `KsOptionGroup` | Dropdown select |
309| `KsAutocomplete` | Autocomplete input with suggestions |
310| `KsCheckbox` / `KsCheckboxGroup` / `KsCheckboxButton` | Checkbox variants |
311| `KsRadio` / `KsRadioGroup` / `KsRadioButton` | Radio button variants |
312| `KsRadioCardGroup` | Single-select radio group rendered as option cards (title + optional hint/icon/disabled); options-driven via `:options` + `v-model` |
313| `KsSwitch` | Toggle switch |
314| `KsDatePicker` / `KsTimePicker` | Date and time pickers |
315| `KsColorPicker` | Color picker |
316| `KsDurationPicker` | ISO 8601 duration picker |
317| `KsCascaderPanel` | Cascading hierarchical selector |
318| `KsUpload` | File upload |
319| `KsForm` / `KsFormItem` | Form container with validation |
320 
321### Data Display
 
 
322 
323| Component | Purpose |
324|-----------|---------|
325| `KsCard` | Card container |
326| `KsTable` / `KsTableColumn` | Basic table |
327| `KsDataTable` / `KsFilter` / `KsBulkSelect` | Advanced data table with filtering, sorting, pagination, bulk actions. **Pagination is fully controlled** — bind `:currentPage` / `:pageSize` (or `v-model:`). See "Data tables & pagination state". |
328| `KsEntityLink` | Clickable cross-entity reference (namespace / flow) for table cells — neutral tag with leading icon, violet on hover |
329| `KsBadge` | Small indicator badge |
330| `KsNewBadge` | Compact uppercase "NEW" pill flagging a newly shipped feature — caller supplies the label via the default slot |
331| `KsTag` / `KsCheckTag` | Tag / label; clickable checkbox-style tag |
332| `KsAvatar` | Avatar with fallback |
333| `KsProgress` | Progress bar |
334| `KsPagination` | Pagination controls |
335| `KsEmpty` | Empty state placeholder |
336| `KsSkeleton` | Skeleton loader |
337| `KsId` | Copyable ID display |
338| `KsDateAgo` | Relative time display ("2 hours ago") |
339| `KsSegmented` | Segmented control |
340| `KsCollapse` / `KsCollapseItem` | Collapsible sections |
341| `KsTree` | Hierarchical tree view |
342| `KsTimeline` / `KsTimelineItem` | Timeline visualization |
343| `KsExecutionStatus` | Execution / task status badge with icon and color |
344| `KsCodeStatus` | Compact validity badge with icon (`valid` / `error`) — caller supplies the label |
345| `KsMarkdown` | Markdown renderer (lazy-load on heavy surfaces) |
346 
347### Charts
 
 
348 
349| Component | Purpose |
350|-----------|---------|
351| `KsEchart` | ECharts base wrapper (lazy-load) |
352| `KsLine` / `KsBar` / `KsPie` | Line, bar, and pie charts (lazy-load) |
353| `KsGraph` | Graph / network visualization (lazy-load) |
354 
355### Navigation
 
 
 
356 
357| Component | Purpose |
358|-----------|---------|
359| `KsTabs` / `KsTabPane` | Tabbed interface |
360| `KsMenu` / `KsMenuItem` | Hierarchical menu |
361| `KsDropdown` / `KsDropdownMenu` / `KsDropdownItem` | Dropdown menu |
362| `KsTopNavBar` | Top navigation bar |
363| `KsSideBar` / `KsSideBarSection` / `KsSideBarItem` | Left sidebar shell (header / scrollable body / footer slots), section with title, and styled link primitive with icon, active and locked states |
364| `KsBreadcrumb` / `KsBreadcrumbItem` | Breadcrumb navigation |
365| `KsSteps` / `KsStep` | Step / wizard progress indicator |
366 
367## Utilities (import from the design system)
368 
369- `State`, `STATES`, `LOG_LEVELS` — execution state constants, icons, and colors
370- `cssVar(name, opacity?)` — read a `--ks-*` CSS custom property at runtime (use this in JS / chart configs instead of hardcoding hex)
371- `dateUtils` — `dateFilter()`, `DATE_FORMAT_STORAGE_KEY`, `TIMEZONE_STORAGE_KEY`
372- `durationUtils` — `duration()`, `humanDuration()` — ISO 8601 ↔ ms and human-readable
373- `stringUtils` — `afterLastDot()`
374- `flowYamlUtils` — YAML parsing / manipulation for flow definitions
375- `Comparators` — enum of filter comparison operators
376- Filter helpers — `decodeSearchParams()`, `encodeFiltersToQuery()`, `getUniqueFilters()`, etc.
377- `applyDefaultFilters()`, `useRouteFilterPolicy()` — filter composables
378- `setMomentInstance()`, `setDateFormatter()` — date library configuration
379- `designSystemLocale`, `setDesignSystemLocale`, `registerDesignSystemI18n` — i18n
380 
381## Composables
382 
383- `useTheme()` — detects and tracks dark / light mode via MutationObserver. Use this instead of reading `document.documentElement` yourself.
384- `useFilters`, `useSavedFilters`, `useDefaultFilter`, `usePreAppliedFilters`, `useRouteFilterPolicy`, `useTableColumns`, `useDataOptions`, `useDragAndDrop`, `usePeriodicRefresh` — data-table filter composables
385- `useDiscardGuard(isDirty, {message?})` — confirm-before-discard for data-entry modals; see "Unsaved input in modals (discard guard)"
386- `useTaskIcon()` — resolves the app-provided task-icon component via `TASK_ICON_INJECTION_KEY` (falling back to a generic placeholder icon). The app provides its own `TaskIcon` component once, at bootstrap (`app.provide(TASK_ICON_INJECTION_KEY, TaskIcon)`) — the design system cannot own that component since it depends on the app's plugin-icon backend API. Used internally by `KsEditor` (Monaco suggestion icons) and the `@kestra-io/topology` package (graph node icons) so both share the same app-provided instance.
387 
388## Design tokens
389 
390Tokens are CSS custom properties declared in [`ks-theme-light.scss`](packages/design-system/src/assets/styles/ks-theme-light.scss), [`ks-theme-dark.scss`](packages/design-system/src/assets/styles/ks-theme-dark.scss) and [`ks-theme-dark-2.scss`](packages/design-system/src/assets/styles/ks-theme-dark-2.scss). Each token is **semantic** — it describes *what the value means*, not what color it is. That is what makes dark mode and rebrands trivial.
391 
392**Always use `var(--ks-*)` in component `<style>` blocks** — not SCSS variables, not hex codes, not `--el-*`, not `--bs-*`.
 
 
393 
394Token families currently exposed:
395 
396- `--ks-bg-*` — backgrounds: surfaces (`base`, `surface`, `elevated`, `sidebar`, `input`, `overlay`, `scrim`), interaction states (`hover`, `hover-elevated`, `active`, `inactive`), component fills (`badge`, `tag`, `tag-hover`, `tag-active`, `tag-inactive`), plus per-state (`--ks-bg-success`, `--ks-bg-error`, `--ks-bg-warning`, `--ks-bg-info`)
397- `--ks-border-*` — `default` / `subtle` / `strong` borders, `focus`, plus per-state (`error`, `success`, `warning`, `info`)
398- `--ks-text-*` — text colors: `primary`, `secondary`, `dim`, `muted`, `inactive`, `link`, named (`blue`, `green`), plus per-state (`error`, `success`, `warning`, `info`)
399- `--ks-icon-*` — icon colors: `default`, `hover`, `active`, `inactive`, `muted`, plus per-state
400- `--ks-btn-*` — button background / border / text variants (`primary`, `secondary`, `run`, `success`) across `default` / `hover` / `active` / `inactive` states
401- `--ks-toggle-*` — toggle / switch states (`default`, `hover`, `active`, `inactive`, `playground`)
402- `--ks-dropdown-*`, `--ks-scrollbar-*`, `--ks-shadow-*` — component-specific tokens
403- `--ks-status-*` — palette for charts and status (`success`, `error`, `warning`, `info`, `running`, `pending`, `neutral`); pair with `cssVar("--ks-status-success")` in JS
404- `--ks-editor-*`, `--ks-dependencies-*`, `--ks-topology-*` — domain-specific surfaces
405 
406When a needed token is missing, **add it** to all three of `ks-theme-light.scss`, `ks-theme-dark.scss` and `ks-theme-dark-2.scss` (and review with design) rather than picking a raw color.
407 
408**SCSS variables — only inside `ui/packages/design-system/`, never in feature code:**
409 
410- **Brand:** `$base-primary-500` (primary, `#8405FF`)
411- **Status palette:** `$base-green-500` (success), `$base-red-500` (danger), `$base-orange-500` (warning), `$base-blue-500` (info)
412- **Grays:** `$base-gray-50` … `$base-gray-950`
413- **Typography:** `$font-family-sans-serif` (Inter), `$font-family-monospace` (JetBrains Mono)
414- **Font sizes:** `$font-size-xs` / `sm` / `md` / `lg` / `xl` / `2xl` / `3xl` / `4xl`
415- **Radii:** `$border-radius` (0.25rem), `$border-radius-sm` (0.15rem), `$border-radius-lg` (0.5rem)
416 
417These exist so the *design system itself* can compose tokens from a single palette. They are not API for feature code — feature code should reach the same values through `--ks-*` tokens.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418 
@@ −1 +1 @@
1−# Coding Agent Guidelines for Kestra Open Source Edition
1+# UI Design System Guidelines
22  
3−This document provides essential information for AI coding agents working on the Kestra codebase.
3+Scope: this file applies to everything under `ui/`. AI coding agents (Claude Code, Cursor, etc.) load this file automatically when working in this directory; humans should treat it as the source of truth for frontend conventions in Kestra.
44  
5−**IMPORTANT — READ FIRST**
5+The Kestra design system lives at [ui/packages/design-system/](packages/design-system/) and is the **single source of truth** for every visual element of the product — colors, fonts, spacing, buttons, forms, dialogs, tables, charts, and so on. Anything rendered to a user must come from it.
66  
7−- **Act as a Senior Software Engineer and Software Architect.** Approach software development with:
8− - **Pragmatism**: Favor simple solutions over clever ones
9− - **Skepticism**: Question decisions that could cause technical debt or scalability issues
10− - **Efficiency**: Only challenge when it genuinely matters
11−- **Think before coding**: explicitly state assumptions, compare alternatives, and justify choices.
12−- **Simplicity first (KISS)**: overengineering and "gas factories" are strictly forbidden.
13−- **Surgical changes only**: touch **only** what is strictly necessary to achieve the goal.
14−- **Goal-driven execution**: define what success looks like *before* writing the first line of code.
15−- **Preserve existing comments**: never delete any existing comment **unless** you are improving its clarity or usefulness.
16−- **Keep comments short and only where they earn their place**: a comment you write should be **one sentence**, or two at most when the *why* genuinely needs it (a non-obvious constraint, a workaround, a subtle ordering or concurrency requirement). Do **not** comment obvious code — no restating what the next line plainly says (`// increment the counter`), no narrating a self-explanatory getter, loop, or well-named call. If the code is readable, the comment is noise; if it isn't, prefer making the code clearer over explaining it.
17−- **Write clear, maintainable, and well-documented code**
18−- **Build & test are mandatory**
7+## What this is, in plain terms
198  
20−## Project
9+Think of the design system as the product's **visual vocabulary**:
2110  
22−Monorepo built with Java (backend) and Vue (frontend), using Gradle as the build system.
11+- A short list of agreed-upon **colors**, **fonts**, and **spacings** (called *design tokens*).
12+- A library of pre-built **components** (`KsButton`, `KsTable`, `KsDialog`, …) that already use those tokens.
13+- A guarantee that anything built from these pieces will look right in **light mode and dark mode**, follow accessibility rules, and stay visually consistent with the rest of Kestra.
2314  
24−## Tech Stack
25−- **Backend:** Java 25, Micronaut Framework, Lombok
26−- **Frontend:** Vue 3, TypeScript, Vite, Element Plus, Pinia
27−- **Build:** Gradle 8.x with multi-project structure (77 submodules)
28−- **Testing:** JUnit 5, Mockito, AssertJ, Vitest, Playwright
15+If a screen feels "off-brand," looks broken in dark mode, or every page styles the same control differently, it's almost always because someone bypassed the design system. The rules below exist to prevent that.
2916  
30−## Critical Code Patterns
17+Under the hood, the design system wraps Element Plus under the `kel` namespace and globally registers every component with a `Ks*` prefix. You should almost never `import` from `element-plus` directly in `ui/src/`.
3118  
32−### Dependency Injection
19+> **Note on `@kestra-io/ui-libs`:** The codebase may still contain imports from `@kestra-io/ui-libs`, the previous shared component library. That repository is sunsetting — all components have been migrated here into `ui/packages/`. Do not add new imports from `@kestra-io/ui-libs`; use `Ks*` components from the design system instead.
3320  
34−**DO**: Use constructor injection with final fields.
21+## Golden rules (non-negotiable)
3522  
36−```java
37−@Singleton
38−public class MyService {
39− private final SomeDependency dependency;
23+These rules are what keep the UI maintainable as it grows. Treat any deviation as a bug.
4024  
41− @Inject
42− public MyService(SomeDependency dependency) {
43− this.dependency = Objects.requireNonNull(dependency);
44− }
45−}
46−```
25+1. **Use a `Ks*` component if one exists.** Check the tables below before writing anything custom or importing from `element-plus`. New screens that mix `<el-button>` and `<KsButton>` are a regression.
26+2. **Colors come from `--ks-*` tokens. Always.** No hex codes, no `rgb(...)`, no Element Plus tokens (`--el-*`), no Bootstrap variables, no SCSS color variables in component code. If the token you need does not exist, talk to design and add it to `ks-theme-light.scss` / `ks-theme-dark.scss` / `ks-theme-dark-2.scss` — do not pick a one-off color.
27+3. **Typography comes from `KsText` or typography tokens.** Use `<KsText>` (with `size`, `type`, `tag`, `truncated`, `lineClamp`) for body copy. For headings or one-off needs, use the `$font-family-*` and `$font-size-*` SCSS variables only inside the design-system package — feature code should not redefine them.
28+4. **No `:deep()` selectors.** Reaching into a child component's internals breaks encapsulation and silently shatters when the design system is upgraded. If you need to style something inside a `Ks*` component, add a prop, a slot, or a CSS variable to the component upstream.
29+5. **No SCSS variables (`$...`) in feature components.** Use `var(--ks-*)` CSS custom properties inside `<style>` blocks. SCSS variables don't react to dark mode, can't be overridden at runtime, and bind your component to a specific theme. SCSS variables are only acceptable inside `ui/packages/design-system/` itself, in mixins, or for math at build time.
30+6. **No magic numbers for theme values.** Spacing, radii, font sizes, and shadows must reference tokens or design-system SCSS variables — never `padding: 13px`, never `border-radius: 6px`. For spacing (`padding`/`margin`/`gap`), reach for the `--ks-spacing-*` scale first (`--ks-spacing-1` = 0.25rem, `-2` = 0.5rem, `-3` = 0.75rem, `-4` = 1rem, `-5` = 1.5rem, `-6` = 2rem, `-7` = 2.5rem, `-8` = 3rem, `-10` = 4rem, `-12` = 5rem, `-16` = 6rem; declared in [`ks-tokens.scss`](packages/design-system/src/assets/styles/ks-tokens.scss)). Only fall back to a raw `rem` value when no token fits — never a hardcoded `px` value (`margin: 0 24px` → `margin: 0 var(--ks-spacing-5)`).
31+7. **Never override Element Plus classes directly.** Don't write `.el-button { ... }` in feature code. If a `Ks*` component is missing a behavior, extend the component in the design system instead of patching CSS at the call site.
32+8. **Don't fork — extend.** If a `Ks*` component is *almost* what you need, add a prop or a slot to the component in `ui/packages/design-system/`. Copy-pasting the component into your feature folder is forbidden.
33+9. **Every new `Ks*` component needs a Storybook story and a unit test.** Stories double as living documentation for design and product reviewers.
34+10. **i18n keys live with the design system component**, not inside feature code, when they belong to the component (e.g. `KsEmpty`, `KsDurationPicker`). Register them via `registerDesignSystemI18n`.
4735  
48−**DON'T**: Use field injection (`@Inject` on fields directly). Always prefer constructor injection.
36+## Best practices for keeping the design system healthy
4937  
50−### Class Structure
38+A design system rots fast if it's treated as a one-time deliverable. Apply these rules every time you touch UI code or review a UI PR.
5139  
52−```java
53−// 1. Package declaration and imports
54−// 2. Class-level annotations (@Slf4j, @Singleton, etc.)
55−// 3. Class declaration with Javadoc
56−// 4. Static constants (UPPER_SNAKE_CASE)
57−// 5. Injected fields (@Inject)
58−// 6. Constructors
59−// 7. Public methods
60−// 8. Protected methods
61−// 9. Private methods
62−// 10. Inner classes/records
63−```
40+### Before you write code
6441  
65−### Annotations
66−- **Micronaut:** `@Singleton`, `@Inject`, `@Controller`, `@Replaces`, `@Requires`
67−- **Validation:** `@Valid`, `@NotNull`, `@Nullable`
68−- **Lombok:** `@Slf4j`, `@Getter`, `@NoArgsConstructor`, `@AllArgsConstructor`
69−- Use `@Builder` for complex object creation
42+- Search the component tables and Storybook first. The most common waste in this codebase is rebuilding something that already exists.
43+- If you can't find what you need, ask: is this a *missing component* (fix it in the DS) or a *missing prop on an existing component* (extend the DS)? Almost never the answer "build it locally."
44+- For anything visible to a user, check both light and dark mode in Storybook before merging.
7045  
71−### Error Handling
46+### While you write code
7247  
73−**DO**:
74−- Use specific exception types — extend `KestraException` or `KestraRuntimeException`
75−- Use `Optional<T>` for potentially absent returned values
76−- Return empty collections (e.g., `List.of()`, `Collections.emptyList()`) for absent values
77−- Use try-with-resources for resource management
78−- Log errors before re-throwing: `log.error("message", exception)`
79−- Write exception messages as plain, complete sentences that state the fact and the actionable detail — build them with `String.formatted()`/`String.format()`, not string concatenation or em dashes, e.g. `"Cannot acquire lock on asset '%s': already locked by '%s' until %s.".formatted(id, owner, until)`
48+- Build screens by *composing* `Ks*` components. A new feature should read like a list of design-system blocks plus business logic — not a wall of custom CSS.
49+- Keep the style **inside** the SFC. `<style scoped src="./x.scss">` is valid and `scoped` still applies, but an external file separates the CSS from the markup it describes for no gain, and it is one more file to open. Block order is `template`, `script`, `style`, enforced by `vue/block-order` in `ui/eslint.config.js`.
50+- Keep `<style>` blocks small. If a component file has more than ~50 lines of CSS, you probably need a new prop, a new slot, or a new `Ks*` component.
51+- Prefer `scoped` styles and rely on design tokens for theming. If you find yourself writing `:deep(.el-...)`, stop — it's a signal the design system needs to expose something.
52+- Write each CSS class selector as a full literal — never construct it with SCSS `&` nesting (`&__row`, `&--active`). Constructed selectors can't be found by search and devtools can't jump from a class to its rule. With `scoped` styles, BEM-style namespacing is redundant anyway: use flat, hyphenated names (`.label-input-row`, not `.label-input { &__row }`).
53+- Use semantic tokens, not raw colors. `var(--ks-text-link)` communicates intent; `var(--ks-text-blue-500)` does not exist for a reason.
54+- Co-locate component-specific tokens (e.g. `--ks-card-shadow`) in the component's SCSS, but always derive them from semantic tokens.
8055  
81−**DON'T**: Use generic `Exception`. Don't return null for collections. Don't write terse or telegraphic exception messages (e.g. dropping articles/verbs) or string-concatenate message parts.
56+### When extending the design system
8257  
83−### Java Language Features
84−- Use java records for simple data carriers
58+- Only expose props that are actually used somewhere in the codebase. Speculative props rot.
59+- Mirror Element Plus prop names where possible — predictability is a feature.
60+- Pass `v-bind="$attrs"` and forward slots so wrappers don't trap consumer extension points.
61+- Add the new component or prop to the relevant table in this file, plus a Storybook story and a unit test, in the same PR.
62+- Document tokens in code comments next to where they're declared in `ks-theme-*.scss`. The `scripts/generate-palette.mjs` file is auto-generated — don't hand-edit it.
8563  
86−### Naming Conventions
87−- Follow Java naming-convention best practices for Classes, Methods, Variables, Constants.
88−- Boolean methods: Start with `is`, `has`, `should`, `can` (e.g., `isReadOnly()`).
64+### When reviewing a UI PR
8965  
90−### File Organization
91−- Use 4-space indentation (configured in .editorconfig)
92−- UTF-8 encoding with LF line endings
93−- No trailing whitespace
66+Reject (or ask to fix) anything that:
9467  
95−### Utility Classes
96−* Mark utility classes as `final` with a private constructor
97−* Use static methods only
98−* Use existing utility classes (e.g., `ListUtils`, `MapUtils`) instead of creating new ones (`io.kestra.core.utils.*`)
68+- Imports from `element-plus` directly into `ui/src/`.
69+- Uses a hex code, `rgb(...)`, `--el-*`, or `--bs-*` for color.
70+- Uses `:deep()` to reach into a `Ks*` or `el-*` component.
71+- Hardcodes pixel values for padding, margin, radii, font sizes, or shadows.
72+- Adds a CSS class that overrides `.el-...` selectors.
73+- Duplicates a component that already exists in the design system.
74+- Adds a `Ks*` component without a Storybook story or test.
75+- Mounts `KsDataTable` without binding `:currentPage` / `:pageSize` (or `v-model:currentPage` / `v-model:pageSize`) — pagination is controlled; see "Data tables & pagination state".
76+- Watches a `computed` that returns a fresh object (spread / `{...}`) with `{deep: true}` — that fires on every dependency change regardless of content. See "The deep-watch / computed-spread trap".
77+- Adds a modal/drawer where the user enters data without guarding accidental dismissal — see "Unsaved input in modals (discard guard)".
9978  
100−**MANDATORY — never hand-roll Pebble delimiter detection.** Pebble has two block delimiter pairs — print blocks (`{{ ... }}`) and execute/statement blocks (`{% ... %}`) — and code that only checks for `{{`/`}}` silently misses `{%`/`%}` blocks. Use `io.kestra.core.utils.PebbleUtil` (`containsOpeningBlockDelimiter`, `startsWithOpeningBlockDelimiter`, `endsWithClosingBlockDelimiter`, `openingBlockDelimiters()`/`closingBlockDelimiters()`) instead of writing a new delimiter regex or literal — it derives the delimiter pairs from Pebble's own `Syntax.Builder` defaults, so it never drifts from what Pebble actually parses.
79+### Accessibility
10180  
102−### Enums
103−- Use enums for fixed sets of constants, including internal fields not exposed over the API — prefer a typed enum over a raw `String`/`int` whenever the value is drawn from a closed set of known cases, even if the set may only ever have a couple of members
104−- Use `@JsonValue` for custom serialization if needed
105−- Use `UNKNOWN` enum value for unknown cases in deserialization
106−- Compare Constants From The Left (a.k.a., Yoda conditions)
107−- Use a static `fromString` method for case-insensitive lookups using `Enums` class.
81+- Every icon-only `KsIconButton` must have an accessible label (`aria-label` or `title`). Screen readers do not see the icon glyph.
82+- Never convey state with color alone — pair status colors with an icon (`KsExecutionStatus` already does this) or a text label.
83+- Use semantic HTML inside slots: real `<button>`, `<a>`, `<label>`, headings in document order. Don't fake interactivity with `<div @click>`.
84+- `KsDialog`, `KsDrawer`, `KsPopover` already manage focus trap and `Escape`-to-close — don't reimplement these in feature code.
85+- Keep tab order logical; rely on the DOM order rather than `tabindex` hacks.
86+- Color contrast comes for free as long as you use `--ks-text-*` against `--ks-background-*` pairings. If you mix-and-match, verify with the browser inspector.
10887  
109−e.g.:
110−```java
111−public enum MyEnum {
112− VALUE_ONE,
113− VALUE_TWO,
114− UNKNOWN;
88+### Internationalization
11589  
116− @JsonCreator
117− public static ResourceType fromString(final String value) {
118− return Enums.getForNameIgnoreCase(value, MyEnum.class, UNKNOWN);
119− }
120−}
121−```
90+- No hardcoded user-facing strings. Always go through i18n.
91+- **In `<template>`, always use the global `$t(...)`** — never the `t` from `useI18n()`. Only call `useI18n()` (`const {t} = useI18n()`) when you need `t` in `<script>` (computed labels, toasts, etc.); if a component needs i18n **only** in its template, use `$t` and don't import `useI18n` at all.
92+- Use `<i18n-t>` for plurals and interpolation — never string-concatenate.
93+- Format dates and times via `dateUtils` (which respects `TIMEZONE_STORAGE_KEY` and `DATE_FORMAT_STORAGE_KEY`); format durations via `durationUtils.humanDuration()`. Don't reach for `Intl.DateTimeFormat` directly.
94+- Strings owned by a `Ks*` component live in the design system's locale files and are registered via `registerDesignSystemI18n`. Strings owned by a feature live in that feature's locale files.
12295  
123−### Documentation
124−- Javadoc for all public classes and methods - be concise
125−- Use `@param`, `@return`, `@throws` appropriately
126−- Use `{@inheritDoc}` for inherited methods
127−- Include usage examples for complex methods
96+### Loading, empty, and error states
12897  
129−## Webserver Constraints
130−- Put classes used by only controllers in the webserver module (not core)
131−- No business code/rule inside controllers - instead use a Service class
132−- All APIs must return a valid JSON object
133−- APIs should not return a response being a JSON array which cannot be evolved in a backwards-compatible way
134−- Unit tests must assert that a user can only access a given API if authorized to do so, and that access is denied otherwise
135−- APIs must be documented with OpenAPI annotations
136−- Use DTOs for requests/responses
137−- Always validate input parameters with `@Valid`
138−- Use `@ExecuteOn(TaskExecutors.IO)` for blocking operations
139−- Return meaningful error responses in controllers
98+Every async surface must render all four states. "Happy path only" is a bug.
14099  
141−## Worker Constraints
142−- Never depend on repositories for code called by the workers - instead use MetaStore/StateStore facades
100+- **Loading:** `KsSkeleton` for content placeholders; `vKsLoading` directive for sections that already have layout; `KsLoading` component for full-page or container-level spinners.
101+- **Empty:** `KsEmpty` with an action where possible — never a blank screen.
102+- **Error:** `KsAlert type="error"` with retry affordance, or `KsMessage` for transient errors.
103+- **Success / data:** the actual content.
143104  
144−## Executor Constraints
145−- Run the `H2RunnerTest` whenever you update part of the executor
105+### Data tables & pagination state
146106  
147−## Testing Guidelines
107+`KsDataTable` is a **fully controlled component** for pagination. `props.currentPage` and `props.pageSize` are the single source of truth — the component holds no internal page mirror. The parent owns the state, binds it (URL or local ref), and the component reacts.
148108  
149−### Java Tests
109+**The contract:**
150110  
151−**DO**:
152−- Place tests in same package structure as source code
153−- Simple unit test with mocks over complex integration tests when possible
154−- Add // Given-When-Then comments for clarity
155−- Test method naming: `should<ExpectedBehavior>When<ConditionOrAction>` (also `...Given<Input>`, `...For<Condition>`, `...If<Condition>`), e.g. `shouldThrowExceptionWhenDividingByZero()`
156−- Use `@MicronautTest` for tests that require Micronaut beans
157−- Use `@KestraTest` for tests that require running Kestra services (e.g., Executor, Scheduler)
158−-
159−```java
160−@KestraTest
161−class ServiceTest {
162− @Inject
163− private ServiceClass service;
111+- Bind `:currentPage` / `:pageSize` (one-way) OR use `v-model:currentPage` / `v-model:pageSize` (two-way).
112+- Listen to `@page-changed` (or rely on `@update:currentPage`/`@update:pageSize` via v-model) and propagate the change to the bound state — typically a `router.push({...route.query, page: String(page), size: String(size)})`.
113+- The component watches `[currentPage, pageSize]` and re-fires `loadData` automatically when either prop changes. Do **not** call `dataTable.reload()` from the parent in response to a page click — the prop change handles it.
114+- `resetAndReload()` emits `update:currentPage(1)` and `page-changed`; if the page was already 1 it just reloads. Useful from a filter-change watcher to bounce back to page 1 + re-fetch.
164115  
165− @Test
166− void shouldPerformActionWhenCondition() {
167− // Given (setup)
116+**URL-driven pattern** — the default for top-level list pages (Logs, Flows, Executions, KV, Secrets, Triggers, FlowsSearch, Blueprints):
168117  
169− // When (action)
118+```vue
119+<KsDataTable
120+ :loadData="loadData"
121+ :currentPage="urlPage"
122+ :pageSize="urlSize"
123+ :total="store.total"
124+ @page-changed="({page, size}) => router.push({query: {...route.query, page: String(page), size: String(size)}})"
125+/>
170126  
171− // Then (assertions)
172− assertThat(result).isNotNull();
173− }
174−}
127+<script setup>
128+const urlPage = computed(() => Number(route.query.page) || 1)
129+const urlSize = computed(() => Number(route.query.size) || 25)
130+</script>
175131 ```
176132  
177−**DON'T**: Use Nested classes for test organization. Avoid complex test hierarchies.
133+**Local-state pattern** — for embedded tables that should not appear in the URL (MetricsTable, side-panel views):
178134  
179−**Assertions:**
180−- Use AssertJ: `assertThat().isEqualTo()`, `assertThat().isNotNull()`, `assertThatThrownBy()`, `assertThatObject()`
181−- Prefer descriptive assertion methods
182−- Use `@MockBean` for mocking dependencies
135+```vue
136+<KsDataTable
137+ v-model:currentPage="currentPage"
138+ v-model:pageSize="pageSize"
139+ :loadData="loadData"
140+ :total="..."
141+/>
183142  
184−**Test Categories:**
185−- Unit tests: Fast, isolated, no external dependencies
186−- Integration tests: Test component interaction, use `@Tag("integration")`
187−- Flaky tests: Use `@Tag("flaky")` for unreliable tests
143+<script setup>
144+const currentPage = ref(1)
145+const pageSize = ref(25)
146+</script>
147+```
188148  
189−### Frontend Tests
190−- Unit tests with Vitest and `@vue/test-utils`
191−- E2E tests with Playwright
192−- Storybook component tests
193−- Use JSdom environment for DOM testing
194−- **Prefer Storybook component tests over Vitest unit tests whenever possible** — components render through their real story setup (props, slots, design-system deps) instead of being stubbed out, catching regressions unit mocks miss. Fall back to a Vitest unit test only when the logic under test isn't component-rendering behavior (e.g. a pure helper/composable) or no story exists and adding one isn't practical.
149+**Never** maintain a separate `internalPage` / `pageNumber` ref *and* bind the prop to a different value — that re-introduces the drift bug (URL says page 2, UI shows page 1) that this contract exists to prevent.
195150  
196−## UI Design System
151+### The deep-watch / computed-spread trap
197152  
198−The full UI design-system rules, component catalogue, token reference, and frontend best practices live in [ui/AGENTS.md](ui/AGENTS.md). That file is auto-loaded by AI coding agents whenever work happens under `ui/` in OSS or `ui-ee/` in Enterprise edition, and should be consulted (and kept up to date) for any frontend change.
153+A `computed` that returns a fresh object (via spread or `{...}`) returns a new reference on every evaluation. Watching it with `{deep: true}` does **not** add structural equality — `deep: true` enables deep dependency tracking; the equality check at the top is still `Object.is`. The callback therefore fires on every dependency change, even when the content is unchanged.
199154  
200−@ui/AGENTS.md
155+This was the root cause of the logs pagination bug: the watcher reset the page to 1 on every `route.query` mutation, including page-only updates from the user clicking the pagination itself.
201156  
202−## Frontend Code Style (Vue 3)
157+**Don't:**
203158  
204−**File Organization:**
205−- Use 2-space indentation for Vue, JSON, YAML, CSS
206−- Use 4-space indentation for JavaScript/TypeScript
207−- Follow Vue 3 Composition API patterns
208−- Organize imports: Vue/framework → third-party → local modules
159+```ts
160+const filterQuery = computed(() => {
161+ const {page: _p, size: _s, sort: _so, ...filters} = route.query
162+ return filters // new object reference on every route.query change
163+})
164+watch(filterQuery, () => dataTable.value?.resetAndReload(), {deep: true})
165+// Fires on every route.query change — page clicks, sort clicks, anything —
166+// and bounces the user back to page 1.
167+```
209168  
210−**Naming Conventions:**
211−- Components: `PascalCase` files (e.g., `MyComponent.vue`)
212−- Variables/functions: `camelCase`
213−- Constants: `UPPER_SNAKE_CASE`
214−- CSS classes: Follow Element Plus conventions
169+**Do:**
215170  
216−**TypeScript:**
217−- Use strict TypeScript configuration
218−- Prefer type definitions over `any`
219−- Use interfaces for object shapes
220−- Use enums for fixed sets of values
171+```ts
172+const filterQueryKey = computed(() => {
173+ const {page: _p, size: _s, sort: _so, ...filters} = route.query
174+ return JSON.stringify(filters) // stable string — same content, same value
175+})
176+watch(filterQueryKey, () => dataTable.value?.resetAndReload())
177+// Fires only when filter content actually changes.
178+```
221179  
222−## Build Commands
180+The general rule: **if you find yourself reaching for `{deep: true}` on a computed source, the source should probably return a primitive (string / number) instead of an object.** Strings compare by value; references compare by identity. Picking the right primitive is the fix.
223181  
224−### Java Backend
182+### Unsaved input in modals (discard guard)
225183  
226−```bash
227−# Clean build
228−./gradlew clean
184+Any modal/drawer where the user **enters data** must not silently lose it on an accidental dismissal. Use the shared `useDiscardGuard` composable — never reimplement the confirm-before-discard logic per modal.
229185  
230−# Full build (includes tests)
231−./gradlew build
186+```ts
187+// ui/src/composables/useDiscardGuard.ts (import path is relative to your component)
188+import {useDiscardGuard} from "../../composables/useDiscardGuard"
232189  
233−# Build without tests (faster)
234−./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel
190+// isDirty: true when there is unsaved input worth a prompt
191+const {guardedClose} = useDiscardGuard(() => /* isDirty */, {message: t("...")}) // message optional; defaults to "discard changes confirmation"
192+const beforeClose = (done: () => void) => guardedClose(() => { reset(); done() })
235193 ```
236194  
237−### Test Commands
195+```vue
196+<KsDialog :beforeClose="beforeClose" ... />
197+<KsDrawer :beforeClose="beforeClose" ... />
198+```
238199  
239−```bash
240−# Run all tests (excludes flaky tests)
241−./gradlew test
200+Rules:
201+- **Guard only *accidental* close paths** — overlay click, `Escape`, the `X`. These all go through `beforeClose`. Explicit **Cancel / Save** buttons set `v-model = false` directly and **must not** be guarded (the user already expressed intent; a prompt there is friction). Note: a programmatic `v-model = false` does **not** trigger `beforeClose` (Element Plus only calls it for user-initiated closes), which is exactly why Cancel/Save bypass it.
202+- **`isDirty` is per-modal.** Compare current input against a baseline captured on open (`JSON.stringify` snapshot), or "any meaningful input"; **ignore empty rows** (e.g. a blank label/tag row is not dirty). Reset dirty-relevant state on open so a reopen starts clean.
203+- **`KsDialog` and `KsDrawer` both expose a `beforeClose` prop** with signature `(done) => void` — call `done()` to proceed with closing. (Element Plus's `ElDrawer.beforeClose` is a prop, not an event; `KsDrawer` forwards it.)
204+- **Don't guard** read-only viewers, action/confirmation dialogs, or ephemeral forms that reset on every open.
242205  
243−# Run only unit tests (fastest)
244−./gradlew unitTest
206+### Icons
245207  
246−# Run integration tests
247−./gradlew integrationTest
208+- All icons come from [`vue-material-design-icons`](https://github.com/robcresswell/vue-material-design-icons) via `<KsIcon>` (or `<KsIconButton>` for clickable icons).
209+- Never inline raw SVG, font-icon classes, or emoji as UI state. If a needed icon is missing, propose adding it to the DS rather than dropping an SVG into a feature folder.
210+- Pass `name` (the kebab-case Material name); size and color come from props or the surrounding token context — don't override with inline `style`.
248211  
249−# Run flaky tests (separate from build)
250−./gradlew flakyTest
212+### Performance
251213  
252−# Run tests for specific module
253−./gradlew :core:test
214+- Lazy-load heavy surfaces: `KsEchart`, `KsLine`, `KsBar`, `KsPie`, `KsGraph`, `KsMarkdown`, code-editor surfaces. Use `defineAsyncComponent` or route-level code splitting.
215+- Prefer `v-show` for frequent toggles (tabs, filters), `v-if` for rare/heavy mounts (modals, big tables).
216+- Pass stable `key` props in lists. Avoid index-based keys when items have IDs.
217+- Don't render giant tables without `KsDataTable`'s pagination/virtualization — server-side paging is the default for anything that can grow.
218+- Watch out for `watch(..., { deep: true })` and `computed` with object identity — they often re-run more than you expect.
254219  
255−# Run single test class
256−./gradlew :module-name:test --tests "ClassName"
220+### Testing UI
257221  
258−# Run single test method
259−./gradlew :module-name:test --tests "ClassName.methodName"
222+- Unit tests with **Vitest** + `@vue/test-utils`, colocated next to the component.
223+- Use `data-test="..."` selectors for E2E tests with **Playwright**. Never select on `.el-*` or `.ks-*` class names — those are not stable contracts and will break on Element Plus / DS upgrades.
224+- Storybook stories cover: each variant prop, dark mode, edge cases (empty content, very long text, error state). A `*.stories.ts` file with one default story is not enough.
225+- Visual regressions caught in Storybook are cheaper to fix than caught in production.
260226  
261−# After running tests: generate a markdown summary of failures only
262−npx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)
263−```
227+### Deprecation contract
264228  
265−### Frontend (UI)
229+When retiring a `Ks*` component, prop, or token:
266230  
267−```bash
268−cd ui
231+1. Mark with a `@deprecated` JSDoc tag *and* a one-line replacement path: `@deprecated since 0.x — use <KsNewThing> instead`.
232+2. Keep it working for at least one minor release; add a `console.warn` in dev mode if the cost is reasonable.
233+3. Migrate all callers in the same release where feasible — don't leave half-migrations.
234+4. Only delete after the deprecation window. A silent removal breaks downstream EE / plugin code.
269235  
270−# Install dependencies
271−npm install
236+## Anti-patterns (do not write these)
272237  
273−# Development server
274−npm run dev
275− 
276−# Type checking
277−npm run check:types
278− 
279−# Build for production
280−npm run build
281− 
282−# Run tests
283−npm run test:all # All tests with coverage
284−npm run test:unit # Unit tests only
285−npm run test:storybook # Storybook tests
286−npm run test:e2e # End-to-end tests
287− 
288−# Linting
289−npm run lint # Fix linting issues
290−npm run test:lint # Check linting only
291− 
292−# Storybook
293−npm run storybook # Development
294−npm run build-storybook # Build
238+```vue
239+<!-- Wrong: raw element-plus, hex color, :deep, SCSS variable in feature code -->
240+<template>
241+ <el-button class="my-btn">Save</el-button>
242+</template>
243+<style lang="scss" scoped>
244+ .my-btn {
245+ background: #8405ff;
246+ font-size: $font-size-md;
247+ }
248+ :deep(.el-button__text) { color: white; }
249+</style>
295250 ```
296251  
297−## Development Workflow
298− 
299−### Running Locally
300− 
301−1. **Start/stop backends:**
302−```bash
303−# Start databases with Docker Compose
304−docker compose -f docker-compose-ci.yml up
305− 
306−# Stop databases with Docker Compose
307−docker compose -f docker-compose-ci.yml down
252+```vue
253+<!-- Right: Ks component, semantic tokens, no deep selector, i18n -->
254+<template>
255+ <KsButton type="primary">{{ t("save") }}</KsButton>
256+</template>
257+<style lang="scss" scoped>
258+ /* Almost always: no custom CSS is needed at all. */
259+</style>
308260 ```
309261  
310−2. **Access application:** http://localhost:8080
262+If your `<style>` block needs to exist:
311263  
312−### Worktree setup
313− 
314−When working in an EE worktree (detected by: the working directory is under a `worktrees/` directory):
315−```bash
316−dev-tools/setup-worktree.sh ../worktrees/foo
264+```scss
265+/* Right: --ks-* tokens, no SCSS vars in feature code, no :deep */
266+.my-feature {
267+ background: var(--ks-bg-surface);
268+ color: var(--ks-text-primary);
269+ border: 1px solid var(--ks-border-primary);
270+}
317271 ```
318−This copies the gitignored `cli/src/main/resources/application-*.yml` files from the main checkout into the worktree. Without this step Kestra cannot boot in the worktree. The script is idempotent — safe to re-run.
319272  
320−### Security Considerations
321−- Use tenant isolation for multi-tenant features
322−- Implement proper authorization with `@HasAnyPermission`
323−- Handle secrets securely (never log sensitive data)
273+## Components
324274  
325−### Performance Best Practices
326−- Implement pagination for large datasets
327−- Use streaming for large file operations
328−- Cache frequently accessed data appropriately
329−- Initialize collections with the expected size to avoid resizing overhead
275+### Basic / Layout
330276  
331−## Troubleshooting
277+| Component | Purpose |
278+|-----------|---------|
279+| `KsButton` / `KsButtonGroup` | Primary action button and grouped buttons |
280+| `KsIcon` / `KsIconButton` | Material Design icon display; icon-only button (always with `aria-label`) |
281+| `KsLink` | Styled hyperlink |
282+| `KsText` | Typography wrapper — preferred over raw `<span>` / `<p>` for theme-aware text |
283+| `KsScrollbar` | Custom-styled scrollbar wrapper |
284+| `KsContainer` / `KsHeader` / `KsMain` | Page layout shell |
285+| `KsRow` / `KsCol` | Responsive grid |
286+| `KsSplitter` / `KsSplitterPanel` | Resizable split-pane layout |
332287  
333−**Common Issues:**
334−- **Build failures:** Run `./gradlew clean` and retry
335−- **Test failures:** Check for service dependencies (Docker containers)
336−- **Frontend issues:** Ensure Node.js version matches package.json requirements
288+### Feedback
337289  
338−**Debugging:**
339−- Use IDE debugging with remote JVM debugging
340−- Use Micronaut's built-in health endpoints
341−- Enable debug logging: `--logging.level.io.kestra=DEBUG`
342−- Use JUnit and Vitest reports for test failures
290+| Component | Purpose |
291+|-----------|---------|
292+| `KsAlert` | Alert banner for messages and status feedback |
293+| `KsDialog` | Modal dialog (handles focus trap + Escape) |
294+| `KsDrawer` | Side drawer / panel |
295+| `KsTooltip` | Hover tooltip |
296+| `KsPopover` | Popover for contextual content |
297+| `KsLoading` (`vKsLoading`) | Loading spinner directive |
298+| `KsMessage` | Toast notification service |
299+| `KsNotification` | Notification service |
300+| `KsMessageBox` | Confirmation dialog service |
343301  
344−## Module Structure
302+### Form
345303  
346−**Core Modules:**
347−- `cli` - Command Line Interface
348−- `core` - Core functionality
349−- `webserver` - Web server
350−- `ui` - Vue 3 frontend application
351−- `executor` - The component responsible for managing execution state
352−- `scheduler` - The component responsible for scheduling polling and schedule triggers
353−- `worker` - The component that executes tasks and manages worker instances
354−- `worker-controller` - The component that manages worker instances and job distribution
355−- `indexer` - The component responsible for indexing executions
356−- `plateform` - provides the Platform Bill of Materials (BOM) for dependency management
304+| Component | Purpose |
305+|-----------|---------|
306+| `KsInput` / `KsPassword` | Text and password inputs |
307+| `KsInputNumber` | Numeric input with increment / decrement |
308+| `KsSelect` / `KsOption` / `KsOptionGroup` | Dropdown select |
309+| `KsAutocomplete` | Autocomplete input with suggestions |
310+| `KsCheckbox` / `KsCheckboxGroup` / `KsCheckboxButton` | Checkbox variants |
311+| `KsRadio` / `KsRadioGroup` / `KsRadioButton` | Radio button variants |
312+| `KsRadioCardGroup` | Single-select radio group rendered as option cards (title + optional hint/icon/disabled); options-driven via `:options` + `v-model` |
313+| `KsSwitch` | Toggle switch |
314+| `KsDatePicker` / `KsTimePicker` | Date and time pickers |
315+| `KsColorPicker` | Color picker |
316+| `KsDurationPicker` | ISO 8601 duration picker |
317+| `KsCascaderPanel` | Cascading hierarchical selector |
318+| `KsUpload` | File upload |
319+| `KsForm` / `KsFormItem` | Form container with validation |
357320  
358−**Queuing Layer:**
359−- `queue` - Core API for queue implementations
360−- `queue-jdbc` - JDBC-based queue implementation
321+### Data Display
361322  
362−**Data Layer:**
363−- `jdbc-*` - Database implementations (H2, Postgres, MySQL)
323+| Component | Purpose |
324+|-----------|---------|
325+| `KsCard` | Card container |
326+| `KsTable` / `KsTableColumn` | Basic table |
327+| `KsDataTable` / `KsFilter` / `KsBulkSelect` | Advanced data table with filtering, sorting, pagination, bulk actions. **Pagination is fully controlled** — bind `:currentPage` / `:pageSize` (or `v-model:`). See "Data tables & pagination state". |
328+| `KsEntityLink` | Clickable cross-entity reference (namespace / flow) for table cells — neutral tag with leading icon, violet on hover |
329+| `KsBadge` | Small indicator badge |
330+| `KsNewBadge` | Compact uppercase "NEW" pill flagging a newly shipped feature — caller supplies the label via the default slot |
331+| `KsTag` / `KsCheckTag` | Tag / label; clickable checkbox-style tag |
332+| `KsAvatar` | Avatar with fallback |
333+| `KsProgress` | Progress bar |
334+| `KsPagination` | Pagination controls |
335+| `KsEmpty` | Empty state placeholder |
336+| `KsSkeleton` | Skeleton loader |
337+| `KsId` | Copyable ID display |
338+| `KsDateAgo` | Relative time display ("2 hours ago") |
339+| `KsSegmented` | Segmented control |
340+| `KsCollapse` / `KsCollapseItem` | Collapsible sections |
341+| `KsTree` | Hierarchical tree view |
342+| `KsTimeline` / `KsTimelineItem` | Timeline visualization |
343+| `KsExecutionStatus` | Execution / task status badge with icon and color |
344+| `KsCodeStatus` | Compact validity badge with icon (`valid` / `error`) — caller supplies the label |
345+| `KsMarkdown` | Markdown renderer (lazy-load on heavy surfaces) |
364346  
365−**Testing Modules:**
366−- `tests` - Common test utilities and base classes
367−- `jmh-benchmark` - JMH benchmarks for performance testing
347+### Charts
368348  
369−**Key Patterns:**
370−- Repository pattern for data access
371−- Service layer for business logic
372−- Controller layer for HTTP endpoints
373−- Builder pattern for object construction (often with Lombok `@Builder`)
349+| Component | Purpose |
350+|-----------|---------|
351+| `KsEchart` | ECharts base wrapper (lazy-load) |
352+| `KsLine` / `KsBar` / `KsPie` | Line, bar, and pie charts (lazy-load) |
353+| `KsGraph` | Graph / network visualization (lazy-load) |
374354  
375−## Pull request guidelines
376−- Always add tests, keep your branch rebased instead of merged, and adhere to the commit message recommendations from https://www.conventionalcommits.org/en/v1.0.0.
377−- Use types: chore, feat, fix, refactor, test, docs, build
378−- Use scopes: apps, assets, core, dashboards, deps, design-system, executions, flows, iam, namespaces, plugins, secrets, storage, scheduler, system, tasks, tenants, tests, topology, triggers, variables, version, worker
355+### Navigation
379356  
380−## Issue guidelines
381−- **Classify an issue with its GitHub issue type, not a `kind/*` label.** The `kind/bug` label is retired — do not add it. Set the type instead: `gh issue create --title … ` followed by `gh issue edit <number> --type Bug`, or `gh issue edit <number> --type Task|Feature|Epic`. Available types are `Task`, `Bug`, `Feature` and `Epic` (list them with `gh api /orgs/kestra-io/issue-types`).
382−- **Do add the `area/*` labels** — `area/frontend`, `area/backend`, `area/devops`, `area/docs`, `area/plugin`, `area/qa`, `area/analytics` — since those drive routing and are still in use.
383−- Leave triage labels such as `kind/cooldown` to `kestrabot`; it applies them automatically on new issues.
357+| Component | Purpose |
358+|-----------|---------|
359+| `KsTabs` / `KsTabPane` | Tabbed interface |
360+| `KsMenu` / `KsMenuItem` | Hierarchical menu |
361+| `KsDropdown` / `KsDropdownMenu` / `KsDropdownItem` | Dropdown menu |
362+| `KsTopNavBar` | Top navigation bar |
363+| `KsSideBar` / `KsSideBarSection` / `KsSideBarItem` | Left sidebar shell (header / scrollable body / footer slots), section with title, and styled link primitive with icon, active and locked states |
364+| `KsBreadcrumb` / `KsBreadcrumbItem` | Breadcrumb navigation |
365+| `KsSteps` / `KsStep` | Step / wizard progress indicator |
384366  
385−This document should be updated as the codebase evolves. When in doubt, follow existing patterns in the codebase and maintain consistency with established conventions.
367+## Utilities (import from the design system)
386368  
387−## UI Translations
369+- `State`, `STATES`, `LOG_LEVELS` — execution state constants, icons, and colors
370+- `cssVar(name, opacity?)` — read a `--ks-*` CSS custom property at runtime (use this in JS / chart configs instead of hardcoding hex)
371+- `dateUtils` — `dateFilter()`, `DATE_FORMAT_STORAGE_KEY`, `TIMEZONE_STORAGE_KEY`
372+- `durationUtils` — `duration()`, `humanDuration()` — ISO 8601 ↔ ms and human-readable
373+- `stringUtils` — `afterLastDot()`
374+- `flowYamlUtils` — YAML parsing / manipulation for flow definitions
375+- `Comparators` — enum of filter comparison operators
376+- Filter helpers — `decodeSearchParams()`, `encodeFiltersToQuery()`, `getUniqueFilters()`, etc.
377+- `applyDefaultFilters()`, `useRouteFilterPolicy()` — filter composables
378+- `setMomentInstance()`, `setDateFormatter()` — date library configuration
379+- `designSystemLocale`, `setDesignSystemLocale`, `registerDesignSystemI18n` — i18n
388380  
389−**MANDATORY — never hardcode user-facing strings.** Every label, button, tooltip, placeholder, dialog/section title, table-column header, and toast/confirm message rendered to the user MUST go through vue-i18n: `t("key")` (or `:label`/`:tooltip` bindings) in components, and `<i18n-t keypath="...">` with named slots when the string embeds markup or a component (e.g. a `<code>` fragment). Never write a literal user-facing string in a template, a `:tooltip`/`:label` attribute, or a `toast.*` call. Reuse existing generic keys (`cancel`, `delete`, `edit`, `save`, `add`, `id`, `description`, `namespace`, `revision`, …) instead of duplicating them; put feature-specific strings under one namespaced object (e.g. `"reusableInputs": { … }`). After adding keys to `en.json`, propagate them to every language (translation generation script) so the missing-keys check stays clean — a key present only in `en.json` fails the check.
381+## Composables
390382  
391−Translation files live in `ui/src/translations/`. There is one JSON file per language code (e.g. `de.json`, `fr.json`) plus the source `en.json`.
383+- `useTheme()` — detects and tracks dark / light mode via MutationObserver. Use this instead of reading `document.documentElement` yourself.
384+- `useFilters`, `useSavedFilters`, `useDefaultFilter`, `usePreAppliedFilters`, `useRouteFilterPolicy`, `useTableColumns`, `useDataOptions`, `useDragAndDrop`, `usePeriodicRefresh` — data-table filter composables
385+- `useDiscardGuard(isDirty, {message?})` — confirm-before-discard for data-entry modals; see "Unsaved input in modals (discard guard)"
386+- `useTaskIcon()` — resolves the app-provided task-icon component via `TASK_ICON_INJECTION_KEY` (falling back to a generic placeholder icon). The app provides its own `TaskIcon` component once, at bootstrap (`app.provide(TASK_ICON_INJECTION_KEY, TaskIcon)`) — the design system cannot own that component since it depends on the app's plugin-icon backend API. Used internally by `KsEditor` (Monaco suggestion icons) and the `@kestra-io/topology` package (graph node icons) so both share the same app-provided instance.
392387  
393−### Checking for missing translations
388+## Design tokens
394389  
395−Run the check script from the `ui/` directory:
390+Tokens are CSS custom properties declared in [`ks-theme-light.scss`](packages/design-system/src/assets/styles/ks-theme-light.scss), [`ks-theme-dark.scss`](packages/design-system/src/assets/styles/ks-theme-dark.scss) and [`ks-theme-dark-2.scss`](packages/design-system/src/assets/styles/ks-theme-dark-2.scss). Each token is **semantic** — it describes *what the value means*, not what color it is. That is what makes dark mode and rebrands trivial.
396391  
397−```bash
398−cd ui && npm run translations:check
399−```
392+**Always use `var(--ks-*)` in component `<style>` blocks** — not SCSS variables, not hex codes, not `--el-*`, not `--bs-*`.
400393  
401−A clean run reports `No missing keys.`, `No extra keys.` and `No stale keys.` for every language. Anything listed must be fixed before merging — the same check runs as a PR gate.
394+Token families currently exposed:
402395  
403−> **Enterprise Edition:** EE-only keys live in `ui-ee/src/translations/ee_translations/en.json` and are checked separately — run `npm run translations:check` in `ui-ee` as well (see `kestra-ee/AGENTS.md` → "Frontend i18n").
396+- `--ks-bg-*` — backgrounds: surfaces (`base`, `surface`, `elevated`, `sidebar`, `input`, `overlay`, `scrim`), interaction states (`hover`, `hover-elevated`, `active`, `inactive`), component fills (`badge`, `tag`, `tag-hover`, `tag-active`, `tag-inactive`), plus per-state (`--ks-bg-success`, `--ks-bg-error`, `--ks-bg-warning`, `--ks-bg-info`)
397+- `--ks-border-*` — `default` / `subtle` / `strong` borders, `focus`, plus per-state (`error`, `success`, `warning`, `info`)
398+- `--ks-text-*` — text colors: `primary`, `secondary`, `dim`, `muted`, `inactive`, `link`, named (`blue`, `green`), plus per-state (`error`, `success`, `warning`, `info`)
399+- `--ks-icon-*` — icon colors: `default`, `hover`, `active`, `inactive`, `muted`, plus per-state
400+- `--ks-btn-*` — button background / border / text variants (`primary`, `secondary`, `run`, `success`) across `default` / `hover` / `active` / `inactive` states
401+- `--ks-toggle-*` — toggle / switch states (`default`, `hover`, `active`, `inactive`, `playground`)
402+- `--ks-dropdown-*`, `--ks-scrollbar-*`, `--ks-shadow-*` — component-specific tokens
403+- `--ks-status-*` — palette for charts and status (`success`, `error`, `warning`, `info`, `running`, `pending`, `neutral`); pair with `cssVar("--ks-status-success")` in JS
404+- `--ks-editor-*`, `--ks-dependencies-*`, `--ks-topology-*` — domain-specific surfaces
404405  
405−### Editing English strings
406+When a needed token is missing, **add it** to all three of `ks-theme-light.scss`, `ks-theme-dark.scss` and `ks-theme-dark-2.scss` (and review with design) rather than picking a raw color.
406407  
407−**Changing an existing English value is a translation change.** Every key carries a fingerprint of the English text its translations were generated from, so editing `en.json` — even just the capitalisation — marks that key stale in all twelve languages and fails `translations:check` until it is regenerated. Run `npm run translations:generate` and commit the result alongside your change.
408+**SCSS variables — only inside `ui/packages/design-system/`, never in feature code:**
408409  
409−This is deliberate: before it existed, edited values were never propagated, and a rename of "SuperAdmin" to "Superadmin" sat un-translated in eleven locales for a year (kestra-io/kestra#10656).
410+- **Brand:** `$base-primary-500` (primary, `#8405FF`)
411+- **Status palette:** `$base-green-500` (success), `$base-red-500` (danger), `$base-orange-500` (warning), `$base-blue-500` (info)
412+- **Grays:** `$base-gray-50` … `$base-gray-950`
413+- **Typography:** `$font-family-sans-serif` (Inter), `$font-family-monospace` (JetBrains Mono)
414+- **Font sizes:** `$font-size-xs` / `sm` / `md` / `lg` / `xl` / `2xl` / `3xl` / `4xl`
415+- **Radii:** `$border-radius` (0.25rem), `$border-radius-sm` (0.15rem), `$border-radius-lg` (0.5rem)
410416  
411−### Adding or regenerating translations
412− 
413−Prefer `npm run translations:generate` (needs `GEMINI_API_KEY`); it fills missing keys and re-translates stale ones on its own, with no flag to remember. Pass `true` to force a full re-translation of everything.
414− 
415−If you must write a translation by hand:
416− 
417−1. Identify gaps by running `npm run translations:check`.
418−2. Follow these translation rules (mirroring `ui/scripts/translations/generateTranslations.ts`, the generator shared by OSS and EE):
419− - **Reserved English terms — never translate:** `kv store`, `namespace`, `tenant`, `flow`, `subflow`, `task`, `log`, `blueprint`, `id`, `trigger`, `label`, `key`, `value`, `input`, `output`, `port`, `worker`, `backfill`, `healthcheck`, `min`, `max`.
420− - **ALL-CAPS status labels stay in English:** `WARNING`, `FAILED`, `SUCCESS`, `PAUSED`, `RUNNING`, etc.
421− - **Preserve `{placeholder}` variables** exactly — vue-i18n uses a **single** pair of braces. Do not translate the name inside the braces, do not rename it, and never write `{{placeholder}}`: double braces are a compile error (`Not allowed nest placeholder`) and make `t()` throw at render time. Each translation must carry exactly the same placeholders as the English source — no invented ones, none dropped.
422− - **Use natural UI terminology** — avoid false friends or overly literal translations (e.g. German: Execution → Ausführung, Theme → Modus, State → Zustand).
423−3. Insert the translated keys into the correct position in the target language JSON, mirroring the key order of `en.json`.
424−4. Re-run `npm run translations:check` to confirm everything is clean before committing.
425− 
426−The tooling itself lives in `ui/scripts/translations/` and is shared with EE, which keeps only thin entry points. Rules live in `.mjs` so the dependency-free PR gate can apply them; file IO and orchestration stay in `.ts`.
427− 
428−### Conflicts in `fingerprints.json`
429− 
430−Two branches that both touch `en.json` will both regenerate `ui/scripts/translations/fingerprints.json`, so it conflicts often. **Never hand-merge the hashes and never pick a side** — a hash says "this English text is what the twelve translations were generated from", so choosing the wrong one silently marks a drifted key as current and the drift becomes invisible again.
431− 
432−Resolve it the same way as a `kestra-sdk` conflict — regenerate:
433− 
434−```bash
435−git checkout --ours ui/src/translations/*.json ui/scripts/translations/fingerprints*.json
436−cd ui && npm run translations:generate # fills whatever the other branch added
437−npm run translations:check # must report no missing / extra / stale keys
438−```
439− 
440−`en.json` itself normally merges cleanly, since branches usually add different keys; it is the generated files that collide.
417+These exist so the *design system itself* can compose tokens from a single palette. They are not API for feature code — feature code should reach the same values through `--ks-*` tokens.
441418  

Also from Kynth Studios

Built for the same person as RuleStack

ToolDrift

What the AI coding tools changed last night

tooldrift.kynth.studio

StillShipping

Which agent tools have stopped shipping

stillshipping.kynth.studio

BlockDex

Search inside every shadcn registry

blockdex.kynth.studio

The studio list

One product, taken apart, once a month

Kynth Studios pulls one shipped product open every month — what it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did. One email a month, nothing in between.

Double opt-in — we send one confirmation link and nothing else until you click it.

RuleStack

Built by

Kynth Studios

the studio behind ToolDrift, StillShipping and BlockDex

part of Toolproof, the measurement layer for AI agent tooling

Directory

Configs
Stacks
Compare formats
AGENTS.md vs CLAUDE.md
Cursor rules alternatives
Diff two configs
Best AGENTS.md examples
Best Cursor rules examples
What goes in a CLAUDE.md

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

© 2026 RuleStack. A Kynth Studios product. Changelog

RuleStack

The studio list

One product, taken apart, once a month

Kynth Studios pulls one shipped product open every month — what it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did. One email a month, nothing in between.

Double opt-in — we send one confirmation link and nothing else until you click it.

RuleStack

Built by

Kynth Studios

the studio behind ToolDrift, StillShipping and BlockDex

part of Toolproof, the measurement layer for AI agent tooling

Directory

Configs
Stacks
Compare formats
AGENTS.md vs CLAUDE.md
Cursor rules alternatives
Diff two configs
Best AGENTS.md examples
Best Cursor rules examples
What goes in a CLAUDE.md

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

© 2026 RuleStack. A Kynth Studios product. Changelog

RuleStack

The studio list

One product, taken apart, once a month

Kynth Studios pulls one shipped product open every month — what it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did. One email a month, nothing in between.

Double opt-in — we send one confirmation link and nothing else until you click it.

RuleStack

Built by

Kynth Studios

the studio behind ToolDrift, StillShipping and BlockDex

part of Toolproof, the measurement layer for AI agent tooling

Directory

Configs
Stacks
Compare formats
AGENTS.md vs CLAUDE.md
Cursor rules alternatives
Diff two configs
Best AGENTS.md examples
Best Cursor rules examples
What goes in a CLAUDE.md

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

© 2026 RuleStack. A Kynth Studios product. Changelog

RuleStack