| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 29 | 58 | 0% |
| Commands | 0 | 0 | 31 | 0% |
| Section tags | 7 | 2 | 6 | 47% |
What each file covers
Sections
0 shared · 29 only in A · 58 only in B- − 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
- + 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
- + Adding missing translations
Commands
0 shared · 0 only in A · 31 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
- + 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:check
- + task
Section tags
7 shared · 2 only in A · 6 only in B- − api
- − do-not
- + setup
- + build
- + types
- + security
- + dependencies
- + agent-behaviour
- test
- code-style
- architecture
- git-pr
- ui
- performance
- docs
Line diff
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 `<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.
50- 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.
51- 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 }`).
52- Use semantic tokens, not raw colors. `var(--ks-text-link)` communicates intent; `var(--ks-text-blue-500)` does not exist for a reason.
53- Co-locate component-specific tokens (e.g. `--ks-card-shadow`) in the component's SCSS, but always derive them from semantic tokens.
54
55### When extending the design system
56
57- Only expose props that are actually used somewhere in the codebase. Speculative props rot.
58- Mirror Element Plus prop names where possible — predictability is a feature.
59- Pass `v-bind="$attrs"` and forward slots so wrappers don't trap consumer extension points.
60- 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.
61- 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.
62
63### When reviewing a UI PR
64
65Reject (or ask to fix) anything that:
66
67- Imports from `element-plus` directly into `ui/src/`.
68- Uses a hex code, `rgb(...)`, `--el-*`, or `--bs-*` for color.
69- Uses `:deep()` to reach into a `Ks*` or `el-*` component.
70- Hardcodes pixel values for padding, margin, radii, font sizes, or shadows.
71- Adds a CSS class that overrides `.el-...` selectors.
72- Duplicates a component that already exists in the design system.
73- Adds a `Ks*` component without a Storybook story or test.
74- Mounts `KsDataTable` without binding `:currentPage` / `:pageSize` (or `v-model:currentPage` / `v-model:pageSize`) — pagination is controlled; see "Data tables & pagination state".
75- 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".
76- Adds a modal/drawer where the user enters data without guarding accidental dismissal — see "Unsaved input in modals (discard guard)".
77
78### Accessibility
79
80- Every icon-only `KsIconButton` must have an accessible label (`aria-label` or `title`). Screen readers do not see the icon glyph.
81- Never convey state with color alone — pair status colors with an icon (`KsExecutionStatus` already does this) or a text label.
82- Use semantic HTML inside slots: real `<button>`, `<a>`, `<label>`, headings in document order. Don't fake interactivity with `<div @click>`.
83- `KsDialog`, `KsDrawer`, `KsPopover` already manage focus trap and `Escape`-to-close — don't reimplement these in feature code.
84- Keep tab order logical; rely on the DOM order rather than `tabindex` hacks.
85- 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.
86
87### Internationalization
88
89- No hardcoded user-facing strings. Always go through i18n.
90- **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.
91- Use `<i18n-t>` for plurals and interpolation — never string-concatenate.
92- 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.
93- 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.
94
95### Loading, empty, and error states
96
97Every async surface must render all four states. "Happy path only" is a bug.
98
99- **Loading:** `KsSkeleton` for content placeholders; `vKsLoading` directive for sections that already have layout; `KsLoading` component for full-page or container-level spinners.
100- **Empty:** `KsEmpty` with an action where possible — never a blank screen.
101- **Error:** `KsAlert type="error"` with retry affordance, or `KsMessage` for transient errors.
102- **Success / data:** the actual content.
103
104### Data tables & pagination state
105
106`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.
107
108**The contract:**
109
110- Bind `:currentPage` / `:pageSize` (one-way) OR use `v-model:currentPage` / `v-model:pageSize` (two-way).
111- 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)})`.
112- 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.
113- `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.
114
115**URL-driven pattern** — the default for top-level list pages (Logs, Flows, Executions, KV, Secrets, Triggers, FlowsSearch, Blueprints):
116
117```vue
118<KsDataTable
119 :loadData="loadData"
120 :currentPage="urlPage"
121 :pageSize="urlSize"
122 :total="store.total"
123 @page-changed="({page, size}) => router.push({query: {...route.query, page: String(page), size: String(size)}})"
124/>
125
126<script setup>
127const urlPage = computed(() => Number(route.query.page) || 1)
128const urlSize = computed(() => Number(route.query.size) || 25)
129</script>
130```
131
132**Local-state pattern** — for embedded tables that should not appear in the URL (MetricsTable, side-panel views):
133
134```vue
135<KsDataTable
136 v-model:currentPage="currentPage"
137 v-model:pageSize="pageSize"
138 :loadData="loadData"
139 :total="..."
140/>
141
142<script setup>
143const currentPage = ref(1)
144const pageSize = ref(25)
145</script>
146```
147
148**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.
149
150### The deep-watch / computed-spread trap
151
152A `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.
153
154This 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.
155
156**Don't:**
157
158```ts
159const filterQuery = computed(() => {
160 const {page: _p, size: _s, sort: _so, ...filters} = route.query
161 return filters // new object reference on every route.query change
162})
163watch(filterQuery, () => dataTable.value?.resetAndReload(), {deep: true})
164// Fires on every route.query change — page clicks, sort clicks, anything —
165// and bounces the user back to page 1.
166```
167
168**Do:**
169
170```ts
171const filterQueryKey = computed(() => {
172 const {page: _p, size: _s, sort: _so, ...filters} = route.query
173 return JSON.stringify(filters) // stable string — same content, same value
174})
175watch(filterQueryKey, () => dataTable.value?.resetAndReload())
176// Fires only when filter content actually changes.
177```
178
179The 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.
180
181### Unsaved input in modals (discard guard)
182
183Any 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.
184
185```ts
186// ui/src/composables/useDiscardGuard.ts (import path is relative to your component)
187import {useDiscardGuard} from "../../composables/useDiscardGuard"
188
189// isDirty: true when there is unsaved input worth a prompt
190const {guardedClose} = useDiscardGuard(() => /* isDirty */, {message: t("...")}) // message optional; defaults to "discard changes confirmation"
191const beforeClose = (done: () => void) => guardedClose(() => { reset(); done() })
192```
193
194```vue
195<KsDialog :beforeClose="beforeClose" ... />
196<KsDrawer :beforeClose="beforeClose" ... />
197```
198
199Rules:
200- **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.
201- **`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.
202- **`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.)
203- **Don't guard** read-only viewers, action/confirmation dialogs, or ephemeral forms that reset on every open.
204
205### Icons
206
207- All icons come from [`vue-material-design-icons`](https://github.com/robcresswell/vue-material-design-icons) via `<KsIcon>` (or `<KsIconButton>` for clickable icons).
208- 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.
209- Pass `name` (the kebab-case Material name); size and color come from props or the surrounding token context — don't override with inline `style`.
210
211### Performance
212
213- Lazy-load heavy surfaces: `KsEchart`, `KsLine`, `KsBar`, `KsPie`, `KsGraph`, `KsMarkdown`, code-editor surfaces. Use `defineAsyncComponent` or route-level code splitting.
214- Prefer `v-show` for frequent toggles (tabs, filters), `v-if` for rare/heavy mounts (modals, big tables).
215- Pass stable `key` props in lists. Avoid index-based keys when items have IDs.
216- Don't render giant tables without `KsDataTable`'s pagination/virtualization — server-side paging is the default for anything that can grow.
217- Watch out for `watch(..., { deep: true })` and `computed` with object identity — they often re-run more than you expect.
218
219### Testing UI
220
221- Unit tests with **Vitest** + `@vue/test-utils`, colocated next to the component.
222- 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.
223- 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.
224- Visual regressions caught in Storybook are cheaper to fix than caught in production.
225
226### Deprecation contract
227
228When retiring a `Ks*` component, prop, or token:
229
2301. Mark with a `@deprecated` JSDoc tag *and* a one-line replacement path: `@deprecated since 0.x — use <KsNewThing> instead`.
2312. Keep it working for at least one minor release; add a `console.warn` in dev mode if the cost is reasonable.
2323. Migrate all callers in the same release where feasible — don't leave half-migrations.
2334. Only delete after the deprecation window. A silent removal breaks downstream EE / plugin code.
234
235## Anti-patterns (do not write these)
236
237```vue
238<!-- Wrong: raw element-plus, hex color, :deep, SCSS variable in feature code -->
239<template>
240 <el-button class="my-btn">Save</el-button>
241</template>
242<style lang="scss" scoped>
243 .my-btn {
244 background: #8405ff;
245 font-size: $font-size-md;
246 }
247 :deep(.el-button__text) { color: white; }
248</style>
249```
250
251```vue
252<!-- Right: Ks component, semantic tokens, no deep selector, i18n -->
253<template>
254 <KsButton type="primary">{{ t("save") }}</KsButton>
255</template>
256<style lang="scss" scoped>
257 /* Almost always: no custom CSS is needed at all. */
258</style>
259```
260
261If your `<style>` block needs to exist:
262
263```scss
264/* Right: --ks-* tokens, no SCSS vars in feature code, no :deep */
265.my-feature {
266 background: var(--ks-bg-surface);
267 color: var(--ks-text-primary);
268 border: 1px solid var(--ks-border-primary);
269}
270```
271
272## Components
273
274### Basic / Layout
275
276| Component | Purpose |
277|-----------|---------|
278| `KsButton` / `KsButtonGroup` | Primary action button and grouped buttons |
279| `KsIcon` / `KsIconButton` | Material Design icon display; icon-only button (always with `aria-label`) |
280| `KsLink` | Styled hyperlink |
281| `KsText` | Typography wrapper — preferred over raw `<span>` / `<p>` for theme-aware text |
282| `KsScrollbar` | Custom-styled scrollbar wrapper |
283| `KsContainer` / `KsHeader` / `KsMain` | Page layout shell |
284| `KsRow` / `KsCol` | Responsive grid |
285| `KsSplitter` / `KsSplitterPanel` | Resizable split-pane layout |
286
287### Feedback
288
289| Component | Purpose |
290|-----------|---------|
291| `KsAlert` | Alert banner for messages and status feedback |
292| `KsDialog` | Modal dialog (handles focus trap + Escape) |
293| `KsDrawer` | Side drawer / panel |
294| `KsTooltip` | Hover tooltip |
295| `KsPopover` | Popover for contextual content |
296| `KsLoading` (`vKsLoading`) | Loading spinner directive |
297| `KsMessage` | Toast notification service |
298| `KsNotification` | Notification service |
299| `KsMessageBox` | Confirmation dialog service |
300
301### Form
302
303| Component | Purpose |
304|-----------|---------|
305| `KsInput` / `KsPassword` | Text and password inputs |
306| `KsInputNumber` | Numeric input with increment / decrement |
307| `KsSelect` / `KsOption` / `KsOptionGroup` | Dropdown select |
308| `KsAutocomplete` | Autocomplete input with suggestions |
309| `KsCheckbox` / `KsCheckboxGroup` / `KsCheckboxButton` | Checkbox variants |
310| `KsRadio` / `KsRadioGroup` / `KsRadioButton` | Radio button variants |
311| `KsSwitch` | Toggle switch |
312| `KsDatePicker` / `KsTimePicker` | Date and time pickers |
313| `KsColorPicker` | Color picker |
314| `KsDurationPicker` | ISO 8601 duration picker |
315| `KsCascaderPanel` | Cascading hierarchical selector |
316| `KsUpload` | File upload |
317| `KsForm` / `KsFormItem` | Form container with validation |
318
319### Data Display
320
321| Component | Purpose |
322|-----------|---------|
323| `KsCard` | Card container |
324| `KsTable` / `KsTableColumn` | Basic table |
325| `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". |
326| `KsEntityLink` | Clickable cross-entity reference (namespace / flow) for table cells — neutral tag with leading icon, violet on hover |
327| `KsBadge` | Small indicator badge |
328| `KsNewBadge` | Compact uppercase "NEW" pill flagging a newly shipped feature — caller supplies the label via the default slot |
329| `KsTag` / `KsCheckTag` | Tag / label; clickable checkbox-style tag |
330| `KsAvatar` | Avatar with fallback |
331| `KsProgress` | Progress bar |
332| `KsPagination` | Pagination controls |
333| `KsEmpty` | Empty state placeholder |
334| `KsSkeleton` | Skeleton loader |
335| `KsId` | Copyable ID display |
336| `KsDateAgo` | Relative time display ("2 hours ago") |
337| `KsSegmented` | Segmented control |
338| `KsCollapse` / `KsCollapseItem` | Collapsible sections |
339| `KsTree` | Hierarchical tree view |
340| `KsTimeline` / `KsTimelineItem` | Timeline visualization |
341| `KsExecutionStatus` | Execution / task status badge with icon and color |
342| `KsCodeStatus` | Compact validity badge with icon (`valid` / `error`) — caller supplies the label |
343| `KsMarkdown` | Markdown renderer (lazy-load on heavy surfaces) |
344
345### Charts
346
347| Component | Purpose |
348|-----------|---------|
349| `KsEchart` | ECharts base wrapper (lazy-load) |
350| `KsLine` / `KsBar` / `KsPie` | Line, bar, and pie charts (lazy-load) |
351| `KsGraph` | Graph / network visualization (lazy-load) |
352
353### Navigation
354
355| Component | Purpose |
356|-----------|---------|
357| `KsTabs` / `KsTabPane` | Tabbed interface |
358| `KsMenu` / `KsMenuItem` | Hierarchical menu |
359| `KsDropdown` / `KsDropdownMenu` / `KsDropdownItem` | Dropdown menu |
360| `KsTopNavBar` | Top navigation bar |
361| `KsSideBar` / `KsSideBarSection` / `KsSideBarItem` | Left sidebar shell (header / scrollable body / footer slots), section with title, and styled link primitive with icon, active and locked states |
362| `KsBreadcrumb` / `KsBreadcrumbItem` | Breadcrumb navigation |
363| `KsSteps` / `KsStep` | Step / wizard progress indicator |
364
365## Utilities (import from the design system)
366
367- `State`, `STATES`, `LOG_LEVELS` — execution state constants, icons, and colors
368- `cssVar(name, opacity?)` — read a `--ks-*` CSS custom property at runtime (use this in JS / chart configs instead of hardcoding hex)
369- `dateUtils` — `dateFilter()`, `DATE_FORMAT_STORAGE_KEY`, `TIMEZONE_STORAGE_KEY`
370- `durationUtils` — `duration()`, `humanDuration()` — ISO 8601 ↔ ms and human-readable
371- `stringUtils` — `afterLastDot()`
372- `flowYamlUtils` — YAML parsing / manipulation for flow definitions
373- `Comparators` — enum of filter comparison operators
374- Filter helpers — `decodeSearchParams()`, `encodeFiltersToQuery()`, `getUniqueFilters()`, etc.
375- `applyDefaultFilters()`, `useRouteFilterPolicy()` — filter composables
376- `setMomentInstance()`, `setDateFormatter()` — date library configuration
377- `designSystemLocale`, `setDesignSystemLocale`, `registerDesignSystemI18n` — i18n
378
379## Composables
380
381- `useTheme()` — detects and tracks dark / light mode via MutationObserver. Use this instead of reading `document.documentElement` yourself.
382- `useFilters`, `useSavedFilters`, `useDefaultFilter`, `usePreAppliedFilters`, `useRouteFilterPolicy`, `useTableColumns`, `useDataOptions`, `useDragAndDrop`, `usePeriodicRefresh` — data-table filter composables
383- `useDiscardGuard(isDirty, {message?})` — confirm-before-discard for data-entry modals; see "Unsaved input in modals (discard guard)"
384- `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.
385
386## Design tokens
387
388Tokens 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.
389
390**Always use `var(--ks-*)` in component `<style>` blocks** — not SCSS variables, not hex codes, not `--el-*`, not `--bs-*`.
391
392Token families currently exposed:
393
394- `--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`)
395- `--ks-border-*` — `default` / `subtle` / `strong` borders, `focus`, plus per-state (`error`, `success`, `warning`, `info`)
396- `--ks-text-*` — text colors: `primary`, `secondary`, `dim`, `muted`, `inactive`, `link`, named (`blue`, `green`), plus per-state (`error`, `success`, `warning`, `info`)
397- `--ks-icon-*` — icon colors: `default`, `hover`, `active`, `inactive`, `muted`, plus per-state
398- `--ks-btn-*` — button background / border / text variants (`primary`, `secondary`, `run`, `success`) across `default` / `hover` / `active` / `inactive` states
399- `--ks-toggle-*` — toggle / switch states (`default`, `hover`, `active`, `inactive`, `playground`)
400- `--ks-dropdown-*`, `--ks-scrollbar-*`, `--ks-shadow-*` — component-specific tokens
401- `--ks-status-*` — palette for charts and status (`success`, `error`, `warning`, `info`, `running`, `pending`, `neutral`); pair with `cssVar("--ks-status-success")` in JS
402- `--ks-editor-*`, `--ks-dependencies-*`, `--ks-topology-*` — domain-specific surfaces
403
404When 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.
405
406**SCSS variables — only inside `ui/packages/design-system/`, never in feature code:**
407
408- **Brand:** `$base-primary-500` (primary, `#8405FF`)
409- **Status palette:** `$base-green-500` (success), `$base-red-500` (danger), `$base-orange-500` (warning), `$base-blue-500` (info)
410- **Grays:** `$base-gray-50` … `$base-gray-950`
411- **Typography:** `$font-family-sans-serif` (Inter), `$font-family-monospace` (JetBrains Mono)
412- **Font sizes:** `$font-size-xs` / `sm` / `md` / `lg` / `xl` / `2xl` / `3xl` / `4xl`
413- **Radii:** `$border-radius` (0.25rem), `$border-radius-sm` (0.15rem), `$border-radius-lg` (0.5rem)
414
415These 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.
416
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
195## UI Design System
196
197The 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.
198
199@ui/AGENTS.md
200
201## Frontend Code Style (Vue 3)
202
203**File Organization:**
204- Use 2-space indentation for Vue, JSON, YAML, CSS
205- Use 4-space indentation for JavaScript/TypeScript
206- Follow Vue 3 Composition API patterns
207- Organize imports: Vue/framework → third-party → local modules
208
209**Naming Conventions:**
210- Components: `PascalCase` files (e.g., `MyComponent.vue`)
211- Variables/functions: `camelCase`
212- Constants: `UPPER_SNAKE_CASE`
213- CSS classes: Follow Element Plus conventions
214
215**TypeScript:**
216- Use strict TypeScript configuration
217- Prefer type definitions over `any`
218- Use interfaces for object shapes
219- Use enums for fixed sets of values
220
221## Build Commands
222
223### Java Backend
224
225```bash
226# Clean build
227./gradlew clean
228
229# Full build (includes tests)
230./gradlew build
231
232# Build without tests (faster)
233./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel
234```
235
236### Test Commands
237
238```bash
239# Run all tests (excludes flaky tests)
240./gradlew test
241
242# Run only unit tests (fastest)
243./gradlew unitTest
244
245# Run integration tests
246./gradlew integrationTest
247
248# Run flaky tests (separate from build)
249./gradlew flakyTest
250
251# Run tests for specific module
252./gradlew :core:test
253
254# Run single test class
255./gradlew :module-name:test --tests "ClassName"
256
257# Run single test method
258./gradlew :module-name:test --tests "ClassName.methodName"
259
260# After running tests: generate a markdown summary of failures only
261npx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)
262```
263
264### Frontend (UI)
265
266```bash
267cd ui
268
269# Install dependencies
270npm install
271
272# Development server
273npm run dev
274
275# Type checking
276npm run check:types
277
278# Build for production
279npm run build
280
281# Run tests
282npm run test:all # All tests with coverage
283npm run test:unit # Unit tests only
284npm run test:storybook # Storybook tests
285npm run test:e2e # End-to-end tests
286
287# Linting
288npm run lint # Fix linting issues
289npm run test:lint # Check linting only
290
291# Storybook
292npm run storybook # Development
293npm run build-storybook # Build
294```
295
296## Development Workflow
297
298### Running Locally
299
3001. **Start/stop backends:**
301```bash
302# Start databases with Docker Compose
303docker compose -f docker-compose-ci.yml up
304
305# Stop databases with Docker Compose
306docker compose -f docker-compose-ci.yml down
307```
308
3092. **Access application:** http://localhost:8080
310
311### Worktree setup
312
313When working in an EE worktree (detected by: the working directory is under a `worktrees/` directory):
314```bash
315dev-tools/setup-worktree.sh ../worktrees/foo
316```
317This 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.
318
319### Security Considerations
320- Use tenant isolation for multi-tenant features
321- Implement proper authorization with `@HasAnyPermission`
322- Handle secrets securely (never log sensitive data)
323
324### Performance Best Practices
325- Implement pagination for large datasets
326- Use streaming for large file operations
327- Cache frequently accessed data appropriately
328- Initialize collections with the expected size to avoid resizing overhead
329
330## Troubleshooting
331
332**Common Issues:**
333- **Build failures:** Run `./gradlew clean` and retry
334- **Test failures:** Check for service dependencies (Docker containers)
335- **Frontend issues:** Ensure Node.js version matches package.json requirements
336
337**Debugging:**
338- Use IDE debugging with remote JVM debugging
339- Use Micronaut's built-in health endpoints
340- Enable debug logging: `--logging.level.io.kestra=DEBUG`
341- Use JUnit and Vitest reports for test failures
342
343## Module Structure
344
345**Core Modules:**
346- `cli` - Command Line Interface
347- `core` - Core functionality
348- `webserver` - Web server
349- `ui` - Vue 3 frontend application
350- `executor` - The component responsible for managing execution state
351- `scheduler` - The component responsible for scheduling polling and schedule triggers
352- `worker` - The component that executes tasks and manages worker instances
353- `worker-controller` - The component that manages worker instances and job distribution
354- `indexer` - The component responsible for indexing executions
355- `plateform` - provides the Platform Bill of Materials (BOM) for dependency management
356
357**Queuing Layer:**
358- `queue` - Core API for queue implementations
359- `queue-jdbc` - JDBC-based queue implementation
360
361**Data Layer:**
362- `jdbc-*` - Database implementations (H2, Postgres, MySQL)
363
364**Testing Modules:**
365- `tests` - Common test utilities and base classes
366- `jmh-benchmark` - JMH benchmarks for performance testing
367
368**Key Patterns:**
369- Repository pattern for data access
370- Service layer for business logic
371- Controller layer for HTTP endpoints
372- Builder pattern for object construction (often with Lombok `@Builder`)
373
374## Pull request guidelines
375- 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.
376- Use types: chore, feat, fix, refactor, test, docs, build
377- 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
378
379## Issue guidelines
380- **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`).
381- **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.
382- Leave triage labels such as `kind/cooldown` to `kestrabot`; it applies them automatically on new issues.
383
384This document should be updated as the codebase evolves. When in doubt, follow existing patterns in the codebase and maintain consistency with established conventions.
385
386## UI Translations
387
388**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.
389
390Translation 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`.
391
392### Checking for missing translations
393
394Run the check script from the `ui/` directory:
395
396```bash
397cd ui && npm run translations:check
398```
399
400A clean run reports `No missing keys. No extra keys.` for every language. Any listed missing keys must be added.
401
402> **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").
403
404### Adding missing translations
405
4061. Identify gaps by running `npm run translations:check` (or by diffing the flattened `en.json` keys against each language file).
4072. Translate only the missing keys — do **not** re-translate keys that already have a value.
4083. Follow these translation rules (mirroring `generate_translations.ts`):
409 - **Reserved English terms — never translate:** `kv store`, `namespace`, `flow`, `subflow`, `task`, `log`, `blueprint`, `id`, `trigger`, `label`, `key`, `value`, `input`, `output`, `port`, `worker`, `backfill`, `healthcheck`, `min`, `max`.
410 - **ALL-CAPS status labels stay in English:** `WARNING`, `FAILED`, `SUCCESS`, `PAUSED`, `RUNNING`, etc.
411 - **Preserve `{{placeholder}}` variables** exactly — do not translate the word inside the braces.
412 - **Use natural UI terminology** — avoid false friends or overly literal translations (e.g. German: Execution → Ausführung, Theme → Modus, State → Zustand).
4134. Insert the translated keys into the correct position in the target language JSON, keeping `sort_keys=True` order (alphabetical within each object).
4145. Re-run `npm run translations:check` to confirm everything is clean before committing.
415
@@ −1 +1 @@
1−# UI Design System Guidelines
1+# Coding Agent Guidelines for Kestra Open Source Edition
22
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.
3+This document provides essential information for AI coding agents working on the Kestra codebase.
44
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.
5+**IMPORTANT — READ FIRST**
66
7−## What this is, in plain terms
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**
819
9−Think of the design system as the product's **visual vocabulary**:
20+## Project
1021
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.
22+Monorepo built with Java (backend) and Vue (frontend), using Gradle as the build system.
1423
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.
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
1629
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/`.
30+## Critical Code Patterns
1831
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.
32+### Dependency Injection
2033
21−## Golden rules (non-negotiable)
34+**DO**: Use constructor injection with final fields.
2235
23−These rules are what keep the UI maintainable as it grows. Treat any deviation as a bug.
36+```java
37+@Singleton
38+public class MyService {
39+ private final SomeDependency dependency;
2440
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`.
41+ @Inject
42+ public MyService(SomeDependency dependency) {
43+ this.dependency = Objects.requireNonNull(dependency);
44+ }
45+}
46+```
3547
36−## Best practices for keeping the design system healthy
48+**DON'T**: Use field injection (`@Inject` on fields directly). Always prefer constructor injection.
3749
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.
50+### Class Structure
3951
40−### Before you write code
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+```
4164
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.
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
4570
46−### While you write code
71+### Error Handling
4772
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 `<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.
50−- 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.
51−- 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 }`).
52−- Use semantic tokens, not raw colors. `var(--ks-text-link)` communicates intent; `var(--ks-text-blue-500)` does not exist for a reason.
53−- Co-locate component-specific tokens (e.g. `--ks-card-shadow`) in the component's SCSS, but always derive them from semantic tokens.
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)`
5480
55−### When extending the design system
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.
5682
57−- Only expose props that are actually used somewhere in the codebase. Speculative props rot.
58−- Mirror Element Plus prop names where possible — predictability is a feature.
59−- Pass `v-bind="$attrs"` and forward slots so wrappers don't trap consumer extension points.
60−- 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.
61−- 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.
83+### Java Language Features
84+- Use java records for simple data carriers
6285
63−### When reviewing a UI PR
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()`).
6489
65−Reject (or ask to fix) anything that:
90+### File Organization
91+- Use 4-space indentation (configured in .editorconfig)
92+- UTF-8 encoding with LF line endings
93+- No trailing whitespace
6694
67−- Imports from `element-plus` directly into `ui/src/`.
68−- Uses a hex code, `rgb(...)`, `--el-*`, or `--bs-*` for color.
69−- Uses `:deep()` to reach into a `Ks*` or `el-*` component.
70−- Hardcodes pixel values for padding, margin, radii, font sizes, or shadows.
71−- Adds a CSS class that overrides `.el-...` selectors.
72−- Duplicates a component that already exists in the design system.
73−- Adds a `Ks*` component without a Storybook story or test.
74−- Mounts `KsDataTable` without binding `:currentPage` / `:pageSize` (or `v-model:currentPage` / `v-model:pageSize`) — pagination is controlled; see "Data tables & pagination state".
75−- 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".
76−- Adds a modal/drawer where the user enters data without guarding accidental dismissal — see "Unsaved input in modals (discard guard)".
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.*`)
7799
78−### Accessibility
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.
79101
80−- Every icon-only `KsIconButton` must have an accessible label (`aria-label` or `title`). Screen readers do not see the icon glyph.
81−- Never convey state with color alone — pair status colors with an icon (`KsExecutionStatus` already does this) or a text label.
82−- Use semantic HTML inside slots: real `<button>`, `<a>`, `<label>`, headings in document order. Don't fake interactivity with `<div @click>`.
83−- `KsDialog`, `KsDrawer`, `KsPopover` already manage focus trap and `Escape`-to-close — don't reimplement these in feature code.
84−- Keep tab order logical; rely on the DOM order rather than `tabindex` hacks.
85−- 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.
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.
86108
87−### Internationalization
109+e.g.:
110+```java
111+public enum MyEnum {
112+ VALUE_ONE,
113+ VALUE_TWO,
114+ UNKNOWN;
88115
89−- No hardcoded user-facing strings. Always go through i18n.
90−- **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.
91−- Use `<i18n-t>` for plurals and interpolation — never string-concatenate.
92−- 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.
93−- 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.
116+ @JsonCreator
117+ public static ResourceType fromString(final String value) {
118+ return Enums.getForNameIgnoreCase(value, MyEnum.class, UNKNOWN);
119+ }
120+}
121+```
94122
95−### Loading, empty, and error states
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
96128
97−Every async surface must render all four states. "Happy path only" is a bug.
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
98140
99−- **Loading:** `KsSkeleton` for content placeholders; `vKsLoading` directive for sections that already have layout; `KsLoading` component for full-page or container-level spinners.
100−- **Empty:** `KsEmpty` with an action where possible — never a blank screen.
101−- **Error:** `KsAlert type="error"` with retry affordance, or `KsMessage` for transient errors.
102−- **Success / data:** the actual content.
141+## Worker Constraints
142+- Never depend on repositories for code called by the workers - instead use MetaStore/StateStore facades
103143
104−### Data tables & pagination state
144+## Executor Constraints
145+- Run the `H2RunnerTest` whenever you update part of the executor
105146
106−`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.
147+## Testing Guidelines
107148
108−**The contract:**
149+### Java Tests
109150
110−- Bind `:currentPage` / `:pageSize` (one-way) OR use `v-model:currentPage` / `v-model:pageSize` (two-way).
111−- 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)})`.
112−- 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.
113−- `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.
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;
114164
115−**URL-driven pattern** — the default for top-level list pages (Logs, Flows, Executions, KV, Secrets, Triggers, FlowsSearch, Blueprints):
165+ @Test
166+ void shouldPerformActionWhenCondition() {
167+ // Given (setup)
116168
117−```vue
118−<KsDataTable
119− :loadData="loadData"
120− :currentPage="urlPage"
121− :pageSize="urlSize"
122− :total="store.total"
123− @page-changed="({page, size}) => router.push({query: {...route.query, page: String(page), size: String(size)}})"
124−/>
169+ // When (action)
125170
126−<script setup>
127−const urlPage = computed(() => Number(route.query.page) || 1)
128−const urlSize = computed(() => Number(route.query.size) || 25)
129−</script>
171+ // Then (assertions)
172+ assertThat(result).isNotNull();
173+ }
174+}
130175 ```
131176
132−**Local-state pattern** — for embedded tables that should not appear in the URL (MetricsTable, side-panel views):
177+**DON'T**: Use Nested classes for test organization. Avoid complex test hierarchies.
133178
134−```vue
135−<KsDataTable
136− v-model:currentPage="currentPage"
137− v-model:pageSize="pageSize"
138− :loadData="loadData"
139− :total="..."
140−/>
179+**Assertions:**
180+- Use AssertJ: `assertThat().isEqualTo()`, `assertThat().isNotNull()`, `assertThatThrownBy()`, `assertThatObject()`
181+- Prefer descriptive assertion methods
182+- Use `@MockBean` for mocking dependencies
141183
142−<script setup>
143−const currentPage = ref(1)
144−const pageSize = ref(25)
145−</script>
146−```
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
147188
148−**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.
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
149194
150−### The deep-watch / computed-spread trap
195+## UI Design System
151196
152−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.
197+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.
153198
154−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.
199+@ui/AGENTS.md
155200
156−**Don't:**
201+## Frontend Code Style (Vue 3)
157202
158−```ts
159−const filterQuery = computed(() => {
160− const {page: _p, size: _s, sort: _so, ...filters} = route.query
161− return filters // new object reference on every route.query change
162−})
163−watch(filterQuery, () => dataTable.value?.resetAndReload(), {deep: true})
164−// Fires on every route.query change — page clicks, sort clicks, anything —
165−// and bounces the user back to page 1.
166−```
203+**File Organization:**
204+- Use 2-space indentation for Vue, JSON, YAML, CSS
205+- Use 4-space indentation for JavaScript/TypeScript
206+- Follow Vue 3 Composition API patterns
207+- Organize imports: Vue/framework → third-party → local modules
167208
168−**Do:**
209+**Naming Conventions:**
210+- Components: `PascalCase` files (e.g., `MyComponent.vue`)
211+- Variables/functions: `camelCase`
212+- Constants: `UPPER_SNAKE_CASE`
213+- CSS classes: Follow Element Plus conventions
169214
170−```ts
171−const filterQueryKey = computed(() => {
172− const {page: _p, size: _s, sort: _so, ...filters} = route.query
173− return JSON.stringify(filters) // stable string — same content, same value
174−})
175−watch(filterQueryKey, () => dataTable.value?.resetAndReload())
176−// Fires only when filter content actually changes.
177−```
215+**TypeScript:**
216+- Use strict TypeScript configuration
217+- Prefer type definitions over `any`
218+- Use interfaces for object shapes
219+- Use enums for fixed sets of values
178220
179−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.
221+## Build Commands
180222
181−### Unsaved input in modals (discard guard)
223+### Java Backend
182224
183−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.
225+```bash
226+# Clean build
227+./gradlew clean
184228
185−```ts
186−// ui/src/composables/useDiscardGuard.ts (import path is relative to your component)
187−import {useDiscardGuard} from "../../composables/useDiscardGuard"
229+# Full build (includes tests)
230+./gradlew build
188231
189−// isDirty: true when there is unsaved input worth a prompt
190−const {guardedClose} = useDiscardGuard(() => /* isDirty */, {message: t("...")}) // message optional; defaults to "discard changes confirmation"
191−const beforeClose = (done: () => void) => guardedClose(() => { reset(); done() })
232+# Build without tests (faster)
233+./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel
192234 ```
193235
194−```vue
195−<KsDialog :beforeClose="beforeClose" ... />
196−<KsDrawer :beforeClose="beforeClose" ... />
197−```
236+### Test Commands
198237
199−Rules:
200−- **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.
201−- **`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.
202−- **`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.)
203−- **Don't guard** read-only viewers, action/confirmation dialogs, or ephemeral forms that reset on every open.
238+```bash
239+# Run all tests (excludes flaky tests)
240+./gradlew test
204241
205−### Icons
242+# Run only unit tests (fastest)
243+./gradlew unitTest
206244
207−- All icons come from [`vue-material-design-icons`](https://github.com/robcresswell/vue-material-design-icons) via `<KsIcon>` (or `<KsIconButton>` for clickable icons).
208−- 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.
209−- Pass `name` (the kebab-case Material name); size and color come from props or the surrounding token context — don't override with inline `style`.
245+# Run integration tests
246+./gradlew integrationTest
210247
211−### Performance
248+# Run flaky tests (separate from build)
249+./gradlew flakyTest
212250
213−- Lazy-load heavy surfaces: `KsEchart`, `KsLine`, `KsBar`, `KsPie`, `KsGraph`, `KsMarkdown`, code-editor surfaces. Use `defineAsyncComponent` or route-level code splitting.
214−- Prefer `v-show` for frequent toggles (tabs, filters), `v-if` for rare/heavy mounts (modals, big tables).
215−- Pass stable `key` props in lists. Avoid index-based keys when items have IDs.
216−- Don't render giant tables without `KsDataTable`'s pagination/virtualization — server-side paging is the default for anything that can grow.
217−- Watch out for `watch(..., { deep: true })` and `computed` with object identity — they often re-run more than you expect.
251+# Run tests for specific module
252+./gradlew :core:test
218253
219−### Testing UI
254+# Run single test class
255+./gradlew :module-name:test --tests "ClassName"
220256
221−- Unit tests with **Vitest** + `@vue/test-utils`, colocated next to the component.
222−- 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.
223−- 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.
224−- Visual regressions caught in Storybook are cheaper to fix than caught in production.
257+# Run single test method
258+./gradlew :module-name:test --tests "ClassName.methodName"
225259
226−### Deprecation contract
260+# After running tests: generate a markdown summary of failures only
261+npx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)
262+```
227263
228−When retiring a `Ks*` component, prop, or token:
264+### Frontend (UI)
229265
230−1. Mark with a `@deprecated` JSDoc tag *and* a one-line replacement path: `@deprecated since 0.x — use <KsNewThing> instead`.
231−2. Keep it working for at least one minor release; add a `console.warn` in dev mode if the cost is reasonable.
232−3. Migrate all callers in the same release where feasible — don't leave half-migrations.
233−4. Only delete after the deprecation window. A silent removal breaks downstream EE / plugin code.
266+```bash
267+cd ui
234268
235−## Anti-patterns (do not write these)
269+# Install dependencies
270+npm install
236271
237−```vue
238−<!-- Wrong: raw element-plus, hex color, :deep, SCSS variable in feature code -->
239−<template>
240− <el-button class="my-btn">Save</el-button>
241−</template>
242−<style lang="scss" scoped>
243− .my-btn {
244− background: #8405ff;
245− font-size: $font-size-md;
246− }
247− :deep(.el-button__text) { color: white; }
248−</style>
249−```
272+# Development server
273+npm run dev
250274
251−```vue
252−<!-- Right: Ks component, semantic tokens, no deep selector, i18n -->
253−<template>
254− <KsButton type="primary">{{ t("save") }}</KsButton>
255−</template>
256−<style lang="scss" scoped>
257− /* Almost always: no custom CSS is needed at all. */
258−</style>
275+# Type checking
276+npm run check:types
277+
278+# Build for production
279+npm run build
280+
281+# Run tests
282+npm run test:all # All tests with coverage
283+npm run test:unit # Unit tests only
284+npm run test:storybook # Storybook tests
285+npm run test:e2e # End-to-end tests
286+
287+# Linting
288+npm run lint # Fix linting issues
289+npm run test:lint # Check linting only
290+
291+# Storybook
292+npm run storybook # Development
293+npm run build-storybook # Build
259294 ```
260295
261−If your `<style>` block needs to exist:
296+## Development Workflow
262297
263−```scss
264−/* Right: --ks-* tokens, no SCSS vars in feature code, no :deep */
265−.my-feature {
266− background: var(--ks-bg-surface);
267− color: var(--ks-text-primary);
268− border: 1px solid var(--ks-border-primary);
269−}
298+### Running Locally
299+
300+1. **Start/stop backends:**
301+```bash
302+# Start databases with Docker Compose
303+docker compose -f docker-compose-ci.yml up
304+
305+# Stop databases with Docker Compose
306+docker compose -f docker-compose-ci.yml down
270307 ```
271308
272−## Components
309+2. **Access application:** http://localhost:8080
273310
274−### Basic / Layout
311+### Worktree setup
275312
276−| Component | Purpose |
277−|-----------|---------|
278−| `KsButton` / `KsButtonGroup` | Primary action button and grouped buttons |
279−| `KsIcon` / `KsIconButton` | Material Design icon display; icon-only button (always with `aria-label`) |
280−| `KsLink` | Styled hyperlink |
281−| `KsText` | Typography wrapper — preferred over raw `<span>` / `<p>` for theme-aware text |
282−| `KsScrollbar` | Custom-styled scrollbar wrapper |
283−| `KsContainer` / `KsHeader` / `KsMain` | Page layout shell |
284−| `KsRow` / `KsCol` | Responsive grid |
285−| `KsSplitter` / `KsSplitterPanel` | Resizable split-pane layout |
313+When working in an EE worktree (detected by: the working directory is under a `worktrees/` directory):
314+```bash
315+dev-tools/setup-worktree.sh ../worktrees/foo
316+```
317+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.
286318
287−### Feedback
319+### Security Considerations
320+- Use tenant isolation for multi-tenant features
321+- Implement proper authorization with `@HasAnyPermission`
322+- Handle secrets securely (never log sensitive data)
288323
289−| Component | Purpose |
290−|-----------|---------|
291−| `KsAlert` | Alert banner for messages and status feedback |
292−| `KsDialog` | Modal dialog (handles focus trap + Escape) |
293−| `KsDrawer` | Side drawer / panel |
294−| `KsTooltip` | Hover tooltip |
295−| `KsPopover` | Popover for contextual content |
296−| `KsLoading` (`vKsLoading`) | Loading spinner directive |
297−| `KsMessage` | Toast notification service |
298−| `KsNotification` | Notification service |
299−| `KsMessageBox` | Confirmation dialog service |
324+### Performance Best Practices
325+- Implement pagination for large datasets
326+- Use streaming for large file operations
327+- Cache frequently accessed data appropriately
328+- Initialize collections with the expected size to avoid resizing overhead
300329
301−### Form
330+## Troubleshooting
302331
303−| Component | Purpose |
304−|-----------|---------|
305−| `KsInput` / `KsPassword` | Text and password inputs |
306−| `KsInputNumber` | Numeric input with increment / decrement |
307−| `KsSelect` / `KsOption` / `KsOptionGroup` | Dropdown select |
308−| `KsAutocomplete` | Autocomplete input with suggestions |
309−| `KsCheckbox` / `KsCheckboxGroup` / `KsCheckboxButton` | Checkbox variants |
310−| `KsRadio` / `KsRadioGroup` / `KsRadioButton` | Radio button variants |
311−| `KsSwitch` | Toggle switch |
312−| `KsDatePicker` / `KsTimePicker` | Date and time pickers |
313−| `KsColorPicker` | Color picker |
314−| `KsDurationPicker` | ISO 8601 duration picker |
315−| `KsCascaderPanel` | Cascading hierarchical selector |
316−| `KsUpload` | File upload |
317−| `KsForm` / `KsFormItem` | Form container with validation |
332+**Common Issues:**
333+- **Build failures:** Run `./gradlew clean` and retry
334+- **Test failures:** Check for service dependencies (Docker containers)
335+- **Frontend issues:** Ensure Node.js version matches package.json requirements
318336
319−### Data Display
337+**Debugging:**
338+- Use IDE debugging with remote JVM debugging
339+- Use Micronaut's built-in health endpoints
340+- Enable debug logging: `--logging.level.io.kestra=DEBUG`
341+- Use JUnit and Vitest reports for test failures
320342
321−| Component | Purpose |
322−|-----------|---------|
323−| `KsCard` | Card container |
324−| `KsTable` / `KsTableColumn` | Basic table |
325−| `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". |
326−| `KsEntityLink` | Clickable cross-entity reference (namespace / flow) for table cells — neutral tag with leading icon, violet on hover |
327−| `KsBadge` | Small indicator badge |
328−| `KsNewBadge` | Compact uppercase "NEW" pill flagging a newly shipped feature — caller supplies the label via the default slot |
329−| `KsTag` / `KsCheckTag` | Tag / label; clickable checkbox-style tag |
330−| `KsAvatar` | Avatar with fallback |
331−| `KsProgress` | Progress bar |
332−| `KsPagination` | Pagination controls |
333−| `KsEmpty` | Empty state placeholder |
334−| `KsSkeleton` | Skeleton loader |
335−| `KsId` | Copyable ID display |
336−| `KsDateAgo` | Relative time display ("2 hours ago") |
337−| `KsSegmented` | Segmented control |
338−| `KsCollapse` / `KsCollapseItem` | Collapsible sections |
339−| `KsTree` | Hierarchical tree view |
340−| `KsTimeline` / `KsTimelineItem` | Timeline visualization |
341−| `KsExecutionStatus` | Execution / task status badge with icon and color |
342−| `KsCodeStatus` | Compact validity badge with icon (`valid` / `error`) — caller supplies the label |
343−| `KsMarkdown` | Markdown renderer (lazy-load on heavy surfaces) |
343+## Module Structure
344344
345−### Charts
345+**Core Modules:**
346+- `cli` - Command Line Interface
347+- `core` - Core functionality
348+- `webserver` - Web server
349+- `ui` - Vue 3 frontend application
350+- `executor` - The component responsible for managing execution state
351+- `scheduler` - The component responsible for scheduling polling and schedule triggers
352+- `worker` - The component that executes tasks and manages worker instances
353+- `worker-controller` - The component that manages worker instances and job distribution
354+- `indexer` - The component responsible for indexing executions
355+- `plateform` - provides the Platform Bill of Materials (BOM) for dependency management
346356
347−| Component | Purpose |
348−|-----------|---------|
349−| `KsEchart` | ECharts base wrapper (lazy-load) |
350−| `KsLine` / `KsBar` / `KsPie` | Line, bar, and pie charts (lazy-load) |
351−| `KsGraph` | Graph / network visualization (lazy-load) |
357+**Queuing Layer:**
358+- `queue` - Core API for queue implementations
359+- `queue-jdbc` - JDBC-based queue implementation
352360
353−### Navigation
361+**Data Layer:**
362+- `jdbc-*` - Database implementations (H2, Postgres, MySQL)
354363
355−| Component | Purpose |
356−|-----------|---------|
357−| `KsTabs` / `KsTabPane` | Tabbed interface |
358−| `KsMenu` / `KsMenuItem` | Hierarchical menu |
359−| `KsDropdown` / `KsDropdownMenu` / `KsDropdownItem` | Dropdown menu |
360−| `KsTopNavBar` | Top navigation bar |
361−| `KsSideBar` / `KsSideBarSection` / `KsSideBarItem` | Left sidebar shell (header / scrollable body / footer slots), section with title, and styled link primitive with icon, active and locked states |
362−| `KsBreadcrumb` / `KsBreadcrumbItem` | Breadcrumb navigation |
363−| `KsSteps` / `KsStep` | Step / wizard progress indicator |
364+**Testing Modules:**
365+- `tests` - Common test utilities and base classes
366+- `jmh-benchmark` - JMH benchmarks for performance testing
364367
365−## Utilities (import from the design system)
368+**Key Patterns:**
369+- Repository pattern for data access
370+- Service layer for business logic
371+- Controller layer for HTTP endpoints
372+- Builder pattern for object construction (often with Lombok `@Builder`)
366373
367−- `State`, `STATES`, `LOG_LEVELS` — execution state constants, icons, and colors
368−- `cssVar(name, opacity?)` — read a `--ks-*` CSS custom property at runtime (use this in JS / chart configs instead of hardcoding hex)
369−- `dateUtils` — `dateFilter()`, `DATE_FORMAT_STORAGE_KEY`, `TIMEZONE_STORAGE_KEY`
370−- `durationUtils` — `duration()`, `humanDuration()` — ISO 8601 ↔ ms and human-readable
371−- `stringUtils` — `afterLastDot()`
372−- `flowYamlUtils` — YAML parsing / manipulation for flow definitions
373−- `Comparators` — enum of filter comparison operators
374−- Filter helpers — `decodeSearchParams()`, `encodeFiltersToQuery()`, `getUniqueFilters()`, etc.
375−- `applyDefaultFilters()`, `useRouteFilterPolicy()` — filter composables
376−- `setMomentInstance()`, `setDateFormatter()` — date library configuration
377−- `designSystemLocale`, `setDesignSystemLocale`, `registerDesignSystemI18n` — i18n
374+## Pull request guidelines
375+- 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.
376+- Use types: chore, feat, fix, refactor, test, docs, build
377+- 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
378378
379−## Composables
379+## Issue guidelines
380+- **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`).
381+- **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.
382+- Leave triage labels such as `kind/cooldown` to `kestrabot`; it applies them automatically on new issues.
380383
381−- `useTheme()` — detects and tracks dark / light mode via MutationObserver. Use this instead of reading `document.documentElement` yourself.
382−- `useFilters`, `useSavedFilters`, `useDefaultFilter`, `usePreAppliedFilters`, `useRouteFilterPolicy`, `useTableColumns`, `useDataOptions`, `useDragAndDrop`, `usePeriodicRefresh` — data-table filter composables
383−- `useDiscardGuard(isDirty, {message?})` — confirm-before-discard for data-entry modals; see "Unsaved input in modals (discard guard)"
384−- `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.
384+This document should be updated as the codebase evolves. When in doubt, follow existing patterns in the codebase and maintain consistency with established conventions.
385385
386−## Design tokens
386+## UI Translations
387387
388−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.
388+**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.
389389
390−**Always use `var(--ks-*)` in component `<style>` blocks** — not SCSS variables, not hex codes, not `--el-*`, not `--bs-*`.
390+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`.
391391
392−Token families currently exposed:
392+### Checking for missing translations
393393
394−- `--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`)
395−- `--ks-border-*` — `default` / `subtle` / `strong` borders, `focus`, plus per-state (`error`, `success`, `warning`, `info`)
396−- `--ks-text-*` — text colors: `primary`, `secondary`, `dim`, `muted`, `inactive`, `link`, named (`blue`, `green`), plus per-state (`error`, `success`, `warning`, `info`)
397−- `--ks-icon-*` — icon colors: `default`, `hover`, `active`, `inactive`, `muted`, plus per-state
398−- `--ks-btn-*` — button background / border / text variants (`primary`, `secondary`, `run`, `success`) across `default` / `hover` / `active` / `inactive` states
399−- `--ks-toggle-*` — toggle / switch states (`default`, `hover`, `active`, `inactive`, `playground`)
400−- `--ks-dropdown-*`, `--ks-scrollbar-*`, `--ks-shadow-*` — component-specific tokens
401−- `--ks-status-*` — palette for charts and status (`success`, `error`, `warning`, `info`, `running`, `pending`, `neutral`); pair with `cssVar("--ks-status-success")` in JS
402−- `--ks-editor-*`, `--ks-dependencies-*`, `--ks-topology-*` — domain-specific surfaces
394+Run the check script from the `ui/` directory:
403395
404−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.
396+```bash
397+cd ui && npm run translations:check
398+```
405399
406−**SCSS variables — only inside `ui/packages/design-system/`, never in feature code:**
400+A clean run reports `No missing keys. No extra keys.` for every language. Any listed missing keys must be added.
407401
408−- **Brand:** `$base-primary-500` (primary, `#8405FF`)
409−- **Status palette:** `$base-green-500` (success), `$base-red-500` (danger), `$base-orange-500` (warning), `$base-blue-500` (info)
410−- **Grays:** `$base-gray-50` … `$base-gray-950`
411−- **Typography:** `$font-family-sans-serif` (Inter), `$font-family-monospace` (JetBrains Mono)
412−- **Font sizes:** `$font-size-xs` / `sm` / `md` / `lg` / `xl` / `2xl` / `3xl` / `4xl`
413−- **Radii:** `$border-radius` (0.25rem), `$border-radius-sm` (0.15rem), `$border-radius-lg` (0.5rem)
402+> **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").
414403
415−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.
404+### Adding missing translations
405+
406+1. Identify gaps by running `npm run translations:check` (or by diffing the flattened `en.json` keys against each language file).
407+2. Translate only the missing keys — do **not** re-translate keys that already have a value.
408+3. Follow these translation rules (mirroring `generate_translations.ts`):
409+ - **Reserved English terms — never translate:** `kv store`, `namespace`, `flow`, `subflow`, `task`, `log`, `blueprint`, `id`, `trigger`, `label`, `key`, `value`, `input`, `output`, `port`, `worker`, `backfill`, `healthcheck`, `min`, `max`.
410+ - **ALL-CAPS status labels stay in English:** `WARNING`, `FAILED`, `SUCCESS`, `PAUSED`, `RUNNING`, etc.
411+ - **Preserve `{{placeholder}}` variables** exactly — do not translate the word inside the braces.
412+ - **Use natural UI terminology** — avoid false friends or overly literal translations (e.g. German: Execution → Ausführung, Theme → Modus, State → Zustand).
413+4. Insert the translated keys into the correct position in the target language JSON, keeping `sort_keys=True` order (alphabetical within each object).
414+5. Re-run `npm run translations:check` to confirm everything is clean before committing.
416415
